Skip to content

[Test] budget 동시성과 reservation 누수 검증 #38

Description

@HuitaePark

검증할 위험

ConcurrentHashMap을 사용한다는 사실만으로 check-and-reserve가 안전해지지는 않는다. 현재 getAccumulatedCost() → 판단 → addCost() 구조는 각 연산이 안전해도 전체 흐름에는 경쟁 조건이 있다.

이 이슈는 production API를 새로 설계하지 않고 #36/#37 구현이 실제 경쟁 상황에서도 상태·금액·통화·idempotency 불변식을 지키는지 검증한다. 동시성 테스트는 수학적 증명이나 선형성 검증기의 대체물이 아니라, 명시된 linearization point와 회계식을 깨뜨리는 회귀를 찾는 결정적 검증이다.

검증할 불변식

admission 시점:
committed
+ active reserved
+ pending reconciliation liability
+ new safeUpperBoundCost
가 bucket limit을 위반하지 않음

상태 합계:
active reserved = RESERVED/IN_FLIGHT reservation estimate 합
pending liability = RECONCILIATION_REQUIRED unresolved estimate 합
committed = newly COMMITTED actual 합
  • admission oversubscription과 actual이 estimate보다 커서 commit 후 limit을 넘는 현상을 구분한다.
  • 서로 다른 통화는 같은 합계에 포함되지 않는다.
  • duplicate/CONFLICT/CURRENCY_MISMATCH는 snapshot을 변경하지 않는다.
  • RECONCILIATION_REQUIRED는 의도하지 않은 reservation 누수가 아니라 조회 가능한 pending liability다.

테스트 구현 원칙

  • sleep()으로 타이밍을 맞추지 않는다.
  • CountDownLatch, CyclicBarrier 또는 Phaser로 같은 시작선과 특정 transition 직전 barrier를 만든다.
  • 가능하면 [Budget] atomic reservation store와 idempotency 구현 #36/#37이 문서화한 linearization point를 test hook 또는 제어 가능한 collaborator로 노출한다. production 코드에 테스트 전용 전역 상태를 넣지 않는다.
  • 고정 executor 또는 virtual thread를 사용하되 모든 future와 예외를 회수한다.
  • 각 테스트에 timeout을 두고 bounded repetition을 사용한다.
  • CI 필수 suite는 짧고 결정적으로 유지하고 장시간 반복 stress는 별도 Gradle task/profile로 분리한다.
  • 내부 Map을 reflection으로 보지 않고 #36의 immutable snapshot/query API를 사용한다.
  • 금액은 double이 아닌 문자열 기반 BigDecimal과 통화를 포함한 Money/Cost로 만든다.
  • 경쟁 중 발생한 모든 task 예외와 listener error를 테스트 실패 또는 명시적 기대 결과로 회수한다.

필수 시나리오

  1. 수백 개의 서로 다른 key가 같은 tenant/window/currency에 동시에 예약된다.
    • CREATED safe upper bound 합이 한도를 위반하지 않는다.
    • 나머지는 BLOCK이며 상태를 변경하지 않는다.
  2. 수백 요청이 같은 idempotency key로 경쟁한다.
    • reservation ID는 하나이며 생성 금액도 한 번만 반영된다.
    • reservation이 COMMITTED된 뒤 재호출해도 새 reservation을 만들지 않는다.
  3. 같은 idempotency key에 서로 다른 payload가 경쟁한다.
    • 한 payload만 생성되고 나머지는 명시적 CONFLICT다.
  4. 같은 reservation에 commit과 release가 동시에 들어온다.
    • 유효한 transition은 하나뿐이고 비용/event가 중복되지 않는다.
  5. 같은 reservation에 두 개의 서로 다른 actual commit이 경쟁한다.
    • 한 actual만 COMMITTED이고 다른 요청은 CONFLICT다.
  6. actual unavailable과 late actual이 경쟁한다.
    • pending liability가 중복되지 않고 최종 COMMITTED 합계가 한 번만 반영된다.
  7. 여러 reservation을 commit/release/reconciliation-required로 나눠 종료한다.
    • active reservation, pending liability와 committed 합계가 예상값과 같다.
  8. bucket 통화와 다른 reserve/commit이 동시에 들어온다.
    • CURRENCY_MISMATCH 요청은 모든 합계를 변경하지 않는다.
  9. tenant/window가 다르면 상태가 섞이지 않는다.
  10. listener 하나가 실패해도 성공한 transition은 보존되고 다른 listener 결과와 duplicate 명령의 idempotency가 유지된다.

Acceptance criteria

  • 정의한 동시성 테스트에서 admission oversubscription이 0건이다.
  • 동일 idempotency key의 이중 예약과 이중 정산이 0건이다.
  • 같은 reservation의 유효한 terminal/late reconciliation transition은 하나뿐이다.
  • known success, confirmed no-charge failure 후 의도하지 않은 RESERVED/IN_FLIGHT가 없다.
  • actual unavailable은 pending liability로 남고 0원 또는 누수로 오판되지 않는다.
  • 통화 불일치가 합계에 섞이지 않는다.
  • 충돌과 미확정 상태가 조용히 유실되지 않는다.
  • CI suite가 sleep 없이 반복 실행에서도 결정적으로 통과한다.
  • 경쟁 중 발생한 task 예외가 빠짐없이 테스트 결과로 전파된다.
  • 실패 시 reservation snapshot, currency와 계산 근거를 확인할 수 있다.
  • 테스트 문서에서 검증 범위와 “동시성 정확성의 완전한 증명은 아님”을 구분한다.

제외 범위

의존관계와 순서

Source

Metadata

Metadata

Assignees

No one assigned

    Labels

    mvpTokenPilot 0.1.0 MVP scope

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions