For each new message, the previous task (if available) should be stopped and a new one started.
SOLUTION
The only solution I can see to my problems described below is to have CancellationTokenSource (CTS) in MessageHandler::Start and pass it to HandleAsync which will propogate it to all services. But, since _provider.StopAsync() must be called on cancelation, OnTaskStopping::_service.StopAsync() must be also called. Thus, I end up using CTS.Cancel and OnTaskStopping. Is this good approach to mix both CancellationToken and have Stop method?
PROBLEMS
Let' assume the MessageHandler::Start::Task.Run hasn't started yet and we have a new incoming message and MessageHandler::Start::Task.WhenAll called first. That means, the OnTaskStopping() -> _service.StopAsync() -> _cts.Cancel() is called. When eventually MessageHandler::Start::Task.Run runs, it will create a new CancellationTokenSource and thus overwrite the _cts.Cancel(). Therefore, the foreach will not be canceled.
If I move _cts = new CancellationTokenSource(); (denote NEW_CTS) to the end of MyService.StartAsync I might have a situation when _cts.Cancel() will be called right after NEW_CTS line. Meaning, when new MessageHandler::Start::Task.Run starts, the MyService.StartAsync will finish immediately since token is already canceled.
CODE
1 public abstract class MessageHandler
2 {
3 private readonly BlockingCollection<string> _pendingMessages;
4 public void Start()
5 {
6 _task = Task.Run(async () =>
7 {
8 Task handlerTask;
9 try
10 {
11 // BlockingCollection is temporary. I'll switch to cahnnel to get async producer/consumer
12 foreach (var msg in _pendingMessages.GetConsumingEnumerable(_cancellationTokenSource.Token))
13 {
14 try
15 {
16 // stop previous task
17 if(handlerTask != null)
18 {
19 await OnTaskStopping();
19.5 await handlerTask;
20 }
21
22 handlerTask = Task.Run(async () => await HandleAsync(msg));
23 }
24 catch (Exception ex)
25 {
26 ...
27 }
28 }
29 }
30 catch { } // OperationCanceledException
31 }
32 }
33 protected abstract Task HandleAsync(string msg);
34 protected abstract Task OnTaskStopping();
35 }
36 public class MyMessageHandler : MessageHandler
37 {
38 private readonly MyService _service;
39 public MyMessageHandler (MyService service)
40 {
41 _service = service;
42 }
43 protected override async Task HandleAsync(string msg)
44 {
45 ...
46 await _service.StartAsync(...);
47 }
48 protected override async Task OnTaskStopping()
49 {
50 await _service.StopAsync();
51 }
52 }
53 public class MyService
54 {
55 private CancellationTokenSource _cts;
56 private readonly IDevicesProvider _provider;
57
58 public MyService()
59 {
60 _cts = new CancellationTokenSource();
61 }
62 public async Task StartAsync(...)
63 {
64 _cts = new CancellationTokenSource();
65 foreach (var item in Items)
66 {
67 if(_cts.IsCancellationRequested)
68 return;
69 ...
70 }
71 //_cts = new CancellationTokenSource();
72 }
73 public async Task<bool> StopAsync()
74 {
75 _cts.Cancel();
76 // THIS MUST HAPPEN
77 return await _provider.StopAsync();
78 }
79 }
You can improve this in a couple of ways.
The first is not to access _cts constantly in StartAsync. Instead, read the token from _cts once into a local / parameter.
I also find it's worth saving the Task returned from StartAsync, so your StopAsync can wait for the cancellation to complete.
Something like:
public class MyService
{
private CancellationTokenSource? _cts;
private Task? _task;
private readonly IDevicesProvider _provider;
public async Task StartAsync(...)
{
await StopAsync(); // Or throw if we're already started?
_cts = new CancellationTokenSource();
_task = RunAsync(_cts.Token);
await _task;
}
private async Task RunAsync(CancellationToken token)
{
foreach (var item in Items)
{
if (token.IsCancellationRequested)
return;
...
}
}
public async Task<bool> StopAsync()
{
bool result = false;
if (_cts != null)
{
_cts.Cancel();
try
{
await _task;
}
catch (OperationCanceledException) { }
try
{
// THIS MUST HAPPEN
result = await _provider.StopAsync();
}
finally
{
_cts = null;
_task = null;
}
}
return result;
}
}
Once you've got that far, it might make more sense to move the await _provider.StopAsync() into the RunAsync:
public class MyService
{
private CancellationTokenSource? _cts;
private Task<bool>? _task;
private readonly IDevicesProvider _provider;
public async Task StartAsync(...)
{
await StopAsync(); // Or throw if we're already started?
_cts = new CancellationTokenSource();
_task = RunAsync(_cts.Token);
await _task;
}
private async Task<bool> RunAsync(CancellationToken token)
{
try
{
foreach (var item in Items)
{
if (token.IsCancellationRequested)
return;
...
}
}
catch (OperationCanceledException) { }
finally
{
return await _provider.StopAsync();
}
}
public async Task<bool> StopAsync()
{
bool result = false;
if (_cts != null)
{
_cts.Cancel();
try
{
result = await _task;
}
finally
{
_cts = null;
_task = null;
}
}
return result;
}
}
Of course in both cases your StartAsync only completes after the task has fully completed (or been stopped) -- you can remove the await _task to avoid this.
Related
I recently had to respond to a production incident where a background thread that executed some code on a loop had stopped executing, but a related thread was continuing to do it's job, in spite of the second thread being explicitly cancelled.
As I read the code, I believe that because of the finally statement (See below) the second thread should have been cancelled and thus further executions stopped. It's worth noting, that the code in the implementation of IBackgroundJob failed with a TaskCancelledException, due to timeouts of > 100 seconds (default for Http I believe).
Can anyone clarify why, when the code exits the inner while loop (either because of cancellation token source state or an exception), the executing code on the second thread (KeepRenewingLock) would continue to run? Would the leaseCancellation?.Cancel() not cancel the token being used by the KeepRenewingLock and thus prevent further lock renewals (incidentally, the lock item behind the scenes continued to be renewed. We could see this from the timestamps on the lock item)
public class Leasing
{
private Task _task;
private readonly CancellationTokenSource _cts;
private readonly IBackgroundJob _backgroundJob;
public Leasing(IBackgroundJob backgroundJob)
{
_backgroundJob = backgroundJob;
_cts = new CancellationTokenSource();
}
public Task StartAsync(CancellationToken cancellationToken)
{
_task = Task.Run(async () =>
{
await RunAsync(_cts.Token);
}, cancellationToken);
return Task.CompletedTask;
}
private async Task RunAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
CancellationTokenSource leaseCancellation = null;
try
{
leaseCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var maintainLockTask = Task.Run(() => KeepRenewingLock(leaseCancellation.Token), leaseCancellation.Token);
while (!cancellationToken.IsCancellationRequested && !maintainLockTask.IsCompleted)
{
await RunBackgroundJobSafely(cancellationToken);
await Task.Delay(TimeSpan.FromMinutes(2), cancellationToken);
}
}
catch (Exception e)
{
//code to log error
}
finally
{
leaseCancellation?.Cancel();
leaseCancellation?.Dispose();
}
}
}
private async Task RunBackgroundJobSafely(CancellationToken cancellationToken)
{
try
{
await _backgroundJob.ExecuteAsync(cancellationToken);
}
catch (Exception ex) when (!_cts.IsCancellationRequested)
{
//log error
}
}
private void KeepRenewingLock(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
//task delay here
//code to renew lease
}
}
catch(Exception ex)
{
//code to log error
}
}
}
public interface IBackgroundJob
{
Task ExecuteAsync(CancellationToken cancellationToken);
}
I have 2 threads, one is put data to IBlockingColection, and the second read it then send to kafka.
My application is asp.net core api.
public class ConcurrentQueueForEvents
{
private readonly BlockingCollection<IDomainEvent> _queue;
public ConcurrentQueueForEvents()
{
_queue = new BlockingCollection<IDomainEvent>(new ConcurrentQueue<IDomainEvent>());
}
public void Enqueue(IDomainEvent item)
{
_queue.Add(item);
OnItemEnqueued();
}
public bool TryDequeue(out IDomainEvent result)
{
result = _queue.Take();
return true;
}
public IEnumerable<IDomainEvent> GetConsumingEnumerable()
{
return _queue.GetConsumingEnumerable();
}
public event EventHandler? ItemEnqueued;
void OnItemEnqueued()
{
ItemEnqueued?.Invoke(this, EventArgs.Empty);
}
}
at the controller, when user act an api, i send event to it, it was register as singleton on StartUp class
public class PushEventToKafkaHandler
{
private readonly ConcurrentQueueForEvents _queue;
private readonly MessageBrokerFactory _messageBrokerFactory;
private readonly IIntegrateEventMapper _mapper;
private readonly AsyncDuplicateLock _locker = new ();
public PushEventToKafkaHandler(ConcurrentQueueForEvents queue,
MessageBrokerFactory messageBrokerFactory, IIntegrateEventMapper mapper)
{
_queue = queue;
_messageBrokerFactory = messageBrokerFactory;
_mapper = mapper;
}
public async Task Delivery()
{
foreach (var value in _queue.GetConsumingEnumerable())
{
var targetMessage = _mapper.GetIntegrateEventFromEvent(value);
var s = _messageBrokerFactory.GetSender(targetMessage.MessageType.Name);
//await Task.Run(() =>
//{
// s.SendAsync(targetMessage);
//});
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} ---- {_queue.GetHashCode()}");
}
}
public void DeliveryByHook()
{
_queue.ItemEnqueued += async (sender, args) =>
{
try
{
_queue.TryDequeue(out var message);
if (message is IIntegrateEvent #event)
{
var targetMessage = _mapper.GetIntegrateEventFromEvent(message);
var s = _messageBrokerFactory.GetSender(targetMessage.MessageType.Name);
await s.SendAsync(targetMessage);
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
};
}
}
this is the pusher that push event to kafka.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env){
var transporter = serviceProvider.GetService<PushEventToKafkaHandler>()!;
Task.Run(() =>
{
transporter.Delivery();
});
}
at StartUp I run it on other thread by Task.Run but, when i call api to send event, the transporter keep remain item on IBlockingCollection.
So when i send 5 times event, instead of sending 5 message, he send total 15 (1+2+3+4+5) messages.
What i wrong here?
update:
after adding log the result is:
] [User.Api] [::1] [1f4f4db8-b260-4cc6-bf87-5ab8d68ecc0a] Request finished HTTP/1.1 POST https://localhost:7162/bet application/json 248 - 200 - application/json;+charset=utf-8 89.7223ms
14 ---- 6555496
14 ---- 6555496
14 ---- 6555496
14 ---- 6555496
14 ---- 6555496
at the 5th of api hitting
after try with sample code, it is my mistake on code.
the caller of Enqueue method is register as singleton, and not flush event after enqueue.
so it keep message, then increase it each api hitting
I have a class which creates a Task which runs during its whole lifetime, that is, until Dispose() is called on it:
In the constructor I call:
_worker = Task.Run(() => ProcessQueueAsync(_workerCancellation.Token), _workerCancellation.Token);
The way I currently do it (which I am also not sure is the right way) is cancelling the CancellationToken, and waiting on the task.
public void Dispose()
{
if (_isDisposed)
{
return;
}
_workerCancellation.Cancel();
_worker.GetAwaiter().GetResult();
_isDisposed = true;
}
When I do the same in the AsyncDispose method like so:
public async ValueTask DisposeAsync()
{
await _worker;
}
I get this warning
How do I correctly dispose of such a worker? Thanks!
As requested, here is the full code of what I am trying to do:
public sealed class ActiveObjectWrapper<T, TS> : IAsyncDisposable
{
private bool _isDisposed = false;
private const int DefaultQueueCapacity = 1024;
private readonly Task _worker;
private readonly CancellationTokenSource _workerCancellation;
private readonly Channel<(T, TaskCompletionSource<TS>)> _taskQueue;
private readonly Func<T, TS> _onReceive;
public ActiveObjectWrapper(Func<T, TS> onReceive, int? queueCapacity = null)
{
_onReceive = onReceive;
_taskQueue = Channel.CreateBounded<(T, TaskCompletionSource<TS>)>(queueCapacity ?? DefaultQueueCapacity);
_workerCancellation = new CancellationTokenSource();
_worker = Task.Run(() => ProcessQueueAsync(_workerCancellation.Token), _workerCancellation.Token);
}
private async Task ProcessQueueAsync(CancellationToken cancellationToken)
{
await foreach (var (value, taskCompletionSource) in _taskQueue.Reader.ReadAllAsync(cancellationToken))
{
try
{
var result = _onReceive(value); // todo: do I need to propagate the cancellation token?
taskCompletionSource.SetResult(result);
}
catch (Exception exception)
{
taskCompletionSource.SetException(exception);
}
}
}
public async Task<TS> EnqueueAsync(T value)
{
// see: https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/
var completionSource = new TaskCompletionSource<TS>(TaskCreationOptions.RunContinuationsAsynchronously);
await _taskQueue.Writer.WriteAsync((value, completionSource));
return await completionSource.Task;
}
public async ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}
_taskQueue.Writer.Complete();
_workerCancellation.Cancel();
await _worker;
_isDisposed = true;
}
}
This is the pattern that I use when implementing both IDisposabe and IDisposeableAsync. It isn't strictly compliant with the .Net recommendations. I found that implementing DisposeAsyncCore() was unnecessary as my classes are sealed.
public void Dispose()
{
Dispose(disposing: true);
//GC.SuppressFinalize(this);
Worker.GetAwaiter().GetResult();
}
public void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
if (disposing)
{
lock (isDisposing)
{
if (isDisposed)
{
return;
}
Cts.Cancel();
Cts.Dispose();
isDisposed = true;
}
}
}
public async ValueTask DisposeAsync()
{
Dispose(disposing: true);
//GC.SuppressFinalize(this);
await Worker;
}
This looks like an attempt to build a TransformBlock<TIn,TOut> on top of Channels. Using a Channel properly wouldn't generate such a warning. There's no reason to use a single processing task, or store it in a field.
Using Blocks
First, the equivalent code using a TransformBlock<Tin,TOut> would be:
var block=new TransformBlock<TIn,TOut>(msgIn=>func(msgIn));
foreach(....)
{
block.Post(someMessage);
}
To stop it, block.Complete() would be enough. Any pending messages would still be processed. A TransformBlock is meant to forward its output to other blocks, eg an ActionBlock or BufferBlock.
var finalBlock=new ActionBlock<TOut>(msgOut=>Console.WriteLine(msgOut));
block.LinkTo(finalBlock,new DataflowLinkOptions{PropagateCompletion = true});
...
block.Complete();
await finalBlock.Completion;
Using Channels
Doing something similar with channels doesn't need explicit classes, or Enqueue/Dequeue methods. Channels are build with separate ChannelReader, ChannelWriter interfaces to make it easier to control ownership, concurrency and completion.
A similar pipeline using channels would require only some methods:
ChannelReader<string> FolderToChannel(string path,CancellationToken token=default)
{
Channel<int> channel=Channel.CreateUnbounded();
var writer=channel.Writer;
_ = Task.Run(async ()=>{
foreach(var path in Directory.EnumerateFiles(path))
{
await _writer.SendAsync(path);
if (token.CancellationRequested)
{
return;
}
}
},token).ContinueWith(t=>_writer.TryComplete(t.Exception));
return channel;
}
This produces a reader that can be passed to a processing method. One that could generate another reader with the results:
static ChannelReader<MyClass> ParseFile(this ChannelReader<string> reader,CancellationToken token)
{
Channel<int> channel=Channel.CreateUnbounded();
var writer=channel.Writer;
_ = Task.Run(async ()=>{
await foreach(var path in reader.ReadAllAsync(token))
{
var json= await File.ReadAllTextAsync(path);
var dto= JsonSerializer.DeserializeObject<MyClass>(json);
await _writer.SendAsync(dto);
}
},token).ContineWith(t=>writer.TryComplete(t.Exception);
return channel;
}
And a final step that only consumes a channel:
static async Task LogIt(this ChannelReader<MyClass> reader,CancellationToken token)
{
await Task.Run(async ()=>{
await foreach(var dto in reader.ReadAllAsync(token))
{
Console.WriteLine(dto);
}
},token);
}
The three steps can be combined very easily:
var cts=new CancellationTokenSource();
var complete=FolderToChannel(somePath,cts.Token)
.ParseFile(cts.Token)
.LogIt(cts.Token);
await complete;
By encapsulating the channel itself and the processing in a method there's no ambiguity about who owns the channel, who is responsible for completion or cancellation
I am using BackgroundService for my tasks and i would like to run different tasks at different times for example i have a task which should run once a day and i have come up with this cron expression "#daily" which is ok for my first task. But for my second task which should run multiple times a day i need multiple cron expressions
for example
( 30 13 * * * ) daily at 13:30
( 10 17 * * * ) daily at 17:10
( 40 20 * * * ) daily at 20:40
( 15 22 * * * ) daily at 22:15
and the classes which i use looks like this
public abstract class BackgroundService : IHostedService
{
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_executingTask = ExecuteAsync(_stoppingCts.Token);
if (_executingTask.IsCompleted)
{
return _executingTask;
}
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
if (_executingTask == null)
{
return;
}
try
{
_stoppingCts.Cancel();
}
finally
{
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
protected virtual async Task ExecuteAsync(CancellationToken stoppingToken)
{
do
{
await Process();
await Task.Delay(5000, stoppingToken);
} while (!stoppingToken.IsCancellationRequested);
}
protected abstract Task Process();
}
public abstract class ScopedProcessor : BackgroundService
{
private IServiceScopeFactory _serviceScopeFactory;
public ScopedProcessor(IServiceScopeFactory serviceScopeFactory) : base()
{
_serviceScopeFactory = serviceScopeFactory;
}
protected override async Task Process()
{
using (var scope = _serviceScopeFactory.CreateScope())
{
await ProcessInScope(scope.ServiceProvider);
}
}
public abstract Task ProcessInScope(IServiceProvider scopeServiceProvider);
}
public abstract class ScheduledProcessor : ScopedProcessor
{
private CrontabSchedule _schedule;
private DateTime _nextRun;
protected abstract string Schedule { get; }
public ScheduledProcessor(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{
_schedule = CrontabSchedule.Parse(Schedule);
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var now = DateTime.Now;
if (now > _nextRun)
{
await Process();
// for the first task which should run daily is ok but
// for my second task i want to run the multiple cron expressions after
// one another
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
await Task.Delay(5000, stoppingToken); // 5 seconds delay
};
}
}
and the actual class which contains the actual task.
public class MyTask : ScheduledProcessor
{
public MyTask(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{
}
// cron expression
protected override string Schedule => "*/1 * * * *"; // every 1 min for testing purpose
// actual task
public override Task ProcessInScope(IServiceProvider scopeServiceProvider)
{
Console.WriteLine("MyTask Running " + DateTime.Now.ToShortTimeString());
// do other work
return Task.CompletedTask;
}
}
instead of executing a single cron expression i want to run multiple cron expressions after one another at a daily basis. Maybe CronTrigger can help but i dont know where and how can i use CronTrigger in my Classes.
I need to implement a library to request vk.com API. The problem is that API supports only 3 requests per second. I would like to have API asynchronous.
Important: API should support safe accessing from multiple threads.
My idea is implement some class called throttler which allow no more than 3 request/second and delay other request.
The interface is next:
public interface IThrottler : IDisposable
{
Task<TResult> Throttle<TResult>(Func<Task<TResult>> task);
}
The usage is like
var audio = await throttler.Throttle(() => api.MyAudio());
var messages = await throttler.Throttle(() => api.ReadMessages());
var audioLyrics = await throttler.Throttle(() => api.AudioLyrics(audioId));
/// Here should be delay because 3 requests executed
var photo = await throttler.Throttle(() => api.MyPhoto());
How to implement throttler?
Currently I implemented it as queue which is processed by background thread.
public Task<TResult> Throttle<TResult>(Func<Task<TResult>> task)
{
/// TaskRequest has method Run() to run task
/// TaskRequest uses TaskCompletionSource to provide new task
/// which is resolved when queue processed til this element.
var request = new TaskRequest<TResult>(task);
requestQueue.Enqueue(request);
return request.ResultTask;
}
This is shorten code of background thread loop which process the queue:
private void ProcessQueue(object state)
{
while (true)
{
IRequest request;
while (requestQueue.TryDequeue(out request))
{
/// Delay method calculates actual delay value and calls Thread.Sleep()
Delay();
request.Run();
}
}
}
Is it possible to implement this without background thread?
So we'll start out with a solution to a simpler problem, that of creating a queue that process up to N tasks concurrently, rather than throttling to N tasks started per second, and build on that:
public class TaskQueue
{
private SemaphoreSlim semaphore;
public TaskQueue()
{
semaphore = new SemaphoreSlim(1);
}
public TaskQueue(int concurrentRequests)
{
semaphore = new SemaphoreSlim(concurrentRequests);
}
public async Task<T> Enqueue<T>(Func<Task<T>> taskGenerator)
{
await semaphore.WaitAsync();
try
{
return await taskGenerator();
}
finally
{
semaphore.Release();
}
}
public async Task Enqueue(Func<Task> taskGenerator)
{
await semaphore.WaitAsync();
try
{
await taskGenerator();
}
finally
{
semaphore.Release();
}
}
}
We'll also use the following helper methods to match the result of a TaskCompletionSource to a `Task:
public static void Match<T>(this TaskCompletionSource<T> tcs, Task<T> task)
{
task.ContinueWith(t =>
{
switch (t.Status)
{
case TaskStatus.Canceled:
tcs.SetCanceled();
break;
case TaskStatus.Faulted:
tcs.SetException(t.Exception.InnerExceptions);
break;
case TaskStatus.RanToCompletion:
tcs.SetResult(t.Result);
break;
}
});
}
public static void Match<T>(this TaskCompletionSource<T> tcs, Task task)
{
Match(tcs, task.ContinueWith(t => default(T)));
}
Now for our actual solution what we can do is each time we need to perform a throttled operation we create a TaskCompletionSource, and then go into our TaskQueue and add an item that starts the task, matches the TCS to its result, doesn't await it, and then delays the task queue for 1 second. The task queue will then not allow a task to start until there are no longer N tasks started in the past second, while the result of the operation itself is the same as the create Task:
public class Throttler
{
private TaskQueue queue;
public Throttler(int requestsPerSecond)
{
queue = new TaskQueue(requestsPerSecond);
}
public Task<T> Enqueue<T>(Func<Task<T>> taskGenerator)
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
var unused = queue.Enqueue(() =>
{
tcs.Match(taskGenerator());
return Task.Delay(TimeSpan.FromSeconds(1));
});
return tcs.Task;
}
public Task Enqueue<T>(Func<Task> taskGenerator)
{
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
var unused = queue.Enqueue(() =>
{
tcs.Match(taskGenerator());
return Task.Delay(TimeSpan.FromSeconds(1));
});
return tcs.Task;
}
}
I solved a similar problem using a wrapper around SemaphoreSlim. In my scenario, I had some other throttling mechanisms as well, and I needed to make sure that requests didn't hit the external API too often even if request number 1 took longer to reach the API than request number 3. My solution was to use a wrapper around SemaphoreSlim that had to be released by the caller, but the actual SemaphoreSlim would not be released until a set time had passed.
public class TimeGatedSemaphore
{
private readonly SemaphoreSlim semaphore;
public TimeGatedSemaphore(int maxRequest, TimeSpan minimumHoldTime)
{
semaphore = new SemaphoreSlim(maxRequest);
MinimumHoldTime = minimumHoldTime;
}
public TimeSpan MinimumHoldTime { get; }
public async Task<IDisposable> WaitAsync()
{
await semaphore.WaitAsync();
return new InternalReleaser(semaphore, Task.Delay(MinimumHoldTime));
}
private class InternalReleaser : IDisposable
{
private readonly SemaphoreSlim semaphoreToRelease;
private readonly Task notBeforeTask;
public InternalReleaser(SemaphoreSlim semaphoreSlim, Task dependantTask)
{
semaphoreToRelease = semaphoreSlim;
notBeforeTask = dependantTask;
}
public void Dispose()
{
notBeforeTask.ContinueWith(_ => semaphoreToRelease.Release());
}
}
}
Example usage:
private TimeGatedSemaphore requestThrottler = new TimeGatedSemaphore(3, TimeSpan.FromSeconds(1));
public async Task<T> MyRequestSenderHelper(string endpoint)
{
using (await requestThrottler.WaitAsync())
return await SendRequestToAPI(endpoint);
}
Here is one solution that uses a Stopwatch:
public class Throttler : IThrottler
{
private readonly Stopwatch m_Stopwatch;
private int m_NumberOfRequestsInLastSecond;
private readonly int m_MaxNumberOfRequestsPerSecond;
public Throttler(int max_number_of_requests_per_second)
{
m_MaxNumberOfRequestsPerSecond = max_number_of_requests_per_second;
m_Stopwatch = Stopwatch.StartNew();
}
public async Task<TResult> Throttle<TResult>(Func<Task<TResult>> task)
{
var elapsed = m_Stopwatch.Elapsed;
if (elapsed > TimeSpan.FromSeconds(1))
{
m_NumberOfRequestsInLastSecond = 1;
m_Stopwatch.Restart();
return await task();
}
if (m_NumberOfRequestsInLastSecond >= m_MaxNumberOfRequestsPerSecond)
{
TimeSpan time_to_wait = TimeSpan.FromSeconds(1) - elapsed;
await Task.Delay(time_to_wait);
m_NumberOfRequestsInLastSecond = 1;
m_Stopwatch.Restart();
return await task();
}
m_NumberOfRequestsInLastSecond++;
return await task();
}
}
Here is how this code can be tested:
class Program
{
static void Main(string[] args)
{
DoIt();
Console.ReadLine();
}
static async Task DoIt()
{
Func<Task<int>> func = async () =>
{
await Task.Delay(100);
return 1;
};
Throttler throttler = new Throttler(3);
for (int i = 0; i < 10; i++)
{
var result = await throttler.Throttle(func);
Console.WriteLine(DateTime.Now);
}
}
}
You can use this as Generic
public TaskThrottle(int maxTasksToRunInParallel)
{
_semaphore = new SemaphoreSlim(maxTasksToRunInParallel);
}
public void TaskThrottler<T>(IEnumerable<Task<T>> tasks, int timeoutInMilliseconds, CancellationToken cancellationToken = default(CancellationToken)) where T : class
{
// Get Tasks as List
var taskList = tasks as IList<Task<T>> ?? tasks.ToList();
var postTasks = new List<Task<int>>();
// When the first task completed, it will flag
taskList.ForEach(x =>
{
postTasks.Add(x.ContinueWith(y => _semaphore.Release(), cancellationToken));
});
taskList.ForEach(x =>
{
// Wait for open slot
_semaphore.Wait(timeoutInMilliseconds, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
x.Start();
});
Task.WaitAll(taskList.ToArray(), cancellationToken);
}
Edit: this solution works but use it only if it is ok to process all request in serial (in one thread). Otherwise use solution accepted as answer.
Well, thanks to Best way in .NET to manage queue of tasks on a separate (single) thread
My question is almost duplicate except adding delay before execution, which is actually simple.
The main helper here is SemaphoreSlim class which allows to restrict degree of parallelism.
So, first create a semaphore:
// Semaphore allows run 1 thread concurrently.
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
And, final version of throttle looks like
public async Task<TResult> Throttle<TResult>(Func<Task<TResult>> task)
{
await semaphore.WaitAsync();
try
{
await delaySource.Delay();
return await task();
}
finally
{
semaphore.Release();
}
}
Delay source is also pretty simple:
private class TaskDelaySource
{
private readonly int maxTasks;
private readonly TimeSpan inInterval;
private readonly Queue<long> ticks = new Queue<long>();
public TaskDelaySource(int maxTasks, TimeSpan inInterval)
{
this.maxTasks = maxTasks;
this.inInterval = inInterval;
}
public async Task Delay()
{
// We will measure time of last maxTasks tasks.
while (ticks.Count > maxTasks)
ticks.Dequeue();
if (ticks.Any())
{
var now = DateTime.UtcNow.Ticks;
var lastTick = ticks.First();
// Calculate interval between last maxTasks task and current time
var intervalSinceLastTask = TimeSpan.FromTicks(now - lastTick);
if (intervalSinceLastTask < inInterval)
await Task.Delay((int)(inInterval - intervalSinceLastTask).TotalMilliseconds);
}
ticks.Enqueue(DateTime.UtcNow.Ticks);
}
}