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

Implement Tar async APIs #70574

Merged
merged 75 commits into from
Jul 2, 2022
Merged

Implement Tar async APIs #70574

merged 75 commits into from
Jul 2, 2022

Conversation

carlossanlop
Copy link
Member

@carlossanlop carlossanlop commented Jun 10, 2022

Addresses: #65951

Submitting it as a draft for these reasons:

  • This PR depends on another that needs to be merged first: Add TarEntry conversion constructors #70325
  • I want to first show each individual async method implemented identically to the synchronous one, with the asynchronous differences only.
  • Add unit tests.
  • Public documentation.
  • Remove duplicate code between sync and async methods.
  • Address feedback.

@carlossanlop carlossanlop added this to the 7.0.0 milestone Jun 10, 2022
@carlossanlop carlossanlop self-assigned this Jun 10, 2022
@dotnet-issue-labeler
Copy link

Note regarding the new-api-needs-documentation label:

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.

@ghost
Copy link

ghost commented Jun 10, 2022

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

Issue Details

Addresses: #65951

Submitting it as a draft for these reasons:

  • This PR depends on another that needs to be merged first: Add TarEntry conversion constructors #70325
  • I show each individual async method implemented identically to the synchronous one, with the asynchronous differences only.
  • I collect some feedback in case any of those calls needs improvement. The first commit with async code is 2e5304330aede14c8bd7ef2f852f90a8a59aade9.
  • I remove duplicate code between sync and async methods.
  • Add unit tests.
Author: carlossanlop
Assignees: carlossanlop
Labels:

area-System.IO

Milestone: 7.0.0

if (EntryType is TarEntryType.SymbolicLink or TarEntryType.HardLink)
{
throw new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType));
}
Copy link
Member

Choose a reason for hiding this comment

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

Typically any exceptions other than argument exceptions are wrapped in the returned task, e.g.

return Task.FromException(new InvalidOperationException(string.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType));

Copy link
Member Author

@carlossanlop carlossanlop Jun 17, 2022

Choose a reason for hiding this comment

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

Will do.

Why do we not wrap argument exceptions? And what about ArgumentNullException?

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 feels like a good candidate for a Roslyn analyzer, if we don't have one yet.

Copy link
Member

Choose a reason for hiding this comment

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

Why do we not wrap argument exceptions?

Because in typical usage they represent a 100% deterministic developer error in how the API is being used. We don't want such errors being eaten by the returned task, especially if doing so could lead to a deadlock, e.g. if this task won't end up performing some work that would ultimately unblock something else.

And what about ArgumentNullException?

That is an argument exception. So same deal.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the explanation.

I opened this API suggestion in case it makes sense to add such analyzer: #70905

Copy link
Member Author

Choose a reason for hiding this comment

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

I know there isn't one, but my question was around handling the exception.

Copy link
Member

@stephentoub stephentoub Jun 24, 2022

Choose a reason for hiding this comment

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

There's no reason to create an asynchronous version of ExtractAsHardLink or CheckIo, so I'm not clear on your question. Can you clarify?

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 the async method that calls ExtractAsHardLink should wrap that in a try catch, and the catch should return Task.FromException?

internal Task MethodAsync(...)
{
try
{
    ExtractAsHardLink(...);
    return Task.CompletedTask;
}
catch (Exception e)
{
    return Task.FromException(e);
}
}

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 other words, I should only create a new async method if it will call another async method inside it.

But in the case of methods that can exceptions, the last async method in the callstack is the one which should handle the exception by catching it and return it inside a Task.

Is this correct?

Copy link
Member

Choose a reason for hiding this comment

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

What really matters is the public function black-box experience.

In the call of

Task t = someObjectOrType.FooAsync(arg0, arg1, arg2);
t.Wait();
Console.WriteLine(t.Exception);

There are two possibilities for the exception:

  1. the call to FooAsync was invalid, in which case the FooAsync method itself should throw, and we never make it to line 2. (ArgumentException)
  2. We created a Task. The task ran... until it failed. The exception is now in the t.Exception property.

For the latter, this is accomplished in one of three ways.

I) After argument validation, FooAsync failed synchronously, and the implementation returns Task.FromException(e).
II) You're building tasks by chaining them together with Task.ContinueWith(action), inside the action you throw an exception (as per normal). The Task system records the exception and changes the task state to Faulted.

