09cc78599a
We restrict each algorithm time loop to a pre-determined amount of time. Exceeding this limit will cause the algorithm to immediately terminate. This quickly becomes an issue when considering users running trainable models that have a long initialization period that exceeds the time loop maximum. This change provides a mechanism through which a long-running scheduled event is permitted to keep running and is permitted to avoid the time loop permitted by requesting additional time. Requests for additional time are limited according to a leaky bucket implementation whose parameters are set via the job's controls structure. The fundamental time unit for the algorithm is a single minute. Here's how it works. If a scheduled event takes longer than one full wall clock second then a request is made to the leaky bucket for one more minute. If the scheduled event continues to take more time, it will continue to request additional minutes. Each requested minute will prevent the algorithm's time loop check from terminating the algorithm. When the bucket is empty and no more minutes are available to be requested, a TimeoutException is thrown causing a cascade that ends in the algorithm's termination and status being flipped to RuntimeError. Additionally, this applies equally to ALL scheduled events. While some helpers were added with the naming of Train and TrainNow to the ScheduleManager, these methods don't do anything special and the infrastructure doesn't otherwise flag them as different, so this feature becomes part of the core Scheduled Event feature set. Further, the live scheduled events were not touched and are still pending further discussion regarding the value added by enforcing a time restriction when simulation time and wall clock time are equivalent. Fixes #3319
173 lines
7.4 KiB
C#
173 lines
7.4 KiB
C#
/*
|
|
* 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.Threading;
|
|
|
|
namespace QuantConnect.Util.RateLimit
|
|
{
|
|
/// <summary>
|
|
/// Provides an implementation of <see cref="ITokenBucket"/> that implements the leaky bucket algorithm
|
|
/// See: https://en.wikipedia.org/wiki/Leaky_bucket
|
|
/// </summary>
|
|
public class LeakyBucket : ITokenBucket
|
|
{
|
|
private readonly object _sync = new object();
|
|
|
|
private long _available;
|
|
private readonly ISleepStrategy _sleep;
|
|
private readonly IRefillStrategy _refill;
|
|
private readonly ITimeProvider _timeProvider;
|
|
|
|
/// <summary>
|
|
/// Gets the maximum capacity of tokens this bucket can hold.
|
|
/// </summary>
|
|
public long Capacity { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the total number of currently available tokens for consumption
|
|
/// </summary>
|
|
public long AvailableTokens
|
|
{
|
|
// synchronized read w/ the modification of available tokens in TryConsume
|
|
get { lock (_sync) return _available; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="LeakyBucket"/> class.
|
|
/// This constructor initializes the bucket using the <see cref="ThreadSleepStrategy.Sleep"/> with a 1 millisecond
|
|
/// sleep to prevent being CPU intensive and uses the <see cref="FixedIntervalRefillStrategy"/> to refill bucket
|
|
/// tokens according to the <paramref name="refillAmount"/> and <paramref name="refillInterval"/> parameters.
|
|
/// </summary>
|
|
/// <param name="capacity">The maximum number of tokens this bucket can hold</param>
|
|
/// <param name="refillAmount">The number of tokens to add to the bucket each <paramref name="refillInterval"/></param>
|
|
/// <param name="refillInterval">The interval which after passing more tokens are added to the bucket</param>
|
|
public LeakyBucket(long capacity, long refillAmount, TimeSpan refillInterval)
|
|
: this(capacity, ThreadSleepStrategy.Sleeping(1),
|
|
new FixedIntervalRefillStrategy(RealTimeProvider.Instance, refillAmount, refillInterval)
|
|
)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="LeakyBucket"/> class
|
|
/// </summary>
|
|
/// <param name="capacity">The maximum number of tokens this bucket can hold</param>
|
|
/// <param name="sleep">Defines the <see cref="ISleepStrategy"/> used when <see cref="Consume"/> is invoked
|
|
/// but the bucket does not have enough tokens yet</param>
|
|
/// <param name="refill">Defines the <see cref="IRefillStrategy"/> that computes how many tokens to add
|
|
/// back to the bucket each time consumption is attempted</param>
|
|
/// <param name="timeProvider">Defines the <see cref="ITimeProvider"/> used to enforce timeouts when
|
|
/// invoking <see cref="Consume"/></param>
|
|
public LeakyBucket(long capacity, ISleepStrategy sleep, IRefillStrategy refill, ITimeProvider timeProvider = null)
|
|
{
|
|
_sleep = sleep;
|
|
_refill = refill;
|
|
Capacity = capacity;
|
|
_available = capacity;
|
|
_timeProvider = timeProvider ?? RealTimeProvider.Instance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Blocks until the specified number of tokens are available for consumption
|
|
/// and then consumes that number of tokens.
|
|
/// </summary>
|
|
/// <param name="tokens">The number of tokens to consume</param>
|
|
/// <param name="timeout">The maximum amount of time, in milliseconds, to block. An exception is
|
|
/// throw in the event it takes longer than the stated timeout to consume the requested number
|
|
/// of tokens</param>
|
|
public void Consume(long tokens, long timeout = Timeout.Infinite)
|
|
{
|
|
if (timeout < Timeout.Infinite)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(timeout),
|
|
"Invalid timeout. Use -1 for no timeout, 0 for immediate timeout and a positive number " +
|
|
"of milliseconds to indicate a timeout. All other values are out of range."
|
|
);
|
|
}
|
|
|
|
var startTime = _timeProvider.GetUtcNow();
|
|
|
|
while (true)
|
|
{
|
|
if (TryConsume(tokens))
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (timeout != Timeout.Infinite)
|
|
{
|
|
// determine if the requested timeout has elapsed
|
|
var currentTime = _timeProvider.GetUtcNow();
|
|
var elapsedMilliseconds = (currentTime - startTime).TotalMilliseconds;
|
|
if (elapsedMilliseconds > timeout)
|
|
{
|
|
throw new TimeoutException("The operation timed out while waiting for the rate limit to be lifted.");
|
|
}
|
|
}
|
|
|
|
_sleep.Sleep();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to consume the specified number of tokens from the bucket. If the
|
|
/// requested number of tokens are not immediately available, then this method
|
|
/// will return false to indicate that zero tokens have been consumed.
|
|
/// </summary>
|
|
public bool TryConsume(long tokens)
|
|
{
|
|
if (tokens <= 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(tokens),
|
|
"Number of tokens to consume must be positive"
|
|
);
|
|
}
|
|
|
|
if (tokens > Capacity)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(tokens),
|
|
"Number of tokens to consume must be less than or equal to the capacity"
|
|
);
|
|
}
|
|
|
|
lock (_sync)
|
|
{
|
|
// determine how many units have become available since last invocation
|
|
var refilled = Math.Max(0, _refill.Refill());
|
|
|
|
// the number of tokens to add, the max of which is the difference between capacity and currently available
|
|
var deltaTokens = Math.Min(Capacity - _available, refilled);
|
|
|
|
// update the available number of units with the new tokens
|
|
_available += deltaTokens;
|
|
|
|
if (tokens > _available)
|
|
{
|
|
// we don't have enough tokens yet
|
|
Logging.Log.Trace($"LeakyBucket.TryConsume({tokens}): Failed to consumed tokens. Available: {_available}");
|
|
return false;
|
|
}
|
|
|
|
// subtract the number of tokens consumed
|
|
_available = _available - tokens;
|
|
Logging.Log.Trace($"LeakyBucket.TryConsume({tokens}): Successfully consumed tokens. Available: {_available}");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|