// 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);
}
}
}
}