try
{
ExecutionContext? ec = CapturedContext;
if (ec == null)
{
// No context, just run the task directly.
InnerInvoke();
}
else
{
// Invoke it under the captured ExecutionContext
if (threadPoolThread is null)
{
ExecutionContext.RunInternal(ec, s_ecCallback, this);
}
else
{
ExecutionContext.RunFromThreadPoolDispatchLoop(threadPoolThread, ec, s_ecCallback, this);
}
}
}
catch (Exception exn)
{
// Record this exception in the task's exception list
HandleException(exn);
}

III) You're writing a method with async/await. That's just a fancy way of doing II.

What that boils down to is:

  • In the public method you might need to be aware of being "inside" or "outside" the task with an exception, here's where Task.FromException comes in.
  • Once you enter an async/await method you can stop thinking, just write throw as normal.
  • If you're making Tasks by hand, and you're inside a callback, throw as normal.
  • If you're making Tasks by hand and you're not inside a callback, Task.FromException may again matter.
    • But why are you making Tasks by hand after the public entrypoint?

internal async ValueTask<bool> TryGetNextHeaderAsync(Stream archiveStream, bool copyData, CancellationToken cancellationToken)
{
// The four supported formats have a header that fits in the default record size
IMemoryOwner<byte> rented = MemoryPool<byte>.Shared.Rent(minBufferSize: TarHelpers.RecordSize);
Copy link
Member

Choose a reason for hiding this comment

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

This should use ArrayPool directly rather than MemoryPool.

Copy link
Member Author

Choose a reason for hiding this comment

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

But a couple of lines below I need to pass the Memory<byte> to archiveStream.ReadExactlyAsync.

Copy link
Member

Choose a reason for hiding this comment

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

Sure... but arrays are implicitly Memory<byte> (or if you want to slice it, then explicitly use AsMemory)

byte[] rented = ArrayPool<byte>.Shared.Rent(TarHelpers.RecordSize);
Memory<byte> buffer = rented.AsMemory(0, TarHelpers.RecordSize);

...

ArrayPool<byte>.Shared.Return(rented);

Copy link
Member Author

Choose a reason for hiding this comment

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

TIL. Thanks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure... but arrays are implicitly Memory<byte> (or if you want to slice it, then explicitly use AsMemory)

Thanks for this explanation in advance. I am starting to see some test failures when attempting to do async writes using the existing code that takes spans, like this:

private int WriteName(Span<byte> buffer, out byte[] fullNameBytes)
{
fullNameBytes = Encoding.ASCII.GetBytes(_name);
int nameBytesLength = Math.Min(fullNameBytes.Length, FieldLengths.Name);
int checksum = WriteLeftAlignedBytesAndGetChecksum(fullNameBytes.AsSpan(0, nameBytesLength), buffer.Slice(FieldLocations.Name, FieldLengths.Name));
return checksum;

/// <exception cref="ObjectDisposedException">The archive stream is disposed.</exception>
/// <exception cref="ArgumentException"><paramref name="fileName"/> or <paramref name="entryName"/> is <see langword="null"/> or empty.</exception>
/// <exception cref="IOException">An I/O problem occurred.</exception>
public async Task WriteEntryAsync(string fileName, string? entryName, CancellationToken cancellationToken = default)
Copy link
Member Author

Choose a reason for hiding this comment

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

@stephentoub this public API returns a Task. My immediate assumption was that all the internal async APIs this method calls should also return a Task. Is there anything preventing me from changing all the internal methods to return a ValueTask instead?

Copy link
Member

Choose a reason for hiding this comment

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

Is there anything preventing me from changing all the internal methods to return a ValueTask instead?

No, but just making that change will typically only yield benefits in the case of Task<T> to ValueTask<T>.

{
ThrowIfDisposed();

ArgumentException.ThrowIfNullOrEmpty(fileName);
Copy link
Member

Choose a reason for hiding this comment

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

The ArgumentException, and possibly the ObjectDisposedException should generally be "outside" the Task. So this method shouldn't be public async Task, just public Task, do argument validation, then it can call an async Task private or local member.

}
finally
{
rented.Dispose();
Copy link
Member

Choose a reason for hiding this comment

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

After switching ot ArrayPool, move this to the try, before the return, but not the finally.

Async methods shouldn't return to the pools via a finally

Copy link
Member

Choose a reason for hiding this comment

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

(Which, I guess, means you can get rid of the whole try/finally)

Copy link
Member

@stephentoub stephentoub Jun 17, 2022

Choose a reason for hiding this comment

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

Async methods shouldn't return to the pools via a finally

Why?

(In general I think it's ok to not return to the array pool in the case of an exception, on the assumption that exceptions are rare, but that goes for sync and async.)

Copy link
Member

Choose a reason for hiding this comment

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

I'm mainly parroting Levi here for "don't return to a pool from a finally in async code", but the reasoning had something to do with the possibility of a native async operation still doing writes when the managed wrapper task has thrown a timeout exception, so returning to the pool in that case could result in the async op writing to the buffer after it has been rented elsewhere.

I remember reading a guidance document at some point, but I can't find it offhand.

From synchronous code, the expectation is more that there shouldn't be a possibility of a second reference, so returning from a finally is OK (though my recollection was that the guidance suggested not to, for a uniformity of style).

Copy link
Member

Choose a reason for hiding this comment

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

but the reasoning had something to do with the possibility of a native async operation still doing writes when the managed wrapper task has thrown a timeout exception, so returning to the pool in that case could result in the async op writing to the buffer after it has been rented elsewhere.

That would be a bug in such an async method. The task returned from an async method should not complete until all work associated with the operation has quiesced, all resources are no longer used, etc.

From synchronous code, the expectation is more that there shouldn't be a possibility of a second reference, so returning from a finally is OK (though my recollection was that the guidance suggested not to, for a uniformity of style).

The exact same issue could exist with synchronous code, if the implementation similarly erroneously returned/threw while resources were still in use.

@carlossanlop
Copy link
Member Author

/azp run runtime-extra-platforms

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

I've reviewed 12 out of 64 files, will get back to this PR later today after I have some more coffee.

Comment on lines +158 to +159
cancellationToken.ThrowIfCancellationRequested();

Copy link
Member

Choose a reason for hiding this comment

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

it seems like there is no need to check for the cancellation again here, as ReadAsync (the caller) always does that

Suggested change
cancellationToken.ThrowIfCancellationRequested();

Copy link
Member Author

Choose a reason for hiding this comment

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

I was under the impression that the token cancellation status should be checked on every method.

Copy link
Member

Choose a reason for hiding this comment

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

I was under the impression that the token cancellation status should be checked on every method

As long as some work was executed between them (so it could have actually changed in the meantime). At least this is how I see it.

// Asynchronously attempts read all the fields of the next header.
// Throws if end of stream is reached or if any data type conversion fails.
// Returns true if all the attributes were read successfully, false otherwise.
internal static async ValueTask<(bool, TarHeader)> TryGetNextHeaderAsync(Stream archiveStream, bool copyData, TarEntryFormat initialFormat, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

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

I currently don't have a good suggestion for how this method could be refactored to reduce code duplication. I'll try to play with the code once your PR gets merged.

Copy link
Member Author

Choose a reason for hiding this comment

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

I know. TarHeader is the problem, since it is a struct. I may have to refactor the code later to get rid of TarHeader completely, move everything to TarEntry.

checksum += WriteCommonFields(buffer, actualLength, actualEntryType);
WriteChecksum(checksum, buffer);
_checksum = WriteChecksum(checksum, buffer);
Copy link
Member

Choose a reason for hiding this comment

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

why the sync method writes the checksum to a field and the async method does not?

Copy link
Member Author

Choose a reason for hiding this comment

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

TarHeader is a struct. It owns the _checksum field. In the sync method, I have no problem updating the value of _checksum inside the method: the target field gets updated. But in the async method, the TarHeader is copied into the method, so the _checksum value that gets updated is the one from the copied header, not the original one.

As discussed on chat, this is a good candidate for refactoring and simplification on a later PR.

await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false);
}

return finalChecksum;
Copy link
Member

Choose a reason for hiding this comment

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

It seems that we could reduce code duplication here by moving part of the common logic to a separate method:

private long WriteCommonFieldsAndChecksumAsV7(Span<byte> buffer)
{
    long actualLength = GetTotalDataBytesToWrite();
    TarEntryType actualEntryType = GetCorrectTypeFlagForFormat(TarEntryFormat.V7);

    int checksum = WriteName(buffer, out _);
    int checksum = WriteNameSpan(buffer, out _);
    checksum += WriteCommonFields(buffer, actualLength, actualEntryType);
    WriteChecksum(checksum, buffer);
    _checksum = WriteChecksum(checksum, buffer);
    
    return actualLength;
}

internal void WriteAsV7(Stream archiveStream, Span<byte> buffer)
{
    long actualLength = WriteCommonFieldsAndChecksumAsV7(buffer);

    archiveStream.Write(buffer);

    if (_dataStream != null)
    {
        WriteData(archiveStream, _dataStream, actualLength);
    }
}

internal async Task<int> WriteAsV7Async(Stream archiveStream, Memory<byte> buffer, CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();

    long actualLength = WriteCommonFieldsAndChecksumAsV7(buffer.Span);

    await archiveStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);

    if (_dataStream != null)
    {
        await WriteDataAsync(archiveStream, _dataStream, actualLength, cancellationToken).ConfigureAwait(false);
    }

    return _checksum;
}

checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType);
checksum += WritePosixMagicAndVersion(buffer.Span);
checksum += WritePosixAndGnuSharedFields(buffer.Span);
int finalChecksum = WriteChecksum(checksum, buffer.Span);
Copy link
Member

Choose a reason for hiding this comment

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

similar to the method above, can we reduce the code duplication between sync and async versions?

longMetadataHeader._mTime = DateTimeOffset.MinValue; // 0
longMetadataHeader._typeFlag = entryType;

longMetadataHeader._dataStream = new MemoryStream();
Copy link
Member

Choose a reason for hiding this comment

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

code duplication

checksum += WriteGnuMagicAndVersion(buffer.Span);
checksum += WritePosixAndGnuSharedFields(buffer.Span);
checksum += WriteGnuFields(buffer.Span);
int finalChecksum = WriteChecksum(checksum, buffer.Span);
Copy link
Member

Choose a reason for hiding this comment

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

code duplication?

_magic = string.Empty;
_version = string.Empty;
_gName = string.Empty;
_uName = string.Empty;
Copy link
Member

Choose a reason for hiding this comment

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

code duplication

checksum += WriteCommonFields(buffer.Span, actualLength, actualEntryType);
checksum += WritePosixMagicAndVersion(buffer.Span);
checksum += WritePosixAndGnuSharedFields(buffer.Span);
int finalChecksum = WriteChecksum(checksum, buffer.Span);
Copy link
Member

Choose a reason for hiding this comment

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

code duplication?

@carlossanlop
Copy link
Member Author

carlossanlop commented Jun 29, 2022

I'm seeing a weird behavior in some async tests in Linux, and I was able to reproduce it locally under certain conditions.

Take for example the test System.Formats.Tar.Tests.TarFile_ExtractToDirectoryAsync_File_Tests.Extract_Archive_File_OverwriteFalse_Async:

[Fact]
public Task Extract_Archive_File_OverwriteFalse_Async()
{
    string sourceArchiveFileName = GetTarFilePath(CompressionMethod.Uncompressed, TestTarFormat.pax, "file");
    using TempDirectory destination = new TempDirectory();
    string filePath = Path.Join(destination.Path, "file.txt");
    File.Create(filePath).Dispose();
    return Assert.ThrowsAsync<IOException>(() => TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false));
}

