35 lines · 1.5 KB
import { accountDb } from "../scoped-db";
import { randomId } from "../crypto";
import type { MeterId } from "~/lib/meters";
import { periodOf } from "./record";

/** Gauge holds outlive the write until the storage rollup (every 5 minutes) has counted the new bytes. */
export const GAUGE_HOLD_TTL_MS = 10 * 60 * 1000;

/**
 * BILL-P9: storage gauges only move when the cron rolls them up, so a read-only check let concurrent writes
 * each see the same headroom. This holds `quantity` in one conditional INSERT (atomic in the account's object)
 * that counts every unexpired hold on the gauge, so two writes can never both fit into the same space. Holds
 * expire rather than settle; until then the stored bytes may count twice, which errs on the strict side.
 */
export async function holdGauge(accountId: string, meter: MeterId, quantity: number, cap: number): Promise<boolean> {
  const now = Date.now();
  const period = periodOf();
  const result = await accountDb(accountId).run(
    `INSERT INTO usage_reservations (id, account_id, meter, period, quantity, expires_at, created_at, nonce)
     SELECT ?1, ?2, ?3, ?4, ?5, ?6, ?7, NULL
     WHERE COALESCE((SELECT current FROM usage_gauges WHERE account_id = ?2 AND meter = ?3 AND period = ?4), 0)
       + COALESCE((SELECT SUM(quantity) FROM usage_reservations WHERE account_id = ?2 AND meter = ?3
         AND expires_at > ?7), 0) + ?5 <= ?8`,
    `gauge:${randomId()}`,
    accountId,
    meter,
    period,
    quantity,
    now + GAUGE_HOLD_TTL_MS,
    now,
    cap,
  );
  return result.meta.changes > 0;
}