FileConnection.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. using System;
  2. using System.Threading;
  3. using Best.HTTP.Caching;
  4. using Best.HTTP.Hosts.Connections.HTTP1;
  5. using Best.HTTP.HostSetting;
  6. using Best.HTTP.Request.Timings;
  7. using Best.HTTP.Response;
  8. using Best.HTTP.Shared;
  9. using Best.HTTP.Shared.Extensions;
  10. using Best.HTTP.Shared.PlatformSupport.FileSystem;
  11. using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
  12. using Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams;
  13. using Best.HTTP.Shared.Streams;
  14. namespace Best.HTTP.Hosts.Connections.File
  15. {
  16. internal sealed class FileConnection : ConnectionBase, IContentConsumer, IDownloadContentBufferAvailable
  17. {
  18. public PeekableContentProviderStream ContentProvider { get; private set; }
  19. PeekableHTTP1Response _response;
  20. NonblockingUnderlyingStream _stream;
  21. UnityEngine.Hash128 _cacheHash;
  22. public FileConnection(HostKey hostKey)
  23. : base(hostKey)
  24. { }
  25. protected override void ThreadFunc()
  26. {
  27. this.CurrentRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);
  28. this.Context.Remove("Request");
  29. this.Context.Add("Request", this.CurrentRequest.Context);
  30. bool isFromLocalCache = this.CurrentRequest.CurrentUri.Host.Equals(HTTPCache.CacheHostName, StringComparison.OrdinalIgnoreCase);
  31. if (this._response == null)
  32. this.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.CurrentRequest, isFromLocalCache, this);
  33. this._response.Context.Add(nameof(FileConnection), this.Context);
  34. StreamList stream = new StreamList();
  35. try
  36. {
  37. var headers = new BufferPoolMemoryStream();
  38. stream.AppendStream(headers);
  39. headers.WriteLine("HTTP/1.1 200 Ok");
  40. System.IO.Stream contentStream = null;
  41. if (isFromLocalCache)
  42. {
  43. var hashStr = this.CurrentRequest.CurrentUri.AbsolutePath.Substring(1);
  44. var hash = UnityEngine.Hash128.Parse(hashStr);
  45. // BeginReadContent tries to acquire a read lock on the content and returns null if couldn't.
  46. contentStream = HTTPManager.LocalCache?.BeginReadContent(hash, this.Context);
  47. if (contentStream == null)
  48. throw new HTTPCacheAcquireLockException($"Coulnd't acquire read lock on cached entity.");
  49. this._cacheHash = hash;
  50. headers.WriteLine($"BestHTTP-Origin: cachefile({hashStr})");
  51. stream.AppendStream(HTTPManager.IOService.CreateFileStream(HTTPManager.LocalCache.GetHeaderPathFromHash(hash), FileStreamModes.OpenRead));
  52. }
  53. else
  54. {
  55. headers.WriteLine($"BestHTTP-Origin: file");
  56. headers.WriteLine("Content-Type: application/octet-stream");
  57. contentStream = HTTPManager.IOService.CreateFileStream(this.CurrentRequest.CurrentUri.LocalPath, FileStreamModes.OpenRead);
  58. }
  59. headers.WriteLine($"Content-Length: {contentStream.Length.ToString()}");
  60. if (!isFromLocalCache)
  61. headers.WriteLine();
  62. headers.Seek(0, System.IO.SeekOrigin.Begin);
  63. stream.AppendStream(contentStream);
  64. this.CurrentRequest.TimeoutSettings.SetProcessing(DateTime.Now);
  65. this._stream = new NonblockingUnderlyingStream(stream, 1024 * 1024, this.Context);
  66. this._stream.SetTwoWayBinding(this);
  67. this._stream.BeginReceive();
  68. this.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
  69. }
  70. catch(Exception ex)
  71. {
  72. FinishedProcessing(ex);
  73. stream?.Dispose();
  74. }
  75. }
  76. void IDownloadContentBufferAvailable.BufferAvailable(DownloadContentStream stream)
  77. {
  78. //HTTPManager.Logger.Verbose(nameof(FileConnection), "IDownloadContentBufferAvailable.BufferAvailable", this.Context);
  79. // Here we should trigger somehow the read stream and that should call OnContent(IPeekableContentProvider provider, PeekableStream peekable)
  80. // to go the regular route.
  81. if (this._response != null)
  82. OnContent();
  83. }
  84. public void SetBinding(PeekableContentProviderStream contentProvider)
  85. {
  86. this.ContentProvider = contentProvider;
  87. }
  88. public void UnsetBinding() => this.ContentProvider = null;
  89. public void OnContent()
  90. {
  91. try
  92. {
  93. if (this.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.Now))
  94. throw new TimeoutException();
  95. if (this.CurrentRequest.IsCancellationRequested)
  96. throw new Exception("Cancellation requested!");
  97. this._response.ProcessPeekable(this.ContentProvider);
  98. }
  99. catch (Exception e)
  100. {
  101. if (this.ShutdownType == ShutdownTypes.Immediate)
  102. return;
  103. FinishedProcessing(e);
  104. }
  105. // After an exception, this._response will be null!
  106. if (this._response != null && this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
  107. FinishedProcessing(null);
  108. }
  109. public void OnConnectionClosed()
  110. {
  111. HTTPManager.Logger.Information(nameof(FileConnection), $"OnConnectionClosed({this.ContentProvider?.Length}, {this._response?.ReadState})", this.Context);
  112. // If the consumer still have a request: error it and close the connection
  113. if (this.CurrentRequest != null && this._response != null)
  114. {
  115. FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
  116. }
  117. else // If no current request: close the connection
  118. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
  119. }
  120. public void OnError(Exception e)
  121. {
  122. HTTPManager.Logger.Information(nameof(FileConnection), $"OnError({this.ContentProvider?.Length}, {this._response?.ReadState}, {this.ShutdownType})", this.Context);
  123. if (this.ShutdownType == ShutdownTypes.Immediate)
  124. return;
  125. FinishedProcessing(e);
  126. }
  127. private void OnCancellationRequested(HTTPRequest req)
  128. {
  129. HTTPManager.Logger.Information(nameof(FileConnection), "OnCancellationRequested()", this.Context);
  130. Interlocked.Exchange(ref this._response, null);
  131. req.OnCancellationRequested -= OnCancellationRequested;
  132. this._stream.Dispose();
  133. }
  134. void FinishedProcessing(Exception ex)
  135. {
  136. // Warning: FinishedProcessing might be called from different threads in parallel:
  137. // - send thread triggered by a write failure
  138. // - read thread oncontent/OnError/OnConnectionClosed
  139. var resp = Interlocked.Exchange(ref this._response, null);
  140. if (resp == null)
  141. return;
  142. HTTPManager.Logger.Verbose(nameof(FileConnection), $"{nameof(FinishedProcessing)}({resp}, {ex})", this.Context);
  143. HTTPManager.LocalCache?.EndReadContent(this._cacheHash, this.Context);
  144. this._cacheHash = new UnityEngine.Hash128();
  145. // Unset the consumer, we no longer expect another OnContent call until further notice.
  146. this._stream?.Unbind();
  147. this._stream?.Dispose();
  148. this._stream = null;
  149. var req = this.CurrentRequest;
  150. req.OnCancellationRequested -= OnCancellationRequested;
  151. bool resendRequest = false;
  152. HTTPRequestStates requestState = HTTPRequestStates.Finished;
  153. HTTPConnectionStates connectionState = HTTPConnectionStates.Recycle;
  154. Exception error = ex;
  155. if (error != null)
  156. {
  157. // Timeout is a non-retryable error
  158. if (ex is TimeoutException)
  159. {
  160. error = null;
  161. requestState = HTTPRequestStates.TimedOut;
  162. }
  163. else if (ex is HTTPCacheAcquireLockException)
  164. {
  165. error = null;
  166. resendRequest = true;
  167. }
  168. else
  169. {
  170. if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries)
  171. {
  172. req.RetrySettings.Retries++;
  173. error = null;
  174. resendRequest = true;
  175. }
  176. else
  177. {
  178. requestState = HTTPRequestStates.Error;
  179. }
  180. }
  181. // Any exception means that the connection is in an unknown state, we shouldn't try to reuse it.
  182. connectionState = HTTPConnectionStates.Closed;
  183. resp.Dispose();
  184. }
  185. else
  186. {
  187. // After HandleResponse connectionState can have the following values:
  188. // - Processing: nothing interesting, caller side can decide what happens with the connection (recycle connection).
  189. // - Closed: server sent an connection: close header.
  190. // - ClosedResendRequest: in this case resendRequest is true, and the connection must not be reused.
  191. // In this case we can send only one ConnectionEvent to handle both case and avoid concurrency issues.
  192. KeepAliveHeader keepAlive = null;
  193. error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref keepAlive, this.Context);
  194. connectionState = HTTPConnectionStates.Recycle;
  195. if (!resendRequest && resp.IsUpgraded)
  196. requestState = HTTPRequestStates.Processing;
  197. }
  198. req.Timing.StartNext(TimingEventNames.Queued);
  199. HTTPManager.Logger.Verbose(nameof(FileConnection), $"{nameof(FinishedProcessing)} final decision. ResendRequest: {resendRequest}, RequestState: {requestState}, ConnectionState: {connectionState}", this.Context);
  200. // If HandleResponse returned with ClosedResendRequest or there were an error and we can retry the request
  201. if (connectionState == HTTPConnectionStates.ClosedResendRequest || (resendRequest && connectionState == HTTPConnectionStates.Closed))
  202. {
  203. ConnectionHelper.ResendRequestAndCloseConnection(this, req);
  204. }
  205. else if (resendRequest && requestState == HTTPRequestStates.Finished)
  206. {
  207. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
  208. ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, connectionState));
  209. }
  210. else
  211. {
  212. // Otherwise set the request's then the connection's state
  213. ConnectionHelper.EnqueueEvents(this, connectionState, req, requestState, error);
  214. }
  215. }
  216. }
  217. }