Lean  $LEAN_TAG$
DataMonitor.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.Collections.Generic;
18 using System.IO;
19 using System.Threading;
20 using Newtonsoft.Json;
23 using QuantConnect.Util;
24 
25 namespace QuantConnect.Data
26 {
27  /// <summary>
28  /// Monitors data requests and reports on missing data
29  /// </summary>
30  public class DataMonitor : IDataMonitor
31  {
32  private bool _exited;
33 
34  private TextWriter _succeededDataRequestsWriter;
35  private TextWriter _failedDataRequestsWriter;
36 
37  private long _succeededDataRequestsCount;
38  private long _failedDataRequestsCount;
39 
40  private long _succeededUniverseDataRequestsCount;
41  private long _failedUniverseDataRequestsCount;
42 
43  private readonly List<double> _requestRates = new();
44  private long _prevRequestsCount;
45  private DateTime _lastRequestRateCalculationTime;
46 
47  private Thread _requestRateCalculationThread;
48  private CancellationTokenSource _cancellationTokenSource;
49 
50  private readonly string _succeededDataRequestsFileName;
51  private readonly string _failedDataRequestsFileName;
52  private readonly string _resultsDestinationFolder;
53 
54  private readonly object _threadLock = new();
55 
56  /// <summary>
57  /// Initializes a new instance of the <see cref="DataMonitor"/> class
58  /// </summary>
59  public DataMonitor()
60  {
61  _resultsDestinationFolder = Globals.ResultsDestinationFolder;
62  _succeededDataRequestsFileName = GetFilePath("succeeded-data-requests.txt");
63  _failedDataRequestsFileName = GetFilePath("failed-data-requests.txt");
64  }
65 
66  /// <summary>
67  /// Event handler for the <see cref="IDataProvider.NewDataRequest"/> event
68  /// </summary>
70  {
71  if (_exited)
72  {
73  return;
74  }
75 
76  Initialize();
77 
78  if (e.Path.Contains("map_files", StringComparison.OrdinalIgnoreCase) ||
79  e.Path.Contains("factor_files", StringComparison.OrdinalIgnoreCase))
80  {
81  return;
82  }
83 
84  var path = StripDataFolder(e.Path);
85  var isUniverseData = path.Contains("coarse", StringComparison.OrdinalIgnoreCase) ||
86  path.Contains("universe", StringComparison.OrdinalIgnoreCase);
87 
88  if (e.Succeded)
89  {
90  WriteLineToFile(_succeededDataRequestsWriter, path, _succeededDataRequestsFileName);
91  Interlocked.Increment(ref _succeededDataRequestsCount);
92  if (isUniverseData)
93  {
94  Interlocked.Increment(ref _succeededUniverseDataRequestsCount);
95  }
96  }
97  else
98  {
99  WriteLineToFile(_failedDataRequestsWriter, path, _failedDataRequestsFileName);
100  Interlocked.Increment(ref _failedDataRequestsCount);
101  if (isUniverseData)
102  {
103  Interlocked.Increment(ref _failedUniverseDataRequestsCount);
104  }
105 
106  if (Logging.Log.DebuggingEnabled)
107  {
108  Logging.Log.Debug($"DataMonitor.OnNewDataRequest(): Data from {path} could not be fetched");
109  }
110  }
111  }
112 
113  /// <summary>
114  /// Terminates the data monitor generating a final report
115  /// </summary>
116  public void Exit()
117  {
118  if (_exited || _requestRateCalculationThread == null)
119  {
120  return;
121  }
122  _exited = true;
123 
124  _requestRateCalculationThread.StopSafely(TimeSpan.FromSeconds(5), _cancellationTokenSource);
125  _succeededDataRequestsWriter?.Close();
126  _failedDataRequestsWriter?.Close();
127 
128  StoreDataMonitorReport(GenerateReport());
129 
130  _succeededDataRequestsWriter.DisposeSafely();
131  _failedDataRequestsWriter.DisposeSafely();
132  _cancellationTokenSource.DisposeSafely();
133  }
134 
135  public void Dispose()
136  {
137  Exit();
138  }
139 
140  protected virtual string StripDataFolder(string path)
141  {
142  if (path.StartsWith(Globals.DataFolder, StringComparison.OrdinalIgnoreCase))
143  {
144  return path.Substring(Globals.DataFolder.Length);
145  }
146 
147  return path;
148  }
149 
150  /// <summary>
151  /// Initializes the <see cref="DataMonitor"/> instance
152  /// </summary>
153  private void Initialize()
154  {
155  if (_requestRateCalculationThread != null)
156  {
157  return;
158  }
159  lock (_threadLock)
160  {
161  if (_requestRateCalculationThread != null)
162  {
163  return;
164  }
165  // we create the files on demand
166  _succeededDataRequestsWriter = OpenStream(_succeededDataRequestsFileName);
167  _failedDataRequestsWriter = OpenStream(_failedDataRequestsFileName);
168 
169  _cancellationTokenSource = new CancellationTokenSource();
170 
171  _requestRateCalculationThread = new Thread(() =>
172  {
173  while (!_cancellationTokenSource.Token.WaitHandle.WaitOne(3000))
174  {
175  ComputeFileRequestFrequency();
176  }
177  })
178  { IsBackground = true };
179  _requestRateCalculationThread.Start();
180  }
181  }
182 
183  private DataMonitorReport GenerateReport()
184  {
185  var report = new DataMonitorReport(_succeededDataRequestsCount,
186  _failedDataRequestsCount,
187  _succeededUniverseDataRequestsCount,
188  _failedUniverseDataRequestsCount,
189  _requestRates);
190 
191  Logging.Log.Trace($"DataMonitor.GenerateReport():{Environment.NewLine}" +
192  $"DATA USAGE:: Total data requests {report.TotalRequestsCount}{Environment.NewLine}" +
193  $"DATA USAGE:: Succeeded data requests {report.SucceededDataRequestsCount}{Environment.NewLine}" +
194  $"DATA USAGE:: Failed data requests {report.FailedDataRequestsCount}{Environment.NewLine}" +
195  $"DATA USAGE:: Failed data requests percentage {report.FailedDataRequestsPercentage}%{Environment.NewLine}" +
196  $"DATA USAGE:: Total universe data requests {report.TotalUniverseDataRequestsCount}{Environment.NewLine}" +
197  $"DATA USAGE:: Succeeded universe data requests {report.SucceededUniverseDataRequestsCount}{Environment.NewLine}" +
198  $"DATA USAGE:: Failed universe data requests {report.FailedUniverseDataRequestsCount}{Environment.NewLine}" +
199  $"DATA USAGE:: Failed universe data requests percentage {report.FailedUniverseDataRequestsPercentage}%");
200 
201  return report;
202  }
203 
204  private void ComputeFileRequestFrequency()
205  {
206  var requestsCount = _succeededDataRequestsCount + _failedDataRequestsCount;
207 
208  if (_lastRequestRateCalculationTime == default)
209  {
210  // First time we calculate the request rate.
211  // We don't have a previous value to compare to so we just store the current value.
212  _lastRequestRateCalculationTime = DateTime.UtcNow;
213  _prevRequestsCount = requestsCount;
214  return;
215  }
216 
217  var requestsCountDelta = requestsCount - _prevRequestsCount;
218  var now = DateTime.UtcNow;
219  var timeDelta = now - _lastRequestRateCalculationTime;
220 
221  _requestRates.Add(Math.Round(requestsCountDelta / timeDelta.TotalSeconds));
222  _prevRequestsCount = requestsCount;
223  _lastRequestRateCalculationTime = now;
224  }
225 
226  /// <summary>
227  /// Stores the data monitor report
228  /// </summary>
229  /// <param name="report">The data monitor report to be stored<param>
230  private void StoreDataMonitorReport(DataMonitorReport report)
231  {
232  if (report == null)
233  {
234  return;
235  }
236 
237  var path = GetFilePath("data-monitor-report.json");
238  var data = JsonConvert.SerializeObject(report, Formatting.None);
239  File.WriteAllText(path, data);
240  }
241 
242  private string GetFilePath(string filename)
243  {
244  var baseFilename = Path.GetFileNameWithoutExtension(filename);
245  var timestamp = DateTime.UtcNow.ToStringInvariant("yyyyMMddHHmmssfff");
246  var extension = Path.GetExtension(filename);
247  return Path.Combine(_resultsDestinationFolder, $"{baseFilename}-{timestamp}{extension}");
248  }
249 
250  private static TextWriter OpenStream(string filename)
251  {
252  var writer = new StreamWriter(filename);
253  return TextWriter.Synchronized(writer);
254  }
255 
256  private static void WriteLineToFile(TextWriter writer, string line, string filename)
257  {
258  try
259  {
260  writer.WriteLine(line);
261  }
262  catch (IOException exception)
263  {
264  Logging.Log.Error($"DataMonitor.OnNewDataRequest(): Failed to write to file {filename}: {exception.Message}");
265  }
266  }
267  }
268 }