// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace HordeServer.Server { /// /// Tracks open HTTP requests to ASP.NET /// Will block a pending shutdown until all requests in progress are finished (or graceful timeout is reached) /// This avoids interrupting long running requests such as artifact uploads. /// public class RequestTrackerService { /// /// Writer for log output /// private readonly ILogger _logger; readonly ConcurrentDictionary _requestsInProgress = new ConcurrentDictionary(); /// /// Constructor /// /// Logger public RequestTrackerService(ILogger logger) { _logger = logger; } /// /// Called by the middleware when a request is started /// /// HTTP Context public void RequestStarted(HttpContext context) { _requestsInProgress[context.TraceIdentifier] = new TrackedRequest(context.Request); } /// /// Called by the middleware when a request is finished (no matter if an exception occurred or not) /// /// HTTP Context public void RequestFinished(HttpContext context) { _requestsInProgress.Remove(context.TraceIdentifier, out _); } /// /// Get current requests in progress /// /// The requests in progress public IReadOnlyDictionary GetRequestsInProgress() { return _requestsInProgress; } private string GetRequestsInProgressAsString() { List> requests = GetRequestsInProgress().ToList(); requests.Sort((a, b) => a.Value.StartedAt.CompareTo(b.Value.StartedAt)); StringBuilder content = new StringBuilder(); foreach (KeyValuePair pair in requests) { int ageInMs = pair.Value.GetTimeSinceStartInMs(); string path = pair.Value.Request.Path; content.AppendLine(CultureInfo.InvariantCulture, $"{ageInMs,9} {path}"); } return content.ToString(); } /// /// Log all requests currently in progress /// public void LogRequestsInProgress() { if (GetRequestsInProgress().Count == 0) { _logger.LogInformation("There are no requests in progress!"); } else { _logger.LogInformation("Current open requests are:\n{RequestsInProgress}", GetRequestsInProgressAsString()); } } } /// /// Value object for tracked requests /// public class TrackedRequest { /// /// When the request was received /// public DateTime StartedAt { get; } /// /// The HTTP request being tracked /// public HttpRequest Request { get; } /// /// Constructor /// /// HTTP request being tracked public TrackedRequest(HttpRequest request) { StartedAt = DateTime.UtcNow; Request = request; } /// /// How long the request has been running /// /// Time elapsed in milliseconds since request started public int GetTimeSinceStartInMs() { return (int)(DateTime.UtcNow - StartedAt).TotalMilliseconds; } } /// /// ASP.NET Middleware to track open requests /// public class RequestTrackerMiddleware { private readonly RequestDelegate _next; /// /// Constructor /// /// Next middleware to call public RequestTrackerMiddleware(RequestDelegate next) { _next = next; } /// /// Invoked by ASP.NET framework itself /// /// HTTP Context /// The RequestTrackerService singleton /// public async Task InvokeAsync(HttpContext context, RequestTrackerService service) { if (!context.Request.Path.StartsWithSegments("/health", StringComparison.Ordinal)) { try { service.RequestStarted(context); await _next(context); } finally { service.RequestFinished(context); } } else { // Ignore requests to /health/* await _next(context); } } } }