Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API Proposal: PipeReader and PipeWriter implementations over a Stream #25087

Closed
davidfowl opened this issue Feb 19, 2018 · 15 comments
Closed

API Proposal: PipeReader and PipeWriter implementations over a Stream #25087

davidfowl opened this issue Feb 19, 2018 · 15 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.IO.Pipelines
Milestone

Comments

@davidfowl
Copy link
Member

This will help with adoption of pipelines. The usual adapter code involves creating 2 pipes and creating 2 async loops that read from the stream and write into the pipe and reading from the pipe and writing into the Stream.

There are 2 directions: Stream -> Pipe and Pipe -> Stream.

  • Read only
  • Write only
public class PipeReader
{
    public static PipeReader Create(Stream stream, StreamPipeReaderOptions readerOptions = null);

    public virtual Stream AsStream();
    public virtual Task CopyToAsync(Stream stream, CancellationToken cancellationToken = default);
}

public class PipeWriter
{
    public static PipeWriter Create(Stream stream, StreamPipeWriterOptions writerOptions = null);

    public virtual Stream AsStream();
    protected internal virtual Task CopyFromAsync(Stream stream, CancellationToken cancellationToken = default);
}

public class StreamPipeWriterOptions
{
    private const int DefaultMinimumBufferSize = 4096;

    public StreamPipeWriterOptions(MemoryPool<byte> pool = null, int minimumBufferSize = DefaultMinimumBufferSize)
    {
        Pool = pool;
        MinimumBufferSize = minimumBufferSize;
    }

    public int MinimumBufferSize { get; }
    public MemoryPool<byte> Pool { get; }
}

public class StreamPipeReaderOptions
{
    private const int DefaultBufferSize = 4096;
    private const int DefaultMinimumReadSize = 1024;

    public StreamPipeReaderOptions(MemoryPool<byte> memoryPool = null, int bufferSize = DefaultBufferSize, int minimumReadSize = DefaultMinimumReadSize)
    {
        Pool = pool;
        BufferSize = bufferSize;
        MinimumReadSize = minimumReadSize;
    }

    public int BufferSize { get; }
    public int MinimumReadSize { get; }
    public MemoryPool<byte> Pool { get; }
}
public static class StreamPipeExtensions
{
    public static Task CopyToAsync(this Stream stream, PipeWriter pipeWriter, CancellationToken cancellationToken = default)
    {
         return pipeWriter.CopyFromAsync(stream, cancellationToken);
    }
}

NOTES:

  • Some streams buffer internally and we may end up copying from the Stream's internal buffer into the pipe's buffers.
  • Even if we can avoid that, we'll end up allocating a Task per read and write operation (though some streams cache the result of the previous operation)
  • We end up paying per read/write costs in general (for e.g. in FileStream allocating via ThreadPoolBoundHandle.AllocateNativeOverlapped per read/write pair).

Most of the per read/write costs can be mitigated by using CopyToAsync (if overridden by the Stream) but there are some downsides there as well.

  • Using the default implementations CopyToAsync allocates an internal buffer if the Stream doesn't have one already and passes that buffer to the other stream. Using the default pipe implementation, we end up copying the Stream's buffer into the pipe's buffer which might be fine but is a bit unfortunate.

We can avoid some of these overheads if we implement a PipeReader on top of CopyToAsync that doesn't use the Pipe internally. The idea here is that we call CopyToAsync on a fake stream that forwards WriteAsync calls to the PipeReader consumer. This implementation would pass buffers directly from the Stream to the consumer. If the consumer doesn't process the entire buffer, only the unconsumed buffer is copied into an internal buffer for the next read.

The write side isn't as problematic because we need to be able to allocate memory to write into the Stream so reusing the pipe isn't so bad here. The implementation here would likely be using a Pipe internally, then writing to the Stream on FlushAsync.

Other implementations

@muratg
Copy link

muratg commented Mar 7, 2018

@joshfree Unassigning @pakrym for now. Once this is triaged for a milestone, we can decide on an owner.

@mgravell
Copy link
Member

