Clinical Sync Batch Engine: first commit

This commit is contained in:
voltsrage
2026-06-23 03:02:30 +08:00
parent 90b8baa2a1
commit c9994b1ba2
60 changed files with 3547 additions and 31 deletions
@@ -1,21 +1,33 @@
using System.Text;
using Confluent.Kafka;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using RabbitMQ.Client.Exceptions;
public class OutboxRelayService : BackgroundService
{
private readonly IServiceProvider _services;
private readonly KafkaOptions _options;
private readonly RabbitMqOptions _rabbitOpts;
private readonly ClinicalSyncOptions _syncOpts;
private readonly ILogger<OutboxRelayService> _logger;
private IProducer<string, string>? _producer;
private IConnection? _rabbitConnection;
private IModel? _rabbitChannel;
private IBasicProperties? _rabbitProps;
public OutboxRelayService(
IServiceProvider services,
IOptions<KafkaOptions> options,
IOptions<RabbitMqOptions> rabbitOpts,
IOptions<ClinicalSyncOptions> syncOpts,
ILogger<OutboxRelayService> logger)
{
_services = services;
_options = options.Value;
_rabbitOpts = rabbitOpts.Value;
_syncOpts = syncOpts.Value;
_logger = logger;
}
@@ -33,6 +45,19 @@ public class OutboxRelayService : BackgroundService
RetryBackoffMs = 100
}).Build();
var factory = new ConnectionFactory
{
HostName = _rabbitOpts.Host,
Port = _rabbitOpts.Port,
UserName = _rabbitOpts.Username,
Password = _rabbitOpts.Password,
DispatchConsumersAsync = true,
};
_rabbitConnection = factory.CreateConnection("outbox-relay");
_rabbitChannel = _rabbitConnection.CreateModel();
_rabbitProps = _rabbitChannel.CreateBasicProperties();
_rabbitProps.Persistent = true;
return base.StartAsync(cancellationToken);
}
@@ -91,26 +116,45 @@ public class OutboxRelayService : BackgroundService
{
try
{
var result = await _producer!.ProduceAsync(
ev.Topic,
new Message<string, string>
{
Key = ev.PartitionKey ?? string.Empty,
Value = ev.Payload
},
ct);
if (ev.Topic == ClinicalSyncOptions.BatchReceivedOutboxTopic)
{
_rabbitChannel!.BasicPublish(
exchange: _syncOpts.SyncExchange,
routingKey: _syncOpts.SyncBatchReceivedRoutingKey,
basicProperties: _rabbitProps,
body: Encoding.UTF8.GetBytes(ev.Payload));
_logger.LogDebug(
"Published sync batch to RabbitMQ exchange={Exchange} routingKey={RoutingKey}",
_syncOpts.SyncExchange, _syncOpts.SyncBatchReceivedRoutingKey);
}
else
{
var result = await _producer!.ProduceAsync(
ev.Topic,
new Message<string, string>
{
Key = ev.PartitionKey ?? string.Empty,
Value = ev.Payload
},
ct);
_logger.LogDebug(
"Published {Topic} offset={Offset} partition={Partition} key={Key}",
ev.Topic, result.Offset.Value, result.Partition.Value, ev.PartitionKey);
}
published.Add(ev.Id);
ev.ProcessedAt = DateTimeOffset.UtcNow;
_logger.LogDebug(
"Published {Topic} offset={Offset} partition={Partition} key={Key}",
ev.Topic, result.Offset.Value, result.Partition.Value, ev.PartitionKey);
}
catch (ProduceException<string, string> ex)
catch (Exception ex) when (ex is ProduceException<string, string> or RabbitMQClientException)
{
ev.RetryCount++;
ev.LastError = ex.Error.Reason;
ev.LastError = ex switch
{
ProduceException<string, string> kex => kex.Error.Reason,
_ => ex.Message
};
if (ev.RetryCount >= _options.OutboxMaxRetries)
{
@@ -122,8 +166,8 @@ public class OutboxRelayService : BackgroundService
else
{
_logger.LogWarning(ex,
"Kafka produce failed for outbox event {Id} — retry {Retry}/{Max}",
ev.Id, ev.RetryCount, _options.OutboxMaxRetries);
"Outbox publish failed for event {Id} — retry {Retry}/{Max} topic={Topic}",
ev.Id, ev.RetryCount, _options.OutboxMaxRetries, ev.Topic);
}
hadFailure = true;
@@ -142,7 +186,9 @@ public class OutboxRelayService : BackgroundService
public override void Dispose()
{
_rabbitChannel?.Dispose();
_rabbitConnection?.Dispose();
_producer?.Dispose();
base.Dispose();
}
}
}