Skip to content

Postgres 백엔드에서 Idempotency-Key가 경고 없이 무시된다 — 드라이버 opt-in 미지원을 기동 시 감지하고 TCK에 멱등성 계약을 넣는다 #228

Closed

Description

@choiyounggi

문제 (실측, main 9611855(v0.8.0 + 168커밋), 2026-10-11 실측)

impl/lnpl/wsgi.py:1961(요청 경로)과 :2121(이벤트 소비 경로)은 멱등성 클레임을
hasattr(repository, "idempotency_begin")로 드라이버 opt-in으로 건다. 지원하지 않는
드라이버면 클레임을 건너뛰고 워크플로를 그냥 실행한다 — 로그도, 진단도, 응답 헤더도 없다.

유일한 운영 DB 드라이버 lnpl-postgres(lnpl_postgres/driver.py, 2026-10-11 main)의 메서드
목록에는 idempotency_begin / idempotency_finish / idempotency_release가 없다(grep 0건).
즉 Postgres로 배치하면 같은 Idempotency-Key로 재시도한 POST가 두 번 실행되고, 운영자는
sqlite 개발 환경에서 통과한 테스트만 보고 이를 알 수 없다. docs/serving.md "멱등성" 절도
fake 백엔드만 예외로 적고 postgres는 언급하지 않는다.

impl/lnpl/testing.py의 RepositoryDriverTCK(line 72)는 멱등성 메서드를 전혀 검사하지 않으므로
"TCK 통과 = serve의 모든 기능이 동작" 이라는 기대가 성립하지 않는다.

범위

  1. 감지: build_app()/serve() 기동 시 repository_factory() 한 번 열어 능력을 확인하고,
    idempotency_begin이 없으면 (a) stderr 경고 1줄 + /-/readyz 본문에 capabilities로 노출,
    또는 (b) --require-idempotency 플래그가 있으면 기동 거부. 요청 시점에는 Idempotency-Key가
    왔는데 저장소가 미지원이면 501 idempotency-unsupported(problem+json)로 명시 실패 —
    조용히 두 번 실행하는 것보다 낫다.
  2. 계약: RepositoryDriver에 supports_idempotency()(또는 lnpl_enforcement의 새 축)로
    능력을 선언하게 하고, docs/backends.md §8에 "serve 전 기능에 필요한 선택 메서드" 표를 추가.
  3. TCK: RepositoryDriverTCK에 멱등성 케이스(claim → in-progress 409 → done 재생 → release)를
    추가하되, 미구현 드라이버는 skip이 아니라 명시적 declared-unsupported로만 통과.
  4. 드라이버 쪽 구현은 lnpl-postgres 이슈(lnpl-postgres#8 in lnpl-postgres)로 분리.

완료 기준

  • Postgres 드라이버 + Idempotency-Key 재전송 2회 → 두 번째는 재생(200, 같은 body) 또는 명시 501. 두 번 실행 0건.
  • lnpl serve --backend postgres:… 기동 로그에 능력 표가 찍힌다.
  • TCK에 멱등성 케이스 ≥3(정상·충돌·해제)이 있고 sqlite가 통과한다.

베스트 프랙티스 (외부 근거)


발견 경위: 2026-10-11 엔터프라이즈 연동성 감사(main 9611855(v0.8.0 + 168커밋), 2026-10-11 실측). 추적 이슈: #255.

Activity

  1. added
    bugSomething isn't working
    production-readinessGaps blocking real production use (beyond the Phase 1 reference impl)
    on Oct 11, 2026
  2. added a commit that references this issue on Oct 15, 2026
  3. choiyounggi commented on Oct 15, 2026

    @choiyounggi
    OwnerAuthor

    Shipped in #256 (merged into main).

    What landed

    • RepositoryDriver.supports_idempotency() (introspection default: True iff idempotency_begin/finish/release are all callable; SqliteRepositoryDriver True, drivers lacking the methods False) replaces the hasattr(repository, "idempotency_begin") selection at both claim sites in wsgi.py.
    • On a RepositoryDriver whose predicate is False, a workflow request carrying Idempotency-Key (serving.md M22) and every POST /-/events/<slug> delivery (E8) is refused with 501 idempotency-unsupported (problem+json, correlation_id), the workflow does not run; requests without the header run as before; --backend fake is unchanged.
    • lnpl serve prints one boot line (lnpl serve: repository driver <module.qualname> -- idempotency: unsupported …), and the GET /-/readyz 200 body carries "capabilities": {"idempotency": <bool>} (information only, never a 503 — the issue /-/healthz·/-/readyz·/-/metrics가 없어 k8s에 올릴 수 없다 — Trace.metric은 배열에 쌓였다 사라진다 (k8s probes / Actuator health groups / Prometheus) #110 closed list of five is unchanged).
    • RepositoryDriverTCK gains declared-unsupported idempotency cases (no skipTest); docs/backends.md has the feature → optional-method → predicate table; docs/serving.md 멱등성/운영 표면 updated.

    Deviations from the issue text (decided during the run)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproduction-readinessGaps blocking real production use (beyond the Phase 1 reference impl)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions