Skip to content

Commit 4e0223e

Browse files
committed
fix(files): preserve empty history and order lifecycle locks
1 parent df7d505 commit 4e0223e

9 files changed

Lines changed: 564 additions & 54 deletions

File tree

‎apps/sim/background/cleanup-soft-deletes.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { and, asc, eq, inArray, isNotNull, isNull, lt, sql } from 'drizzle-orm'
1919
import { type CleanupJobPayload, runCleanupWithLimits } from '@/lib/billing/cleanup-dispatcher'
2020
import {
2121
decrementStorageUsageForBillingContextInTx,
22+
lockWorkspaceStorageForMutationInTx,
2223
resolveStorageBillingContext,
2324
} from '@/lib/billing/storage'
2425
import {
@@ -329,6 +330,7 @@ async function deleteExpiredBillableWorkspaceFileRows(
329330
try {
330331
const deletedCount = await db.transaction(async (tx) => {
331332
await lockWorkspaceProject(tx, workspaceId)
333+
await lockWorkspaceStorageForMutationInTx(tx, workspaceId)
332334
const billingContext = await resolveStorageBillingContext(workspaceId, tx)
333335
await releaseWorkspaceFileVersionsForPurgeInTx(
334336
tx,

‎apps/sim/lib/billing/storage/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export {
1818
incrementAdmittedStorageUsageForBillingContextInTx,
1919
incrementStorageUsageForBillingContextInTx,
2020
type LegacyStorageUsageDelta,
21+
lockWorkspaceStorageForMutationInTx,
2122
maybeNotifyStorageLimitForBillingContext,
2223
type WorkspaceStorageUsageDelta,
2324
} from './tracking'

‎apps/sim/lib/billing/storage/tracking.ts‎

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,29 @@ export async function applyStorageUsageDeltasInTx(
528528
return destinationUpdatedUsage
529529
}
530530

531+
/** Locks the canonical workspace before existing-file mutations that later change its storage ledger. */
532+
export async function lockWorkspaceStorageForMutationInTx(
533+
tx: DbOrTx,
534+
workspaceId: string
535+
): Promise<LockedWorkspaceStorage> {
536+
const [workspacePayer] = await tx
537+
.select({
538+
billedAccountUserId: workspace.billedAccountUserId,
539+
organizationId: workspace.organizationId,
540+
storageUsedBytes: workspace.storageUsedBytes,
541+
})
542+
.from(workspace)
543+
.where(eq(workspace.id, workspaceId))
544+
.for('no key update')
545+
.limit(1)
546+
547+
if (!workspacePayer) {
548+
throw new Error(`Workspace ${workspaceId} not found for storage accounting`)
549+
}
550+
551+
return { id: workspaceId, ...workspacePayer }
552+
}
553+
531554
/**
532555
* Mutates the durable workspace total and its current routed payer as one
533556
* transaction. The workspace row is the serialization point shared with payer
@@ -545,25 +568,9 @@ async function mutateWorkspaceStorageUsage(
545568
maximumUsage: number | undefined,
546569
context: StorageBillingContext
547570
): Promise<WorkspaceStorageMutationResult> {
548-
const [workspacePayer] = await tx
549-
.select({
550-
billedAccountUserId: workspace.billedAccountUserId,
551-
organizationId: workspace.organizationId,
552-
storageUsedBytes: workspace.storageUsedBytes,
553-
})
554-
.from(workspace)
555-
.where(eq(workspace.id, workspaceId))
556-
.for('no key update')
557-
.limit(1)
558-
559-
if (!workspacePayer) {
560-
throw new Error(`Workspace ${workspaceId} not found for storage accounting`)
561-
}
571+
const workspacePayer = await lockWorkspaceStorageForMutationInTx(tx, workspaceId)
562572

563-
const billingEntity = assertWorkspaceStorageContext(
564-
{ id: workspaceId, ...workspacePayer },
565-
context
566-
)
573+
const billingEntity = assertWorkspaceStorageContext(workspacePayer, context)
567574
const currentPayerUsage = await lockStorageUsageForMutation(tx, billingEntity)
568575

569576
if (mutation === 'decrement' && workspacePayer.storageUsedBytes < bytes) {

0 commit comments

Comments
 (0)