using System.Text.Json; using Confluent.Kafka; using Microsoft.Extensions.Options; public class WarningAlertService : BackgroundService { private readonly IServiceProvider _services; private readonly KafkaOptions _kafkaOptions; private readonly ILogger _logger; public WarningAlertService( IServiceProvider services, IOptions kafkaOptions, ILogger logger) { _services = services; _kafkaOptions = kafkaOptions.Value; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var config = new ConsumerConfig { BootstrapServers = _kafkaOptions.BootstrapServers, GroupId = "warning-evaluator", AutoOffsetReset = AutoOffsetReset.Earliest, EnableAutoCommit = false }; using var consumer = new ConsumerBuilder(config).Build(); consumer.Subscribe(_kafkaOptions.Topics.ObservationRecorded); _logger.LogInformation("WarningAlertService started — consumer group: warning-evaluator"); var guard = new PoisonPillGuard("warning-evaluator", _kafkaOptions.MaxPoisonRetries, _logger); try { while (!stoppingToken.IsCancellationRequested) { ConsumeResult? result = null; try { result = consumer.Consume(stoppingToken); var evt = JsonSerializer.Deserialize( result.Message.Value, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; using var scope = _services.CreateScope(); var evaluator = scope.ServiceProvider.GetRequiredService(); await evaluator.EvaluateAsync( evt.ObservationId, evt.EncounterId, evt.PatientId, evt.ObservationCode, evt.Value, stoppingToken); 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, "WarningAlertService failed on topic={Topic} offset={Offset} — will retry", result?.Topic, result?.Offset.Value); await Task.Delay(2000, stoppingToken); } } } finally { consumer.Close(); } } }