-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Add DuplexStream and implement for NetworkStream and QuicStream #51434
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @carlossanlop Issue DetailsResolves #43290. Adds DuplexStream for NetworkStream and QuicStream, and new conformance tests. Adding issues #51431, #51432, and #51433 for filling out other @karelz please adjust labels if OCD kicks in :D
|
PipeStream is also duplex, yes? What do we want to do for such duplex streams that lack the notion of closing one direction only... just have CloseWrites Dispose? (We can actually implement named pipes on Unix to close writes if that's desired, as it wraps socket, but that would lead to another visible behavioral difference between platforms. ) In addition to SslStream, we also need to fix NegiotiateStream presumably. And some of the streams in HttpClient, even for 1.1, probably need to be addressed, e.g. the "raw" stream used for switching protocols. It's fine to have issues for these rather than doing them all in this first PR, but I'd like to see those issues addressed soon after. |
} | ||
catch (Exception ex) when (ex is not OutOfMemoryException) | ||
{ | ||
throw new IOException("Unable to complete writes: " + ex.Message, ex); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll do a pass to move all resources into a resx subsequently?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, there's a QUIC issue for it.
throw CreateNotSupportedException(); | ||
|
||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => | ||
TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this just throw like the other read operations?
{ | ||
if (_disposed != 0) ThrowException(); | ||
static void ThrowException() => throw new ObjectDisposedException(nameof(WriteOnlyProxyStream)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use the ODE ThrowHelper instead?
} | ||
|
||
private Exception CreateNotSupportedException() => | ||
new NotSupportedException(SR.DuplexStream_InvalidReadOnWriteOnlyStream); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use the NSE ThrowHelper instead?
What about places in dotnet/runtime where we should be checking for and using DuplexStream? Can you also file appropriate issues in dotnet/aspnetcore? Presumably there are multiple streams there that should be updated. |
If we want to use We might have it dispose. At least this will avoid having If your code requires shutdown to work, then the experience will be that your (maybe very long) operation will only fail near the end, potentially far from where you passed in a If your code doesn't require shutdown, but has points where it could reasonably use it, then the dispose will break things. Consider this example: void SendSingleHttpRequest(...)
{
using DuplexStream stream = ...;
WriteConnectionCloseRequest();
// it's the last request on this stream; HTTP doesn't require this to
// work but it's not incorrect or even unreasonable for someone to do it.
stream.CompleteWrites();
// you still want to read though, so Dispose doesn't work.
ReadRequest();
} An alternative that fixes that is to have it forward to |
Yeah, I'll do a quick search to find these. |
It occurs to me as well that introducing this new base type that someone has to opt into using gives us a rare opportunity to rethink the surface area in general. For example, we can change the delegation strategy between overloads, overriding Read/WriteAsync that work with Memory to be abstract and overriding the array based overloads to call them, similarly inverting the relationship between the array and span based sync methods, overriding the APM methods to use the Async overloads with TaskToApm rather than queuing sync calls, etc. It doesn't need to be in this PR, but we should take a hard look at that soon or we'll lose our chance. cc: @geoffkizer, @bartonjs |
Yep, totally agree. Let's take advantage of this unique opportunity. |
How will that play out for someone overriding |
NetworkStream overrides everything. The difference shouldn't be perceptible to a type derived from it. |
@@ -101,7 +101,9 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati | |||
|
|||
public ValueTask ShutdownWriteCompleted(CancellationToken cancellationToken = default) => _provider.ShutdownWriteCompleted(cancellationToken); | |||
|
|||
public void Shutdown() => _provider.Shutdown(); | |||
public override void CompleteWrites() => _provider.CompleteWrites(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we replacing Shutdown
? I thought this is a replacement for ShutdownWriteCompleted
? Or did I completely miss the whole point of this change somehow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ShutdownWriteCompleted will become something like Task Completed { get; }
to denote when both sides have finished using the stream (i.e. full graceful shutdown), which is what its current behavior is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that original ShutdownWriteCompleted
means "I'm finished writing you can start sending response", which I thought is the meaning of the new CompleteWrites
?
I also thought that original Shutdown
is the final step of the communication, i.e.: after the response has been sent.
I don't know anything about Task Completed { get; }
, I don't think we got that far with the QUIC API reviews, but if I understand the meaning that should correspond to Shutdown
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shutdown
is "shutdown writes, send EOF to peer".
ShutdownWriteCompleted
was "wait for peer to acknowledge that shutdown" which we found to be mostly useless so I believe we changed it to mean "wait for peer to acknowledge that shutdown AND to shutdown their side too". It has a terrible name. This is what I'd like to see changed to a Task Completed { get; }
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ShutdownWriteCompleted
actually remained for now, I've added new ShutdownCompleted
alongside... But I agree ShutdownWriteCompleted
doesn't have any use for us anymore, so it can just be removed anytime
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was completely bamboozled by the naming and made assumption based on that.
I think that's what I meant by "override all the things", but I'll admit we didn't talk about that line in the meeting 😄.
That's further than I had thought, but I like it. I don't see a lot of pushback on it, but it's probably worth throwing back through the big room once you know where all you want to write |
@@ -2938,6 +2938,72 @@ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken | |||
} | |||
} | |||
|
|||
/// <summary>Base class for a connected stream that can have writes completed.</summary> | |||
public abstract class DuplexConnectedStreamConformanceTests : ConnectedStreamConformanceTests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about a test for calling Write after CompleteWrites?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, a test that CompleteWrites implicitly does a Flush? (i.e. call Write without Flush, then CompleteWrites and verify the unflushed data was sent)
Are we closing this for now, per discussion on #51566? |
Resolves #43290.
Adds
DuplexStream
forNetworkStream
andQuicStream
, and new conformance tests.Adding issues #51431, #51432, and #51433 for filling out other
Stream
implementations.@karelz please adjust labels if OCD kicks in :D