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

FileStream and PipeStream code reuse for async File IO on Windows #81960

Closed
wants to merge 8 commits into from

Conversation

adamsitnik
Copy link
Member

No description provided.

@ghost
Copy link

ghost commented Feb 10, 2023

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

null

Author: adamsitnik
Assignees: -
Labels:

area-System.IO

Milestone: -

// 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();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle);

_threadPoolBinding = threadPoolBinding;
_canSeek = canSeek;
_source.RunContinuationsAsynchronously = true;
_preAllocatedOverlapped = PreAllocatedOverlapped.UnsafeCreate(s_ioCallback, this, null);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so far SafeFileHandle was using the UnsafeCreate (not touching the execution context):

_preallocatedOverlapped = PreAllocatedOverlapped.UnsafeCreate(s_ioCallback, this, null);

while PipeStream used the "safe" ctor:

_preallocatedOverlapped = new PreAllocatedOverlapped(s_ioCallback, this, null);

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)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

special handling these failure for only the read operations was so far done only by the pipes:

if (!_isWrite)
{
bool messageCompletion = true;
switch (errorCode)
{
case Interop.Errors.ERROR_BROKEN_PIPE:
case Interop.Errors.ERROR_PIPE_NOT_CONNECTED:
case Interop.Errors.ERROR_NO_DATA:
errorCode = 0;

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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some errors, PipeStream modifies it's state before throwing an exception or returning result. This state is publicly exposed (IsMessageComplete for example).

private protected override void CompleteCore(uint errorCode, uint numBytes)
{
if (!_isWrite)
{
bool messageCompletion = true;
switch (errorCode)
{
case Interop.Errors.ERROR_BROKEN_PIPE:
case Interop.Errors.ERROR_PIPE_NOT_CONNECTED:
case Interop.Errors.ERROR_NO_DATA:
errorCode = 0;
break;
case Interop.Errors.ERROR_MORE_DATA:
errorCode = 0;
messageCompletion = false;
break;
}
_pipeStream.UpdateMessageCompletion(messageCompletion);

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;
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; }

public abstract partial class PipeStream : Stream
{
internal const bool CheckOperationsRequiresSetHandle = true;
internal ThreadPoolBoundHandle? _threadPoolBinding;

and add ctors that accept a handle and ThreadPoolBoundHandle

@@ -278,7 +278,7 @@ public async Task CancelTokenOn_ServerWaitForConnectionAsyncWithOuterCancellatio
Task waitForConnectionTask = server.WaitForConnectionAsync(cts.Token);

Assert.True(InteropTest.CancelIoEx(server.SafePipeHandle), "Outer cancellation failed");
await Assert.ThrowsAsync<IOException>(() => waitForConnectionTask);
await Assert.ThrowsAsync<OperationCanceledException>(() => waitForConnectionTask);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in theory this is a breaking change. I wanted to align WaitForConnectionAsync with ReadAsync and WriteAsync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

@adamsitnik adamsitnik Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -502,8 +502,6 @@ public async Task ReadAsync_DisconnectDuringRead_Returns0()
}

[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")]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@adamsitnik
Copy link
Member Author

/azp list

@azure-pipelines

This comment was marked as resolved.

@adamsitnik
Copy link
Member Author

/azp run runtime-libraries-coreclr outerloop-windows

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jeffhandley
Copy link
Member

@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.

@ghost ghost locked as resolved and limited conversation to collaborators Apr 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants