Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/a05-f027-maximum-attempts.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

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}