HostVariant.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using Best.HTTP.Hosts.Connections;
  5. using Best.HTTP.Hosts.Connections.File;
  6. using Best.HTTP.Hosts.Settings;
  7. using Best.HTTP.Shared;
  8. using Best.HTTP.Shared.Extensions;
  9. using Best.HTTP.Shared.Logger;
  10. namespace Best.HTTP.HostSetting
  11. {
  12. /// <summary>
  13. /// An enumeration representing the protocol support for a host.
  14. /// </summary>
  15. public enum HostProtocolSupport : byte
  16. {
  17. /// <summary>
  18. /// Protocol support is unknown or undetermined.
  19. /// </summary>
  20. Unknown = 0x00,
  21. /// <summary>
  22. /// The host supports HTTP/1.
  23. /// </summary>
  24. HTTP1 = 0x01,
  25. /// <summary>
  26. /// The host supports HTTP/2.
  27. /// </summary>
  28. HTTP2 = 0x02,
  29. /// <summary>
  30. /// This is a file-based host.
  31. /// </summary>
  32. File = 0x03,
  33. }
  34. /// <summary>
  35. /// <para>The HostVariant class is a critical component in managing HTTP connections and handling HTTP requests for a specific host. It maintains a queue of requests and a list of active connections associated with the host, ensuring efficient utilization of available resources. Additionally, it supports protocol version detection (HTTP/1 or HTTP/2) for optimized communication with the host.</para>
  36. /// <list type="bullet">
  37. /// <item><description>It maintains a queue of requests to ensure efficient and controlled use of available connections.</description></item>
  38. /// <item><description>It supports HTTP/1 and HTTP/2 protocol versions, allowing requests to be sent using the appropriate protocol based on the host's protocol support.</description></item>
  39. /// <item><description>Provides methods for sending requests, recycling connections, managing connection state, and handling the shutdown of connections and the host variant itself.</description></item>
  40. /// <item><description>It includes logging for diagnostic purposes, helping to monitor and debug the behavior of connections and requests.</description></item>
  41. /// </list>
  42. /// <para>In summary, the HostVariant class plays a central role in managing HTTP connections and requests for a specific host, ensuring efficient and reliable communication with that host while supporting different protocol versions.</para>
  43. /// </summary>
  44. public class HostVariant
  45. {
  46. public HostKey Host { get; private set; }
  47. public HostProtocolSupport ProtocolSupport { get; private set; }
  48. public DateTime LastProtocolSupportUpdate { get; private set; }
  49. public LoggingContext Context { get; private set; }
  50. // All the connections. Free and processing ones too.
  51. protected List<ConnectionBase> Connections = new List<ConnectionBase>();
  52. // Queued requests that aren't passed yet to a connection.
  53. protected Queue<HTTPRequest> Queue = new Queue<HTTPRequest>();
  54. // Host-variant settings
  55. protected HostVariantSettings _settings;
  56. // Cached list
  57. protected List<KeyValuePair<int, ConnectionBase>> availableConnections;
  58. public HostVariant(HostKey host)
  59. {
  60. this.Host = host;
  61. if (this.Host.Uri.IsFile)
  62. this.ProtocolSupport = HostProtocolSupport.File;
  63. this.Context = new LoggingContext(this);
  64. this.Context.Add("Host", this.Host.Host);
  65. this._settings = HTTPManager.PerHostSettings.Get(this).HostVariantSettings;
  66. this.availableConnections = new List<KeyValuePair<int, ConnectionBase>>(2);
  67. }
  68. public virtual void AddProtocol(HostProtocolSupport protocolSupport)
  69. {
  70. this.LastProtocolSupportUpdate = HTTPManager.CurrentFrameDateTime;
  71. var oldProtocol = this.ProtocolSupport;
  72. if (oldProtocol != protocolSupport)
  73. {
  74. this.ProtocolSupport = protocolSupport;
  75. HTTPManager.Logger.Information(nameof(HostVariant), $"AddProtocol({oldProtocol} => {protocolSupport})", this.Context);
  76. }
  77. TryToSendQueuedRequests();
  78. }
  79. public virtual HostVariant Send(HTTPRequest request)
  80. {
  81. if (HTTPManager.Logger.IsDiagnostic)
  82. HTTPManager.Logger.Verbose(nameof(HostVariant), $"Send({request})", this.Context);
  83. request.Context.Remove(nameof(HostVariant));
  84. request.Context.Add(nameof(HostVariant), this.Context);
  85. this.Queue.Enqueue(request);
  86. return TryToSendQueuedRequests();
  87. }
  88. public virtual HostVariant TryToSendQueuedRequests()
  89. {
  90. if (this.Queue.Count == 0)
  91. return this;
  92. (int activeConnections, int theoreticalMaximumPerConnection, int assignedRequests) = QueryAnyAvailableOrNew(ref availableConnections);
  93. if (HTTPManager.Logger.IsDiagnostic)
  94. HTTPManager.Logger.Verbose(nameof(HostVariant), $"QueryAnyAvailableOrNew => ({activeConnections}, {theoreticalMaximumPerConnection}, {assignedRequests}), available: {availableConnections.Count}, protocol: {this.ProtocolSupport}, max connections: {this._settings.MaxConnectionPerVariant}", this.Context);
  95. if (availableConnections.Count == 0)
  96. {
  97. if (activeConnections > 0 && this.ProtocolSupport == HostProtocolSupport.Unknown)
  98. return this;
  99. if (activeConnections < this._settings.MaxConnectionPerVariant)
  100. {
  101. int queueSize = this.Queue.Count;
  102. int currentMaximum = (activeConnections * theoreticalMaximumPerConnection) - assignedRequests;
  103. while (activeConnections < this._settings.MaxConnectionPerVariant && currentMaximum < queueSize)
  104. {
  105. availableConnections.Add(new KeyValuePair<int, ConnectionBase>(0, CreateNew()));
  106. currentMaximum += theoreticalMaximumPerConnection;
  107. activeConnections++;
  108. }
  109. }
  110. else
  111. return this;
  112. }
  113. // Sort connections by theirs key (assigned requests count)
  114. availableConnections.Sort((a, b) => a.Key - b.Key);
  115. while (this.Queue.Count > 0 && availableConnections.Count > 0)
  116. {
  117. var nextRequest = this.Queue.Peek();
  118. // If the queue is large, or timeouts are set low, a request might be in a queue while its state is set to > Finished.
  119. // So we have to prevent sending it again.
  120. if (nextRequest.State <= HTTPRequestStates.Queued)
  121. {
  122. var kvp = availableConnections[0];
  123. nextRequest.Context.Remove(nameof(HostVariant));
  124. nextRequest.Context.Add(nameof(HostVariant), this.Context);
  125. if (HTTPManager.Logger.IsDiagnostic)
  126. HTTPManager.Logger.Information(nameof(HostVariant), $"Send({kvp.Value.GetType().Name})", nextRequest.Context);
  127. RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(nextRequest, HTTPRequestStates.Processing, null));
  128. OnConnectionStartedProcessingRequest(kvp.Value, nextRequest);
  129. // then start process the request
  130. kvp.Value.Process(nextRequest);
  131. if (kvp.Key + 1 >= kvp.Value.MaxAssignedRequests)
  132. availableConnections.RemoveAt(0);
  133. else
  134. availableConnections[0] = new KeyValuePair<int, ConnectionBase>(kvp.Key + 1, kvp.Value);
  135. availableConnections.Sort((a, b) => a.Key - b.Key);
  136. }
  137. this.Queue.Dequeue();
  138. }
  139. return this;
  140. }
  141. public virtual (int activeConnections, int theoreticalMaximumPerConnection, int assignedRequests) QueryAnyAvailableOrNew(ref List<KeyValuePair<int, ConnectionBase>> connectionCollector)
  142. {
  143. int activeConnections = 0;
  144. int maxAssignedRequest = 1;
  145. int assignedRequests = 0;
  146. connectionCollector.Clear();
  147. // Check the last created connection first. This way, if a higher level protocol is present that can handle more requests (== HTTP/2) that protocol will be chosen
  148. // and others will be closed when their inactivity time is reached.
  149. for (int i = Connections.Count - 1; i >= 0; --i)
  150. {
  151. var conn = Connections[i];
  152. if (conn.State == HTTPConnectionStates.Initial ||
  153. conn.State == HTTPConnectionStates.Free ||
  154. (conn.CanProcessMultiple && conn.AssignedRequests < conn.MaxAssignedRequests))
  155. connectionCollector.Add(new KeyValuePair<int, ConnectionBase>(conn.AssignedRequests, conn));
  156. maxAssignedRequest = Math.Max(maxAssignedRequest, conn.MaxAssignedRequests);
  157. assignedRequests += conn.AssignedRequests;
  158. activeConnections++;
  159. }
  160. return (activeConnections, Math.Max(1, (int)(maxAssignedRequest * this._settings.MaxAssignedRequestsFactor)), assignedRequests);
  161. }
  162. public virtual ConnectionBase CreateNew()
  163. {
  164. if (HTTPManager.Logger.IsDiagnostic)
  165. HTTPManager.Logger.Verbose(nameof(HostVariant), $"CreateNew({this.Host})", this.Context);
  166. ConnectionBase conn = this._settings.ConnectionFactory?.Invoke(this._settings, this);
  167. if (conn == null)
  168. {
  169. if (this.ProtocolSupport == HostProtocolSupport.File)
  170. conn = new FileConnection(this.Host);
  171. else
  172. {
  173. #if UNITY_WEBGL && !UNITY_EDITOR
  174. conn = new Best.HTTP.Hosts.Connections.WebGL.WebGLXHRConnection(this.Host);
  175. #else
  176. conn = new HTTPOverTCPConnection(this.Host);
  177. #endif
  178. }
  179. }
  180. Connections.Add(conn);
  181. return conn;
  182. }
  183. protected virtual void OnConnectionStartedProcessingRequest(ConnectionBase connection, HTTPRequest request)
  184. {
  185. }
  186. public virtual HostVariant RecycleConnection(ConnectionBase conn)
  187. {
  188. conn.State = HTTPConnectionStates.Free;
  189. Best.HTTP.Shared.Extensions.Timer.Add(new TimerData(TimeSpan.FromSeconds(1), conn, CloseConnectionAfterInactivity));
  190. return this;
  191. }
  192. protected virtual bool RemoveConnectionImpl(ConnectionBase conn, HTTPConnectionStates setState)
  193. {
  194. HTTPManager.Logger.Information(typeof(HostVariant).Name, $"RemoveConnectionImpl({conn}, {setState})", this.Context);
  195. conn.State = setState;
  196. conn.Dispose();
  197. bool found = this.Connections.Remove(conn);
  198. if (!found) //
  199. HTTPManager.Logger.Information(typeof(HostVariant).Name, $"RemoveConnectionImpl - Couldn't find connection! key: {conn.HostKey}", this.Context);
  200. return found;
  201. }
  202. public virtual HostVariant RemoveConnection(ConnectionBase conn, HTTPConnectionStates setState)
  203. {
  204. RemoveConnectionImpl(conn, setState);
  205. return this;
  206. }
  207. public ConnectionBase Find(Predicate<ConnectionBase> match) => this.Connections.Find(match);
  208. public bool HasConnection(ConnectionBase connection) => this.Connections.Contains(connection);
  209. protected virtual bool CloseConnectionAfterInactivity(DateTime now, object context)
  210. {
  211. var conn = context as ConnectionBase;
  212. bool closeConnection = conn.State == HTTPConnectionStates.Free && now - conn.LastProcessTime >= conn.KeepAliveTime;
  213. if (closeConnection)
  214. {
  215. HTTPManager.Logger.Information(typeof(HostVariant).Name, string.Format("CloseConnectionAfterInactivity - [{0}] Closing! State: {1}, Now: {2}, LastProcessTime: {3}, KeepAliveTime: {4}",
  216. conn.ToString(), conn.State, now.ToString(System.Globalization.CultureInfo.InvariantCulture), conn.LastProcessTime.ToString(System.Globalization.CultureInfo.InvariantCulture), conn.KeepAliveTime), this.Context);
  217. RemoveConnection(conn, HTTPConnectionStates.Closed);
  218. return false;
  219. }
  220. // repeat until the connection's state is free
  221. return conn.State == HTTPConnectionStates.Free;
  222. }
  223. public virtual void RemoveAllIdleConnections()
  224. {
  225. for (int i = 0; i < this.Connections.Count; i++)
  226. if (this.Connections[i].State == HTTPConnectionStates.Free)
  227. {
  228. int countBefore = this.Connections.Count;
  229. RemoveConnection(this.Connections[i], HTTPConnectionStates.Closed);
  230. if (countBefore != this.Connections.Count)
  231. i--;
  232. }
  233. }
  234. public virtual void Shutdown()
  235. {
  236. this.Queue.Clear();
  237. foreach (var conn in this.Connections)
  238. {
  239. // Swallow any exceptions, we are quitting anyway.
  240. try
  241. {
  242. conn.Shutdown(ShutdownTypes.Immediate);
  243. }
  244. catch { }
  245. }
  246. //this.Connections.Clear();
  247. }
  248. public virtual void SaveTo(System.IO.BinaryWriter bw)
  249. {
  250. bw.Write(this.LastProtocolSupportUpdate.ToBinary());
  251. bw.Write((byte)this.ProtocolSupport);
  252. }
  253. public virtual void LoadFrom(int version, System.IO.BinaryReader br)
  254. {
  255. this.LastProtocolSupportUpdate = DateTime.FromBinary(br.ReadInt64());
  256. this.ProtocolSupport = (HostProtocolSupport)br.ReadByte();
  257. if (DateTime.Now - this.LastProtocolSupportUpdate >= TimeSpan.FromDays(1))
  258. {
  259. if (HTTPManager.Logger.IsDiagnostic)
  260. HTTPManager.Logger.Verbose(nameof(HostVariant), $"LoadFrom - Too Old! LastProtocolSupportUpdate: {this.LastProtocolSupportUpdate.ToString(CultureInfo.InvariantCulture)}, ProtocolSupport: {this.ProtocolSupport}", this.Context);
  261. this.ProtocolSupport = HostProtocolSupport.Unknown;
  262. }
  263. else if (HTTPManager.Logger.IsDiagnostic)
  264. HTTPManager.Logger.Verbose(nameof(HostVariant), $"LoadFrom - LastProtocolSupportUpdate: {this.LastProtocolSupportUpdate.ToString(CultureInfo.InvariantCulture)}, ProtocolSupport: {this.ProtocolSupport}", this.Context);
  265. }
  266. public override string ToString() => $"{this.Host}, {this.Queue.Count}/{this.Connections?.Count}, {this.ProtocolSupport}";
  267. }
  268. }