mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
feat: add endpoint benchmarking on startup
- New EndpointBenchmarkService pings all endpoints on startup - Measures average response time and success rate - Reorders endpoints by performance (fastest first) - RoundRobinFallbackHelper now uses benchmarked order - Racing still happens, but starts with fastest endpoints - Reduces latency by prioritizing known-fast servers - Logs benchmark results for visibility
This commit is contained in:
@@ -477,6 +477,9 @@ else
|
||||
builder.Services.AddSingleton<IStartupValidator, SubsonicStartupValidator>();
|
||||
}
|
||||
|
||||
// Register endpoint benchmark service
|
||||
builder.Services.AddSingleton<EndpointBenchmarkService>();
|
||||
|
||||
builder.Services.AddSingleton<IStartupValidator, DeezerStartupValidator>();
|
||||
builder.Services.AddSingleton<IStartupValidator, QobuzStartupValidator>();
|
||||
builder.Services.AddSingleton<IStartupValidator>(sp =>
|
||||
@@ -484,6 +487,7 @@ builder.Services.AddSingleton<IStartupValidator>(sp =>
|
||||
sp.GetRequiredService<Microsoft.Extensions.Options.IOptions<SquidWTFSettings>>(),
|
||||
sp.GetRequiredService<IHttpClientFactory>().CreateClient(),
|
||||
squidWtfApiUrls,
|
||||
sp.GetRequiredService<EndpointBenchmarkService>(),
|
||||
sp.GetRequiredService<ILogger<SquidWTFStartupValidator>>()));
|
||||
builder.Services.AddSingleton<IStartupValidator, LyricsStartupValidator>();
|
||||
|
||||
|
||||
135
allstarr/Services/Common/EndpointBenchmarkService.cs
Normal file
135
allstarr/Services/Common/EndpointBenchmarkService.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace allstarr.Services.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks API endpoints on startup and maintains performance metrics.
|
||||
/// Used to prioritize faster endpoints in racing scenarios.
|
||||
/// </summary>
|
||||
public class EndpointBenchmarkService
|
||||
{
|
||||
private readonly ILogger<EndpointBenchmarkService> _logger;
|
||||
private readonly Dictionary<string, EndpointMetrics> _metrics = new();
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
|
||||
public EndpointBenchmarkService(ILogger<EndpointBenchmarkService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmarks a list of endpoints by making test requests.
|
||||
/// Returns endpoints sorted by average response time (fastest first).
|
||||
/// </summary>
|
||||
public async Task<List<string>> BenchmarkEndpointsAsync(
|
||||
List<string> endpoints,
|
||||
Func<string, CancellationToken, Task<bool>> testFunc,
|
||||
int pingCount = 3,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("🏁 Benchmarking {Count} endpoints with {Pings} pings each...", endpoints.Count, pingCount);
|
||||
|
||||
var tasks = endpoints.Select(async endpoint =>
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var successCount = 0;
|
||||
var totalMs = 0L;
|
||||
|
||||
for (int i = 0; i < pingCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pingStart = Stopwatch.GetTimestamp();
|
||||
var success = await testFunc(endpoint, cancellationToken);
|
||||
var pingMs = Stopwatch.GetElapsedTime(pingStart).TotalMilliseconds;
|
||||
|
||||
if (success)
|
||||
{
|
||||
successCount++;
|
||||
totalMs += (long)pingMs;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Benchmark ping failed for {Endpoint}", endpoint);
|
||||
}
|
||||
|
||||
// Small delay between pings
|
||||
if (i < pingCount - 1)
|
||||
{
|
||||
await Task.Delay(100, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
|
||||
var avgMs = successCount > 0 ? totalMs / successCount : long.MaxValue;
|
||||
var metrics = new EndpointMetrics
|
||||
{
|
||||
Endpoint = endpoint,
|
||||
AverageResponseMs = avgMs,
|
||||
SuccessRate = (double)successCount / pingCount,
|
||||
LastBenchmark = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _lock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
_metrics[endpoint] = metrics;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
|
||||
_logger.LogInformation(" {Endpoint}: {AvgMs}ms avg, {SuccessRate:P0} success rate",
|
||||
endpoint, avgMs, metrics.SuccessRate);
|
||||
|
||||
return metrics;
|
||||
}).ToList();
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
// Sort by: success rate first (must be > 0), then by average response time
|
||||
var sorted = results
|
||||
.Where(m => m.SuccessRate > 0)
|
||||
.OrderByDescending(m => m.SuccessRate)
|
||||
.ThenBy(m => m.AverageResponseMs)
|
||||
.Select(m => m.Endpoint)
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation("✅ Benchmark complete. Fastest: {Fastest} ({Ms}ms)",
|
||||
sorted.FirstOrDefault() ?? "none",
|
||||
results.Where(m => m.SuccessRate > 0).MinBy(m => m.AverageResponseMs)?.AverageResponseMs ?? 0);
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the metrics for a specific endpoint.
|
||||
/// </summary>
|
||||
public EndpointMetrics? GetMetrics(string endpoint)
|
||||
{
|
||||
_metrics.TryGetValue(endpoint, out var metrics);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all endpoint metrics sorted by performance.
|
||||
/// </summary>
|
||||
public List<EndpointMetrics> GetAllMetrics()
|
||||
{
|
||||
return _metrics.Values
|
||||
.OrderByDescending(m => m.SuccessRate)
|
||||
.ThenBy(m => m.AverageResponseMs)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class EndpointMetrics
|
||||
{
|
||||
public string Endpoint { get; set; } = string.Empty;
|
||||
public long AverageResponseMs { get; set; }
|
||||
public double SuccessRate { get; set; }
|
||||
public DateTime LastBenchmark { get; set; }
|
||||
}
|
||||
@@ -26,6 +26,31 @@ public class RoundRobinFallbackHelper
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the endpoint order based on benchmark results (fastest first).
|
||||
/// </summary>
|
||||
public void SetEndpointOrder(List<string> orderedEndpoints)
|
||||
{
|
||||
lock (_urlIndexLock)
|
||||
{
|
||||
// Reorder _apiUrls to match the benchmarked order
|
||||
var reordered = orderedEndpoints.Where(e => _apiUrls.Contains(e)).ToList();
|
||||
|
||||
// Add any endpoints that weren't benchmarked (shouldn't happen, but be safe)
|
||||
foreach (var url in _apiUrls.Where(u => !reordered.Contains(u)))
|
||||
{
|
||||
reordered.Add(url);
|
||||
}
|
||||
|
||||
_apiUrls.Clear();
|
||||
_apiUrls.AddRange(reordered);
|
||||
_currentUrlIndex = 0;
|
||||
|
||||
_logger.LogInformation("📊 {Service} endpoints reordered by benchmark: {Endpoints}",
|
||||
_serviceName, string.Join(", ", _apiUrls.Take(3)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries the request with the next provider in round-robin, then falls back to others on failure.
|
||||
/// This distributes load evenly across all providers while maintaining reliability.
|
||||
|
||||
@@ -14,14 +14,23 @@ public class SquidWTFStartupValidator : BaseStartupValidator
|
||||
{
|
||||
private readonly SquidWTFSettings _settings;
|
||||
private readonly RoundRobinFallbackHelper _fallbackHelper;
|
||||
private readonly EndpointBenchmarkService _benchmarkService;
|
||||
private readonly ILogger<SquidWTFStartupValidator> _logger;
|
||||
|
||||
public override string ServiceName => "SquidWTF";
|
||||
|
||||
public SquidWTFStartupValidator(IOptions<SquidWTFSettings> settings, HttpClient httpClient, List<string> apiUrls, ILogger<SquidWTFStartupValidator> logger)
|
||||
public SquidWTFStartupValidator(
|
||||
IOptions<SquidWTFSettings> settings,
|
||||
HttpClient httpClient,
|
||||
List<string> apiUrls,
|
||||
EndpointBenchmarkService benchmarkService,
|
||||
ILogger<SquidWTFStartupValidator> logger)
|
||||
: base(httpClient)
|
||||
{
|
||||
_settings = settings.Value;
|
||||
_fallbackHelper = new RoundRobinFallbackHelper(apiUrls, logger, "SquidWTF");
|
||||
_benchmarkService = benchmarkService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +50,47 @@ public class SquidWTFStartupValidator : BaseStartupValidator
|
||||
|
||||
WriteStatus("SquidWTF Quality", quality, ConsoleColor.Cyan);
|
||||
|
||||
// Benchmark all endpoints to determine fastest
|
||||
var apiUrls = _fallbackHelper.EndpointCount > 0
|
||||
? Enumerable.Range(0, _fallbackHelper.EndpointCount).Select(_ => "").ToList() // Placeholder, we'll get actual URLs from fallback helper
|
||||
: new List<string>();
|
||||
|
||||
// Get the actual API URLs by reflection (not ideal, but works for now)
|
||||
var fallbackHelperType = _fallbackHelper.GetType();
|
||||
var apiUrlsField = fallbackHelperType.GetField("_apiUrls", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
if (apiUrlsField != null)
|
||||
{
|
||||
apiUrls = (List<string>)apiUrlsField.GetValue(_fallbackHelper)!;
|
||||
}
|
||||
|
||||
if (apiUrls.Count > 1)
|
||||
{
|
||||
WriteStatus("Benchmarking Endpoints", $"{apiUrls.Count} endpoints", ConsoleColor.Cyan);
|
||||
|
||||
var orderedEndpoints = await _benchmarkService.BenchmarkEndpointsAsync(
|
||||
apiUrls,
|
||||
async (endpoint, ct) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(endpoint, ct);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
},
|
||||
pingCount: 2,
|
||||
cancellationToken);
|
||||
|
||||
if (orderedEndpoints.Count > 0)
|
||||
{
|
||||
_fallbackHelper.SetEndpointOrder(orderedEndpoints);
|
||||
WriteDetail($"Fastest endpoint: {orderedEndpoints.First()}");
|
||||
}
|
||||
}
|
||||
|
||||
// Test connectivity with fallback
|
||||
var result = await _fallbackHelper.TryWithFallbackAsync(async (baseUrl) =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user