Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/a05-f020-inspect-claim.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

96 lines
5.0 KiB
Plaintext

# 청구 경로는 행을 잠근 뒤 데이터베이스 시각을 읽고 만료를 먼저 본다
127: IdempotencyRecordRow row =
128: rows.findForUpdate(request.scope().digest()).orElseThrow(this::indeterminateClaim);
129: Instant dbNow = rows.databaseNow();
130: if (inserted == 1) {
131: return acquired(row);
132: }
133:
134: if (isExpiredCompleted(row, dbNow)) {
135: return resetClaim(request, row);
136: }
137: if (!row.requestHash().equals(request.requestFingerprint().hex())) {
138: return new IdempotencyClaimOutcome.FingerprintMismatch();
139: }
140: if (row.state() == IdempotencyState.COMPLETED && row.replayUntil() != null) {
141: return new IdempotencyClaimOutcome.CompletedReplay(
142: new StoredResponse(row.responsePayload()), row.replayUntil());
143: }
586: private static boolean isExpiredCompleted(IdempotencyRecordRow row, Instant dbNow) {
587- return row.state() == IdempotencyState.COMPLETED
588- && row.replayUntil() != null
589- && !dbNow.isBefore(row.replayUntil());
590- }
# claim 본문에서 databaseNow 를 부르는 줄: 1
# 조회 경로 전문
393: public IdempotencyInspection inspect(IdempotencyInspectionRequest request) {
394: Objects.requireNonNull(request, "request");
395: Optional<IdempotencyRecordRow> found = rows.find(request.scope().digest());
396: if (found.isEmpty()) {
397: return IdempotencyInspection.outcome(IdempotencyInspectionOutcome.ABSENT);
398: }
399: IdempotencyRecordRow row = found.get();
400: if (!row.requestHash().equals(request.requestFingerprint().hex())) {
401: return IdempotencyInspection.outcome(IdempotencyInspectionOutcome.FINGERPRINT_MISMATCH);
402: }
403: boolean sameAttempt = sameClaimAttempt(row, request.claimAttempt());
404: if (row.state() == IdempotencyState.COMPLETED && row.responsePayload() != null) {
405: return new IdempotencyInspection(
406: IdempotencyInspectionOutcome.COMPLETED_REPLAY,
407: Optional.empty(),
408: Optional.empty(),
409: Optional.of(new StoredResponse(row.responsePayload())),
410: Optional.ofNullable(row.replayUntil()));
411: }
412: if (sameAttempt && row.state() == IdempotencyState.CLAIMED) {
413: return inspectionWithOwner(IdempotencyInspectionOutcome.CLAIMED_SAME_OPERATION, row);
414: }
415: if (sameAttempt && row.state() == IdempotencyState.EXECUTING) {
416: return inspectionWithOwner(IdempotencyInspectionOutcome.EXECUTING_SAME_OPERATION, row);
417: }
418: if (row.ownerToken().equals(request.claimAttempt().ownerToken()) && !sameAttempt) {
419: return IdempotencyInspection.outcome(IdempotencyInspectionOutcome.OPERATION_CONFLICT);
420: }
421: return switch (row.state()) {
422: case FAILED_RETRYABLE ->
423: IdempotencyInspection.outcome(IdempotencyInspectionOutcome.FAILED_RETRYABLE);
424: case ABANDONED -> IdempotencyInspection.outcome(IdempotencyInspectionOutcome.ABANDONED);
425: default -> IdempotencyInspection.outcome(IdempotencyInspectionOutcome.IN_PROGRESS_OTHER);
426: };
427: }
# inspect 본문에서 databaseNow 를 부르는 줄: 0
# inspect 본문이 replayUntil 로 하는 일
18: Optional.ofNullable(row.replayUntil()));
# 만료 비교는 파일에 한 곳뿐이고, 그것을 부르는 곳
134: if (isExpiredCompleted(row, dbNow)) {
586: private static boolean isExpiredCompleted(IdempotencyRecordRow row, Instant dbNow) {
# 조회가 쓰는 조회 SQL 에 시간 술어가 있는가
# 조회 결과를 받는 세 자리
146: case COMPLETED_REPLAY -> codec.deserialize(requireResponse(inspection).payload());
147- case FINGERPRINT_MISMATCH ->
148- throw IdempotencyRequestMismatchException.forScopeDigest(diagnostic(request.scope()));
149- case IN_PROGRESS_OTHER ->
--
192: case COMPLETED_REPLAY -> codec.deserialize(requireResponse(inspection).payload());
193- case UNAVAILABLE -> throw new IdempotencyUnavailableException();
194- default -> throw recovery("execution start is indeterminate");
195- };
--
261: case COMPLETED_REPLAY -> {
262- StoredResponse stored = requireResponse(inspection);
263- if (stored.payload().equals(codec.serialize(result))) {
264- yield result;
# 출하 통합 시험의 만료 시험이 무엇을 만료시키는가
130: void expiredClaimCanBeTakenOverButTheStaleOwnerCannotStart() {
164: void expiredExecutingRecordRequiresReconciliationAndIsNeverBlindlyTakenOver() {
138: Duration.ofMillis(25))))
171: store.claim(request(original, Duration.ofMillis(25))))
366: private static IdempotencyClaimRequest request(
367- IdempotencyClaimAttempt attempt, Duration processingLease) {
368- return new IdempotencyClaimRequest(
369- SCOPE, FINGERPRINT, attempt, processingLease, Duration.ofHours(24), "json.v1", 2);
grep: adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRecordRows.java: No such file or directory