mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
fix(scripts): tell an empty cleanup scan apart from an unreachable one
The cleanup printed one number for expired owners, the count of workspaces it found, so zero meant either that nobody had passed the grace period or that everyone who had owns nothing. The first is normal and the second means media is held alive by rows the cleanup cannot reach, and telling them apart took a hand-written query against production. Both counts are reported now. Bunny and R2 results were also discarded. The workspace row is deleted first, so a refused storage delete leaves media that nothing points at, and nothing recorded that it happened. logCleanupWarnings already exists for this and is now called with the per-workspace result.
This commit is contained in:
@@ -319,6 +319,7 @@ async function main() {
|
|||||||
console.log(`[bunny-orphan-cleanup] Grace period: ${graceHours}h`);
|
console.log(`[bunny-orphan-cleanup] Grace period: ${graceHours}h`);
|
||||||
|
|
||||||
const expiredBillingCleanup = await cleanupExpiredBillingWorkspaces({ dryRun });
|
const expiredBillingCleanup = await cleanupExpiredBillingWorkspaces({ dryRun });
|
||||||
|
console.log(`[bunny-orphan-cleanup] Expired owners past grace: ${expiredBillingCleanup.owners}`);
|
||||||
console.log(
|
console.log(
|
||||||
`[bunny-orphan-cleanup] Expired owner workspaces scanned: ${expiredBillingCleanup.scanned}`
|
`[bunny-orphan-cleanup] Expired owner workspaces scanned: ${expiredBillingCleanup.scanned}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { db } from '../lib/db';
|
|||||||
import { buildExpiredBillingWhereInput } from '../lib/billing';
|
import { buildExpiredBillingWhereInput } from '../lib/billing';
|
||||||
import { collectWorkspaceMediaUrls, deleteMediaFilesBestEffort } from '../lib/r2-cleanup';
|
import { collectWorkspaceMediaUrls, deleteMediaFilesBestEffort } from '../lib/r2-cleanup';
|
||||||
import { cleanupBunnyStreamVideosBestEffort } from '../lib/bunny-stream-cleanup';
|
import { cleanupBunnyStreamVideosBestEffort } from '../lib/bunny-stream-cleanup';
|
||||||
|
import { logCleanupWarnings } from '../lib/cleanup-warnings';
|
||||||
|
|
||||||
type ExpiredWorkspaceTarget = {
|
type ExpiredWorkspaceTarget = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -9,46 +10,58 @@ type ExpiredWorkspaceTarget = {
|
|||||||
ownerEmail: string | null;
|
ownerEmail: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getExpiredWorkspaceTargets(): Promise<ExpiredWorkspaceTarget[]> {
|
/**
|
||||||
|
* 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({
|
const expiredOwners = await db.user.findMany({
|
||||||
where: buildExpiredBillingWhereInput(),
|
where: buildExpiredBillingWhereInput(),
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (expiredOwners.length === 0) {
|
if (expiredOwners.length === 0) {
|
||||||
return [];
|
return { owners: 0, workspaces: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.workspace
|
const workspaces = await db.workspace.findMany({
|
||||||
.findMany({
|
where: {
|
||||||
where: {
|
ownerId: { in: expiredOwners.map((owner) => owner.id) },
|
||||||
ownerId: { in: expiredOwners.map((owner) => owner.id) },
|
},
|
||||||
},
|
select: {
|
||||||
select: {
|
id: true,
|
||||||
id: true,
|
ownerId: true,
|
||||||
ownerId: true,
|
owner: {
|
||||||
owner: {
|
select: {
|
||||||
select: {
|
email: true,
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
},
|
||||||
.then((workspaces) =>
|
});
|
||||||
workspaces.map((workspace) => ({
|
|
||||||
id: workspace.id,
|
return {
|
||||||
ownerId: workspace.ownerId,
|
owners: expiredOwners.length,
|
||||||
ownerEmail: workspace.owner.email,
|
workspaces: workspaces.map((workspace) => ({
|
||||||
}))
|
id: workspace.id,
|
||||||
);
|
ownerId: workspace.ownerId,
|
||||||
|
ownerEmail: workspace.owner.email,
|
||||||
|
})),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cleanupExpiredBillingWorkspaces(options?: { dryRun?: boolean }) {
|
export async function cleanupExpiredBillingWorkspaces(options?: { dryRun?: boolean }) {
|
||||||
const dryRun = options?.dryRun ?? false;
|
const dryRun = options?.dryRun ?? false;
|
||||||
const workspaces = await getExpiredWorkspaceTargets();
|
const { owners, workspaces } = await getExpiredWorkspaceTargets();
|
||||||
|
|
||||||
if (workspaces.length === 0) {
|
if (workspaces.length === 0) {
|
||||||
return { scanned: 0, deleted: 0 };
|
return { owners, scanned: 0, deleted: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
let 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 db.workspace.delete({ where: { id: workspace.id } });
|
||||||
await Promise.all([
|
const [bunny, r2] = await Promise.all([
|
||||||
cleanupBunnyStreamVideosBestEffort(bunnyRefs),
|
cleanupBunnyStreamVideosBestEffort(bunnyRefs),
|
||||||
deleteMediaFilesBestEffort(mediaUrls),
|
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;
|
deleted += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { scanned: workspaces.length, deleted };
|
return { owners, scanned: workspaces.length, deleted };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ async function main() {
|
|||||||
console.log(`[r2-orphan-cleanup] Starting (${dryRun ? 'dry-run' : 'delete mode'})`);
|
console.log(`[r2-orphan-cleanup] Starting (${dryRun ? 'dry-run' : 'delete mode'})`);
|
||||||
|
|
||||||
const expiredBillingCleanup = await cleanupExpiredBillingWorkspaces({ dryRun });
|
const expiredBillingCleanup = await cleanupExpiredBillingWorkspaces({ dryRun });
|
||||||
|
console.log(`[r2-orphan-cleanup] Expired owners past grace: ${expiredBillingCleanup.owners}`);
|
||||||
console.log(
|
console.log(
|
||||||
`[r2-orphan-cleanup] Expired owner workspaces scanned: ${expiredBillingCleanup.scanned}`
|
`[r2-orphan-cleanup] Expired owner workspaces scanned: ${expiredBillingCleanup.scanned}`
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user