|
| 1 | +package com.stackup.stackup.common.health; |
| 2 | + |
| 3 | +import static org.assertj.core.api.Assertions.assertThat; |
| 4 | +import static org.mockito.Mockito.when; |
| 5 | + |
| 6 | +import com.stackup.stackup.common.config.properties.RabbitMqProperties; |
| 7 | +import org.junit.jupiter.api.BeforeEach; |
| 8 | +import org.junit.jupiter.api.Test; |
| 9 | +import org.junit.jupiter.api.extension.ExtendWith; |
| 10 | +import org.mockito.Mock; |
| 11 | +import org.mockito.junit.jupiter.MockitoExtension; |
| 12 | +import org.springframework.amqp.core.AmqpAdmin; |
| 13 | +import org.springframework.amqp.core.QueueInformation; |
| 14 | +import org.springframework.boot.health.contributor.Health; |
| 15 | +import org.springframework.boot.health.contributor.Status; |
| 16 | + |
| 17 | +/** |
| 18 | + * AI 생존을 HTTP 가 아니라 큐 컨슈머 수로 판단한다 — Core→AI HTTP 의존을 만들지 않기 위해서다 |
| 19 | + * (아키텍처 §4.1). 컨슈머 0 은 "프로세스는 떠 있지만 일을 안 받는" 상태까지 잡아낸다. |
| 20 | + */ |
| 21 | +@ExtendWith(MockitoExtension.class) |
| 22 | +class AiServerHealthIndicatorTest { |
| 23 | + |
| 24 | + private static final String QUEUE = "ai.generate.questions"; |
| 25 | + |
| 26 | + @Mock AmqpAdmin amqpAdmin; |
| 27 | + |
| 28 | + AiServerHealthIndicator indicator; |
| 29 | + |
| 30 | + @BeforeEach |
| 31 | + void setUp() { |
| 32 | + indicator = new AiServerHealthIndicator(amqpAdmin, propertiesWithQueue(QUEUE)); |
| 33 | + } |
| 34 | + |
| 35 | + @Test |
| 36 | + void up_whenQueueHasConsumers() { |
| 37 | + when(amqpAdmin.getQueueInfo(QUEUE)).thenReturn(new QueueInformation(QUEUE, 3, 2)); |
| 38 | + |
| 39 | + Health health = indicator.health(); |
| 40 | + |
| 41 | + assertThat(health.getStatus()).isEqualTo(Status.UP); |
| 42 | + assertThat(health.getDetails()).containsEntry("consumers", 2); |
| 43 | + assertThat(health.getDetails()).containsEntry("pendingMessages", 3L); |
| 44 | + } |
| 45 | + |
| 46 | + // 큐는 있는데 아무도 안 먹고 있으면 면접이 시작되지 않는다 — UP 으로 볼 수 없다. |
| 47 | + @Test |
| 48 | + void down_whenNoConsumers() { |
| 49 | + when(amqpAdmin.getQueueInfo(QUEUE)).thenReturn(new QueueInformation(QUEUE, 12, 0)); |
| 50 | + |
| 51 | + Health health = indicator.health(); |
| 52 | + |
| 53 | + assertThat(health.getStatus()).isEqualTo(Status.DOWN); |
| 54 | + assertThat(health.getDetails()).containsEntry("consumers", 0); |
| 55 | + // 쌓인 메시지 수가 함께 보여야 얼마나 밀렸는지 판단할 수 있다. |
| 56 | + assertThat(health.getDetails()).containsEntry("pendingMessages", 12L); |
| 57 | + } |
| 58 | + |
| 59 | + @Test |
| 60 | + void down_whenQueueMissing() { |
| 61 | + when(amqpAdmin.getQueueInfo(QUEUE)).thenReturn(null); |
| 62 | + |
| 63 | + assertThat(indicator.health().getStatus()).isEqualTo(Status.DOWN); |
| 64 | + } |
| 65 | + |
| 66 | + // 브로커가 죽은 경우는 rabbitmq 컴포넌트가 따로 알려준다. 여기서 DOWN 을 겹쳐 내면 |
| 67 | + // "AI 가 죽었다" 로 오독된다 — 판단 불가로 남긴다. |
| 68 | + @Test |
| 69 | + void unknown_whenBrokerUnreachable() { |
| 70 | + when(amqpAdmin.getQueueInfo(QUEUE)).thenThrow(new IllegalStateException("connection refused")); |
| 71 | + |
| 72 | + Health health = indicator.health(); |
| 73 | + |
| 74 | + assertThat(health.getStatus()).isEqualTo(Status.UNKNOWN); |
| 75 | + assertThat(health.getDetails()).containsEntry("queue", QUEUE); |
| 76 | + } |
| 77 | + |
| 78 | + private RabbitMqProperties propertiesWithQueue(String generateQuestions) { |
| 79 | + return new RabbitMqProperties( |
| 80 | + "core", "1", |
| 81 | + new RabbitMqProperties.Message("application/json", "UTF-8", "X-Trace-Id"), |
| 82 | + new RabbitMqProperties.Template(true), |
| 83 | + new RabbitMqProperties.Exchanges(true, false, |
| 84 | + new RabbitMqProperties.Exchanges.Names("core.ai", "ai.core", "realtime")), |
| 85 | + new RabbitMqProperties.Queues(true, |
| 86 | + new RabbitMqProperties.Queues.Names( |
| 87 | + "ai.analyze.resume", "ai.analyze.repository", "ai.analyze.web", |
| 88 | + "ai.analyze.cover_letter", generateQuestions, "ai.generate.followup", |
| 89 | + "ai.generate.feedback", "ai.analyze.voice", "ai.generate.tts", |
| 90 | + "core.callback.analysis", "core.callback.questions", "core.callback.feedback", |
| 91 | + "core.callback.voice", "core.callback.tts")), |
| 92 | + new RabbitMqProperties.RoutingKeyProperties( |
| 93 | + "analyze.resume", "analyze.repository", "analyze.web", "analyze.cover_letter", |
| 94 | + "generate.questions", "generate.followup", "generate.feedback", "analyze.voice", |
| 95 | + "generate.tts", "callback.analysis", "callback.questions", "callback.feedback", |
| 96 | + "callback.voice", "callback.tts", "session.notify", "realtime.user.notify", |
| 97 | + "realtime.document.notify"), |
| 98 | + new RabbitMqProperties.DeadLetter("dlx", "dlq."), |
| 99 | + new RabbitMqProperties.Retry(3, java.time.Duration.ofSeconds(1), 2.0, |
| 100 | + java.time.Duration.ofSeconds(10)) |
| 101 | + ); |
| 102 | + } |
| 103 | +} |
0 commit comments