/** * Per-org S2 basin provisioning. Gated by * `REALTIME_STREAMS_PER_ORG_BASINS_ENABLED`: when off, all orgs share * `REALTIME_STREAMS_S2_BASIN` and this module no-ops. * * Pure retention-string in / S2-call out. Plan vocabulary lives in the * cloud billing app, which calls into the admin sync route to drive * provisioning + reconfiguration. */ import type { PrismaClientOrTransaction } from "~/db.server"; import { prisma } from "~/db.server"; import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server"; import { parseDuration } from "./duration.server"; export function isPerOrgBasinsEnabled(): boolean { return env.REALTIME_STREAMS_PER_ORG_BASINS_ENABLED === "true"; } export function defaultRetention(): string { return env.REALTIME_STREAMS_BASIN_DEFAULT_RETENTION; } // Org id is a cuid — fixed-length and stable, so the basin name is // collision-free without truncation. Slugs are user-editable and would // drift. export function basinNameForOrg(org: { id: string }): string { const prefix = env.REALTIME_STREAMS_BASIN_NAME_PREFIX; const envName = env.REALTIME_STREAMS_BASIN_NAME_ENV; return `${prefix}-${envName}-org-${org.id}`; } type ProvisionInput = { id: string; retention?: string; streamBasinName: string | null | undefined; }; type ProvisionResult = | { kind: "skipped"; reason: "feature-disabled" | "already-provisioned"; basin: string | null } | { kind: "provisioned"; basin: string; retention: string }; // Idempotent. Treats S2 409 as success (race with another caller, or // previous run that crashed after S2 ack but before the column write). export async function provisionBasinForOrg( org: ProvisionInput, prismaClient: PrismaClientOrTransaction = prisma ): Promise { if (!isPerOrgBasinsEnabled()) { return { kind: "skipped", reason: "feature-disabled", basin: null }; } if (org.streamBasinName) { return { kind: "skipped", reason: "already-provisioned", basin: org.streamBasinName }; } const accessToken = env.REALTIME_STREAMS_S2_ACCESS_TOKEN; if (!accessToken) { throw new Error( "REALTIME_STREAMS_S2_ACCESS_TOKEN must be set when REALTIME_STREAMS_PER_ORG_BASINS_ENABLED=true" ); } const basin = basinNameForOrg(org); const retention = org.retention ?? defaultRetention(); await s2CreateBasin(basin, { accessToken, retentionPolicy: retention, storageClass: env.REALTIME_STREAMS_BASIN_STORAGE_CLASS, deleteOnEmptyMinAge: env.REALTIME_STREAMS_BASIN_DELETE_ON_EMPTY_MIN_AGE, }); await prismaClient.organization.update({ where: { id: org.id }, data: { streamBasinName: basin }, }); // streamBasinName is embedded in every env of the org; drop all its cached env rows. controlPlaneResolver.invalidateOrganization(org.id); logger.info("[streamBasinProvisioner] provisioned basin for org", { orgId: org.id, basin, retention, }); return { kind: "provisioned", basin, retention }; } export async function reconfigureBasinForOrg(orgId: string, retention: string): Promise { if (!isPerOrgBasinsEnabled()) return; const accessToken = env.REALTIME_STREAMS_S2_ACCESS_TOKEN; if (!accessToken) { throw new Error( "REALTIME_STREAMS_S2_ACCESS_TOKEN must be set when REALTIME_STREAMS_PER_ORG_BASINS_ENABLED=true" ); } const org = await prisma.organization.findFirst({ where: { id: orgId }, select: { id: true, streamBasinName: true }, }); if (!org?.streamBasinName) return; await s2ReconfigureBasin(org.streamBasinName, { accessToken, retentionPolicy: retention }); logger.info("[streamBasinProvisioner] reconfigured basin retention", { orgId, basin: org.streamBasinName, retention, }); } type EnsureResult = | { kind: "skipped"; reason: "feature-disabled" | "org-not-found" } | { kind: "provisioned"; basin: string; retention: string } | { kind: "reconfigured"; basin: string; retention: string }; // Idempotent: provisions if the org has no basin, PATCHes retention if // it does. The single entrypoint the cloud billing app drives — both // for the live plan-change path and the bulk backfill. export async function ensureBasinForOrg(orgId: string, retention: string): Promise { if (!isPerOrgBasinsEnabled()) { return { kind: "skipped", reason: "feature-disabled" }; } const org = await prisma.organization.findFirst({ where: { id: orgId }, select: { id: true, streamBasinName: true }, }); if (!org) return { kind: "skipped", reason: "org-not-found" }; if (!org.streamBasinName) { const result = await provisionBasinForOrg({ id: org.id, streamBasinName: null, retention }); if (result.kind === "provisioned") { return { kind: "provisioned", basin: result.basin, retention: result.retention }; } return { kind: "skipped", reason: "feature-disabled" }; } await reconfigureBasinForOrg(org.id, retention); return { kind: "reconfigured", basin: org.streamBasinName, retention }; } // Inverse of ensureBasinForOrg: nulls the column so future runs/sessions // land in the shared global basin. The S2 basin lingers; existing streams // age out on their original retention. export async function deprovisionBasinForOrg( orgId: string ): Promise<{ kind: "deprovisioned" } | { kind: "skipped"; reason: "no-basin" }> { const org = await prisma.organization.findFirst({ where: { id: orgId }, select: { id: true, streamBasinName: true }, }); if (!org?.streamBasinName) return { kind: "skipped", reason: "no-basin" }; await prisma.organization.update({ where: { id: org.id }, data: { streamBasinName: null }, }); // streamBasinName is embedded in every env of the org; drop all its cached env rows. controlPlaneResolver.invalidateOrganization(org.id); logger.info("[streamBasinProvisioner] deprovisioned basin for org", { orgId, previousBasin: org.streamBasinName, }); return { kind: "deprovisioned" }; } // S2 REST: POST /v1/basins to create, PATCH /v1/basins/{name} to // reconfigure. Wire shape takes integer seconds; we accept human strings // like "7d" / "1y" as env-var ergonomics and parse them here. type CreateBasinOptions = { accessToken: string; retentionPolicy: string; storageClass: "express" | "standard"; deleteOnEmptyMinAge: string; }; async function s2CreateBasin(name: string, opts: CreateBasinOptions): Promise { const url = `https://aws.s2.dev/v1/basins`; const body = { basin: name, config: { create_stream_on_append: true, create_stream_on_read: true, default_stream_config: { storage_class: opts.storageClass, retention_policy: { age: parseDuration(opts.retentionPolicy) }, delete_on_empty: { min_age_secs: parseDuration(opts.deleteOnEmptyMinAge) }, }, }, }; const res = await fetch(url, { signal: AbortSignal.timeout(10_000), method: "POST", headers: { Authorization: `Bearer ${opts.accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); // 409 = basin already exists; treat as success (idempotent). if (res.ok || res.status === 409) return; const text = await res.text().catch(() => ""); throw new Error(`S2 createBasin failed: ${res.status} ${res.statusText} ${text}`); } type ReconfigureBasinOptions = { accessToken: string; retentionPolicy: string; }; async function s2ReconfigureBasin(name: string, opts: ReconfigureBasinOptions): Promise { const url = `https://aws.s2.dev/v1/basins/${encodeURIComponent(name)}`; const body = { default_stream_config: { retention_policy: { age: parseDuration(opts.retentionPolicy) }, }, }; const res = await fetch(url, { signal: AbortSignal.timeout(10_000), method: "PATCH", headers: { Authorization: `Bearer ${opts.accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (res.ok) return; const text = await res.text().catch(() => ""); throw new Error(`S2 reconfigureBasin failed: ${res.status} ${res.statusText} ${text}`); }