134 lines · 5.5 KB
import { DurableObject } from "cloudflare:workers";
import { backoffMs, flushIsolating, ISOLATE_AFTER } from "../do-flush";
import { accountDb } from "../scoped-db";
import { eventStatements, periodOf } from "./record";
import type { UsageInput } from "./record";

const FLUSH_DELAY_MS = 5_000;
const RETRY_DELAY_MS = 30_000;
/** D1 batches are one transaction; this keeps each well under the statement limits. */
const FLUSH_BATCH = 100;
/** Flushed keys are remembered this long so a retried request is still recognised as a duplicate. */
const LEDGER_MS = 24 * 60 * 60 * 1000;

type Pending = { key: string; input: string; at: number };

/**
 * One per billed account: buffers high-volume usage events (git operations, downloads) and lands them in
 * the account's AccountState as one batched transaction every few seconds instead of one write per request. D1 stays the billing
 * source of truth: events keep their idempotency keys and are inserted with INSERT OR IGNORE, so a flush
 * that is retried after a partial failure never double counts. Quota checks read D1 and therefore see
 * buffered usage up to FLUSH_DELAY_MS late; anything that must be exact uses `recordUsage` directly.
 */
export class MeterBuffer extends DurableObject<Env> {
  private ready = false;

  private init(): void {
    if (this.ready) return;
    this.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS events (
      key TEXT PRIMARY KEY,
      input TEXT NOT NULL,
      at INTEGER NOT NULL,
      flushed_at INTEGER
    )`);
    this.ctx.storage.sql.exec("CREATE INDEX IF NOT EXISTS events_pending ON events(flushed_at, at)");
    this.ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS dead_letters (
      key TEXT PRIMARY KEY,
      input TEXT NOT NULL,
      error TEXT NOT NULL,
      at INTEGER NOT NULL
    )`);