RequestEvents.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using Best.HTTP.Caching;
  5. using Best.HTTP.HostSetting;
  6. using Best.HTTP.Request.Settings;
  7. using Best.HTTP.Request.Timings;
  8. using Best.HTTP.Shared;
  9. using Best.HTTP.Shared.Extensions;
  10. namespace Best.HTTP.Hosts.Connections
  11. {
  12. public enum RequestEvents
  13. {
  14. Upgraded,
  15. DownloadProgress,
  16. UploadProgress,
  17. StreamingData,
  18. DownloadStarted,
  19. StateChange,
  20. SetState,
  21. QueuedResend,
  22. Resend,
  23. Headers
  24. }
  25. public readonly struct RequestEventInfo
  26. {
  27. public readonly HTTPRequest SourceRequest;
  28. public readonly RequestEvents Event;
  29. public readonly HTTPRequestStates State;
  30. public readonly Exception Error;
  31. public readonly long Progress;
  32. public readonly long ProgressLength;
  33. public readonly byte[] Data;
  34. public readonly int DataLength;
  35. // Headers
  36. public readonly Dictionary<string, List<string>> Headers;
  37. public RequestEventInfo(HTTPRequest request, RequestEvents @event)
  38. {
  39. this.SourceRequest = request;
  40. this.Event = @event;
  41. this.State = HTTPRequestStates.Initial;
  42. this.Error = null;
  43. this.Progress = this.ProgressLength = 0;
  44. this.Data = null;
  45. this.DataLength = 0;
  46. // Headers
  47. this.Headers = null;
  48. }
  49. public RequestEventInfo(HTTPRequest request, RequestEvents @event, HTTPRequestStates newState)
  50. {
  51. this.SourceRequest = request;
  52. this.Event = @event;
  53. this.State = newState;
  54. this.Error = null;
  55. this.Progress = this.ProgressLength = 0;
  56. this.Data = null;
  57. this.DataLength = 0;
  58. // Headers
  59. this.Headers = null;
  60. }
  61. public RequestEventInfo(HTTPRequest request, HTTPRequestStates newState)
  62. {
  63. this.SourceRequest = request;
  64. this.Event = RequestEvents.StateChange;
  65. this.State = newState;
  66. this.Error = null;
  67. this.Progress = this.ProgressLength = 0;
  68. this.Data = null;
  69. this.DataLength = 0;
  70. // Headers
  71. this.Headers = null;
  72. }
  73. public RequestEventInfo(HTTPRequest request, HTTPRequestStates newState, Exception error)
  74. {
  75. this.SourceRequest = request;
  76. this.Event = RequestEvents.SetState;
  77. this.State = newState;
  78. this.Error = error;
  79. this.Progress = this.ProgressLength = 0;
  80. this.Data = null;
  81. this.DataLength = 0;
  82. // Headers
  83. this.Headers = null;
  84. }
  85. public RequestEventInfo(HTTPRequest request, RequestEvents @event, long progress, long progressLength)
  86. {
  87. this.SourceRequest = request;
  88. this.Event = @event;
  89. this.State = HTTPRequestStates.Initial;
  90. this.Error = null;
  91. this.Progress = progress;
  92. this.ProgressLength = progressLength;
  93. this.Data = null;
  94. this.DataLength = 0;
  95. // Headers
  96. this.Headers = null;
  97. }
  98. public RequestEventInfo(HTTPRequest request, byte[] data, int dataLength)
  99. {
  100. this.SourceRequest = request;
  101. this.Event = RequestEvents.StreamingData;
  102. this.State = HTTPRequestStates.Initial;
  103. this.Error = null;
  104. this.Progress = this.ProgressLength = 0;
  105. this.Data = data;
  106. this.DataLength = dataLength;
  107. // Headers
  108. this.Headers = null;
  109. }
  110. public RequestEventInfo(HTTPRequest request, Dictionary<string, List<string>> headers)
  111. {
  112. this.SourceRequest = request;
  113. this.Event = RequestEvents.Headers;
  114. this.State = HTTPRequestStates.Initial;
  115. this.Error = null;
  116. this.Progress = this.ProgressLength = 0;
  117. this.Data = null;
  118. this.DataLength = 0;
  119. // Headers
  120. this.Headers = headers;
  121. }
  122. public override string ToString()
  123. {
  124. switch (this.Event)
  125. {
  126. case RequestEvents.Upgraded:
  127. return string.Format("[RequestEventInfo Event: Upgraded, Source: {0}]", this.SourceRequest.Context.Hash);
  128. case RequestEvents.DownloadProgress:
  129. return string.Format("[RequestEventInfo Event: DownloadProgress, Progress: {1}, ProgressLength: {2}, Source: {0}]", this.SourceRequest.Context.Hash, this.Progress, this.ProgressLength);
  130. case RequestEvents.UploadProgress:
  131. return string.Format("[RequestEventInfo Event: UploadProgress, Progress: {1}, ProgressLength: {2}, Source: {0}]", this.SourceRequest.Context.Hash, this.Progress, this.ProgressLength);
  132. case RequestEvents.StreamingData:
  133. return string.Format("[RequestEventInfo Event: StreamingData, DataLength: {1}, Source: {0}]", this.SourceRequest.Context.Hash, this.DataLength);
  134. case RequestEvents.DownloadStarted:
  135. return $"[RequestEventInfo Event: DownloadStarted, Source: {this.SourceRequest.Context.Hash}]";
  136. case RequestEvents.StateChange:
  137. return string.Format("[RequestEventInfo Event: StateChange, State: {1}, Source: {0}]", this.SourceRequest.Context.Hash, this.State);
  138. case RequestEvents.SetState:
  139. return string.Format("[RequestEventInfo Event: SetState, State: {1}, Source: {0}]", this.SourceRequest.Context.Hash, this.State);
  140. case RequestEvents.Resend:
  141. return string.Format("[RequestEventInfo Event: Resend, Source: {0}]", this.SourceRequest.Context.Hash);
  142. case RequestEvents.Headers:
  143. return string.Format("[RequestEventInfo Event: Headers, Source: {0}]", this.SourceRequest.Context.Hash);
  144. case RequestEvents.QueuedResend:
  145. return $"[RequestEventInfo Event: QueuedResend, Source: {this.SourceRequest.Context.Hash}]";
  146. default:
  147. throw new NotImplementedException(this.Event.ToString());
  148. }
  149. }
  150. }
  151. class ProgressFlattener
  152. {
  153. struct FlattenedProgress
  154. {
  155. public HTTPRequest request;
  156. public OnProgressDelegate onProgress;
  157. public long progress;
  158. public long length;
  159. }
  160. private FlattenedProgress[] progresses;
  161. private bool hasProgress;
  162. public void InsertOrUpdate(RequestEventInfo info, OnProgressDelegate onProgress)
  163. {
  164. if (progresses == null)
  165. progresses = new FlattenedProgress[1];
  166. hasProgress = true;
  167. var newProgress = new FlattenedProgress { request = info.SourceRequest, progress = info.Progress, length = info.ProgressLength, onProgress = onProgress };
  168. int firstEmptyIdx = -1;
  169. for (int i = 0; i < progresses.Length; i++)
  170. {
  171. var progress = progresses[i];
  172. if (object.ReferenceEquals(progress.request, info.SourceRequest))
  173. {
  174. progresses[i] = newProgress;
  175. return;
  176. }
  177. if (firstEmptyIdx == -1 && progress.request == null)
  178. firstEmptyIdx = i;
  179. }
  180. if (firstEmptyIdx == -1)
  181. {
  182. Array.Resize(ref progresses, progresses.Length + 1);
  183. progresses[progresses.Length - 1] = newProgress;
  184. }
  185. else
  186. progresses[firstEmptyIdx] = newProgress;
  187. }
  188. public void DispatchProgressCallbacks()
  189. {
  190. if (progresses == null || !hasProgress)
  191. return;
  192. for (int i = 0; i < progresses.Length; ++i)
  193. {
  194. var @event = progresses[i];
  195. var source = @event.request;
  196. if (source != null && @event.onProgress != null)
  197. {
  198. try
  199. {
  200. @event.onProgress(source, @event.progress, @event.length);
  201. }
  202. catch (Exception ex)
  203. {
  204. HTTPManager.Logger.Exception("ProgressFlattener", "DispatchProgressCallbacks", ex, source.Context);
  205. }
  206. }
  207. }
  208. Array.Clear(progresses, 0, progresses.Length);
  209. hasProgress = false;
  210. }
  211. }
  212. public static class RequestEventHelper
  213. {
  214. private static ConcurrentQueue<RequestEventInfo> requestEventQueue = new ConcurrentQueue<RequestEventInfo>();
  215. #pragma warning disable 0649
  216. public static Action<RequestEventInfo> OnEvent;
  217. #pragma warning restore
  218. // Low frame rate and high download/upload speed can add more download/upload progress events to dispatch in one frame.
  219. // This can add higher CPU usage as it might cause updating the UI/do other things unnecessary in the same frame.
  220. // To avoid this, instead of calling the events directly, we store the last event's data and call download/upload callbacks only once per frame.
  221. private static ProgressFlattener downloadProgress;
  222. private static ProgressFlattener uploadProgress;
  223. public static void EnqueueRequestEvent(RequestEventInfo ev)
  224. {
  225. if (HTTPManager.Logger.IsDiagnostic)
  226. HTTPManager.Logger.Information("RequestEventHelper", "Enqueue " + ev.ToString(), ev.SourceRequest.Context);
  227. requestEventQueue.Enqueue(ev);
  228. }
  229. internal static void Clear()
  230. {
  231. requestEventQueue.Clear();
  232. }
  233. internal static void ProcessQueue()
  234. {
  235. RequestEventInfo requestEvent;
  236. while (requestEventQueue.TryDequeue(out requestEvent))
  237. {
  238. HTTPRequest source = requestEvent.SourceRequest;
  239. if (HTTPManager.Logger.IsDiagnostic)
  240. HTTPManager.Logger.Information("RequestEventHelper", "Processing request event: " + requestEvent.ToString(), source.Context);
  241. if (OnEvent != null)
  242. {
  243. try
  244. {
  245. using (var _ = new Unity.Profiling.ProfilerMarker(nameof(OnEvent)).Auto())
  246. OnEvent(requestEvent);
  247. }
  248. catch (Exception ex)
  249. {
  250. HTTPManager.Logger.Exception("RequestEventHelper", "ProcessQueue", ex, source.Context);
  251. }
  252. }
  253. switch (requestEvent.Event)
  254. {
  255. case RequestEvents.DownloadProgress:
  256. try
  257. {
  258. if (source.DownloadSettings.OnDownloadProgress != null)
  259. {
  260. if (downloadProgress == null)
  261. downloadProgress = new ProgressFlattener();
  262. downloadProgress.InsertOrUpdate(requestEvent, source.DownloadSettings.OnDownloadProgress);
  263. }
  264. }
  265. catch (Exception ex)
  266. {
  267. HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.DownloadProgress", ex, source.Context);
  268. }
  269. break;
  270. case RequestEvents.UploadProgress:
  271. try
  272. {
  273. if (source.UploadSettings.OnUploadProgress != null)
  274. {
  275. if (uploadProgress == null)
  276. uploadProgress = new ProgressFlattener();
  277. uploadProgress.InsertOrUpdate(requestEvent, source.UploadSettings.OnUploadProgress);
  278. }
  279. }
  280. catch (Exception ex)
  281. {
  282. HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.UploadProgress", ex, source.Context);
  283. }
  284. break;
  285. case RequestEvents.QueuedResend:
  286. HandleQueued(source);
  287. goto case RequestEvents.Resend;
  288. case RequestEvents.Resend:
  289. source.State = HTTPRequestStates.Initial;
  290. var host = HostManager.GetHostVariant(source);
  291. host.Send(source);
  292. break;
  293. case RequestEvents.Headers:
  294. {
  295. try
  296. {
  297. var response = source.Response;
  298. if (source.DownloadSettings.OnHeadersReceived != null && response != null)
  299. source.DownloadSettings.OnHeadersReceived(source, response, requestEvent.Headers);
  300. }
  301. catch (Exception ex)
  302. {
  303. HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.Headers", ex, source.Context);
  304. }
  305. break;
  306. }
  307. case RequestEvents.DownloadStarted:
  308. try
  309. {
  310. // It's possible that response.DownStream is already null when this event is handled!
  311. var response = source.Response;
  312. var downStream = response?.DownStream;
  313. if (response != null && downStream != null)
  314. source.DownloadSettings.OnDownloadStarted?.Invoke(source, response, response.DownStream);
  315. }
  316. catch(Exception ex)
  317. {
  318. HTTPManager.Logger.Exception("RequestEventHelper", "DownloadStarted", ex, source.Context);
  319. }
  320. break;
  321. case RequestEvents.SetState:
  322. // In a case where the request is aborted its state is set to a >= Finished state then,
  323. // on another thread the request processing will fail too queuing up a >= Finished state again.
  324. if (source.State >= HTTPRequestStates.Finished && requestEvent.State >= HTTPRequestStates.Finished)
  325. continue;
  326. // It's different from the next condition! (this is >= and the next is only >)
  327. if (requestEvent.State >= HTTPRequestStates.Finished)
  328. source?.Response?.DownStream?.CompleteAdding(requestEvent.Error);
  329. if (requestEvent.State > HTTPRequestStates.Finished)
  330. {
  331. HTTPManager.Logger.Information("RequestEventHelper", $"{requestEvent.State}: discarding response!", source.Response?.Context ?? source.Context);
  332. source.Response?.Dispose();
  333. source.Response = null;
  334. }
  335. source.Exception = requestEvent.Error;
  336. source.State = requestEvent.State;
  337. // https://www.rfc-editor.org/rfc/rfc5861.html#section-1
  338. // The stale-if-error HTTP Cache-Control extension allows a cache to
  339. // return a stale response when an error -- e.g., a 500 Internal Server
  340. // Error, a network segment, or DNS failure -- is encountered, rather
  341. // than returning a "hard" error.
  342. if (requestEvent.State > HTTPRequestStates.Finished && requestEvent.State != HTTPRequestStates.Aborted)
  343. {
  344. if (HTTPManager.LocalCache != null && !source.DownloadSettings.DisableCache)
  345. {
  346. var hash = Caching.HTTPCache.CalculateHash(source.MethodType, source.CurrentUri);
  347. if (HTTPManager.LocalCache.CanServeWithoutValidation(hash, ErrorTypeForValidation.ConnectionError, source.Context))
  348. {
  349. HTTPManager.LocalCache.Redirect(source, hash);
  350. goto case RequestEvents.Resend;
  351. }
  352. }
  353. }
  354. goto case RequestEvents.StateChange;
  355. case RequestEvents.StateChange:
  356. try
  357. {
  358. using (var _ = new Unity.Profiling.ProfilerMarker(nameof(RequestEventHelper.HandleRequestStateChange)).Auto())
  359. RequestEventHelper.HandleRequestStateChange(requestEvent);
  360. }
  361. catch(Exception ex)
  362. {
  363. HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange", ex, source.Context);
  364. }
  365. break;
  366. }
  367. }
  368. uploadProgress?.DispatchProgressCallbacks();
  369. downloadProgress?.DispatchProgressCallbacks();
  370. }
  371. // TODO: don't start/repeat if can't time out?
  372. private static bool AbortRequestWhenTimedOut(DateTime now, object context)
  373. {
  374. HTTPRequest request = context as HTTPRequest;
  375. if (request.State >= HTTPRequestStates.Finished)
  376. return false; // don't repeat
  377. var downStream= request.Response?.DownStream;
  378. if (downStream != null && downStream.DoFullCheck(limit: 2))
  379. {
  380. var warning = $"Request's download stream is full({downStream.Length:N0}/{downStream.MaxBuffered:N0}) without any Read attempt! You can either increase HTTPRequest's DownloadSettings.ContentStreamMaxBuffered or use streaming. Request's uri: {request.Uri}. See https://bestdocshub.pages.dev/HTTP/getting-started/downloads/ for more details!";
  381. if (HTTPManager.Logger.IsDiagnostic)
  382. HTTPManager.Logger.Warning(nameof(RequestEventHelper), warning, request.Context);
  383. else
  384. UnityEngine.Debug.Log(warning);
  385. // increase buffer limit
  386. downStream.EmergencyIncreaseMaxBuffered();
  387. return false;
  388. }
  389. // Upgradable protocols will shut down themselves
  390. if (request?.Response?.IsUpgraded is bool upgraded && upgraded)
  391. return false;
  392. if (request.TimeoutSettings.IsTimedOut(HTTPManager.CurrentFrameDateTime))
  393. {
  394. HTTPManager.Logger.Information("RequestEventHelper", "AbortRequestWhenTimedOut - Request timed out. CurrentUri: " + request.CurrentUri.ToString(), request.Context);
  395. request.Abort();
  396. return false; // don't repeat
  397. }
  398. return true; // repeat
  399. }
  400. private static void HandleQueued(HTTPRequest source)
  401. {
  402. source.Timing.StartNext(TimingEventNames.Queued);
  403. source.TimeoutSettings.QueuedAt = HTTPManager.CurrentFrameDateTime;
  404. Timer.Add(new TimerData(TimeSpan.FromSeconds(1), source, AbortRequestWhenTimedOut));
  405. }
  406. static readonly string[] RequestStateNames = new string[] { "Initial", "Queued", "Processing", "Finished", "Error", "Aborted", "ConnectionTimedOut", "TimedOut" };
  407. private static void HandleRequestStateChange(RequestEventInfo @event)
  408. {
  409. HTTPRequest source = @event.SourceRequest;
  410. // Because there's a race condition between setting the request's State in its Abort() function running on Unity's main thread
  411. // and the HTTP1/HTTP2 handlers running on an another one.
  412. // Because of these race conditions cases violating expectations can be:
  413. // 1.) State is finished but the response null
  414. // 2.) State is (Connection)TimedOut and the response non-null
  415. // We have to make sure that no callbacks are called twice and in the request must be in a consistent state!
  416. // State | Request
  417. // --------- +---------
  418. // 1 Null
  419. // Finished | Skip
  420. // Timeout/Abort | Deliver
  421. //
  422. // 2 Non-Null
  423. // Finished | Deliver
  424. // Timeout/Abort | Skip
  425. using var _ = new Unity.Profiling.ProfilerMarker(RequestStateNames[(int)@event.State]).Auto();
  426. switch (@event.State)
  427. {
  428. case HTTPRequestStates.Queued:
  429. HandleQueued(source);
  430. break;
  431. case HTTPRequestStates.ConnectionTimedOut:
  432. case HTTPRequestStates.TimedOut:
  433. case HTTPRequestStates.Error:
  434. case HTTPRequestStates.Aborted:
  435. if (HTTPManager.Logger.IsDiagnostic)
  436. HTTPManager.Logger.Information("RequestEventHelper", $"{@event.State}: discarding response!", source.Response?.Context ?? source.Context);
  437. source.Response?.Dispose();
  438. source.Response = null;
  439. goto case HTTPRequestStates.Finished;
  440. case HTTPRequestStates.Finished:
  441. // Dispatch any collected download/upload progress, otherwise they would _after_ the callback!
  442. uploadProgress?.DispatchProgressCallbacks();
  443. downloadProgress?.DispatchProgressCallbacks();
  444. if (source.Callback != null)
  445. {
  446. source.Timing.AddEvent(new TimingEventInfo(source, TimingEvents.Finish, null));
  447. source.Timing.StartNext(TimingEventNames.Callback);
  448. try
  449. {
  450. using (var __ = new Unity.Profiling.ProfilerMarker(nameof(source.Callback)).Auto())
  451. source.Callback(source, source.Response);
  452. }
  453. catch (Exception ex)
  454. {
  455. HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange " + @event.State, ex, source.Context);
  456. }
  457. }
  458. source.Timing.Finish();
  459. if (source.Callback == null)
  460. {
  461. // This delay required because with coroutines these lines are executed first
  462. // before the coroutine has a chance to do something with a finished request.
  463. // By adding a delay there's a time window that the coroutine can run its logic too inbetween.
  464. Timer.Add(new TimerData(TimeSpan.FromSeconds(1), source, OnDelayedDisposeTimer));
  465. }
  466. else
  467. {
  468. if (HTTPManager.Logger.IsDiagnostic)
  469. HTTPManager.Logger.Information("RequestEventHelper", $"{nameof(OnDelayedDisposeTimer)} - disposing response!", source.Context);
  470. source.Dispose();
  471. }
  472. HostManager.GetHostVariant(source)
  473. .TryToSendQueuedRequests();
  474. break;
  475. }
  476. }
  477. private static bool OnDelayedDisposeTimer(DateTime time, object request)
  478. {
  479. var source = request as HTTPRequest;
  480. if (HTTPManager.Logger.IsDiagnostic)
  481. HTTPManager.Logger.Information("RequestEventHelper", $"{nameof(OnDelayedDisposeTimer)} - disposing response!", source.Context);
  482. source.Dispose();
  483. return false;
  484. }
  485. }
  486. }