문제와 현재 코드 근거
현재 DefaultLedgerAdvisor.after()는 다음 작업을 별도로 수행한다.
ledgerManager.record()로 비용 계산과 listener event 발행
- 같은 usage로 비용을 다시 계산
BudgetStateStore.addCost()로 누적
estimate와 actual을 연결하는 reservation이 없고 callback이 중복되면 ledger와 budget이 모두 중복 기록될 수 있다. listener가 실패하면 event는 일부 전달된 상태로 예외가 전파되고 budget 누적은 실행되지 않을 수도 있다. UsageSource.UNAVAILABLE도 실제 0 token과 구별해 정산할 경로가 없다.
목표 상태 머신
#46 Accounting lifecycle ADR의 전이표를 구현 기준으로 사용한다.
RESERVED
├─ IN_FLIGHT
└─ RELEASED // provider dispatch 전 또는 미과금 확인
IN_FLIGHT
├─ COMMITTED // actual known
├─ RELEASED // provider 미과금이 확인된 경우만
└─ RECONCILIATION_REQUIRED // dispatch 후 actual unknown
RECONCILIATION_REQUIRED
├─ COMMITTED // late actual known
└─ WRITTEN_OFF // 명시적인 운영자/정책 결정
RECONCILIATION_REQUIRED는 terminal이 아니라 해결 대기 상태다.
- actual unknown은 0원이나 RELEASED가 아니다.
- unresolved estimate는
pendingReconciliationLiability로 옮겨 admission 계산에 계속 포함한다.
EXPIRED가 필요하면 provider dispatch 전 RESERVED에만 허용한다. IN_FLIGHT를 단순 만료해 비용을 지우지 않는다.
- automatic TTL scheduler는 MVP 범위가 아니다.
회계 전이 불변식
- commit은
activeReserved -= estimate, committed += actual을 한 bucket 임계 구역 안에서 처리한다.
- actual unavailable 전이는
activeReserved -= estimate, pendingReconciliationLiability += estimate를 원자적으로 처리한다.
- late actual commit은
pendingReconciliationLiability -= estimate, committed += actual을 원자적으로 처리한다.
- actual이 estimate보다 커도 이미 발생한 비용이므로 commit을 거부하지 않는다. 초과분과 over-limit 상태를 결과에 남기고 이후 예약을 차단한다.
- actual이 작으면 남는 liability를 해제한다.
- terminal 명령 재호출은 상태와 event를 바꾸지 않는 idempotent REUSED/no-op 결과다.
- 다른 actual로 두 번째 commit, commit 후 release 같은 상충 전이는 기존 상태를 보존하고 CONFLICT를 반환한다.
- bucket 통화와 actual 통화가 다르면 상태를 바꾸지 않고 CURRENCY_MISMATCH 또는 RECONCILIATION_REQUIRED로 남긴다. 환율 변환은 하지 않는다.
AccountingService와 단일 쓰기 주체
Advisor가 LedgerManager.record()와 reservation store를 따로 호출하지 않게 한다. 이름은 예시이며 다음 의미를 가진 framework-independent orchestration API를 둔다.
AccountingService
├─ markInFlight(reservationId)
├─ commit(reservationId, actualUsage/model/pricing input)
├─ markReconciliationRequired(reservationId, reason)
├─ reconcileLateActual(reservationId, actualUsage/model/pricing input)
├─ release(reservationId, reason)
└─ writeOff(reservationId, reason)
처리 순서는 다음과 같다.
actual usage normalization
→ actual cost 계산 1회
→ reservation/bucket atomic transition
→ newly applied transition인 경우 accounting event 1회 생성
→ listener에 best-effort at-most-once dispatch
- 비용 계산과 상태 전이가 성공하기 전에
CostRecordedEvent를 발행하지 않는다.
- duplicate/CONFLICT 결과에서는 ledger, metric과 notification event를 다시 생성하지 않는다.
- 상태 전이의 exactly-once/idempotency와 event 전달 보장은 구분한다.
- durable outbox가 없는 MVP의 event dispatch는 best-effort at-most-once이며 exactly-once delivery를 주장하지 않는다.
- listener 실패는 성공한 상태 전이를 rollback하지 않는다. 구체 격리/error handler 정책은 #46과 #40을 따른다.
correlation과 결과
reconciliation result/event는 최소한 다음을 포함한다.
- requestId, attemptId, reservationId
- budget key/window
- request model과 response model
- pricing policy/version
- estimate, actual과 delta
- currency
- 이전 상태, 최종 상태와 bounded reason
raw prompt, response 본문과 무제한 오류 메시지는 event 기본 payload에 포함하지 않는다.
테스트
동시 commit/release 및 수백 요청 경쟁은 #38에서 확장한다.
Acceptance criteria
제외 범위
의존관계와 순서
Source
문제와 현재 코드 근거
현재
DefaultLedgerAdvisor.after()는 다음 작업을 별도로 수행한다.ledgerManager.record()로 비용 계산과 listener event 발행BudgetStateStore.addCost()로 누적estimate와 actual을 연결하는 reservation이 없고 callback이 중복되면 ledger와 budget이 모두 중복 기록될 수 있다. listener가 실패하면 event는 일부 전달된 상태로 예외가 전파되고 budget 누적은 실행되지 않을 수도 있다.
UsageSource.UNAVAILABLE도 실제 0 token과 구별해 정산할 경로가 없다.목표 상태 머신
#46 Accounting lifecycle ADR의 전이표를 구현 기준으로 사용한다.
RECONCILIATION_REQUIRED는 terminal이 아니라 해결 대기 상태다.pendingReconciliationLiability로 옮겨 admission 계산에 계속 포함한다.EXPIRED가 필요하면 provider dispatch 전RESERVED에만 허용한다.IN_FLIGHT를 단순 만료해 비용을 지우지 않는다.회계 전이 불변식
activeReserved -= estimate,committed += actual을 한 bucket 임계 구역 안에서 처리한다.activeReserved -= estimate,pendingReconciliationLiability += estimate를 원자적으로 처리한다.pendingReconciliationLiability -= estimate,committed += actual을 원자적으로 처리한다.AccountingService와 단일 쓰기 주체Advisor가
LedgerManager.record()와 reservation store를 따로 호출하지 않게 한다. 이름은 예시이며 다음 의미를 가진 framework-independent orchestration API를 둔다.처리 순서는 다음과 같다.
CostRecordedEvent를 발행하지 않는다.correlation과 결과
reconciliation result/event는 최소한 다음을 포함한다.
raw prompt, response 본문과 무제한 오류 메시지는 event 기본 payload에 포함하지 않는다.
테스트
동시 commit/release 및 수백 요청 경쟁은 #38에서 확장한다.
Acceptance criteria
RECONCILIATION_REQUIRED가 late actual로 해결될 수 있다.AccountingService경로에서 한 번만 계산·정산된다.제외 범위
의존관계와 순서
Source