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

Calling Pipe.Writer.WriteAsync after Pipe.Writer.Advance flush truncated data to the PipeReader. #62167

Closed
Kuinox opened this issue Nov 30, 2021 · 5 comments · Fixed by #62306

Comments

@Kuinox
Copy link
Contributor

Kuinox commented Nov 30, 2021

Description

(I edited this issue when I noticed the bug did not affected only ReadAtLeastAsync, original title was 'Pipe.ReadAtLeastAsync return with less bytes than asked.')

Calling Pipe.Writer.WriteAsync after Pipe.Writer.Advance flush truncated data to the PipeReader.
I initially found this bug because ReadAtLeastAsync returned me a 12 bytes buffer, when I asked for 26.
image

Reproduction Steps

Edit:

[Test]
public async Task ReadAtLeastAsync_works()
{
    Pipe pipe = new Pipe(new PipeOptions(minimumSegmentSize: 1));
    Task<ReadResult> resTask = pipe.Reader.ReadAtLeastAsync( 26 ).AsTask();

    pipe.Writer.GetSpan( 14 );
    pipe.Writer.Advance( 14 );
    await pipe.Writer.WriteAsync( new byte[12] );
    ReadResult res = await resTask;
    res.Buffer.Length.Should().Be( 26 ); //fails
}

Expected behavior

Data written is not overwritten.

Actual behavior

Only 12 bytes are returned. The first write is partially overwritten.

Regression?

The reproduction does not show the bug on version 4.5.4.

Known Workarounds

Call FlushAsync before calling WriteAsync if you did Advance() before.

Configuration

N/A.

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Nov 30, 2021
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@halter73
Copy link
Member

halter73 commented Dec 1, 2021

It should not be possible for PipeReader.ReadAtLeastAsyncCore(...) to return at all unless result.Buffer.Length >= minimumSize || result.IsCompleted || result.IsCanceled.

if (buffer.Length >= minimumSize || result.IsCompleted || result.IsCanceled)
{
return result;
}

Is it possible that you're using a PipeReader with a custom ReadAtLeastAsyncCore(...) implementation? Or could buffer.Length have been a smaller value than 26 when ReadAtLeastAsync(buffer.Length, cts.Token) was initially called?

@halter73 halter73 added the needs-author-action An issue or pull request that requires more info or actions from the author. label Dec 1, 2021
@Kuinox
Copy link
Contributor Author

Kuinox commented Dec 2, 2021

Is it possible that you're using a PipeReader with a custom ReadAtLeastAsyncCore(...) implementation?

I'm using the duplex "Pipe" implementation.
I just managed to make a minimal reproduction:

[Test]
public async Task ReadAtLeastAsync_works()
{
    Pipe pipe = new Pipe(new PipeOptions(minimumSegmentSize: 1));
    Task<ReadResult> resTask = pipe.Reader.ReadAtLeastAsync( 26 ).AsTask();

    pipe.Writer.GetSpan( 14 );
    pipe.Writer.Advance( 14 );
    await pipe.Writer.WriteAsync( new byte[12] );
    ReadResult res = await resTask;
    res.Buffer.Length.Should().Be( 26 ); //fails
}

Can I also do the PR to fixes this bug, if possible ?

@Kuinox
Copy link
Contributor Author

Kuinox commented Dec 2, 2021

Looks like the bug is worst than I thought;
I tried to understand what happened to the missing data.

 [Test]
  public async Task ReadAtLeastAsync_works()
  {
      Memory<byte> buffer = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 };
      Pipe pipe = new( new PipeOptions( minimumSegmentSize: 1 ) );
      Task<ReadResult> resTask = pipe.Reader.ReadAtLeastAsync( 26 ).AsTask();

      var mem = pipe.Writer.GetMemory( 14 )[..14];
      buffer.CopyTo( mem );
      pipe.Writer.Advance( 14 );
      await pipe.Writer.WriteAsync( new byte[12] { 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 } );
      ReadResult res = await resTask;
      Console.WriteLine( string.Join( ", ", res.Buffer.ToArray() ) );
      res.Buffer.Length.Should().Be( 26 );
  }

Output: 0, 1, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25

It looks like it's a regression from 4.5.4(pass test) from to 5.0.0(fail test).

I'm feeling that I should not WriteAsync after Advance without calling FlushAsync, but nothing indicate in the docs that you shouldn't do that.

This bug (if this is indeed a bug and not missing docs) is not exclusive to ReadAtLeastAsync and and also drop bytes if you try to use ReadAsync.

Kuinox added a commit to Kuinox/runtime that referenced this issue Dec 2, 2021
@Kuinox Kuinox changed the title Pipe.ReadAtLeastAsync return with less bytes than asked. Calling Pipe.Writer.WriteAsync after Pipe.Writer.Advance flush truncated data to the PipeReader. Dec 3, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Dec 3, 2021
@Kuinox
Copy link
Contributor Author

Kuinox commented Dec 3, 2021

I went ahead and made a PR because the fix is one line.

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Dec 3, 2021
BrennanConroy pushed a commit that referenced this issue Dec 3, 2021
@BrennanConroy BrennanConroy removed the untriaged New issue has not been triaged by the area owner label Dec 3, 2021
@BrennanConroy BrennanConroy added this to the 7.0.0 milestone Dec 3, 2021
github-actions bot pushed a commit that referenced this issue Dec 3, 2021
github-actions bot pushed a commit that referenced this issue Dec 3, 2021
@halter73 halter73 removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Dec 14, 2021
safern pushed a commit that referenced this issue Dec 15, 2021
safern added a commit that referenced this issue Dec 16, 2021
…ter .Advance(int) (#62348)

* Fixes #62167. WriteAsync was bugged when writing multiple segments if called after an .Advance(int)

* Add packaging changes to IO Pipelines

Co-authored-by: Kuinox <[email protected]>
Co-authored-by: Santiago Fernandez Madero <[email protected]>
safern added a commit that referenced this issue Jan 7, 2022
…ter .Advance(int) (#62350)

* Fixes #62167. WriteAsync was bugged when writing multiple segments if called after an .Advance(int)

* Add packaging changes

Co-authored-by: Kuinox <[email protected]>
Co-authored-by: Santiago Fernandez Madero <[email protected]>
@ghost ghost locked as resolved and limited conversation to collaborators Jan 14, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.