Lean  $LEAN_TAG$
AlgorithmTimeLimitManager.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Threading;
18 using QuantConnect.Logging;
19 using QuantConnect.Util;
21 
23 {
24  /// <summary>
25  /// Provides an implementation of <see cref="IIsolatorLimitResultProvider"/> that tracks the algorithm
26  /// manager's time loops and enforces a maximum amount of time that each time loop may take to execute.
27  /// The isolator uses the result provided by <see cref="IsWithinLimit"/> to determine if it should
28  /// terminate the algorithm for violation of the imposed limits.
29  /// </summary>
31  {
32  private volatile bool _failed;
33  private volatile bool _stopped;
34  private long _additionalMinutes;
35 
36  private volatile ReferenceWrapper<DateTime> _currentTimeStepTime;
37  private readonly TimeSpan _timeLoopMaximum;
38 
39  /// <summary>
40  /// Gets the additional time bucket which is responsible for tracking additional time requested
41  /// for processing via long-running scheduled events. In LEAN, we use the <see cref="LeakyBucket"/>
42  /// </summary>
44 
45  /// <summary>
46  /// Initializes a new instance of <see cref="AlgorithmTimeLimitManager"/> to manage the
47  /// creation of <see cref="IsolatorLimitResult"/> instances as it pertains to the
48  /// algorithm manager's time loop
49  /// </summary>
50  /// <param name="additionalTimeBucket">Provides a bucket of additional time that can be requested to be
51  /// spent to give execution time for things such as training scheduled events</param>
52  /// <param name="timeLoopMaximum">Specifies the maximum amount of time the algorithm is permitted to
53  /// spend in a single time loop. This value can be overriden if certain actions are taken by the
54  /// algorithm, such as invoking the training methods.</param>
55  public AlgorithmTimeLimitManager(ITokenBucket additionalTimeBucket, TimeSpan timeLoopMaximum)
56  {
57  _timeLoopMaximum = timeLoopMaximum;
58  AdditionalTimeBucket = additionalTimeBucket;
59  _currentTimeStepTime = new ReferenceWrapper<DateTime>(DateTime.MinValue);
60  }
61 
62  /// <summary>
63  /// Invoked by the algorithm at the start of each time loop. This resets the current time step
64  /// elapsed time.
65  /// </summary>
66  /// <remarks>
67  /// This class is the result of a mechanical refactor with the intention of preserving all existing
68  /// behavior, including setting the <code>_currentTimeStepTime</code> to <see cref="DateTime.MinValue"/>
69  /// </remarks>
70  public void StartNewTimeStep()
71  {
72  if (_stopped)
73  {
74  throw new InvalidOperationException("The AlgorithmTimeLimitManager may not be stopped and restarted.");
75  }
76 
77  // maintains existing implementation behavior to reset the time to min value and then
78  // when the isolator pings IsWithinLimit, invocation of CurrentTimeStepElapsed will cause
79  // it to update to the current time. This was done as a performance improvement and moved
80  // accessing DateTime.UtcNow from the algorithm manager thread to the isolator thread
81  _currentTimeStepTime = new ReferenceWrapper<DateTime>(DateTime.MinValue);
82  Interlocked.Exchange(ref _additionalMinutes, 0L);
83  }
84 
85  /// <summary>
86  /// Stops this instance from tracking the algorithm manager's time loop elapsed time.
87  /// This is invoked at the end of the algorithm to prevent the isolator from terminating
88  /// the algorithm during final clean up and shutdown.
89  /// </summary>
90  internal void StopEnforcingTimeLimit()
91  {
92  _stopped = true;
93  }
94 
95  /// <summary>
96  /// Determines whether or not the algorithm time loop is considered within the limits
97  /// </summary>
99  {
100  TimeSpan currentTimeStepElapsed;
101  var message = IsOutOfTime(out currentTimeStepElapsed) ? GetErrorMessage(currentTimeStepElapsed) : string.Empty;
102  return new IsolatorLimitResult(currentTimeStepElapsed, message);
103  }
104 
105  /// <summary>
106  /// Requests additional time to continue executing the current time step.
107  /// At time of writing, this is intended to be used to provide training scheduled events
108  /// additional time to allow complex training models time to execute while also preventing
109  /// abuse by enforcing certain control parameters set via the job packet.
110  ///
111  /// Each time this method is invoked, this time limit manager will increase the allowable
112  /// execution time by the specified number of whole minutes
113  /// </summary>
114  public void RequestAdditionalTime(int minutes)
115  {
116  if (!TryRequestAdditionalTime(minutes))
117  {
118  _failed = true;
119  Log.Debug($"AlgorithmTimeLimitManager.RequestAdditionalTime({minutes}): Failed to acquire additional time. Marking failed.");
120  }
121  }
122 
123  /// <summary>
124  /// Attempts to requests additional time to continue executing the current time step.
125  /// At time of writing, this is intended to be used to provide training scheduled events
126  /// additional time to allow complex training models time to execute while also preventing
127  /// abuse by enforcing certain control parameters set via the job packet.
128  ///
129  /// Each time this method is invoked, this time limit manager will increase the allowable
130  /// execution time by the specified number of whole minutes
131  /// </summary>
132  public bool TryRequestAdditionalTime(int minutes)
133  {
134  Log.Debug($"AlgorithmTimeLimitManager.TryRequestAdditionalTime({minutes}): Requesting additional time. Available: {AdditionalTimeBucket.AvailableTokens}");
135 
136  // safely attempts to consume from the bucket, returning false if insufficient resources available
137  if (AdditionalTimeBucket.TryConsume(minutes))
138  {
139  var newValue = Interlocked.Add(ref _additionalMinutes, minutes);
140  Log.Debug($"AlgorithmTimeLimitManager.TryRequestAdditionalTime({minutes}): Success: AdditionalMinutes: {newValue}");
141  return true;
142  }
143 
144  return false;
145  }
146 
147  /// <summary>
148  /// Determines whether or not the algorithm should be terminated due to exceeding the time limits
149  /// </summary>
150  private bool IsOutOfTime(out TimeSpan currentTimeStepElapsed)
151  {
152  if (_stopped)
153  {
154  currentTimeStepElapsed = TimeSpan.Zero;
155  return false;
156  }
157 
158  currentTimeStepElapsed = GetCurrentTimeStepElapsed();
159  if (_failed)
160  {
161  return true;
162  }
163 
164  var additionalMinutes = TimeSpan.FromMinutes(Interlocked.Read(ref _additionalMinutes));
165  return currentTimeStepElapsed > _timeLoopMaximum.Add(additionalMinutes);
166  }
167 
168  /// <summary>
169  /// Gets the current amount of time that has elapsed since the beginning of the
170  /// most recent algorithm manager time loop
171  /// </summary>
172  private TimeSpan GetCurrentTimeStepElapsed()
173  {
174  var currentValue = _currentTimeStepTime.Value;
175  if (currentValue == DateTime.MinValue)
176  {
177  _currentTimeStepTime = new ReferenceWrapper<DateTime>(DateTime.UtcNow);
178  return TimeSpan.Zero;
179  }
180  // here we use currentValue on purpose since '_currentTimeStepTime' could have been overwritten to 'DateTime.MinValue'
181  return DateTime.UtcNow - currentValue;
182  }
183 
184  private string GetErrorMessage(TimeSpan currentTimeStepElapsed)
185  {
186  var message = $"Algorithm took longer than {_timeLoopMaximum.TotalMinutes} minutes on a single time loop.";
187 
188  var minutesAboveStandardLimit = _additionalMinutes - (int) _timeLoopMaximum.TotalMinutes;
189  if (minutesAboveStandardLimit > 0)
190  {
191  message = $"{message} An additional {minutesAboveStandardLimit} minutes were also allocated and consumed.";
192  }
193 
194  message = $"{message} CurrentTimeStepElapsed: {currentTimeStepElapsed.TotalMinutes:0.0} minutes";
195 
196  return message;
197  }
198  }
199 }