I made it non-async, and it is returning the Task directly.

If I execute all the tests locally using dotnet build /t:test System.Formats.Tar.Tests.csproj, all the tests pass.

But if I execute that test individually using dotnet build /t:test System.Formats.Tar.Tests.csproj /p:XUnitMethodName=System.Formats.Tar.Tests.TarFile_ExtractToDirectoryAsync_File_Tests.Extract_Archive_File_OverwriteFalse_Async, then it fails with the following error:

Starting:    System.Formats.Tar.Tests (parallel test collections = on, max threads = 12)
      System.Formats.Tar.Tests.TarFile_ExtractToDirectoryAsync_File_Tests.Extract_Archive_File_OverwriteFalse_Async [FAIL]
        Assert.Throws() Failure
        Expected: typeof(System.IO.IOException)
        Actual:   (No exception was thrown)

If I change the method and make it async, and make it await the Task instead of returning it, then it passes:

[Fact]
public async Task Extract_Archive_File_OverwriteFalse_Async()
{
    ...
    await Assert.ThrowsAsync<IOException>(() => TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false));
}

Anyone knows if this is expected or is it a bug? Regardless of the answer, it seems I'll have to change the tests that return a Task so they instead await it.

@stephentoub
Copy link
Member

I made it non-async, and it is returning the Task directly.

