# 주제: 종료 시 드레인 루프가 프로덕션에서 아무도 증가시키지 않는 카운터를 폴링한다 # revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 # severity: P2 # ---- 종료 순서 ---- # MessagingShutdownLifecycle.java:83-102 # public void stop() { # if (!running.compareAndSet(true, false)) return; # admission.stopAcceptingNewWork(); // (1) 새 발행 거절 # Instant startedAt = clock.get(); # drain.beginDrain(startedAt); // (2) 드레인 시작 # Instant deadline = startedAt.plus(drainDeadline); # while (!drain.isDrained(clock.get()) && clock.get().isBefore(deadline)) { # Thread.sleep(POLL.toMillis()); // (3) 드레인 완료까지 대기 # } # } # 클래스 javadoc:15-19 # "a context shutdown went straight to closing beans while publishes were still in flight ... # which is the shape that turns a deploy into duplicated effects and unexplained ambiguous # publishes." # ---- 두 개의 서로 다른 in-flight 카운터 ---- # (A) MessagingAdmissionController.inFlight() <- InFlightLimiter. 발행이 증가시킨다. # MessagingCoreAutoConfiguration:439-447 이 이것을 DefaultMessagePublisher 에 주입한다. # MessagingEndpoint:55 가 이것을 actuator 로 보고한다. # (B) GracefulShutdownCoordinator.inFlight() <- tryBeginWork()/endWork() 가 증가·감소시킨다. # MessagingShutdownLifecycle 의 드레인 루프가 폴링하는 것은 이쪽이다. # ---- (B)를 증가시키는 프로덕션 코드가 없다 ---- # command: git grep -n "tryBeginWork|endWork()|mayCreateRetryAttempt" -- src | grep "/src/main/" src/messaging/messaging-kafka/src/main/java/.../KafkaConsumerRegistrar.java:233,244,289,325 src/messaging/messaging-rabbit/src/main/java/.../RabbitConsumerRegistrar.java:103,150,217 # => 프로덕션 소비자는 두 개의 ConsumerRegistrar 뿐이다. # 그 둘은 src/main 에서 한 번도 생성되지 않는다(EVD-269 계열, 그리고 EVD-316 의 스타터 빈 전수). # # command: git grep -n "GracefulShutdownCoordinator" -- src/messaging/messaging-runtime-core # exit: 1 (DefaultMessagePublisher 는 이 타입을 모른다) # DefaultMessagePublisher 생성자 파라미터(6-arg, 스타터가 쓰는 것): # DestinationProfileRegistry, DestinationAccessPolicy, MessageCodecRegistry, # MessagingAdmissionController, MessagingRuntimeRegistry, MessagingTransport # -> GracefulShutdownCoordinator 없음. # ---- 결과 ---- # 부팅된 애플리케이션에서 GracefulShutdownCoordinator.inFlight() 는 항상 0이다. # GracefulShutdownCoordinator.isDrained(now):126-128 # if (!draining.get()) return false; # if (inFlight.get() == 0) return true; <-- 항상 여기서 true # => beginDrain 직후 첫 검사에서 isDrained 가 true 이고, while 루프는 한 번도 sleep 하지 않는다. # stop() 은 즉시 반환한다. # # 즉 (1) 새 발행 거절은 실제로 동작하지만, # (3) "이미 진행 중인 발행을 기다린다" 는 부분은 동작하지 않는다. # 기다려야 할 대상은 (A)의 카운터인데 루프는 (B)를 본다. # javadoc 이 고쳤다고 말하는 "publishes were still in flight" 상태로 빈 소멸이 시작된다. # ---- 테스트가 이것을 드러내지 못하는 이유 ---- # MessagingShutdownLifecycleTest.java:52-73 # void aLeakedHandlerDoesNotHoldTheProcessOpen() { # // Work that begins and never ends: the drain can only finish by reaching its deadline. # GracefulShutdownCoordinator leaking = new GracefulShutdownCoordinator(Duration.ofMillis(100)); # leaking.tryBeginWork(); <-- 테스트가 직접 카운터를 채운다 # ... # } # => 프로덕션에서 아무도 호출하지 않는 메서드를 테스트가 손으로 호출해 상황을 만든다. # # MessagingShutdownLifecycleTest.java:37-50 # void stoppingRefusesNewPublishesFirst() { # lifecycle.stop(); # assertThatThrownBy(() -> admission.admit("order-events", 16)) ... // (1) 검증 # assertThat(drain.isAcceptingWork()).isFalse(); // (2) 검증 # assertThat(lifecycle.isRunning()).isFalse(); # } # => 공유 drain 의 inFlight 는 0이므로 즉시 드레인 완료된다. # 이 테스트는 "기다렸다" 를 단언하지 않는다 — (1)과 (2)만 본다. # ---- 참고: 8단계 계약과의 관계 ---- # MessagingLifecycle.ShutdownPhase (messaging-transport-spi) 는 8단계를 선언하고 구현체가 0이다. # MessagingShutdownLifecycle 이 수행하는 것: # STOP_PUBLISH_ADMISSION -> admission.stopAcceptingNewWork() (실제 동작) # STOP_NEW_HANDLERS -> drain.beginDrain() 이 isAcceptingWork()=false 로 만듦 # (그러나 그것을 읽는 프로덕션 코드가 없음) # DRAIN_HANDLERS -> 폴링 루프 (그러나 카운터가 항상 0) # CLOSE_CONNECTIONS -> Spring 빈 소멸. KafkaMessagingAutoConfiguration:128 # @Bean(destroyMethod = "close") 가 producer 를 닫는다. # PAUSE_CONSUMERS / FLUSH_SETTLEMENTS / AWAIT_PRODUCER_CONFIRMS / RELEASE_OUTBOX_LEASES # -> 수행 주체 없음 # (단계별 상세 판정은 analysis/messaging/messaging-transport-spi.md 소유)