FileStream and PipeStream code reuse for async File IO on Windows#81960
FileStream and PipeStream code reuse for async File IO on Windows#81960adamsitnik wants to merge 8 commits intodotnet:mainfrom
Conversation
…PipeStream in case of pipes
…nceledException when user obtains handle and calls CancelIoEx (this is what ReadAsync and WriteAsync do)
|
Tagging subscribers to this area: @dotnet/area-system-io Issue Detailsnull
|
5bbfd49 to
6d74c6a
Compare
6d74c6a to
e76bd6e
Compare
| // For invalid handles, detect the error and mark our handle | ||
| // as invalid to give slightly better error messages. Also | ||
| // help ensure we avoid handle recycling bugs. | ||
| _fileHandle!.SetHandleAsInvalid(); |
There was a problem hiding this comment.
in case Dispose happened after the operation was started, only PipeStream was so far disposing the handle:
RandomAccess was doing that only when the handle was disposed before the operation was started:
| _threadPoolBinding = threadPoolBinding; | ||
| _canSeek = canSeek; | ||
| _source.RunContinuationsAsynchronously = true; | ||
| _preAllocatedOverlapped = PreAllocatedOverlapped.UnsafeCreate(s_ioCallback, this, null); |
There was a problem hiding this comment.
so far SafeFileHandle was using the UnsafeCreate (not touching the execution context):
while PipeStream used the "safe" ctor:
We should unify that and I am not sure which option to choose. If the safe ctor should be the default we need to add tests for this scenario (currently all are passing)
There was a problem hiding this comment.
Do we run any user code as part of s_ioCallback? I believe the answer is "no", in which case it can be UnsafeCreate.
| { | ||
| case Interop.Errors.ERROR_SUCCESS: | ||
| case Interop.Errors.ERROR_PIPE_CONNECTED: // special case for when the client has already connected (used only by ConnectionValueTaskSource) | ||
| case Interop.Errors.ERROR_BROKEN_PIPE when _isRead: // write to broken pipe should throw, read should return 0 |
There was a problem hiding this comment.
special handling these failure for only the read operations was so far done only by the pipes:
I've missed it in #77543
| case Interop.Errors.ERROR_NO_DATA: | ||
| // The handle was closed | ||
| case Interop.Errors.ERROR_INVALID_HANDLE: | ||
| _pipeStream.State = PipeState.Broken; |
There was a problem hiding this comment.
the need of updating this state is one of the reasons why relaxing RandomAccess requirements to support non-seekable files was no the way to go to share the code between FileStream and PipeStream. We could ofc just await the operations and call last error, but it would not be performant.
There was a problem hiding this comment.
the need of updating this state is one of the reasons why relaxing RandomAccess requirements to support non-seekable files was no the way to go to share the code between FileStream and PipeStream
Can you elaborate? I'd really like to get to a place where PipeStream doesn't need its own implementation and can just use RandomAccess.
There was a problem hiding this comment.
For some errors, PipeStream modifies it's state before throwing an exception or returning result. This state is publicly exposed (IsMessageComplete for example).
In case of IsMessageComplete we would need to have sth like this:
public class PipeStream
{
public ValueTask<int> ReadAsync(Buffer, CancellationToken)
{
int result = await RandomAccess.ReadAsync(!Handle, Buffer, CancellationToken);
if (LastError() == ERROR_MORE_DATA)
{
UpdateMessageCompletion(messageCompletion)
}
return result;
}
}There was a problem hiding this comment.
Another problem is that SafePipeStream binds a handle to the thread pool and currently there are no public APIs that would allow for creating SafeFileHandle out of SafePipeHandle.
Most likely both SafePipeHandle and SafeFileHandle would need to expose ThreadPoolBoundHandle properties (Windows-only, low level, easy to mess up things):
and add ctors that accept a handle and ThreadPoolBoundHandle
|
|
||
| Assert.True(InteropTest.CancelIoEx(server.SafePipeHandle), "Outer cancellation failed"); | ||
| await Assert.ThrowsAsync<IOException>(() => waitForConnectionTask); | ||
| await Assert.ThrowsAsync<OperationCanceledException>(() => waitForConnectionTask); |
There was a problem hiding this comment.
in theory this is a breaking change. I wanted to align WaitForConnectionAsync with ReadAsync and WriteAsync.
There was a problem hiding this comment.
in theory this is a breaking change
We've gone to some lengths to maintain this behavior from .NET Framework. If we want to take a breaking change around it, I'm ok with that, but I don't have a great sense of how impactful that will be.
There was a problem hiding this comment.
I don't have a great sense of how impactful that will be
I hope that calling CancelIoEx rather than passing cancellable token to WaitForConnectionAsync is rare. I don't have strong opinion about it.
| } | ||
|
|
||
| [PlatformSpecific(TestPlatforms.Windows)] // Unix named pipes are on sockets, where small writes with an empty buffer will succeed immediately | ||
| [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] |
There was a problem hiding this comment.
this was just a copy paste error (the test is marked as [PlatformSpecific(TestPlatforms.Windows)] so we don't need to skip it on other platforms)
|
/azp list |
This comment was marked as resolved.
This comment was marked as resolved.
|
/azp run runtime-libraries-coreclr outerloop-windows |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
@adamsitnik I'm going to close this PR since it's gone stale and we're not actively driving it forward. We can resume this effort later in the release cycle if/as time permits. |
No description provided.