You have a race condition. Your code:

    using TempDirectory destination = new TempDirectory();
    string filePath = Path.Join(destination.Path, "file.txt");
    File.Create(filePath).Dispose();
    return Assert.ThrowsAsync<IOException>(() => TarFile.ExtractToDirectoryAsync(sourceArchiveFileName, destination.Path, overwriteFiles: false));

is using a using on the TempDirectory. The moment you return the task from ThrowsAsync, that destination will be disposed. That may or may not be before your ExtractToDirectoryAsync call has completed.

@carlossanlop
Copy link
Member Author

/azp run runtime-extra-platforms

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

LGTM, great work Carlos!

I found some issues, but they are related to style (nits) and code duplication, not actual bugs. So please fell free to merge the PR now and we can work on the refactor later.

The amount of tests is insane! I'll be dreaming of fact and theories after reading source code of so many tests ;)

Comment on lines +274 to +275
// If 'disposing' is 'false', the method was called from the finalizer.
private async ValueTask DisposeAsync(bool disposing)
Copy link
Member

Choose a reason for hiding this comment

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

the finalizer should be calling the synchronous dispose method, not the async one. And this is how it's implemented, but the comment above (most likely due to copy paste-error) is incorrect. So disposing will be always true for this method. IMO we should remove the parameter and inline this method into the public DisposeAsync

