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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
@@ -0,0 +1,124 @@
|
||||
# 클래스 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"));
|
||||
}
|
||||
Reference in New Issue
Block a user