Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/a11-f001-close.txt
T
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

125 lines
5.2 KiB
Plaintext

# 클래스 javadoc 이 적은 성질
/**
* Atomic pointer from profile name to its current runtime generation (design §7.2).
*
* <p>A swap publishes the replacement first and drains the predecessor afterwards, so a rotation is
* never observable as a gap. The single scheduled executor exists only to enforce drain deadlines
* and is created lazily; it is shut down with the registry so no thread outlives it.
*/
# close() 전체
120: public void close() {
121: List<ClientRuntime> all = new ArrayList<>();
122: runtimes.values().forEach(holder -> all.add(holder.get()));
123: // Retired-but-still-draining generations are closed too; they used to survive registry
124: // shutdown entirely.
125: all.addAll(retired);
126: // Every runtime is closed even when one refuses. forEach stopped at the first exception, so a
127: // single misbehaving pool left every remaining connection, thread and socket open — shutdown
128: // leaked more the worse the failure was.
129: RuntimeException firstFailure = null;
130: for (ClientRuntime runtime : all) {
131: try {
132: runtime.forceClose();
133: } catch (RuntimeException failure) {
134: if (firstFailure == null) {
135: firstFailure = failure;
136: } else {
137: firstFailure.addSuppressed(failure);
138: }
139: }
140: }
141: retired.clear();
142: runtimes.clear();
143: if (firstFailure != null) {
144: throw firstFailure;
145: }
146: ScheduledExecutorService scheduler = drainScheduler.getAndSet(null);
147: if (scheduler != null) {
148: // Await termination: a registry that returns while its drain thread is still alive would
149: // leak a thread per rotation cycle, which the resource-bound suite exists to catch.
150: scheduler.shutdownNow();
151: try {
152: if (!scheduler.awaitTermination(SHUTDOWN_AWAIT_MILLIS, TimeUnit.MILLISECONDS)) {
153: throw new IllegalStateException("http client drain scheduler did not terminate");
154: }
155: } catch (InterruptedException interrupted) {
156: Thread.currentThread().interrupt();
157: }
158: }
159: }
# 런타임 닫기는 상태를 먼저 바꾼다
90: /** Forced close at the drain deadline; in-flight calls lose their connections by design. */
91: public final void forceClose() {
92: close();
93: }
94:
95: @Override
96: public final void close() {
97: ClientRuntimeState previous = state.getAndSet(ClientRuntimeState.CLOSED);
98: if (previous != ClientRuntimeState.CLOSED) {
99: resourceCloser.run();
100: }
101: }
102:}
# 배수 스레드를 만드는 곳과 그 조건
99: previous.beginDrain(drainTimeout);
100: if (previous.state() != ClientRuntimeState.CLOSED && !drainTimeout.isZero()) {
101: ScheduledFuture<?> unusedDrainDeadline =
102: scheduler()
103: .schedule(
172: return existing;
173: }
174: ScheduledExecutorService created =
175: Executors.newSingleThreadScheduledExecutor(
176: runnable -> {
177: Thread thread = new Thread(runnable, "httpclient-runtime-drain");
178: thread.setDaemon(true);
179: return thread;
# 주석이 감시자로 지목한 묶음
18:/** Repeated rotation must not accumulate generations or threads (design §7.2, §28.8). */
19:class RuntimeRotationDrainTest {
20:
21: @Test
22: void repeatedRotationDrainsEveryPreviousGenerationAndLeavesNoThread() {
30: try (ClientRuntimeRegistry registry = new ClientRuntimeRegistry(Map.of(first.name(), first))) {
31: for (int generation = 2; generation <= 50; generation++) {
32: ClientRuntime replacement =
33: new ClientRuntime(
34: ClientProfiles.builder("rotating").build(),
35: new RuntimeGeneration(generation),
36: closed::incrementAndGet);
37: registry.swap(first.name(), replacement, Duration.ofSeconds(1));
38: }
44: PerformanceAssertions.structural("every retired generation was closed", closed.get() == 50);
45: assertThat(first.state()).isEqualTo(ClientRuntimeState.CLOSED);
46: Set<String> threadNames =
47: Thread.getAllStackTraces().keySet().stream()
48: .map(Thread::getName)
49: .collect(Collectors.toSet());
50: assertThat(threadNames).noneMatch(name -> name.startsWith("httpclient-runtime-drain"));
51: }
# 같은 성질을 보는 단위 test
@Test
void closingTheRegistryReleasesEveryGenerationAndLeavesNoThread() {
ClientRuntime first = running(1);
ClientRuntime second = running(2);
ClientRuntimeRegistry registry = new ClientRuntimeRegistry(Map.of(first.name(), first));
ClientRuntimeLease lease = registry.acquire(first.name());
registry.swap(first.name(), second, Duration.ofSeconds(30));
registry.close();
lease.close();
assertThat(first.state()).isEqualTo(ClientRuntimeState.CLOSED);
assertThat(second.state()).isEqualTo(ClientRuntimeState.CLOSED);
Set<String> threadNames =
Thread.getAllStackTraces().keySet().stream()
.map(Thread::getName)
.collect(Collectors.toSet());
assertThat(threadNames).noneMatch(name -> name.startsWith("httpclient-runtime-drain"));
}