Skip to content

fix(exchange): approved RI exchanges reserve stale quotes outside the shared daily-cap lock #656

Closed

Description

@cristim

Summary

Manual RI approvals do not atomically reserve the execution ceiling. This remains separate from the same-day AUTO concurrency fix in LeanerCloud/cloud-commitments-go#41.

Current behaviour

At platform baseline 3131779, token and session approval transition pending to processing in internal/api/handler_ri_exchange.go:2295 and :2355 using the unlocked status UPDATE in internal/config/store_postgres.go:2918. executeApprovedExchange then reads daily spend separately at internal/api/handler_ri_exchange.go:2605, computes headroom at :2623 and passes it to Execute at :2633. The processing row contains the initial quote rather than the full fresh-quote ceiling. Its own quote is also included in the aggregate used to check that quote again.

Steps to verify the gap

Use fixture exchange clients and real local PostgreSQL; do not make purchases. With daily cap1000, transition two pending rows whose initial quotes are100 each to processing. Coordinate both daily reads to return200 before execution. Each gets ceiling800 and can accept a fresh quote700, totaling1400. Separately, approve one initial600 quote under cap1000: its own processing600 is included in spend, then600 is added again and the legitimate exchange is rejected. These are code-derived scenarios, not observed production purchases.

Expected behaviour

Manual and AUTO exchange paths share one atomic global reservation protocol. Fresh accepted payments stay within the reserved ceiling, and an approval does not double-count its own row.

Proposed fix and acceptance

Atomically reserve or replace the pending row under the same global advisory lock introduced for go#41, exclude its own previous amount when computing headroom, reserve the complete permitted ceiling and settle actual payment on the same ID. Add deterministic two-approval and AUTO-plus-approval interleaving tests, the legitimate600 approval, and real PostgreSQL lock verification. Relevant paths: internal/api/handler_ri_exchange.go and internal/config/store_postgres.go.

References and severity

Refs LeanerCloud/cloud-commitments-go#41 and #348. High severity when triggered; limited overlap-dependent impact. This issue records source inspection, not a completed failing regression or a production incident.

Activity

  1. cristim commented on Oct 9, 2026

    @cristim
    MemberAuthor

    Deployed: merged in #690 as cf0b1a0 (squash, head 6bafb61).

    Verified (real PostgreSQL 16, local image, fixture exchange clients, no purchases or cloud calls):

    • two concurrent approvals reserve ceilings summing exactly the daily headroom; the race test holds the global advisory lock from a third transaction and fails 5/5 when the lock is removed, passes 10/10 with it (independent reviewer reproduced)
    • a legitimate $600 approval under a $1000 cap is no longer rejected for counting its own row
    • AUTO reservation and an approval racing divide the headroom instead of doubling it
    • the fresh accepted amount settles on the same row; the full ceiling bounds Execute
    • unit packages config/api/server/analytics/mocks pass; all 25 required CI checks green at the merged head

    Not verified: behaviour against live AWS (fixtures only), and whether pkg/exchange enforces MaxPaymentDueUSD against the fresh quote (library not part of this change).

    Recommend closing. Follow-up: #692 (stale processing rows now hold headroom until settled; AUTO only logs them).

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions