Skip to content
Decision 2 · SQL + concurrency test report

Set the credits aside before admitting paid work

The database lock shared by reserving and spending credits, the capture path that rolls back on a failed charge, and the three-way concurrency test.

Quoted verbatim from HYVIRAL’s specifications, code and logs. Internal paths and identifiers are redacted.

The decision

Several jobs can each read a sufficient balance and start against the same credits (HYVIRAL’s prepaid units), and a rejected charge cannot undo provider costs already incurred. So billing sets the credits aside before paid work and settles the actual amount afterwards.

A hold reduces the amount available to other jobs without immediately charging the wallet. Placement shares a transaction-scoped wallet lock with credit consumption. An active reference is reusable, so retrying placement does not create a second reservation.

The cost is lifecycle management: held credits are temporarily unavailable, and unsuccessful or abandoned work needs release or expiry handling.

The lifecycle: place an active hold → run the admitted paid work → capture the actual amount, or release or expire the hold. Repeating a completed capture returns its first result; a failed consumption leaves the transaction uncommitted.

From the source

Placement SQL. One lock key serializes placement and consumption for the same wallet:

  -- Same lock key as consume_credits_with_priority: placements and consumes for
  -- one user serialize against each other, which is what makes the check below
  -- authoritative rather than advisory.
  PERFORM pg_advisory_xact_lock(
    hashtextextended('billing.wallet:' || p_user_id::text, 0)
  );

Capture SQL. Failed consumption raises inside the transaction, rolling back the captured-state update:

    IF NOT v_consume.success THEN
      -- Surface rather than swallow: the hold guaranteed this could be paid, so
      -- a denial here means the guarantee was violated and the caller must see
      -- it. Raising rolls back the status update too, leaving the hold active
      -- for the reaper rather than stranding it as captured-but-unpaid.
      RAISE EXCEPTION 'capture_credit_hold: consume denied (%)',
        COALESCE(v_consume.error_message, 'unknown');
    END IF;

Concurrency test, as reported in the 12 August 2026 specification:

  • The Problem Statement's scenario, reproduced and closed. Available squeezed to exactly 3,000; three concurrent place_credit_hold(2400) calls. Exactly one succeeded; the other two were denied with Insufficient available credits.

What this shows

The mechanism is in the SQL: a shared advisory lock makes the balance check authoritative, and capture cannot leave a hold marked captured without the matching charge. The reported test shows the admission behavior under contention: one admission and two denials for 2,400 credits each against 3,000 available.

Back to the case All code & records