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>
125 lines
5.2 KiB
Plaintext
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"));
|
|
}
|