diff --git a/SPMS.API/Controllers/PublicController.cs b/SPMS.API/Controllers/PublicController.cs index 47ffc71..f6d16e3 100644 --- a/SPMS.API/Controllers/PublicController.cs +++ b/SPMS.API/Controllers/PublicController.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Swashbuckle.AspNetCore.Annotations; using SPMS.Domain.Common; using SPMS.Infrastructure; +using SPMS.Infrastructure.Messaging; namespace SPMS.API.Controllers; @@ -14,10 +15,17 @@ namespace SPMS.API.Controllers; public class PublicController : ControllerBase { private readonly AppDbContext _dbContext; + private readonly RabbitMQConnection _rabbitConnection; + private readonly RabbitMQInitializer _rabbitInitializer; - public PublicController(AppDbContext dbContext) + public PublicController( + AppDbContext dbContext, + RabbitMQConnection rabbitConnection, + RabbitMQInitializer rabbitInitializer) { _dbContext = dbContext; + _rabbitConnection = rabbitConnection; + _rabbitInitializer = rabbitInitializer; } [HttpPost("health")] @@ -39,11 +47,26 @@ public class PublicController : ControllerBase allHealthy = false; } - // 2. Redis 연결 확인 (Phase 2에서 구현 예정) + // 2. Redis 연결 확인 (Phase 3-2에서 구현 예정) checks["redis"] = new { status = "not_configured" }; - // 3. RabbitMQ 연결 확인 (Phase 2에서 구현 예정) - checks["rabbitmq"] = new { status = "not_configured" }; + // 3. RabbitMQ 연결 확인 + var rabbitConnected = _rabbitConnection.IsConnected; + var rabbitInitialized = _rabbitInitializer.IsInitialized; + if (rabbitConnected && rabbitInitialized) + { + checks["rabbitmq"] = new { status = "healthy" }; + } + else + { + checks["rabbitmq"] = new + { + status = "unhealthy", + connected = rabbitConnected, + initialized = rabbitInitialized + }; + allHealthy = false; + } if (allHealthy) { diff --git a/SPMS.Infrastructure/DependencyInjection.cs b/SPMS.Infrastructure/DependencyInjection.cs index cadd401..87d794f 100644 --- a/SPMS.Infrastructure/DependencyInjection.cs +++ b/SPMS.Infrastructure/DependencyInjection.cs @@ -58,7 +58,8 @@ public static class DependencyInjection // RabbitMQ services.Configure(configuration.GetSection(RabbitMQSettings.SectionName)); services.AddSingleton(); - services.AddHostedService(); + services.AddSingleton(); + services.AddHostedService(sp => sp.GetRequiredService()); services.AddScoped(); // Push Senders diff --git a/SPMS.Infrastructure/Messaging/RabbitMQConnection.cs b/SPMS.Infrastructure/Messaging/RabbitMQConnection.cs index bb80f6e..94d4c22 100644 --- a/SPMS.Infrastructure/Messaging/RabbitMQConnection.cs +++ b/SPMS.Infrastructure/Messaging/RabbitMQConnection.cs @@ -12,6 +12,8 @@ public class RabbitMQConnection : IAsyncDisposable private readonly SemaphoreSlim _semaphore = new(1, 1); private IConnection? _connection; + public bool IsConnected => _connection is { IsOpen: true }; + public RabbitMQConnection( IOptions settings, ILogger logger) diff --git a/SPMS.Infrastructure/Messaging/RabbitMQInitializer.cs b/SPMS.Infrastructure/Messaging/RabbitMQInitializer.cs index 1c22bba..0016765 100644 --- a/SPMS.Infrastructure/Messaging/RabbitMQInitializer.cs +++ b/SPMS.Infrastructure/Messaging/RabbitMQInitializer.cs @@ -6,11 +6,14 @@ using SPMS.Application.Settings; namespace SPMS.Infrastructure.Messaging; -public class RabbitMQInitializer : IHostedService +public class RabbitMQInitializer : BackgroundService { private readonly RabbitMQConnection _connection; private readonly RabbitMQSettings _settings; private readonly ILogger _logger; + private static readonly TimeSpan RetryInterval = TimeSpan.FromSeconds(30); + + public bool IsInitialized { get; private set; } public RabbitMQInitializer( RabbitMQConnection connection, @@ -22,70 +25,76 @@ public class RabbitMQInitializer : IHostedService _logger = logger; } - public async Task StartAsync(CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - try + while (!stoppingToken.IsCancellationRequested) { - await using var channel = await _connection.CreateChannelAsync(cancellationToken); - - // Exchange 선언: Direct, Durable - await channel.ExchangeDeclareAsync( - exchange: _settings.Exchange, - type: ExchangeType.Direct, - durable: true, - autoDelete: false, - arguments: null, - cancellationToken: cancellationToken); - - _logger.LogInformation("Exchange 선언 완료: {Exchange}", _settings.Exchange); - - var queueArgs = new Dictionary + try { - { "x-message-ttl", _settings.MessageTtl } - }; - - // Push Queue 선언 - await channel.QueueDeclareAsync( - queue: _settings.PushQueue, - durable: true, - exclusive: false, - autoDelete: false, - arguments: queueArgs, - cancellationToken: cancellationToken); - - await channel.QueueBindAsync( - queue: _settings.PushQueue, - exchange: _settings.Exchange, - routingKey: "push", - cancellationToken: cancellationToken); - - _logger.LogInformation("Queue 선언 및 바인딩 완료: {Queue} → {Exchange} (routing_key: push)", - _settings.PushQueue, _settings.Exchange); - - // Schedule Queue 선언 - await channel.QueueDeclareAsync( - queue: _settings.ScheduleQueue, - durable: true, - exclusive: false, - autoDelete: false, - arguments: queueArgs, - cancellationToken: cancellationToken); - - await channel.QueueBindAsync( - queue: _settings.ScheduleQueue, - exchange: _settings.Exchange, - routingKey: "schedule", - cancellationToken: cancellationToken); - - _logger.LogInformation("Queue 선언 및 바인딩 완료: {Queue} → {Exchange} (routing_key: schedule)", - _settings.ScheduleQueue, _settings.Exchange); - } - catch (Exception ex) - { - _logger.LogError(ex, "RabbitMQ Exchange/Queue 초기화 실패"); - throw; + await InitializeAsync(stoppingToken); + IsInitialized = true; + _logger.LogInformation("RabbitMQ 초기화 완료"); + return; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "RabbitMQ 초기화 실패 — {RetrySeconds}초 후 재시도합니다.", RetryInterval.TotalSeconds); + await Task.Delay(RetryInterval, stoppingToken); + } } } - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + private async Task InitializeAsync(CancellationToken cancellationToken) + { + await using var channel = await _connection.CreateChannelAsync(cancellationToken); + + await channel.ExchangeDeclareAsync( + exchange: _settings.Exchange, + type: ExchangeType.Direct, + durable: true, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + + _logger.LogInformation("Exchange 선언 완료: {Exchange}", _settings.Exchange); + + var queueArgs = new Dictionary + { + { "x-message-ttl", _settings.MessageTtl } + }; + + await channel.QueueDeclareAsync( + queue: _settings.PushQueue, + durable: true, + exclusive: false, + autoDelete: false, + arguments: queueArgs, + cancellationToken: cancellationToken); + + await channel.QueueBindAsync( + queue: _settings.PushQueue, + exchange: _settings.Exchange, + routingKey: "push", + cancellationToken: cancellationToken); + + _logger.LogInformation("Queue 선언 및 바인딩 완료: {Queue} → {Exchange} (routing_key: push)", + _settings.PushQueue, _settings.Exchange); + + await channel.QueueDeclareAsync( + queue: _settings.ScheduleQueue, + durable: true, + exclusive: false, + autoDelete: false, + arguments: queueArgs, + cancellationToken: cancellationToken); + + await channel.QueueBindAsync( + queue: _settings.ScheduleQueue, + exchange: _settings.Exchange, + routingKey: "schedule", + cancellationToken: cancellationToken); + + _logger.LogInformation("Queue 선언 및 바인딩 완료: {Queue} → {Exchange} (routing_key: schedule)", + _settings.ScheduleQueue, _settings.Exchange); + } }