diff --git a/scripts/bunny-orphan-cleanup.ts b/scripts/bunny-orphan-cleanup.ts index 0a44248..8874482 100644 --- a/scripts/bunny-orphan-cleanup.ts +++ b/scripts/bunny-orphan-cleanup.ts @@ -319,6 +319,7 @@ async function main() { console.log(`[bunny-orphan-cleanup] Grace period: ${graceHours}h`); const expiredBillingCleanup = await cleanupExpiredBillingWorkspaces({ dryRun }); + console.log(`[bunny-orphan-cleanup] Expired owners past grace: ${expiredBillingCleanup.owners}`); console.log( `[bunny-orphan-cleanup] Expired owner workspaces scanned: ${expiredBillingCleanup.scanned}` ); diff --git a/scripts/expired-billing-cleanup.ts b/scripts/expired-billing-cleanup.ts index a3ce1e6..29efdac 100644 --- a/scripts/expired-billing-cleanup.ts +++ b/scripts/expired-billing-cleanup.ts @@ -2,6 +2,7 @@ import { db } from '../lib/db'; import { buildExpiredBillingWhereInput } from '../lib/billing'; import { collectWorkspaceMediaUrls, deleteMediaFilesBestEffort } from '../lib/r2-cleanup'; import { cleanupBunnyStreamVideosBestEffort } from '../lib/bunny-stream-cleanup'; +import { logCleanupWarnings } from '../lib/cleanup-warnings'; type ExpiredWorkspaceTarget = { id: string; @@ -9,46 +10,58 @@ type ExpiredWorkspaceTarget = { ownerEmail: string | null; }; -async function getExpiredWorkspaceTargets(): Promise { +/** + * The owners past their grace period, and the workspaces they own. + * + * Both counts are reported, because they answer different questions and a single number + * conflated them: zero workspaces can mean nobody expired, or that everyone who expired owns + * nothing. The first is normal, the second means media is being kept alive by rows the + * cleanup cannot reach, and telling them apart used to require a hand-written query. + */ +async function getExpiredWorkspaceTargets(): Promise<{ + owners: number; + workspaces: ExpiredWorkspaceTarget[]; +}> { const expiredOwners = await db.user.findMany({ where: buildExpiredBillingWhereInput(), select: { id: true }, }); if (expiredOwners.length === 0) { - return []; + return { owners: 0, workspaces: [] }; } - return db.workspace - .findMany({ - where: { - ownerId: { in: expiredOwners.map((owner) => owner.id) }, - }, - select: { - id: true, - ownerId: true, - owner: { - select: { - email: true, - }, + const workspaces = await db.workspace.findMany({ + where: { + ownerId: { in: expiredOwners.map((owner) => owner.id) }, + }, + select: { + id: true, + ownerId: true, + owner: { + select: { + email: true, }, }, - }) - .then((workspaces) => - workspaces.map((workspace) => ({ - id: workspace.id, - ownerId: workspace.ownerId, - ownerEmail: workspace.owner.email, - })) - ); + }, + }); + + return { + owners: expiredOwners.length, + workspaces: workspaces.map((workspace) => ({ + id: workspace.id, + ownerId: workspace.ownerId, + ownerEmail: workspace.owner.email, + })), + }; } export async function cleanupExpiredBillingWorkspaces(options?: { dryRun?: boolean }) { const dryRun = options?.dryRun ?? false; - const workspaces = await getExpiredWorkspaceTargets(); + const { owners, workspaces } = await getExpiredWorkspaceTargets(); if (workspaces.length === 0) { - return { scanned: 0, deleted: 0 }; + return { owners, scanned: 0, deleted: 0 }; } let deleted = 0; @@ -102,12 +115,15 @@ export async function cleanupExpiredBillingWorkspaces(options?: { dryRun?: boole ]; await db.workspace.delete({ where: { id: workspace.id } }); - await Promise.all([ + const [bunny, r2] = await Promise.all([ cleanupBunnyStreamVideosBestEffort(bunnyRefs), deleteMediaFilesBestEffort(mediaUrls), ]); + // The rows are already gone, so a refused delete leaves media nothing points at. The + // orphan sweep in the calling script picks those up, but only a log says it happened. + logCleanupWarnings({ entityType: 'workspace', entityId: workspace.id }, { bunny, r2 }); deleted += 1; } - return { scanned: workspaces.length, deleted }; + return { owners, scanned: workspaces.length, deleted }; } diff --git a/scripts/r2-orphan-cleanup.ts b/scripts/r2-orphan-cleanup.ts index 99dde3b..e35bc98 100644 --- a/scripts/r2-orphan-cleanup.ts +++ b/scripts/r2-orphan-cleanup.ts @@ -209,6 +209,7 @@ async function main() { console.log(`[r2-orphan-cleanup] Starting (${dryRun ? 'dry-run' : 'delete mode'})`); const expiredBillingCleanup = await cleanupExpiredBillingWorkspaces({ dryRun }); + console.log(`[r2-orphan-cleanup] Expired owners past grace: ${expiredBillingCleanup.owners}`); console.log( `[r2-orphan-cleanup] Expired owner workspaces scanned: ${expiredBillingCleanup.scanned}` );