The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
182 lines
13 KiB
Plaintext
182 lines
13 KiB
Plaintext
# MessagingLifecycle 전문 (67 줄)
|
|
MessagingLifecycle.java:1 package dev.caskeleton.messaging.transport;
|
|
MessagingLifecycle.java:2
|
|
MessagingLifecycle.java:3 import java.time.Duration;
|
|
MessagingLifecycle.java:4
|
|
MessagingLifecycle.java:5 /**
|
|
MessagingLifecycle.java:6 * The ordered lifecycle every messaging runtime implements.
|
|
MessagingLifecycle.java:7 *
|
|
MessagingLifecycle.java:8 * <p>The order in {@link ShutdownPhase} is the contract, not an implementation detail. Closing
|
|
MessagingLifecycle.java:9 * connections before settlements have been transmitted loses the settlements, and pausing consumers
|
|
MessagingLifecycle.java:10 * after draining lets fresh deliveries arrive into a runtime that is already shutting down. Each
|
|
MessagingLifecycle.java:11 * adapter implements the phases; none of them chooses the order.
|
|
MessagingLifecycle.java:12 *
|
|
MessagingLifecycle.java:13 * <p>Implementations are driven by the Spring lifecycle rather than a JVM shutdown hook alone. A
|
|
MessagingLifecycle.java:14 * shutdown hook runs after the context has already begun disposing beans, so a handler mid-drain
|
|
MessagingLifecycle.java:15 * can find its datasource closed underneath it.
|
|
MessagingLifecycle.java:16 */
|
|
MessagingLifecycle.java:17 public interface MessagingLifecycle {
|
|
MessagingLifecycle.java:18
|
|
MessagingLifecycle.java:19 /** The fixed phases of an orderly shutdown, in the order they must run. */
|
|
MessagingLifecycle.java:20 enum ShutdownPhase {
|
|
MessagingLifecycle.java:21 /** Refuse new publishes. Nothing in flight is affected. */
|
|
MessagingLifecycle.java:22 STOP_PUBLISH_ADMISSION,
|
|
MessagingLifecycle.java:23 /** Refuse to start new delivery handlers. */
|
|
MessagingLifecycle.java:24 STOP_NEW_HANDLERS,
|
|
MessagingLifecycle.java:25 /** Ask the broker to stop delivering. */
|
|
MessagingLifecycle.java:26 PAUSE_CONSUMERS,
|
|
MessagingLifecycle.java:27 /** Let running handlers finish. */
|
|
MessagingLifecycle.java:28 DRAIN_HANDLERS,
|
|
MessagingLifecycle.java:29 /** Transmit the settlements those handlers produced. */
|
|
MessagingLifecycle.java:30 FLUSH_SETTLEMENTS,
|
|
MessagingLifecycle.java:31 /** Wait for outstanding producer confirms so publishes are not left ambiguous. */
|
|
MessagingLifecycle.java:32 AWAIT_PRODUCER_CONFIRMS,
|
|
MessagingLifecycle.java:33 /** Return outbox leases so another relay can claim the rows immediately. */
|
|
MessagingLifecycle.java:34 RELEASE_OUTBOX_LEASES,
|
|
MessagingLifecycle.java:35 /** Close connections and channels. */
|
|
MessagingLifecycle.java:36 CLOSE_CONNECTIONS
|
|
MessagingLifecycle.java:37 }
|
|
MessagingLifecycle.java:38
|
|
MessagingLifecycle.java:39 /** The default drain budget from the design. */
|
|
MessagingLifecycle.java:40 Duration DEFAULT_DRAIN_DEADLINE = Duration.ofSeconds(30);
|
|
MessagingLifecycle.java:41
|
|
MessagingLifecycle.java:42 /**
|
|
MessagingLifecycle.java:43 * Starts the runtime.
|
|
MessagingLifecycle.java:44 *
|
|
MessagingLifecycle.java:45 * @throws IllegalStateException when the runtime was already started
|
|
MessagingLifecycle.java:46 */
|
|
MessagingLifecycle.java:47 void start();
|
|
MessagingLifecycle.java:48
|
|
MessagingLifecycle.java:49 /**
|
|
MessagingLifecycle.java:50 * Runs the shutdown phases in order, bounded by the drain deadline.
|
|
MessagingLifecycle.java:51 *
|
|
MessagingLifecycle.java:52 * <p>Work still running at the deadline is abandoned <em>unsettled</em> so the broker redelivers
|
|
MessagingLifecycle.java:53 * it. Unconfirmed publishes are recorded as ambiguous rather than as successes.
|
|
MessagingLifecycle.java:54 *
|
|
MessagingLifecycle.java:55 * @param drainDeadline how long in-flight work may take
|
|
MessagingLifecycle.java:56 * @return the phase that was still running when the deadline passed, or {@link
|
|
MessagingLifecycle.java:57 * ShutdownPhase#CLOSE_CONNECTIONS} when the shutdown completed
|
|
MessagingLifecycle.java:58 */
|
|
MessagingLifecycle.java:59 ShutdownPhase shutdown(Duration drainDeadline);
|
|
MessagingLifecycle.java:60
|
|
MessagingLifecycle.java:61 /**
|
|
MessagingLifecycle.java:62 * Reports whether the runtime is running and accepting work.
|
|
MessagingLifecycle.java:63 *
|
|
MessagingLifecycle.java:64 * @return true between {@link #start()} and the first shutdown phase
|
|
MessagingLifecycle.java:65 */
|
|
MessagingLifecycle.java:66 boolean isRunning();
|
|
MessagingLifecycle.java:67 }
|
|
ShutdownPhase 상수 개수 : 8 개
|
|
인터페이스가 요구하는 메서드 :
|
|
:40 Duration DEFAULT_DRAIN_DEADLINE = Duration.ofSeconds(30);
|
|
:47 void start();
|
|
:59 ShutdownPhase shutdown(Duration drainDeadline);
|
|
:66 boolean isRunning();
|
|
|
|
# MessagingLifecycleTest 전문 (59 줄)
|
|
MessagingLifecycleTest.java:1 package dev.caskeleton.messaging.transport;
|
|
MessagingLifecycleTest.java:2
|
|
MessagingLifecycleTest.java:3 import static org.assertj.core.api.Assertions.assertThat;
|
|
MessagingLifecycleTest.java:4
|
|
MessagingLifecycleTest.java:5 import dev.caskeleton.messaging.transport.MessagingLifecycle.ShutdownPhase;
|
|
MessagingLifecycleTest.java:6 import java.time.Duration;
|
|
MessagingLifecycleTest.java:7 import java.util.List;
|
|
MessagingLifecycleTest.java:8 import org.junit.jupiter.api.Test;
|
|
MessagingLifecycleTest.java:9
|
|
MessagingLifecycleTest.java:10 class MessagingLifecycleTest {
|
|
MessagingLifecycleTest.java:11
|
|
MessagingLifecycleTest.java:12 @Test
|
|
MessagingLifecycleTest.java:13 void publishAdmissionClosesBeforeConsumersArePaused() {
|
|
MessagingLifecycleTest.java:14 List<ShutdownPhase> order = List.of(ShutdownPhase.values());
|
|
MessagingLifecycleTest.java:15
|
|
MessagingLifecycleTest.java:16 assertThat(order.indexOf(ShutdownPhase.STOP_PUBLISH_ADMISSION))
|
|
MessagingLifecycleTest.java:17 .isLessThan(order.indexOf(ShutdownPhase.PAUSE_CONSUMERS));
|
|
MessagingLifecycleTest.java:18 }
|
|
MessagingLifecycleTest.java:19
|
|
MessagingLifecycleTest.java:20 @Test
|
|
MessagingLifecycleTest.java:21 void handlersDrainBeforeTheirSettlementsAreFlushed() {
|
|
MessagingLifecycleTest.java:22 List<ShutdownPhase> order = List.of(ShutdownPhase.values());
|
|
MessagingLifecycleTest.java:23
|
|
MessagingLifecycleTest.java:24 assertThat(order.indexOf(ShutdownPhase.DRAIN_HANDLERS))
|
|
MessagingLifecycleTest.java:25 .as("flushing before the handlers finish would lose the settlements they produce")
|
|
MessagingLifecycleTest.java:26 .isLessThan(order.indexOf(ShutdownPhase.FLUSH_SETTLEMENTS));
|
|
MessagingLifecycleTest.java:27 }
|
|
MessagingLifecycleTest.java:28
|
|
MessagingLifecycleTest.java:29 @Test
|
|
MessagingLifecycleTest.java:30 void connectionsCloseLastOfAll() {
|
|
MessagingLifecycleTest.java:31 ShutdownPhase[] order = ShutdownPhase.values();
|
|
MessagingLifecycleTest.java:32
|
|
MessagingLifecycleTest.java:33 assertThat(order[order.length - 1])
|
|
MessagingLifecycleTest.java:34 .as("every earlier phase needs the connection that this one closes")
|
|
MessagingLifecycleTest.java:35 .isEqualTo(ShutdownPhase.CLOSE_CONNECTIONS);
|
|
MessagingLifecycleTest.java:36 }
|
|
MessagingLifecycleTest.java:37
|
|
MessagingLifecycleTest.java:38 @Test
|
|
MessagingLifecycleTest.java:39 void producerConfirmsAreAwaitedBeforeTheConnectionGoesAway() {
|
|
MessagingLifecycleTest.java:40 List<ShutdownPhase> order = List.of(ShutdownPhase.values());
|
|
MessagingLifecycleTest.java:41
|
|
MessagingLifecycleTest.java:42 assertThat(order.indexOf(ShutdownPhase.AWAIT_PRODUCER_CONFIRMS))
|
|
MessagingLifecycleTest.java:43 .as("a confirm that arrives after close cannot be observed, leaving the publish ambiguous")
|
|
MessagingLifecycleTest.java:44 .isLessThan(order.indexOf(ShutdownPhase.CLOSE_CONNECTIONS));
|
|
MessagingLifecycleTest.java:45 }
|
|
MessagingLifecycleTest.java:46
|
|
MessagingLifecycleTest.java:47 @Test
|
|
MessagingLifecycleTest.java:48 void outboxLeasesAreReleasedBeforeClosing() {
|
|
MessagingLifecycleTest.java:49 List<ShutdownPhase> order = List.of(ShutdownPhase.values());
|
|
MessagingLifecycleTest.java:50
|
|
MessagingLifecycleTest.java:51 assertThat(order.indexOf(ShutdownPhase.RELEASE_OUTBOX_LEASES))
|
|
MessagingLifecycleTest.java:52 .isLessThan(order.indexOf(ShutdownPhase.CLOSE_CONNECTIONS));
|
|
MessagingLifecycleTest.java:53 }
|
|
MessagingLifecycleTest.java:54
|
|
MessagingLifecycleTest.java:55 @Test
|
|
MessagingLifecycleTest.java:56 void theDefaultDrainDeadlineMatchesTheDesign() {
|
|
MessagingLifecycleTest.java:57 assertThat(MessagingLifecycle.DEFAULT_DRAIN_DEADLINE).isEqualTo(Duration.ofSeconds(30));
|
|
MessagingLifecycleTest.java:58 }
|
|
MessagingLifecycleTest.java:59 }
|
|
|
|
# 그 두 이름을 저장소 어디서 쓰는가
|
|
pathspec 없이 이름이 나오는 파일 전부 :
|
|
docs/superpowers/plans/2026-08-10-messaging-platform-implementation-plan.md
|
|
src/messaging/messaging-transport-spi/src/main/java/dev/caskeleton/messaging/transport/MessagingLifecycle.java
|
|
src/messaging/messaging-transport-spi/src/test/java/dev/caskeleton/messaging/transport/MessagingLifecycleTest.java
|
|
implements MessagingLifecycle 를 가진 파일 : 0 개
|
|
[자기시험] implements MessagingLifecycleZZZ 를 가진 파일 : 0 개 (0 이어야 정상)
|
|
[대조] implements MessagingTransport 를 가진 파일 : 6 개 (0 이면 검색식이 깨진 것)
|
|
messaging/messaging-kafka · main · KafkaMessagingTransport.java:41 public final class KafkaMessagingTransport implements MessagingTransport {
|
|
messaging/messaging-nats-experimental · main · NatsJetStreamTransport.java:54 public final class NatsJetStreamTransport implements MessagingTransport {
|
|
messaging/messaging-pulsar-experimental · main · PulsarMessagingTransport.java:47 public final class PulsarMessagingTransport implements MessagingTransport {
|
|
messaging/messaging-rabbit · main · RabbitMessagingTransport.java:39 public final class RabbitMessagingTransport implements MessagingTransport {
|
|
messaging/messaging-runtime-core · test · DefaultMessagePublisherTest.java:426 private static final class RecordingTransport implements MessagingTransport {
|
|
messaging/messaging-transport-spi · test · MessagingRuntimeRegistryTest.java:230 final class FakeTransport implements MessagingTransport {
|
|
|
|
# DEFAULT_DRAIN_DEADLINE 이라는 이름을 가진 선언과 그 사용처
|
|
messaging/messaging-outbox-jdbc-postgresql · main · OutboxRelayWorker.java:35 public static final Duration DEFAULT_DRAIN_DEADLINE = Duration.ofSeconds(30);
|
|
messaging/messaging-outbox-jdbc-postgresql · main · OutboxRelayWorker.java:129 stop(DEFAULT_DRAIN_DEADLINE);
|
|
messaging/messaging-transport-spi · main · DefaultMessagingRuntimeRegistry.java:28 private static final Duration DEFAULT_DRAIN_DEADLINE = Duration.ofSeconds(30);
|
|
messaging/messaging-transport-spi · main · DefaultMessagingRuntimeRegistry.java:36 this(DEFAULT_DRAIN_DEADLINE);
|
|
messaging/messaging-transport-spi · main · MessagingLifecycle.java:40 Duration DEFAULT_DRAIN_DEADLINE = Duration.ofSeconds(30);
|
|
messaging/messaging-transport-spi · test · MessagingLifecycleTest.java:57 assertThat(MessagingLifecycle.DEFAULT_DRAIN_DEADLINE).isEqualTo(Duration.ofSeconds(30));
|
|
|
|
# 프로퍼티로 들어오는 드레인 마감과 그것을 받는 자리
|
|
messaging/messaging-spring-boot-starter · main · MessagingCoreAutoConfiguration.java:192 return new DefaultMessagingRuntimeRegistry(properties.getShutdown().getDrainDeadline());
|
|
messaging/messaging-spring-boot-starter · main · MessagingCoreAutoConfiguration.java:204 return new GracefulShutdownCoordinator(properties.getShutdown().getDrainDeadline());
|
|
messaging/messaging-spring-boot-starter · main · MessagingCoreAutoConfiguration.java:243 admission, drain, properties.getShutdown().getDrainDeadline());
|
|
messaging/messaging-spring-boot-starter · main · MessagingSettings.java:281 private Duration drainDeadline = Duration.ofSeconds(30);
|
|
messaging/messaging-spring-boot-starter · test · MessagingAutoConfigurationTest.java:83 assertThat(properties.getShutdown().getDrainDeadline())
|
|
|
|
# 오늘 종료를 담당하는 구현
|
|
messaging/messaging-spring-boot-starter · main · MessagingOutboxRelayLifecycle.java:23 public final class MessagingOutboxRelayLifecycle implements SmartLifecycle {
|
|
messaging/messaging-spring-boot-starter · main · MessagingShutdownLifecycle.java:30 public final class MessagingShutdownLifecycle implements SmartLifecycle {
|
|
messaging/messaging-spring-boot-starter · test · MessagingOutboxRelayLifecycleTest.java:38 class MessagingOutboxRelayLifecycleTest {
|
|
messaging/messaging-spring-boot-starter · test · MessagingShutdownLifecycleTest.java:25 class MessagingShutdownLifecycleTest {
|
|
|
|
# 열거형 상수 순서를 바꾸면 무엇이 깨지는가 (레포 사본을 /tmp 에서 컴파일)
|
|
사본을 그대로 컴파일했을 때
|
|
통과 6 · 실패 0
|
|
CLOSE_CONNECTIONS 를 첫 상수로 옮겼을 때
|
|
통과 3 · 실패 3
|
|
깨진 시험 : connectionsCloseLastOfAll
|
|
깨진 시험 : outboxLeasesAreReleasedBeforeClosing
|
|
깨진 시험 : producerConfirmsAreAwaitedBeforeTheConnectionGoesAway
|
|
레포가 수정되지 않았는지 : 0 건 변경
|