这是indexloc提供的服务,不要输入任何密码
Skip to content

The cancellationToken parameter of an IAsyncEnumerable returning method is not cancelled #9614

@Tragetaschen

Description

@Tragetaschen
public interface IMyGrain : IGrainWithGuidKey
{
    IAsyncEnumerable<int> Generate(CancellationToken cancellationToken);
}

public class MyGrain : Grain, IMyGrain
{
    public IAsyncEnumerable<int> Generate(CancellationToken cancellationToken)
    {
        var cw = Channel.CreateUnbounded<int>();
        _ = G(cw.Writer, cancellationToken);
        return cw.Reader.ReadAllAsync(cancellationToken);
    }

    private async Task G(ChannelWriter<int> writer, CancellationToken cancellationToken)
    {
        writer.TryWrite(1);
        await Task.Delay(10_000, cancellationToken);
        writer.TryWrite(2);
    }
}

// on the client

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
    await foreach (var item in myGrain.Generate(cts.Token))
    {
        Console.WriteLine(item);
    }
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation was cancelled.");
}

The grain doesn't implement the iterator, but instead returns a different IAsyncEnumerable from the ChannelReader. The TryWrite(2) is always executed, the cancellationToken parameter never triggers cancellation.

If the Generate method would use an iterator, then it would see cancellation, but not because the cts from the client is cancelled, but because the AsyncEnumerableGrainExtension canceles the EnumeratorState's CTS as part of the DisposeAsync call to the enumerator when leaving the await foreach. In the example reproduction, this is also happening, but to the enumerator returned from the channel's implementation and that's not the CancellationToken from the parameter.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions