# 청구 경로는 행을 잠근 뒤 데이터베이스 시각을 읽고 만료를 먼저 본다
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
