Files
jobtrackingapp/JobTrackerApi/Services/SummarizerService.cs
T

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