154 lines · 6.8 KB
import { first } from "../db";
import { backfillOnce, backfillRepos } from "../backfill";
import { locatedScopes } from "../scoped-db/locators";
import { accountDb, repoDb, stmt } from "../scoped-db";
import type { ScopedStatement } from "../scoped-db";
import { noteScratchAccount } from "./directory";
import { gaugeStatement, resolveAccount } from "./record";
import type { MeterId } from "~/lib/meters";

/**
 * Storage gauges without a cron over every repository: each repository or site scope (RepoState) computes what
 * it stores per billed account and writes it to that account's `scope_gauges`; the account's gauge for a meter
 * is the sum over its scopes. A scope recomputes after a debounce when its storage tables change (RepoState
 * alarm task `gauges`), and an account re-sums when its `scope_gauges` change (AccountState task `scope-gauges`).
 */
export const SCOPE_METERS: MeterId[] = [
  "storage.ci_cache",
  "storage.artifacts",
  "storage.r2",
  "storage.sites",
  "domains.custom",
];

type GaugeRow = { accountId: string; meter: MeterId; value: number };

async function scopeOwner(scopeId: string): Promise<string | null> {
  if (scopeId.startsWith("site:")) return null;
  return (await first<{ owner_id: string }>("SELECT owner_id FROM repos WHERE id = ?", scopeId))?.owner_id ?? null;
}

/** What this scope stores per billed account (claimed scratch ids resolved to their claimer). */
export async function computeScopeGauges(scopeId: string): Promise<GaugeRow[]> {
  const db = repoDb(scopeId);
  const raw: GaugeRow[] = [];
  const owner = await scopeOwner(scopeId);
  if (owner) {
    const [cache, artifacts, stored] = await Promise.all([
      // Cache objects are content-addressed within the repository: bill each stored object once.
      db.first<{ value: number | null }>(
        "SELECT SUM(size) AS value FROM (SELECT DISTINCT r2_key, size FROM action_caches WHERE size > 0)",