Copy link
Member

Choose a reason for hiding this comment

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

Since the type is sealed it makes sense to do all of the DisposeAsync logic in DisposeAsync, instead of DisposeAsyncCore() (which this method should have been called for the basic async dispose pattern).

Similarly, Dispose(bool) can be merged into Dispose(), and all of the GC.SuppressFinalize(this)s can be removed.

header._version ??= string.Empty;
header._gName ??= string.Empty;
header._uName ??= string.Empty;
header._prefix ??= string.Empty;
Copy link
Member

Choose a reason for hiding this comment

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

code duplication

}

// We're currently processing an extended attributes header, so we can never have two extended entries in a row
if (actualHeader._typeFlag is TarEntryType.GlobalExtendedAttributes or
Copy link
Member

Choose a reason for hiding this comment

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

code duplication

Debug.Assert(secondHeader._linkName != null);
thirdHeader._name = header._name;
thirdHeader._linkName = secondHeader._linkName;
}
Copy link
Member

Choose a reason for hiding this comment

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

code duplication

@@ -269,6 +307,32 @@ private void Dispose(bool disposing)
}
}

// Asynchronously disposes the current instance.
// If 'disposing' is 'false', the method was called from the finalizer.
Copy link
Member

Choose a reason for hiding this comment

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

ditto

{
protected async Task Read_Archive_File_Async_Internal(TarEntryFormat format, TestTarFormat testFormat)
{
string testCaseName = "file";
Copy link
Member

Choose a reason for hiding this comment

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

nit: it could be const

Suggested change
string testCaseName = "file";
const string testCaseName = "file";

Comment on lines +19 to +20
TarReader reader = new TarReader(ms);
await using (reader)
Copy link
Member

Choose a reason for hiding this comment

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

the new C# syntax for using does not support async using? that's a pitty

Copy link
Member Author

@carlossanlop carlossanlop Jul 1, 2022

Choose a reason for hiding this comment

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

Yep, it does. I was initially working on VS Code and I was getting errors saying that syntax wasn't supported. So I submitted changes like this one.

But then I started using Visual Studio 2022 again, and tried using the regular using syntax, and it works. So I joined most of the await usings like this one in my last commit, but missed a few others. I'll push another commit merging the rest of the await usings with the declaration of the disposable objects.

Comment on lines +158 to +159
cancellationToken.ThrowIfCancellationRequested();

Copy link
Member

Choose a reason for hiding this comment

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

I was under the impression that the token cancellation status should be checked on every method

As long as some work was executed between them (so it could have actually changed in the meantime). At least this is how I see it.

@carlossanlop carlossanlop merged commit 70cf817 into dotnet:main Jul 2, 2022
@carlossanlop carlossanlop deleted the TarAsync branch July 2, 2022 03:20
@ghost ghost locked as resolved and limited conversation to collaborators Aug 1, 2022
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.

5 participants