add all
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
using Best.HTTP.Caching;
|
||||
using Best.HTTP.Hosts.Connections.HTTP1;
|
||||
using Best.HTTP.HostSetting;
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Response;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.FileSystem;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp.Streams;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
namespace Best.HTTP.Hosts.Connections.File
|
||||
{
|
||||
internal sealed class FileConnection : ConnectionBase, IContentConsumer, IDownloadContentBufferAvailable
|
||||
{
|
||||
public PeekableContentProviderStream ContentProvider { get; private set; }
|
||||
|
||||
PeekableHTTP1Response _response;
|
||||
NonblockingUnderlyingStream _stream;
|
||||
|
||||
UnityEngine.Hash128 _cacheHash;
|
||||
|
||||
public FileConnection(HostKey hostKey)
|
||||
: base(hostKey)
|
||||
{ }
|
||||
|
||||
protected override void ThreadFunc()
|
||||
{
|
||||
this.CurrentRequest.Timing.StartNext(TimingEventNames.Waiting_TTFB);
|
||||
|
||||
this.Context.Remove("Request");
|
||||
this.Context.Add("Request", this.CurrentRequest.Context);
|
||||
|
||||
bool isFromLocalCache = this.CurrentRequest.CurrentUri.Host.Equals(HTTPCache.CacheHostName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (this._response == null)
|
||||
this.CurrentRequest.Response = this._response = new PeekableHTTP1Response(this.CurrentRequest, isFromLocalCache, this);
|
||||
this._response.Context.Add(nameof(FileConnection), this.Context);
|
||||
|
||||
StreamList stream = new StreamList();
|
||||
try
|
||||
{
|
||||
var headers = new BufferPoolMemoryStream();
|
||||
stream.AppendStream(headers);
|
||||
|
||||
headers.WriteLine("HTTP/1.1 200 Ok");
|
||||
|
||||
System.IO.Stream contentStream = null;
|
||||
|
||||
if (isFromLocalCache)
|
||||
{
|
||||
var hashStr = this.CurrentRequest.CurrentUri.AbsolutePath.Substring(1);
|
||||
var hash = UnityEngine.Hash128.Parse(hashStr);
|
||||
|
||||
// BeginReadContent tries to acquire a read lock on the content and returns null if couldn't.
|
||||
contentStream = HTTPManager.LocalCache?.BeginReadContent(hash, this.Context);
|
||||
if (contentStream == null)
|
||||
throw new HTTPCacheAcquireLockException($"Coulnd't acquire read lock on cached entity.");
|
||||
|
||||
this._cacheHash = hash;
|
||||
|
||||
headers.WriteLine($"BestHTTP-Origin: cachefile({hashStr})");
|
||||
|
||||
stream.AppendStream(HTTPManager.IOService.CreateFileStream(HTTPManager.LocalCache.GetHeaderPathFromHash(hash), FileStreamModes.OpenRead));
|
||||
}
|
||||
else
|
||||
{
|
||||
headers.WriteLine($"BestHTTP-Origin: file");
|
||||
headers.WriteLine("Content-Type: application/octet-stream");
|
||||
|
||||
contentStream = HTTPManager.IOService.CreateFileStream(this.CurrentRequest.CurrentUri.LocalPath, FileStreamModes.OpenRead);
|
||||
}
|
||||
|
||||
headers.WriteLine($"Content-Length: {contentStream.Length.ToString()}");
|
||||
if (!isFromLocalCache)
|
||||
headers.WriteLine();
|
||||
|
||||
headers.Seek(0, System.IO.SeekOrigin.Begin);
|
||||
|
||||
stream.AppendStream(contentStream);
|
||||
|
||||
this.CurrentRequest.TimeoutSettings.SetProcessing(DateTime.UtcNow);
|
||||
|
||||
this._stream = new NonblockingUnderlyingStream(stream, 1024 * 1024, this.Context);
|
||||
this._stream.SetTwoWayBinding(this);
|
||||
this._stream.BeginReceive();
|
||||
|
||||
this.CurrentRequest.OnCancellationRequested += OnCancellationRequested;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
FinishedProcessing(ex);
|
||||
stream?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void IDownloadContentBufferAvailable.BufferAvailable(DownloadContentStream stream)
|
||||
{
|
||||
//HTTPManager.Logger.Verbose(nameof(FileConnection), "IDownloadContentBufferAvailable.BufferAvailable", this.Context);
|
||||
|
||||
// Here we should trigger somehow the read stream and that should call OnContent(IPeekableContentProvider provider, PeekableStream peekable)
|
||||
// to go the regular route.
|
||||
|
||||
if (this._response != null)
|
||||
OnContent();
|
||||
}
|
||||
|
||||
public void SetBinding(PeekableContentProviderStream contentProvider)
|
||||
{
|
||||
this.ContentProvider = contentProvider;
|
||||
}
|
||||
|
||||
public void UnsetBinding() => this.ContentProvider = null;
|
||||
|
||||
public void OnContent()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (this.CurrentRequest.TimeoutSettings.IsTimedOut(DateTime.UtcNow))
|
||||
throw new TimeoutException();
|
||||
|
||||
if (this.CurrentRequest.IsCancellationRequested)
|
||||
throw new Exception("Cancellation requested!");
|
||||
|
||||
this._response.ProcessPeekable(this.ContentProvider);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
|
||||
// After an exception, this._response will be null!
|
||||
if (this._response != null && this._response.ReadState == PeekableHTTP1Response.PeekableReadState.Finished)
|
||||
FinishedProcessing(null);
|
||||
}
|
||||
|
||||
public void OnConnectionClosed()
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(FileConnection), $"OnConnectionClosed({this.ContentProvider?.Length}, {this._response?.ReadState})", this.Context);
|
||||
|
||||
// If the consumer still have a request: error it and close the connection
|
||||
if (this.CurrentRequest != null && this._response != null)
|
||||
{
|
||||
FinishedProcessing(new Exception("Underlying TCP connection closed unexpectedly!"));
|
||||
}
|
||||
else // If no current request: close the connection
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, HTTPConnectionStates.Closed));
|
||||
}
|
||||
|
||||
public void OnError(Exception e)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(FileConnection), $"OnError({this.ContentProvider?.Length}, {this._response?.ReadState}, {this.ShutdownType})", this.Context);
|
||||
|
||||
if (this.ShutdownType == ShutdownTypes.Immediate)
|
||||
return;
|
||||
|
||||
FinishedProcessing(e);
|
||||
}
|
||||
|
||||
private void OnCancellationRequested(HTTPRequest req)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(FileConnection), "OnCancellationRequested()", this.Context);
|
||||
|
||||
Interlocked.Exchange(ref this._response, null);
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
this._stream.Dispose();
|
||||
}
|
||||
|
||||
void FinishedProcessing(Exception ex)
|
||||
{
|
||||
// Warning: FinishedProcessing might be called from different threads in parallel:
|
||||
// - send thread triggered by a write failure
|
||||
// - read thread oncontent/OnError/OnConnectionClosed
|
||||
|
||||
var resp = Interlocked.Exchange(ref this._response, null);
|
||||
if (resp == null)
|
||||
return;
|
||||
|
||||
HTTPManager.Logger.Verbose(nameof(FileConnection), $"{nameof(FinishedProcessing)}({resp}, {ex})", this.Context);
|
||||
|
||||
HTTPManager.LocalCache?.EndReadContent(this._cacheHash, this.Context);
|
||||
this._cacheHash = new UnityEngine.Hash128();
|
||||
|
||||
// Unset the consumer, we no longer expect another OnContent call until further notice.
|
||||
this._stream?.Unbind();
|
||||
this._stream?.Dispose();
|
||||
this._stream = null;
|
||||
|
||||
var req = this.CurrentRequest;
|
||||
|
||||
req.OnCancellationRequested -= OnCancellationRequested;
|
||||
|
||||
bool resendRequest = false;
|
||||
HTTPRequestStates requestState = HTTPRequestStates.Finished;
|
||||
HTTPConnectionStates connectionState = HTTPConnectionStates.Recycle;
|
||||
Exception error = ex;
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
// Timeout is a non-retryable error
|
||||
if (ex is TimeoutException)
|
||||
{
|
||||
error = null;
|
||||
requestState = HTTPRequestStates.TimedOut;
|
||||
}
|
||||
else if (ex is HTTPCacheAcquireLockException)
|
||||
{
|
||||
error = null;
|
||||
resendRequest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (req.RetrySettings.Retries < req.RetrySettings.MaxRetries && !HTTPManager.IsQuitting)
|
||||
{
|
||||
req.RetrySettings.Retries++;
|
||||
error = null;
|
||||
resendRequest = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
requestState = HTTPRequestStates.Error;
|
||||
}
|
||||
}
|
||||
|
||||
// Any exception means that the connection is in an unknown state, we shouldn't try to reuse it.
|
||||
connectionState = HTTPConnectionStates.Closed;
|
||||
|
||||
resp.Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
// After HandleResponse connectionState can have the following values:
|
||||
// - Processing: nothing interesting, caller side can decide what happens with the connection (recycle connection).
|
||||
// - Closed: server sent an connection: close header.
|
||||
// - ClosedResendRequest: in this case resendRequest is true, and the connection must not be reused.
|
||||
// In this case we can send only one ConnectionEvent to handle both case and avoid concurrency issues.
|
||||
|
||||
KeepAliveHeader keepAlive = null;
|
||||
error = ConnectionHelper.HandleResponse(req, out resendRequest, out connectionState, ref keepAlive, this.Context);
|
||||
connectionState = HTTPConnectionStates.Recycle;
|
||||
|
||||
if (!resendRequest && resp.IsUpgraded)
|
||||
requestState = HTTPRequestStates.Processing;
|
||||
}
|
||||
|
||||
req.Timing.StartNext(TimingEventNames.Queued);
|
||||
|
||||
HTTPManager.Logger.Verbose(nameof(FileConnection), $"{nameof(FinishedProcessing)} final decision. ResendRequest: {resendRequest}, RequestState: {requestState}, ConnectionState: {connectionState}", this.Context);
|
||||
|
||||
// If HandleResponse returned with ClosedResendRequest or there were an error and we can retry the request
|
||||
if (connectionState == HTTPConnectionStates.ClosedResendRequest || (resendRequest && connectionState == HTTPConnectionStates.Closed))
|
||||
{
|
||||
ConnectionHelper.ResendRequestAndCloseConnection(this, req);
|
||||
}
|
||||
else if (resendRequest && requestState == HTTPRequestStates.Finished)
|
||||
{
|
||||
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, RequestEvents.Resend));
|
||||
ConnectionEventHelper.EnqueueConnectionEvent(new ConnectionEventInfo(this, connectionState));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise set the request's then the connection's state
|
||||
ConnectionHelper.EnqueueEvents(this, connectionState, req, requestState, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user