Skip to content

async TimeoutPolicy even in pessimistic mode cannot time out on a badly-behaved not-truly-async-but-sync-instead API #318

@joaoasrosa

Description

@joaoasrosa

Hi,
I'm using Polly framework (namely Circuit Breaker and Retry policies) in an async context. I'm trying to integrate the Timeout policy with it, but it isn't triggered.

I build a little POC, where we can see an example of the policy within a repository, being called by the WebApi.

The policy looks like:

    public class ResiliencePolicy
    {
        public ResiliencePolicy()
        {
            Policy = Init();
        }

        public Policy Policy { get; }

        protected virtual Policy Init()
        {
            var timeoutPolicy = Policy.TimeoutAsync(
                TimeSpan.FromMilliseconds(100),
                TimeoutStrategy.Pessimistic,
                OnTimeoutAsync);

            return timeoutPolicy;
        }

        protected virtual Task OnTimeoutAsync(Context context, TimeSpan timeSpan, Task task)
        {
            return Task.FromResult(0);
        }
    }

and the policy is used within the repository:

    public class DocumentRepository : IDocumentRepository
    {
        private readonly ResiliencePolicy _resiliencePolicy;
        private readonly IAmazonS3 _s3Client;

        public DocumentRepository(IAmazonS3 s3Client, ResiliencePolicy resiliencePolicy)
        {
            _s3Client = s3Client;
            _resiliencePolicy = resiliencePolicy;
        }

        public async Task<Document> GetDocumentAsync(string bucket, string objectName)
        {
            Document document = null;

            var response =
                await _resiliencePolicy.Policy.ExecuteAsync(async () =>
                    await _s3Client.GetObjectAsync(bucket, objectName));

            if (response.HttpStatusCode == HttpStatusCode.OK)
                document = new Document
                {
                    Name = objectName,
                    Version = response.VersionId
                };

            return document;
        }
    }

With unit tests we can try to prove the timeout is triggered:

    public class DocumentRepositoryTests
    {
        public DocumentRepositoryTests()
        {
            _clientMock = new Mock<IAmazonS3>();
            _policy = new ResiliencePolicyStub();
            _sut = new DocumentRepository(_clientMock.Object, _policy);
        }

        private readonly Mock<IAmazonS3> _clientMock;
        private readonly ResiliencePolicyStub _policy;
        private readonly DocumentRepository _sut;

        [Fact]
        public async Task GetDocumentAsync_WhenCallAboveThreshold_TriggersTimeout()
        {
            _clientMock.Setup(x =>
                    x.GetObjectAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
                .Callback((string b, string o, CancellationToken c) => { Thread.Sleep(2000); })
                .ReturnsAsync(new GetObjectResponse());

            await Record.ExceptionAsync(async () => await _sut.GetDocumentAsync("dummy", "document.json"));

            _policy.TimeoutTriggered.Should().BeTrue();
        }
    }

with the ResiliencePolicyStub as:

    public class ResiliencePolicyStub : ResiliencePolicy
    {
        public bool TimeoutTriggered { get; private set; }

        protected override async Task OnTimeoutAsync(Context context, TimeSpan timeSpan, Task task)
        {
            TimeoutTriggered = true;
            await base.OnTimeoutAsync(context, timeSpan, task);
        }
    }

The IAmazonS3 mock has a callback to enforce the thread sleep, with a value above the timeout threshold. The delegate function is not triggered (the TimeoutTriggered property doesn't change) neither it receives an exception (TimeoutException).

From the samples there is another mechanism in place, however, from an API perspective the calls are async by "default".

What is missing with the timeout policy configuration?

Note: the example was built with dotnet core 2.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions