Files
voltsrage 2a3ef62a7d
CI / frontend (push) Failing after 57s
CI / backend (push) Failing after 6m27s
Add deployment
2026-08-05 00:26:20 +08:00

99 lines
3.7 KiB
C#

using System.Text.Json;
using Confluent.Kafka;
using Microsoft.Extensions.Options;
public class TrendAnalyzerService : BackgroundService
{
private readonly IServiceProvider _services;
private readonly KafkaOptions _kafkaOptions;
private readonly ILogger<TrendAnalyzerService> _logger;
public TrendAnalyzerService(
IServiceProvider services,
IOptions<KafkaOptions> kafkaOptions,
ILogger<TrendAnalyzerService> logger)
{
_services = services;
_kafkaOptions = kafkaOptions.Value;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var config = new ConsumerConfig
{
BootstrapServers = _kafkaOptions.BootstrapServers,
GroupId = "trend-analyzer",
AutoOffsetReset = AutoOffsetReset.Earliest,
EnableAutoCommit = false
}.ApplySecurity(_kafkaOptions);
using var consumer = new ConsumerBuilder<string, string>(config).Build();
consumer.Subscribe(_kafkaOptions.Topics.ObservationRecorded);
_logger.LogInformation("TrendAnalyzerService started — consumer group: trend-analyzer");
var guard = new PoisonPillGuard("trend-analyzer", _kafkaOptions.MaxPoisonRetries, _logger);
try
{
while (!stoppingToken.IsCancellationRequested)
{
ConsumeResult<string, string>? result = null;
try
{
result = consumer.Consume(stoppingToken);
var evt = JsonSerializer.Deserialize<TrendObservationEvent>(
result.Message.Value,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
using var scope = _services.CreateScope();
var detector = scope.ServiceProvider.GetRequiredService<TrendDetector>();
var outcome = await detector.ProcessObservationAsync(
evt.EncounterId,
evt.PatientId,
evt.ObservationCode,
evt.Value,
evt.RecordedAt,
stoppingToken);
if (outcome.Outcome == TrendOutcome.EncounterNotFound)
_logger.LogWarning(
"Skipping stale observation.recorded event — encounter={Id} code={Code} offset={Offset}",
evt.EncounterId, outcome.ObservationCode, result.Offset.Value);
else if (outcome.Outcome == TrendOutcome.RapidDeterioration)
_logger.LogInformation(
"RAPID_DETERIORATION alert via consumer — encounter={Id} code={Code} rate={Rate}/min",
evt.EncounterId, outcome.ObservationCode, outcome.RatePerMinute);
consumer.Commit(result);
guard.OnSuccess();
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
if (result is not null && guard.ShouldSkip(result, ex))
{
consumer.Commit(result);
continue;
}
_logger.LogError(ex,
"TrendAnalyzerService failed on topic={Topic} offset={Offset} — will retry",
result?.Topic, result?.Offset.Value);
await Task.Delay(2000, stoppingToken);
}
}
}
finally
{
consumer.Close();
}
}
}