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
+113
@@ -0,0 +1,113 @@
|
||||
# 회수 메서드가 자기 javadoc 에 적어 둔 것
|
||||
108: * because {@code recordAttempt} matches on it too.
|
||||
109: *
|
||||
110: * <p>The item comes back as FAILED rather than PENDING, and its attempt counter advances. An item
|
||||
111: * whose worker dies every time is then bounded by the same retry budget as one that fails
|
||||
112: * outright, instead of being reclaimed forever.
|
||||
113: *
|
||||
114: * @return 1 when this caller reclaimed it, 0 when someone else already had
|
||||
115: */
|
||||
116: @Modifying(clearAutomatically = true, flushAutomatically = true)
|
||||
117: @Query(
|
||||
118: """
|
||||
119: update CleanupItemEntity c
|
||||
120: set c.status = 'FAILED',
|
||||
121: c.attempt = c.attempt + 1,
|
||||
122: c.nextAttemptAt = :now,
|
||||
123: c.lastErrorCode = 'CLAIM_LEASE_EXPIRED',
|
||||
124: c.claimOwner = null,
|
||||
125: c.claimToken = null,
|
||||
126: c.leaseUntil = null,
|
||||
127: c.updatedAt = :now
|
||||
128: where c.cleanupId = :cleanupId
|
||||
129: and c.claimToken = :token
|
||||
130: and c.status = 'IN_PROGRESS'
|
||||
131: """)
|
||||
# 저장소 전체에서 attempt 를 비교하는 줄: 2
|
||||
# 저장소가 attempt 에 하는 일 전부
|
||||
57: * Records the outcome of an attempt, for the worker that still holds the claim.
|
||||
68: c.attempt = c.attempt + 1,
|
||||
110: * <p>The item comes back as FAILED rather than PENDING, and its attempt counter advances. An item
|
||||
121: c.attempt = c.attempt + 1,
|
||||
|
||||
# 예산을 끊는 곳은 코드베이스에 한 군데다
|
||||
JpaCleanupQueue.java:45: private static final String STATUS_ABANDONED = "ABANDONED";
|
||||
JpaCleanupQueue.java:150: exhausted ? STATUS_ABANDONED : STATUS_FAILED,
|
||||
CleanupType.java:16: ABANDONED_LEASE,
|
||||
145: public void markFailed(CleanupItem item, String reasonCode, Instant nextAttemptAt) {
|
||||
146- Instant now = clock.instant();
|
||||
147- boolean exhausted = item.attempt() + 1 >= MAXIMUM_ATTEMPTS;
|
||||
148- settle(
|
||||
149- item.cleanupId(),
|
||||
150- exhausted ? STATUS_ABANDONED : STATUS_FAILED,
|
||||
151- nextAttemptAt,
|
||||
152- reasonCode,
|
||||
153- now);
|
||||
154- }
|
||||
|
||||
# 회수 대상을 고르는 조회 — 만료된 것만, 시도 한계 없음
|
||||
93: @Query(
|
||||
94: """
|
||||
95: select c from CleanupItemEntity c
|
||||
96: where c.status = 'IN_PROGRESS'
|
||||
97: and c.leaseUntil is not null
|
||||
98: and c.leaseUntil < :now
|
||||
99: order by c.leaseUntil
|
||||
100: """)
|
||||
101: List<CleanupItemEntity> findExpiredClaims(@Param("now") Instant now, Limit limit);
|
||||
# 다시 청구되는 조건 — 상태만이 아니라 다음 시도 시각도 본다
|
||||
16: @Query(
|
||||
17: """
|
||||
18: select c from CleanupItemEntity c
|
||||
19: where c.status in ('PENDING', 'FAILED')
|
||||
20: and c.nextAttemptAt <= :now
|
||||
21: order by c.nextAttemptAt asc
|
||||
22: """)
|
||||
23: List<CleanupItemEntity> findDue(@Param("now") Instant now, Limit limit);
|
||||
|
||||
# 회수 래퍼가 next_attempt_at 에 넣는 시각
|
||||
122: public int reclaimExpiredClaims(Instant now, int limit) {
|
||||
123- if (limit < 1) {
|
||||
124- throw new IllegalArgumentException("limit must be positive");
|
||||
125- }
|
||||
126- int reclaimed = 0;
|
||||
127- for (CleanupItemEntity abandoned : items.findExpiredClaims(now, Limit.of(limit))) {
|
||||
128- // Matched on the token that was read, so the reaper that loses the race changes nothing —
|
||||
129- // and the worker that eventually wakes up finds its own token gone and settles nothing.
|
||||
130- reclaimed +=
|
||||
131- items.reclaimExpiredClaim(
|
||||
132- abandoned.getCleanupId(), abandoned.getClaimToken(), clock.instant());
|
||||
133- heldClaims.remove(abandoned.getCleanupId());
|
||||
134- }
|
||||
135- return reclaimed;
|
||||
136- }
|
||||
# 배치는 시각을 한 번 잡아 회수와 청구에 같이 쓴다
|
||||
69: Instant now = clock.instant();
|
||||
70- // Abandoned claims come back first. A claim takes an item out of the due set, so an item whose
|
||||
71- // worker performed the physical delete and then died is invisible to claimDue — nothing ever
|
||||
72- // reclaimed it, and the file's quota and lifecycle stayed unsettled for good. Reclaiming before
|
||||
73- // claiming means the recovered items are eligible in this same pass.
|
||||
74- transactions.inWrite(() -> queue.reclaimExpiredClaims(now, maxItems));
|
||||
75- List<CleanupItem> due = transactions.inWrite(() -> queue.claimDue(now, maxItems));
|
||||
76-
|
||||
77- int deleted = 0;
|
||||
78- int skippedActiveLease = 0;
|
||||
79- int skippedStateChanged = 0;
|
||||
71: private static final java.time.Duration LEASE = java.time.Duration.ofMinutes(10);
|
||||
|
||||
# 정상 실패 경로는 실제로 불린다
|
||||
87: markFailed(item, "BATCH_BYTE_BUDGET_EXHAUSTED", now);
|
||||
123: markFailed(item, "CLEANUP_ATTEMPT_FAILED", now);
|
||||
140: markFailed(item, "ACTIVE_WRITER_LEASE", now);
|
||||
209: queue.markFailed(item, reasonCode, now.plus(retryBackoff));
|
||||
121- return Outcome.of(OutcomeKind.SKIPPED_STATE_CHANGED, 0);
|
||||
122- } catch (RuntimeException failure) {
|
||||
123: markFailed(item, "CLEANUP_ATTEMPT_FAILED", now);
|
||||
124- return Outcome.of(OutcomeKind.FAILED, 0);
|
||||
125- }
|
||||
126- }
|
||||
|
||||
# 도달성
|
||||
51: return registrar -> registrar.addFixedDelayTask(worker::runBatch, interval);
|
||||
854: enabled: ${APP_FILESERVER_PLATFORM_ENABLED:false}
|
||||
914: enabled: ${APP_FILESERVER_PLATFORM_CLEANUP_ENABLED:false}
|
||||
Reference in New Issue
Block a user