123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595 |
- using System;
- using System.Collections.Concurrent;
- using System.Collections.Generic;
- using Best.HTTP.Caching;
- using Best.HTTP.HostSetting;
- using Best.HTTP.Request.Settings;
- using Best.HTTP.Request.Timings;
- using Best.HTTP.Shared;
- using Best.HTTP.Shared.Extensions;
- namespace Best.HTTP.Hosts.Connections
- {
- public enum RequestEvents
- {
- Upgraded,
- DownloadProgress,
- UploadProgress,
- StreamingData,
- DownloadStarted,
- StateChange,
- SetState,
- QueuedResend,
- Resend,
- Headers
- }
- public readonly struct RequestEventInfo
- {
- public readonly HTTPRequest SourceRequest;
- public readonly RequestEvents Event;
- public readonly HTTPRequestStates State;
- public readonly Exception Error;
- public readonly long Progress;
- public readonly long ProgressLength;
- public readonly byte[] Data;
- public readonly int DataLength;
- // Headers
- public readonly Dictionary<string, List<string>> Headers;
- public RequestEventInfo(HTTPRequest request, RequestEvents @event)
- {
- this.SourceRequest = request;
- this.Event = @event;
- this.State = HTTPRequestStates.Initial;
- this.Error = null;
- this.Progress = this.ProgressLength = 0;
- this.Data = null;
- this.DataLength = 0;
- // Headers
- this.Headers = null;
- }
- public RequestEventInfo(HTTPRequest request, RequestEvents @event, HTTPRequestStates newState)
- {
- this.SourceRequest = request;
- this.Event = @event;
- this.State = newState;
- this.Error = null;
- this.Progress = this.ProgressLength = 0;
- this.Data = null;
- this.DataLength = 0;
- // Headers
- this.Headers = null;
- }
- public RequestEventInfo(HTTPRequest request, HTTPRequestStates newState)
- {
- this.SourceRequest = request;
- this.Event = RequestEvents.StateChange;
- this.State = newState;
- this.Error = null;
- this.Progress = this.ProgressLength = 0;
- this.Data = null;
- this.DataLength = 0;
- // Headers
- this.Headers = null;
- }
- public RequestEventInfo(HTTPRequest request, HTTPRequestStates newState, Exception error)
- {
- this.SourceRequest = request;
- this.Event = RequestEvents.SetState;
- this.State = newState;
- this.Error = error;
- this.Progress = this.ProgressLength = 0;
- this.Data = null;
- this.DataLength = 0;
- // Headers
- this.Headers = null;
- }
- public RequestEventInfo(HTTPRequest request, RequestEvents @event, long progress, long progressLength)
- {
- this.SourceRequest = request;
- this.Event = @event;
- this.State = HTTPRequestStates.Initial;
- this.Error = null;
- this.Progress = progress;
- this.ProgressLength = progressLength;
- this.Data = null;
- this.DataLength = 0;
- // Headers
- this.Headers = null;
- }
- public RequestEventInfo(HTTPRequest request, byte[] data, int dataLength)
- {
- this.SourceRequest = request;
- this.Event = RequestEvents.StreamingData;
- this.State = HTTPRequestStates.Initial;
- this.Error = null;
- this.Progress = this.ProgressLength = 0;
- this.Data = data;
- this.DataLength = dataLength;
- // Headers
- this.Headers = null;
- }
- public RequestEventInfo(HTTPRequest request, Dictionary<string, List<string>> headers)
- {
- this.SourceRequest = request;
- this.Event = RequestEvents.Headers;
- this.State = HTTPRequestStates.Initial;
- this.Error = null;
- this.Progress = this.ProgressLength = 0;
- this.Data = null;
- this.DataLength = 0;
- // Headers
- this.Headers = headers;
- }
- public override string ToString()
- {
- switch (this.Event)
- {
- case RequestEvents.Upgraded:
- return string.Format("[RequestEventInfo Event: Upgraded, Source: {0}]", this.SourceRequest.Context.Hash);
- case RequestEvents.DownloadProgress:
- return string.Format("[RequestEventInfo Event: DownloadProgress, Progress: {1}, ProgressLength: {2}, Source: {0}]", this.SourceRequest.Context.Hash, this.Progress, this.ProgressLength);
- case RequestEvents.UploadProgress:
- return string.Format("[RequestEventInfo Event: UploadProgress, Progress: {1}, ProgressLength: {2}, Source: {0}]", this.SourceRequest.Context.Hash, this.Progress, this.ProgressLength);
- case RequestEvents.StreamingData:
- return string.Format("[RequestEventInfo Event: StreamingData, DataLength: {1}, Source: {0}]", this.SourceRequest.Context.Hash, this.DataLength);
- case RequestEvents.DownloadStarted:
- return $"[RequestEventInfo Event: DownloadStarted, Source: {this.SourceRequest.Context.Hash}]";
- case RequestEvents.StateChange:
- return string.Format("[RequestEventInfo Event: StateChange, State: {1}, Source: {0}]", this.SourceRequest.Context.Hash, this.State);
- case RequestEvents.SetState:
- return string.Format("[RequestEventInfo Event: SetState, State: {1}, Source: {0}]", this.SourceRequest.Context.Hash, this.State);
- case RequestEvents.Resend:
- return string.Format("[RequestEventInfo Event: Resend, Source: {0}]", this.SourceRequest.Context.Hash);
- case RequestEvents.Headers:
- return string.Format("[RequestEventInfo Event: Headers, Source: {0}]", this.SourceRequest.Context.Hash);
- case RequestEvents.QueuedResend:
- return $"[RequestEventInfo Event: QueuedResend, Source: {this.SourceRequest.Context.Hash}]";
- default:
- throw new NotImplementedException(this.Event.ToString());
- }
- }
- }
- class ProgressFlattener
- {
- struct FlattenedProgress
- {
- public HTTPRequest request;
- public OnProgressDelegate onProgress;
- public long progress;
- public long length;
- }
- private FlattenedProgress[] progresses;
- private bool hasProgress;
- public void InsertOrUpdate(RequestEventInfo info, OnProgressDelegate onProgress)
- {
- if (progresses == null)
- progresses = new FlattenedProgress[1];
- hasProgress = true;
- var newProgress = new FlattenedProgress { request = info.SourceRequest, progress = info.Progress, length = info.ProgressLength, onProgress = onProgress };
- int firstEmptyIdx = -1;
- for (int i = 0; i < progresses.Length; i++)
- {
- var progress = progresses[i];
- if (object.ReferenceEquals(progress.request, info.SourceRequest))
- {
- progresses[i] = newProgress;
- return;
- }
- if (firstEmptyIdx == -1 && progress.request == null)
- firstEmptyIdx = i;
- }
- if (firstEmptyIdx == -1)
- {
- Array.Resize(ref progresses, progresses.Length + 1);
- progresses[progresses.Length - 1] = newProgress;
- }
- else
- progresses[firstEmptyIdx] = newProgress;
- }
- public void DispatchProgressCallbacks()
- {
- if (progresses == null || !hasProgress)
- return;
- for (int i = 0; i < progresses.Length; ++i)
- {
- var @event = progresses[i];
- var source = @event.request;
- if (source != null && @event.onProgress != null)
- {
- try
- {
- @event.onProgress(source, @event.progress, @event.length);
- }
- catch (Exception ex)
- {
- HTTPManager.Logger.Exception("ProgressFlattener", "DispatchProgressCallbacks", ex, source.Context);
- }
- }
- }
- Array.Clear(progresses, 0, progresses.Length);
- hasProgress = false;
- }
- }
- public static class RequestEventHelper
- {
- private static ConcurrentQueue<RequestEventInfo> requestEventQueue = new ConcurrentQueue<RequestEventInfo>();
- #pragma warning disable 0649
- public static Action<RequestEventInfo> OnEvent;
- #pragma warning restore
- // Low frame rate and high download/upload speed can add more download/upload progress events to dispatch in one frame.
- // This can add higher CPU usage as it might cause updating the UI/do other things unnecessary in the same frame.
- // 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.
- private static ProgressFlattener downloadProgress;
- private static ProgressFlattener uploadProgress;
- public static void EnqueueRequestEvent(RequestEventInfo ev)
- {
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("RequestEventHelper", "Enqueue " + ev.ToString(), ev.SourceRequest.Context);
- requestEventQueue.Enqueue(ev);
- }
- internal static void Clear()
- {
- requestEventQueue.Clear();
- }
- internal static void ProcessQueue()
- {
- RequestEventInfo requestEvent;
- while (requestEventQueue.TryDequeue(out requestEvent))
- {
- HTTPRequest source = requestEvent.SourceRequest;
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("RequestEventHelper", "Processing request event: " + requestEvent.ToString(), source.Context);
- if (OnEvent != null)
- {
- try
- {
- using (var _ = new Unity.Profiling.ProfilerMarker(nameof(OnEvent)).Auto())
- OnEvent(requestEvent);
- }
- catch (Exception ex)
- {
- HTTPManager.Logger.Exception("RequestEventHelper", "ProcessQueue", ex, source.Context);
- }
- }
-
- switch (requestEvent.Event)
- {
- case RequestEvents.DownloadProgress:
- try
- {
- if (source.DownloadSettings.OnDownloadProgress != null)
- {
- if (downloadProgress == null)
- downloadProgress = new ProgressFlattener();
- downloadProgress.InsertOrUpdate(requestEvent, source.DownloadSettings.OnDownloadProgress);
- }
- }
- catch (Exception ex)
- {
- HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.DownloadProgress", ex, source.Context);
- }
- break;
- case RequestEvents.UploadProgress:
- try
- {
- if (source.UploadSettings.OnUploadProgress != null)
- {
- if (uploadProgress == null)
- uploadProgress = new ProgressFlattener();
- uploadProgress.InsertOrUpdate(requestEvent, source.UploadSettings.OnUploadProgress);
- }
- }
- catch (Exception ex)
- {
- HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.UploadProgress", ex, source.Context);
- }
- break;
- case RequestEvents.QueuedResend:
- HandleQueued(source);
- goto case RequestEvents.Resend;
- case RequestEvents.Resend:
- source.State = HTTPRequestStates.Initial;
- var host = HostManager.GetHostVariant(source);
- host.Send(source);
- break;
- case RequestEvents.Headers:
- {
- try
- {
- var response = source.Response;
- if (source.DownloadSettings.OnHeadersReceived != null && response != null)
- source.DownloadSettings.OnHeadersReceived(source, response, requestEvent.Headers);
- }
- catch (Exception ex)
- {
- HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.Headers", ex, source.Context);
- }
- break;
- }
- case RequestEvents.DownloadStarted:
- try
- {
- // It's possible that response.DownStream is already null when this event is handled!
- var response = source.Response;
- var downStream = response?.DownStream;
- if (response != null && downStream != null)
- source.DownloadSettings.OnDownloadStarted?.Invoke(source, response, response.DownStream);
- }
- catch(Exception ex)
- {
- HTTPManager.Logger.Exception("RequestEventHelper", "DownloadStarted", ex, source.Context);
- }
- break;
- case RequestEvents.SetState:
- // In a case where the request is aborted its state is set to a >= Finished state then,
- // on another thread the request processing will fail too queuing up a >= Finished state again.
- if (source.State >= HTTPRequestStates.Finished && requestEvent.State >= HTTPRequestStates.Finished)
- continue;
- // It's different from the next condition! (this is >= and the next is only >)
- if (requestEvent.State >= HTTPRequestStates.Finished)
- source?.Response?.DownStream?.CompleteAdding(requestEvent.Error);
- if (requestEvent.State > HTTPRequestStates.Finished)
- {
- HTTPManager.Logger.Information("RequestEventHelper", $"{requestEvent.State}: discarding response!", source.Response?.Context ?? source.Context);
- source.Response?.Dispose();
- source.Response = null;
- }
- source.Exception = requestEvent.Error;
- source.State = requestEvent.State;
- // https://www.rfc-editor.org/rfc/rfc5861.html#section-1
- // The stale-if-error HTTP Cache-Control extension allows a cache to
- // return a stale response when an error -- e.g., a 500 Internal Server
- // Error, a network segment, or DNS failure -- is encountered, rather
- // than returning a "hard" error.
- if (requestEvent.State > HTTPRequestStates.Finished && requestEvent.State != HTTPRequestStates.Aborted)
- {
- if (HTTPManager.LocalCache != null && !source.DownloadSettings.DisableCache)
- {
- var hash = Caching.HTTPCache.CalculateHash(source.MethodType, source.CurrentUri);
- if (HTTPManager.LocalCache.CanServeWithoutValidation(hash, ErrorTypeForValidation.ConnectionError, source.Context))
- {
- HTTPManager.LocalCache.Redirect(source, hash);
- goto case RequestEvents.Resend;
- }
- }
- }
- goto case RequestEvents.StateChange;
- case RequestEvents.StateChange:
- try
- {
- using (var _ = new Unity.Profiling.ProfilerMarker(nameof(RequestEventHelper.HandleRequestStateChange)).Auto())
- RequestEventHelper.HandleRequestStateChange(requestEvent);
- }
- catch(Exception ex)
- {
- HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange", ex, source.Context);
- }
- break;
- }
- }
- uploadProgress?.DispatchProgressCallbacks();
- downloadProgress?.DispatchProgressCallbacks();
- }
- // TODO: don't start/repeat if can't time out?
- private static bool AbortRequestWhenTimedOut(DateTime now, object context)
- {
- HTTPRequest request = context as HTTPRequest;
- if (request.State >= HTTPRequestStates.Finished)
- return false; // don't repeat
- var downStream= request.Response?.DownStream;
- if (downStream != null && downStream.DoFullCheck(limit: 2))
- {
- 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!";
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Warning(nameof(RequestEventHelper), warning, request.Context);
- else
- UnityEngine.Debug.Log(warning);
- // increase buffer limit
- downStream.EmergencyIncreaseMaxBuffered();
- return false;
- }
- // Upgradable protocols will shut down themselves
- if (request?.Response?.IsUpgraded is bool upgraded && upgraded)
- return false;
- if (request.TimeoutSettings.IsTimedOut(HTTPManager.CurrentFrameDateTime))
- {
- HTTPManager.Logger.Information("RequestEventHelper", "AbortRequestWhenTimedOut - Request timed out. CurrentUri: " + request.CurrentUri.ToString(), request.Context);
- request.Abort();
- return false; // don't repeat
- }
- return true; // repeat
- }
- private static void HandleQueued(HTTPRequest source)
- {
- source.Timing.StartNext(TimingEventNames.Queued);
- source.TimeoutSettings.QueuedAt = HTTPManager.CurrentFrameDateTime;
- Timer.Add(new TimerData(TimeSpan.FromSeconds(1), source, AbortRequestWhenTimedOut));
- }
- static readonly string[] RequestStateNames = new string[] { "Initial", "Queued", "Processing", "Finished", "Error", "Aborted", "ConnectionTimedOut", "TimedOut" };
- private static void HandleRequestStateChange(RequestEventInfo @event)
- {
- HTTPRequest source = @event.SourceRequest;
- // Because there's a race condition between setting the request's State in its Abort() function running on Unity's main thread
- // and the HTTP1/HTTP2 handlers running on an another one.
- // Because of these race conditions cases violating expectations can be:
- // 1.) State is finished but the response null
- // 2.) State is (Connection)TimedOut and the response non-null
- // We have to make sure that no callbacks are called twice and in the request must be in a consistent state!
- // State | Request
- // --------- +---------
- // 1 Null
- // Finished | Skip
- // Timeout/Abort | Deliver
- //
- // 2 Non-Null
- // Finished | Deliver
- // Timeout/Abort | Skip
- using var _ = new Unity.Profiling.ProfilerMarker(RequestStateNames[(int)@event.State]).Auto();
- switch (@event.State)
- {
- case HTTPRequestStates.Queued:
- HandleQueued(source);
- break;
- case HTTPRequestStates.ConnectionTimedOut:
- case HTTPRequestStates.TimedOut:
- case HTTPRequestStates.Error:
- case HTTPRequestStates.Aborted:
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("RequestEventHelper", $"{@event.State}: discarding response!", source.Response?.Context ?? source.Context);
- source.Response?.Dispose();
- source.Response = null;
- goto case HTTPRequestStates.Finished;
- case HTTPRequestStates.Finished:
- // Dispatch any collected download/upload progress, otherwise they would _after_ the callback!
- uploadProgress?.DispatchProgressCallbacks();
- downloadProgress?.DispatchProgressCallbacks();
- if (source.Callback != null)
- {
- source.Timing.AddEvent(new TimingEventInfo(source, TimingEvents.Finish, null));
- source.Timing.StartNext(TimingEventNames.Callback);
- try
- {
- using (var __ = new Unity.Profiling.ProfilerMarker(nameof(source.Callback)).Auto())
- source.Callback(source, source.Response);
- }
- catch (Exception ex)
- {
- HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange " + @event.State, ex, source.Context);
- }
- }
- source.Timing.Finish();
- if (source.Callback == null)
- {
- // This delay required because with coroutines these lines are executed first
- // before the coroutine has a chance to do something with a finished request.
- // By adding a delay there's a time window that the coroutine can run its logic too inbetween.
- Timer.Add(new TimerData(TimeSpan.FromSeconds(1), source, OnDelayedDisposeTimer));
- }
- else
- {
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("RequestEventHelper", $"{nameof(OnDelayedDisposeTimer)} - disposing response!", source.Context);
- source.Dispose();
- }
- HostManager.GetHostVariant(source)
- .TryToSendQueuedRequests();
- break;
- }
- }
- private static bool OnDelayedDisposeTimer(DateTime time, object request)
- {
- var source = request as HTTPRequest;
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("RequestEventHelper", $"{nameof(OnDelayedDisposeTimer)} - disposing response!", source.Context);
- source.Dispose();
- return false;
- }
- }
- }
|