Files
llm-wiki/raw/errors/idempotency-expired-row-reclaim-409-loop-2026-06-09.md
T

1.9 KiB

title, source_type, status, related_branches, related_projects, tags, created, status_label
title source_type status related_branches related_projects tags created status_label
error / idempotency-expired-row-reclaim-409-loop-2026-06-09 error-note raw
feature-rate-limit-idempotency-contract
ca-skeleton
error
ca-skeleton
idempotency
concurrency
ttl
2026-06-09 resolved

error: idempotency-expired-row-reclaim-409-loop-2026-06-09

Layer: raw/errors/ — TDD 중 발견한 만료 row 재선점 누락 버그.

Parent / 부모

증상 / Symptom

IdempotencyExecutorexpired_record_is_treated_as_absent_and_reclaimed 테스트가 IdempotencyInFlightException(409)으로 실패. 만료된 idempotency record가 있을 때 새 요청이 재처리되지 못하고 in-flight 409로 오판됨.

원인 / Root cause

IdempotencyStore.find(scope, now)는 만료 row를 Optional.empty()로 반환하지만, 저장소의 tryBegin(insert)이 여전히 존재하는 만료 row와 unique 제약에서 충돌 → false 반환. executor는 "타 호출자가 선점했다"고 판단해 200ms wait 후 409. 즉 find의 만료 필터와 tryBegin의 물리 row 존재가 불일치.

해결 / Resolution

만료 row는 재선점 가능해야 한다는 계약을 명문화:

  • 포트 IdempotencyStore.tryBegin javadoc에 "expired record는 reclaim 대상" 명시.
  • 실제 어댑터(IdempotencyStoreAdapter.tryBegin)는 lookup 후 expiresAt <= nowdelete 후 insert (caller 트랜잭션 내 atomic, unique 제약 + DataIntegrityViolationException로 race 중재).
  • 테스트 fake(FakeStore.find)는 read 시 만료 row를 lazy purge.

교훈 / Lesson

만료(soft delete/TTL) 시맨틱은 읽기 필터와 쓰기 선점이 같은 기준을 공유해야 한다. read에서만 만료를 숨기고 write 경로가 물리 row를 그대로 보면 "유령 충돌"이 발생한다. 관련: raw/branch-notes/feature-rate-limit-idempotency-contract