Files
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
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>
2026-09-04 22:51:59 +09:00

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 건 변경