Files
vigilcare-clinical/VigilCareClinicalAPI/Notifications/RabbitMqTopologyProvisioner.cs
T

164 lines
6.4 KiB
C#

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using RabbitMQ.Client.Exceptions;
public sealed class RabbitMqTopologyProvisioner : IHostedService
{
public const string Exchange = "clinical.notifications.exchange";
public const string PagingKey = "alerts.paging";
public const string EscalKey = "alerts.escalation";
public const string DischargeKey = "notifications.discharge";
public const string ReconciliationKey = "notifications.reconciliation";
public const string SyncExchange = "clinical.sync";
public const string SyncBatchReceivedKey = "sync.batch_received";
public const string SyncBatchQueue = "clinical.sync.batch_received";
private readonly RabbitMqOptions _opts;
private readonly IHostEnvironment _env;
private readonly ILogger<RabbitMqTopologyProvisioner> _logger;
public RabbitMqTopologyProvisioner(
IOptions<RabbitMqOptions> opts,
IHostEnvironment env,
ILogger<RabbitMqTopologyProvisioner> logger)
{
_opts = opts.Value;
_env = env;
_logger = logger;
}
public Task StartAsync(CancellationToken ct)
{
var factory = BuildFactory();
using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();
if (_env.IsDevelopment() || _env.IsEnvironment("Testing"))
{
// Use a throwaway channel — a failed purge on a missing queue closes the
// channel, which would break the declare calls below.
using var cleanup = connection.CreateModel();
try
{
// x-message-ttl is immutable once the queue exists. Integration tests
// use 5 s while local dev uses 5 min — delete so config drives the TTL.
cleanup.QueueDelete("alerts.paging.dlq", ifUnused: false, ifEmpty: false);
}
catch (OperationInterruptedException ex)
{
_logger.LogDebug(ex, "DLQ delete skipped — queue may not exist yet");
}
if (_env.IsEnvironment("Testing"))
{
foreach (var queue in new[] { "alerts.paging.queue", "alerts.escalation.queue" })
{
try { cleanup.QueuePurge(queue); }
catch (OperationInterruptedException ex)
{
_logger.LogDebug(ex, "Queue purge skipped for {Queue}", queue);
}
}
}
}
// Direct exchange — routing key determines destination queue.
channel.ExchangeDeclare(Exchange, ExchangeType.Direct, durable: true);
// --- alerts.paging.queue ---
// Dead-letters to the default exchange with routing key = alerts.paging.dlq.
// Prefetch is set per-consumer, not here.
channel.QueueDeclare(
queue: "alerts.paging.queue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: new Dictionary<string, object>
{
["x-dead-letter-exchange"] = "", // default exchange
["x-dead-letter-routing-key"] = "alerts.paging.dlq",
});
channel.QueueBind("alerts.paging.queue", Exchange, PagingKey);
// --- alerts.paging.dlq ---
// Messages land here after NACK from the paging worker.
// After x-message-ttl expires, re-routes to clinical.notifications.exchange
// with routing key alerts.escalation → reaches alerts.escalation.queue.
channel.QueueDeclare(
queue: "alerts.paging.dlq",
durable: true,
exclusive: false,
autoDelete: false,
arguments: new Dictionary<string, object>
{
["x-message-ttl"] = (int)_opts.PagingAckTimeoutMs,
["x-dead-letter-exchange"] = Exchange,
["x-dead-letter-routing-key"] = EscalKey,
});
// DLQ is reached via the default exchange — no binding to the direct exchange needed.
// --- alerts.escalation.queue ---
channel.QueueDeclare(
queue: "alerts.escalation.queue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind("alerts.escalation.queue", Exchange, EscalKey);
// --- notifications.discharge.queue ---
channel.QueueDeclare(
queue: "notifications.discharge.queue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind("notifications.discharge.queue", Exchange, DischargeKey);
// --- notifications.appointment.queue (placeholder) ---
channel.QueueDeclare(
queue: "notifications.appointment.queue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind("notifications.appointment.queue", Exchange, "notifications.appointment");
channel.QueueDeclare(
queue: "notifications.reconciliation.queue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind("notifications.reconciliation.queue", Exchange, ReconciliationKey);
channel.ExchangeDeclare(SyncExchange, ExchangeType.Topic, durable: true);
channel.QueueDeclare(
queue: SyncBatchQueue,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind(SyncBatchQueue, SyncExchange, SyncBatchReceivedKey);
_logger.LogInformation(
"RabbitMQ topology provisioned. Exchange={Exchange} PagingDlqTtlMs={Ttl}",
Exchange, _opts.PagingAckTimeoutMs);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
public IConnectionFactory BuildFactory() => new ConnectionFactory
{
HostName = _opts.Host,
Port = _opts.Port,
UserName = _opts.Username,
Password = _opts.Password,
DispatchConsumersAsync = true,
};
}