/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Threading; using QuantConnect.Interfaces; using QuantConnect.Logging; using QuantConnect.Orders; using QuantConnect.Util; namespace QuantConnect.Lean.Engine.TransactionHandlers { /// /// Runs order requests on background worker threads that pull from a single shared queue. The pool grows on /// demand when the workers get saturated and keeps every request of an order processed in order. /// /// /// Workers pull from one shared queue, so the load spreads across them instead of pinning each order to a thread /// up front. To keep a single order (or combo group) in order, only one of its requests runs at a time. While one /// runs the rest wait parked, and the same worker takes them next in arrival order. This state only exists while /// an order has requests in flight, so nothing needs releasing once the order closes. When a single consumer /// drains the queue, a lone fixed worker or the caller itself in synchronous mode (through /// ), arrival order is already preserved so the per-order bookkeeping is skipped. /// public class OrderRequestProcessingPool : IDisposable { // maximum time to wait for each worker thread to stop when disposing the pool private static readonly TimeSpan ShutdownTimeout = TimeSpan.FromSeconds(60); // the shared queue of requests cleared to run. every worker pulls from here so the load stays balanced private readonly IBusyCollection _readyQueue; private readonly List _threads; // for each order (or combo group) being processed, the follow up requests waiting their turn in arrival order, // or null until a second request actually needs parking. while the key is here the order is already running private readonly Dictionary<(bool IsGroup, int Id), Queue> _inFlight = new(); // guards the in flight map, the threads list and the growth/shutdown flags private readonly Lock _lock = new(); // maximum number of worker threads the pool can grow to on demand private readonly int _maximumThreads; // true when there are no worker threads and the caller drains the single queue itself private readonly bool _synchronous; // true when a single consumer drains the queue (synchronous or a single fixed worker), which already // preserves arrival order across all orders so the per-order serialization is skipped entirely private readonly bool _singleConsumer; // set under the lock when shutting down so the pool stops growing while the queue drains, before the // cancellation token is cancelled as the final hard stop private bool _shuttingDown; // number of workers currently processing a request, used to decide when the pool is saturated private int _busyWorkers; private readonly Action _processRequest; private readonly Action _onError; private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); /// /// True while the pool is processing order requests, false once it has been shut down. /// public bool IsActive { get; private set; } /// /// The number of worker threads currently running. /// public int ThreadCount { get { lock (_lock) { return _threads.Count; } } } /// /// Creates a threaded pool and starts its initial worker threads. When concurrency is enabled the pool /// starts at and grows on demand up to , /// otherwise it runs a single fixed worker thread. /// /// True to grow the pool on demand, false to run a single worker thread /// The number of worker threads the pool starts with when growing /// The maximum number of worker threads the pool can grow to on demand /// Handles a single order request /// Invoked when processing fails unexpectedly public OrderRequestProcessingPool(bool concurrencyEnabled, int minimumThreads, int maximumThreads, Action processRequest, Action onError) { _synchronous = false; _processRequest = processRequest; _onError = onError; // concurrency grows the pool minimum..maximum on demand, otherwise a single fixed thread is used _maximumThreads = concurrencyEnabled ? Math.Max(1, maximumThreads) : 1; _singleConsumer = _maximumThreads == 1; var initialThreadsCount = concurrencyEnabled ? Math.Min(Math.Max(1, minimumThreads), _maximumThreads) : 1; _readyQueue = new BusyBlockingCollection(); _threads = new(_maximumThreads); IsActive = true; for (var i = 0; i < initialThreadsCount; i++) { AddThread().Start(); } } /// /// Private constructor for the synchronous pool, a single non blocking queue and no worker threads. /// private OrderRequestProcessingPool(Action processRequest, Action onError) { _synchronous = true; _processRequest = processRequest; _onError = onError; _maximumThreads = 1; _singleConsumer = true; _readyQueue = new BusyCollection(); _threads = new(0); IsActive = true; } /// /// Creates a synchronous pool with no worker threads. Its single queue is drained on the caller thread /// via . /// /// Handles a single order request /// Invoked when processing fails unexpectedly public static OrderRequestProcessingPool Synchronous(Action processRequest, Action onError) { return new OrderRequestProcessingPool(processRequest, onError); } /// /// Dispatches an order request to be processed. If the order already has a request in flight, the new one /// waits parked so its worker runs it next and the order stays in arrival order. Otherwise it is queued for /// any worker to pick up, growing the pool first when every worker is already busy. /// /// The order request to process /// The order the request belongs to, used to keep its requests ordered public void Dispatch(OrderRequest request, Order order) { // a single consumer drains in arrival order across all orders, no need to serialize per order if (_singleConsumer) { _readyQueue.Add(new WorkItem(request, default)); return; } var key = GetRoutingKey(order); WorkItem readyItem = default; Thread newThread = null; var run = false; lock (_lock) { if (_inFlight.TryGetValue(key, out var parked)) { // the order is already being processed, park this request so its worker runs it next in order, // allocating the queue only now that a second request has actually arrived if (parked == null) { _inFlight[key] = parked = new Queue(); } parked.Enqueue(request); } else { // claim the order without a queue, most orders never get a second request. grow the pool if // every worker is already busy so this request would wait _inFlight[key] = null; newThread = TryExpand(); readyItem = new WorkItem(request, key); run = true; } } // start the new worker and add outside the lock: starting an OS thread and a potentially blocking // add on a bounded queue shouldn't stall other dispatchers if (run) { newThread?.Start(); _readyQueue.Add(readyItem); } } /// /// Drains the pending order requests on the calling thread. Only used in synchronous mode, where there /// are no worker threads and the caller pumps the single queue itself. /// public void ProcessPending() { Drain(item => _processRequest(item.Request)); } /// /// Waits until no order has requests in flight, up to the given timeout. In practice only the synchronous /// early return runs. The threaded branch below is defensive, since its callers only reach it in backtesting /// where the pool is synchronous, so it never runs in a live deployment. /// /// The maximum time to wait /// True if the pool was still processing when the timeout elapsed public bool WaitForProcessing(TimeSpan timeout) { // synchronous mode has no worker thread to drain the queue, the caller pumps it via ProcessPending if (_synchronous) { return false; } // re-check each pass since the shared queue signals idle as soon as a worker finds it empty, even if // another worker is still processing or a request is parked while (IsProcessing()) { if (!_readyQueue.WaitHandle.WaitOne(timeout, _cancellationTokenSource.Token)) { return true; } } return false; } /// /// Whether any order still has a request in flight, either queued, being processed or parked. /// private bool IsProcessing() { lock (_lock) { return _inFlight.Count > 0 || _readyQueue.IsBusy; } } /// /// Stops every worker thread and waits for them to terminate, then releases the pool resources. /// public void Dispose() { lock (_lock) { // already disposed, nothing else to do if (_shuttingDown) { return; } // stop growing so the threads list is frozen and safe to iterate without taking a snapshot _shuttingDown = true; } // let the workers drain whatever is queued and parked: once adding is complete their consuming // enumerables finish naturally when the queue empties, so join before cancelling anything. Only // escalate to StopSafely, which cancels the shared token and drops pending requests, on timeout _readyQueue.CompleteAdding(); foreach (var thread in _threads) { try { if (thread != null && !thread.Join(ShutdownTimeout)) { Log.Error($"OrderRequestProcessingPool.Dispose(): Exceeded timeout: {(int)ShutdownTimeout.TotalSeconds} seconds waiting for '{thread.Name}' to finish processing"); thread.StopSafely(ShutdownTimeout, _cancellationTokenSource); } } catch (ThreadStateException) { // registered by a concurrent Dispatch but not started yet, nothing to drain on it } } IsActive = false; _readyQueue.DisposeSafely(); _cancellationTokenSource.DisposeSafely(); } /// /// Creates and registers a worker thread without starting it, so callers can start it outside the lock. /// Callers growing the pool on demand must hold . /// /// The new worker thread, for the caller to start private Thread AddThread() { var thread = new Thread(Run) { IsBackground = true, Name = $"Transaction Thread {_threads.Count}" }; _threads.Add(thread); return thread; } /// /// Grows the pool by one worker when every existing worker is already busy, up to the maximum. /// Caller must hold and start the returned thread, if any, outside of it. /// /// The new worker thread to start, null when the pool doesn't need to grow private Thread TryExpand() { if (_shuttingDown || _threads.Count >= _maximumThreads) { return null; } // only grow when every worker is already busy, so the request being enqueued would have to wait if (Volatile.Read(ref _busyWorkers) >= _threads.Count) { Log.Trace($"OrderRequestProcessingPool.TryExpand(): adding new thread, current count {_threads.Count}"); return AddThread(); } return null; } /// /// Worker thread loop that consumes ready requests until the pool is shut down. A single fixed worker /// already consumes in arrival order so it skips the per-order bookkeeping. /// private void Run() { if (_singleConsumer) { Drain(item => _processRequest(item.Request)); } else { Drain(ProcessInOrder); } } /// /// Consumes ready requests on the calling thread until the queue completes adding or the pool is shut down. /// private void Drain(Action process) { try { foreach (var item in _readyQueue.GetConsumingEnumerable(_cancellationTokenSource.Token)) { process(item); } } catch (Exception err) { // unexpected error, we need to close down shop _onError(err); } } /// /// Processes a request and then drains, in arrival order, every follow up request parked for the same order, /// so a single worker handles the whole order in sequence before moving on to other work. /// private void ProcessInOrder(WorkItem item) { var request = item.Request; Interlocked.Increment(ref _busyWorkers); try { while (request != null) { _processRequest(request); lock (_lock) { var parked = _inFlight[item.Key]; if (parked != null && parked.Count > 0) { request = parked.Dequeue(); } else { // no more requests for this order in flight, drop its bookkeeping _inFlight.Remove(item.Key); request = null; } } } } finally { Interlocked.Decrement(ref _busyWorkers); } } /// /// Builds the routing key that ties an order's requests together, the combo group when it has one, otherwise /// the order itself. Order ids and group ids are separate counters that can share a value, so the flag keeps /// a simple order and a combo group from colliding. /// private static (bool IsGroup, int Id) GetRoutingKey(Order order) { var group = order.GroupOrderManager; return group?.Id > 0 ? (true, group.Id) : (false, order.Id); } /// /// Pairs a request with its routing key so the worker can drain the rest of the order without re-deriving it. /// private readonly struct WorkItem { public OrderRequest Request { get; } public (bool IsGroup, int Id) Key { get; } public WorkItem(OrderRequest request, (bool IsGroup, int Id) key) { Request = request; Key = key; } } } }