Found during review of #1092.
Ordering
SchedulingService::release_hold and SchedulingService::cancel_appointment both
resolve the offering from the current in-memory policy before anything consults
the stored attempt receipt:
release_hold loads the claim, checks its kind, then
self.policy.offering(&hold.offering), failing with
ServiceError::internal("a committed hold names no offering in the policy").
cancel_appointment loads the booking, then
self.policy.offering(&appointment.offering), failing with
ServiceError::internal("a committed appointment names no offering in the policy").
ServiceError::internal returns ProblemCode::ServiceUnavailable. The idempotency
replay lives further down, inside the store call, so it is never reached.
Why the offering can be gone
apply_policy fences only offerings with active claims:
WHERE state='active'
AND (kind='booking' OR (kind='hold' AND hold_expires_at > $1))
A released hold and a cancelled appointment are both non-active, so an offering
whose commitments have all been closed may be removed from the policy, and the
publication is accepted.
PostgresStore::claim selects on claim_id with no state filter, and
SchedulingService::booking filters on kind only. So the released hold and the
cancelled appointment are both still found by a retry.
Result
For the remainder of retention.attempt_receipt_days, a retry of the very same
release or cancellation request, which is the retry the receipt exists to serve,
returns service.unavailable instead of replaying its recorded success. The
caller cannot distinguish that from a transient outage and will keep retrying.
Options
- Consult the stored receipt before resolving the current policy, and replay a
terminal recorded answer without needing the offering.
- Retain in the receipt whatever authorization terms the replay needs, so
require_permission does not depend on a live offering.
- Fence offering removal while replayable receipts remain, which trades the
problem for a longer publication fence.
The first two keep apply_policy as permissive as it is now, which seems
preferable.
Found during review of #1092.
Ordering
SchedulingService::release_holdandSchedulingService::cancel_appointmentbothresolve the offering from the current in-memory policy before anything consults
the stored attempt receipt:
release_holdloads the claim, checks its kind, thenself.policy.offering(&hold.offering), failing withServiceError::internal("a committed hold names no offering in the policy").cancel_appointmentloads the booking, thenself.policy.offering(&appointment.offering), failing withServiceError::internal("a committed appointment names no offering in the policy").ServiceError::internalreturnsProblemCode::ServiceUnavailable. The idempotencyreplay lives further down, inside the store call, so it is never reached.
Why the offering can be gone
apply_policyfences only offerings with active claims:A released hold and a cancelled appointment are both non-active, so an offering
whose commitments have all been closed may be removed from the policy, and the
publication is accepted.
PostgresStore::claimselects onclaim_idwith no state filter, andSchedulingService::bookingfilters on kind only. So the released hold and thecancelled appointment are both still found by a retry.
Result
For the remainder of
retention.attempt_receipt_days, a retry of the very samerelease or cancellation request, which is the retry the receipt exists to serve,
returns
service.unavailableinstead of replaying its recorded success. Thecaller cannot distinguish that from a transient outage and will keep retrying.
Options
terminal recorded answer without needing the offering.
require_permissiondoes not depend on a live offering.problem for a longer publication fence.
The first two keep
apply_policyas permissive as it is now, which seemspreferable.