FYI I have working tested versions of these in Pipelines.Sockets.Unofficial, because I need them for StackExchange.Redis (in the absence of a TLS pipe API, we're using SslStream here); everything StreamConnector.* here: https://github.com/mgravell/Pipelines.Sockets.Unofficial/tree/master/src/Pipelines.Sockets.Unofficial

@AArnott
Copy link
Contributor

AArnott commented Aug 16, 2018

I wrote up extension methods to do this: dotnet/Nerdbank.Streams#11

@davidfowl
Copy link
Member Author

We need an API design for this. We'll do this in 3.0.

@davidfowl davidfowl changed the title Add PipeReader and PipeWriter implementations over a Stream API Proposal: PipeReader and PipeWriter implementations over a Stream Oct 18, 2018
@AArnott
Copy link
Contributor

AArnott commented Oct 19, 2018

     public static (Pipe left, Pipe right) CreateDuplexPipe(PipeOptions oneOptions, PipeOptions twoOptions);

It looks like you're divided between calling the pipes left and right vs. one and two. Perhaps pick a naming scheme that can be applied to both parameters and named tuple items.

@davidfowl
Copy link
Member Author

It looks like you're divided between calling the pipes left and right vs. one and two. Perhaps pick a naming scheme that can be applied to both parameters and named tuple items.

I expect we'll spend the most time on this in the API review meeting. It was one of the more contentious APIs:

https://github.com/aspnet/KestrelHttpServer/blob/release/2.2/src/Kestrel.Core/Internal/DuplexPipe.cs

@davidfowl davidfowl self-assigned this Oct 19, 2018
@Joe4evr
Copy link
Contributor

Joe4evr commented Nov 1, 2018

public static (Pipe left, Pipe right) CreateDuplexPipe

💡 According to the latest API Design Review video, public APIs that return Tuples should use PropertyCase for naming their elements.

Also, I feel like naming them "left" and "right" is rather unintuitive. Since it makes a duplex pipe, why not return one already initialized as the "Reader" and the other as the "Writer"?

@benaadams
Copy link
Member

Or input / output

@jkotalik jkotalik self-assigned this Nov 13, 2018
@terrajobst
Copy link
Member

terrajobst commented Feb 7, 2019

Video

Notes are here: dotnet/apireviews#90

@davidfowl
Copy link
Member Author

davidfowl commented Feb 9, 2019

@terrajobst After implementing this I think we should have an overload of CopyFromAsync should take a bufferSize argument and also add an overload on StreamPipeExtensions to flow the same bufferSize argument. We'd default to 0 which means whatever the PipeWriter.GetMemory() default is.

cc @pakrym

@jkotalik
Copy link
Contributor

@stephentoub @terrajobst what are your thoughts on making the stream wrappers public? That way, we can derive them and disable Synchronous IO for our implementations in ASP.NET Core.

@stephentoub
Copy link
Member

what are your thoughts on making the stream wrappers public? That way, we can derive them and disable Synchronous IO for our implementations in ASP.NET Core.

We discussed this in the API review, and @davidfowl seemed comfortable with not exposing them. I think the general sentiment was that it'd be better to start with just these virtuals, and if exposing the concrete types seemed useful in the future, we could choose to do so subsequently.

That said, I'm ok with it if it's valuable. But I suggest we treat that as a separate API issue that's proposed/reviewed like any other, even if it's prioritized.

@davidfowl
Copy link
Member Author

We'll do it once we finish this initial pass, there are a few API tweaks I want to add/make after finishing this implementation.

@stephentoub
Copy link
Member

@davidfowl, is this done such that this issue can be closed, or is it tracking additional work beyond dotnet/corefx#35399?

@davidfowl
Copy link
Member Author

davidfowl commented Mar 17, 2019

There’s one part of the work that I still have to do. That is write the stream -> pipe adapter

@msftgits msftgits transferred this issue from dotnet/corefx Jan 31, 2020
@msftgits msftgits added this to the 3.0 milestone Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 18, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.IO.Pipelines
Projects
None yet
Development

No branches or pull requests