using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; namespace CryptoExchange.Net.Objects { /// /// Queue for processing items /// /// Item type public class ProcessQueue { private readonly Channel _channel; private readonly Func _processor; private Task? _processTask; private CancellationTokenSource? _cts; private bool _processTillEmpty; /// /// Event for when an exception is thrown in the processing handler /// public event Action? Exception; /// /// ctor /// /// The function to async handle the updates /// The max number of items to be queued up. When happens when the queue is full and a new write is attempted can be specified with fullMode /// What should happen if the queue contains maxQueuedItems pending items. If no max is set this setting is ignored public ProcessQueue(Func processor, int? maxQueuedItems = null, QueueFullBehavior? fullBehavior = null) { _processor = processor; if (maxQueuedItems == null) { _channel = Channel.CreateUnbounded(new UnboundedChannelOptions { AllowSynchronousContinuations = false, SingleReader = true, SingleWriter = true }); } else { _channel = Channel.CreateBounded(new BoundedChannelOptions(maxQueuedItems.Value) { AllowSynchronousContinuations = false, SingleReader = true, SingleWriter = true, FullMode = MapMode(fullBehavior) }); } } private BoundedChannelFullMode MapMode(QueueFullBehavior? behavior) => behavior switch { QueueFullBehavior.DropOldest => BoundedChannelFullMode.DropOldest, QueueFullBehavior.DropNewest => BoundedChannelFullMode.DropNewest, QueueFullBehavior.DropWrite => BoundedChannelFullMode.DropWrite, _ => BoundedChannelFullMode.DropWrite }; /// /// Start the processing of the queue /// public Task StartAsync() { _cts = new CancellationTokenSource(); _processTask = Task.Run(async () => { try { await foreach (var item in _channel.Reader.ReadAllAsync(_cts.Token).ConfigureAwait(false)) { if (_cts.IsCancellationRequested && !_processTillEmpty) // Items might still be processed even if CT is canceled return; try { await _processor.Invoke(item).ConfigureAwait(false); } catch (Exception ex) { Exception?.Invoke(ex); } } } catch (OperationCanceledException) { } }); return Task.CompletedTask; } /// /// Stop processing the queue /// /// Whether updates still pending in the queue should be discarded /// public async Task StopAsync(bool discardPending = true) { if (_processTask == null) return; _processTillEmpty = !discardPending; _cts!.Cancel(); await _processTask.ConfigureAwait(false); _channel.Writer.TryComplete(_processTask.Exception); } /// /// Write an update to queue /// public bool Write(T item) { if (_cts?.IsCancellationRequested == true) return false; var write = _channel.Writer.TryWrite(item); if (!write) LibraryHelpers.StaticLogger?.Log(LogLevel.Warning, "Failed to write item to process queue. Item will be discarded"); return write; } } }