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

Deadlock ComposedAwaitableConstraint and Cancellation #27

Open
Karql opened this issue Nov 2, 2020 · 2 comments
Open

Deadlock ComposedAwaitableConstraint and Cancellation #27

Karql opened this issue Nov 2, 2020 · 2 comments

Comments

@Karql
Copy link

Karql commented Nov 2, 2020

Hi!

At the begining I would like you for great stuff ;)

I've found a bug when using ComposedAwaitableConstraint with Cancellation token.

My use case:

  • limiting api request
  • call api paraller in tasks
  • one of batch requests return session expired
  • canel other request
  • login
  • repeate

Simplified example:

        private static async Task MainAsync()
        {
            var limiter = TimeLimiter.Compose(
                new CountByIntervalAwaitableConstraint(1, TimeSpan.FromSeconds(1)),
                new CountByIntervalAwaitableConstraint(1000, TimeSpan.FromHours(1))
            );

            Func<int, CancellationToken, Task> f = async (i, token) =>
            {
                await limiter.Enqueue(async () =>
                {
                    await Task.Delay(i * 100);
                    Console.WriteLine(i);
                }, token);
            };

            while (true)
            {
                var cts = new CancellationTokenSource(500);
                var combined = Task.WhenAll(Enumerable.Range(0, 10).Select(i => f(i, cts.Token)));

                try
                {
                    await combined;
                }

                catch
                {
                    ;
                }
            }
        }

Problem is here:

        public async Task<IDisposable> WaitForReadiness(CancellationToken cancellationToken)
        {
            await _Semaphore.WaitAsync(cancellationToken);
            IDisposable[] disposables;
            try 
            {
                disposables = await Task.WhenAll(_AwaitableConstraint1.WaitForReadiness(cancellationToken), _AwaitableConstraint2.WaitForReadiness(cancellationToken));
            }
            catch (Exception) 
            {
                _Semaphore.Release();
                throw;
            } 
            return new DisposeAction(() => 
            {
                foreach (var disposable in disposables)
                {
                    disposable.Dispose();
                }
                _Semaphore.Release();
            });
        }

_AwaitableConstraint2 (for hour) return:

return new DisposeAction(OnEnded);

_AwaitableConstraint1 (for second) throw TaskCancelled exception

Result is that OnEnded wont be never called and _Semaphores wont be released.

I can create new limiter after cancellation but maybe expose method like. TryRelease() on IAwaitableConstraint and call it on exception woluld be better option?

Best regards,
Mateusz

@David-Desmaisons
Copy link
Owner

Could you create a branch with the corresponding test so I can check and debug what is hapenning in this case?
Thanks

Karql added a commit to Karql/RateLimiter that referenced this issue Nov 3, 2020
@Karql
Copy link
Author

Karql commented Nov 3, 2020

Hi ;)

https://github.com/Karql/RateLimiter/tree/issue-27

Put break point here:
b1

When debugger stops on it put break point in CountByIntervalAwaitableConstraint.WaitForReadiness(CancellationToken cancellationToken)
Second call this method show that _Semaphore is closed

b2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants