190 lines
6.6 KiB
C#
190 lines
6.6 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
|
|
namespace JobTrackerApi.Services
|
|
{
|
|
public sealed record SummarizerMetrics(
|
|
bool Healthy,
|
|
string? Model,
|
|
double? HealthLatencyMs,
|
|
int Requests,
|
|
int CacheHits,
|
|
int CacheMisses,
|
|
int Failures,
|
|
double? AverageLatencyMs,
|
|
DateTimeOffset? LastSuccessAt,
|
|
DateTimeOffset? LastFailureAt,
|
|
string? LastError
|
|
);
|
|
|
|
public interface ISummarizerService
|
|
{
|
|
Task<string?> SummarizeAsync(string text, int maxLength = 150, int minLength = 30);
|
|
Task<SummarizerMetrics> GetMetricsAsync(CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public class SummarizerService : ISummarizerService
|
|
{
|
|
private readonly IHttpClientFactory _httpFactory;
|
|
private readonly IMemoryCache _cache;
|
|
private readonly object _metricsLock = new();
|
|
private int _requests;
|
|
private int _cacheHits;
|
|
private int _cacheMisses;
|
|
private int _failures;
|
|
private long _totalLatencyTicks;
|
|
private DateTimeOffset? _lastSuccessAt;
|
|
private DateTimeOffset? _lastFailureAt;
|
|
private string? _lastError;
|
|
|
|
public SummarizerService(IHttpClientFactory httpFactory, IMemoryCache cache)
|
|
{
|
|
_httpFactory = httpFactory;
|
|
_cache = cache;
|
|
}
|
|
|
|
public async Task<string?> SummarizeAsync(string text, int maxLength = 150, int minLength = 30)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(text)) return null;
|
|
|
|
var key = $"summ:{text.GetHashCode()}:{maxLength}:{minLength}";
|
|
Interlocked.Increment(ref _requests);
|
|
|
|
if (_cache.TryGetValue<string>(key, out var cached))
|
|
{
|
|
Interlocked.Increment(ref _cacheHits);
|
|
lock (_metricsLock)
|
|
{
|
|
_lastSuccessAt = DateTimeOffset.UtcNow;
|
|
_lastError = null;
|
|
}
|
|
return cached;
|
|
}
|
|
|
|
Interlocked.Increment(ref _cacheMisses);
|
|
|
|
var client = _httpFactory.CreateClient("summarizer");
|
|
var payload = JsonSerializer.Serialize(new { text, max_length = maxLength, min_length = minLength });
|
|
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
try
|
|
{
|
|
var res = await client.PostAsync("/summarize", content);
|
|
sw.Stop();
|
|
Interlocked.Add(ref _totalLatencyTicks, sw.ElapsedTicks);
|
|
if (!res.IsSuccessStatusCode) return null;
|
|
|
|
using var stream = await res.Content.ReadAsStreamAsync();
|
|
using var doc = await JsonDocument.ParseAsync(stream);
|
|
if (doc.RootElement.TryGetProperty("summary", out var el))
|
|
{
|
|
var s = el.GetString();
|
|
if (!string.IsNullOrWhiteSpace(s)) _cache.Set(key, s, TimeSpan.FromHours(6));
|
|
lock (_metricsLock)
|
|
{
|
|
_lastSuccessAt = DateTimeOffset.UtcNow;
|
|
_lastError = null;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
sw.Stop();
|
|
Interlocked.Add(ref _totalLatencyTicks, sw.ElapsedTicks);
|
|
Interlocked.Increment(ref _failures);
|
|
lock (_metricsLock)
|
|
{
|
|
_lastFailureAt = DateTimeOffset.UtcNow;
|
|
_lastError = ex.Message;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public async Task<SummarizerMetrics> GetMetricsAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
var client = _httpFactory.CreateClient("summarizer");
|
|
string? model = null;
|
|
double? healthLatencyMs = null;
|
|
var healthy = false;
|
|
string? healthError = null;
|
|
|
|
try
|
|
{
|
|
var sw = Stopwatch.StartNew();
|
|
using var res = await client.GetAsync("/health", cancellationToken);
|
|
sw.Stop();
|
|
healthLatencyMs = Math.Round(sw.Elapsed.TotalMilliseconds, 1);
|
|
healthy = res.IsSuccessStatusCode;
|
|
|
|
if (healthy)
|
|
{
|
|
using var stream = await res.Content.ReadAsStreamAsync(cancellationToken);
|
|
using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
if (doc.RootElement.TryGetProperty("model", out var modelEl))
|
|
{
|
|
model = modelEl.GetString();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
healthError = $"Health check returned {(int)res.StatusCode}.";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
healthError = ex.Message;
|
|
}
|
|
|
|
var requests = Volatile.Read(ref _requests);
|
|
var cacheHits = Volatile.Read(ref _cacheHits);
|
|
var cacheMisses = Volatile.Read(ref _cacheMisses);
|
|
var failures = Volatile.Read(ref _failures);
|
|
var totalLatencyTicks = Volatile.Read(ref _totalLatencyTicks);
|
|
|
|
DateTimeOffset? lastSuccessAt;
|
|
DateTimeOffset? lastFailureAt;
|
|
string? lastError;
|
|
lock (_metricsLock)
|
|
{
|
|
lastSuccessAt = _lastSuccessAt;
|
|
lastFailureAt = _lastFailureAt;
|
|
lastError = _lastError;
|
|
}
|
|
|
|
if (!healthy && !string.IsNullOrWhiteSpace(healthError))
|
|
{
|
|
lastError = healthError;
|
|
}
|
|
|
|
double? averageLatencyMs = requests > 0
|
|
? Math.Round(TimeSpan.FromTicks(totalLatencyTicks).TotalMilliseconds / requests, 1)
|
|
: null;
|
|
|
|
return new SummarizerMetrics(
|
|
Healthy: healthy,
|
|
Model: model,
|
|
HealthLatencyMs: healthLatencyMs,
|
|
Requests: requests,
|
|
CacheHits: cacheHits,
|
|
CacheMisses: cacheMisses,
|
|
Failures: failures,
|
|
AverageLatencyMs: averageLatencyMs,
|
|
LastSuccessAt: lastSuccessAt,
|
|
LastFailureAt: lastFailureAt,
|
|
LastError: lastError
|
|
);
|
|
}
|
|
}
|
|
}
|