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