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>
114 lines
5.2 KiB
Plaintext
114 lines
5.2 KiB
Plaintext
# 회수 메서드가 자기 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}
|