Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions apps/sim/background/cleanup-logs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,17 @@ const {
}
})

vi.mock('@sim/db', () => ({
db: {
vi.mock('@sim/db', () => {
const db = {
execute: mockExecute,
select: mockSelect,
},
}))
}
return {
db,
// Cleanup-pool client shares the instance so the seeded chains still apply.
dbFor: () => db,
}
})

vi.mock('@sim/db/schema', () => ({
executionLargeValueDependencies: {
Expand Down Expand Up @@ -255,7 +260,7 @@ describe('cleanup logs worker', () => {
workspaceIds: ['workspace-1'],
})

expect(mockMarkLargeValuesDeleted).toHaveBeenCalledWith([largeValueKey])
expect(mockMarkLargeValuesDeleted).toHaveBeenCalledWith([largeValueKey], expect.anything())
expect(mockDeleteFileMetadata).toHaveBeenCalledTimes(2)
})

Expand Down
21 changes: 15 additions & 6 deletions apps/sim/background/cleanup-logs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { db } from '@sim/db'
import { dbFor } from '@sim/db'
import {
executionLargeValueDependencies,
executionLargeValueReferences,
Expand Down Expand Up @@ -30,6 +30,9 @@ import { deleteFileMetadata } from '@/lib/uploads/server/metadata'

const logger = createLogger('CleanupLogs')

/** All cleanup queries run on the dedicated cleanup pool. */
const cleanupDb = dbFor('cleanup')
Comment thread
TheodoreSpeaks marked this conversation as resolved.

interface FileDeleteStats {
filesTotal: number
filesDeleted: number
Expand Down Expand Up @@ -107,7 +110,7 @@ async function deleteLargeValueKeys(keys: string[]): Promise<{ deleted: number;

if (deletedKeys.length > 0) {
try {
await markLargeValuesDeleted(deletedKeys)
await markLargeValuesDeleted(deletedKeys, cleanupDb)
} catch (error) {
logger.error('Failed to mark large execution values as deleted:', { error })
return { deleted: 0, failed: result.failed.length + deletedKeys.length }
Expand Down Expand Up @@ -153,7 +156,7 @@ async function cleanupLargeExecutionValues(
LARGE_VALUE_CLEANUP_BATCH_SIZE,
LARGE_VALUE_CLEANUP_TOTAL_KEY_LIMIT - attempted
)
const rows = await db
const rows = await cleanupDb
.select({ key: executionLargeValues.key })
.from(executionLargeValues)
.where(
Expand Down Expand Up @@ -219,7 +222,7 @@ async function cleanupLegacyLargeExecutionValues(
LARGE_VALUE_CLEANUP_BATCH_SIZE,
LARGE_VALUE_CLEANUP_TOTAL_KEY_LIMIT - attempted
)
const rows = await db
const rows = await cleanupDb
.select({ key: workspaceFiles.key })
.from(workspaceFiles)
.where(
Expand Down Expand Up @@ -353,7 +356,11 @@ async function cleanupLargeValueMetadata(workspaceIds: string[], label: string):
const tombstonesDeletedBefore = new Date(
Date.now() - LARGE_VALUE_TOMBSTONE_RETENTION_HOURS * 60 * 60 * 1000
)
const result = await pruneLargeValueMetadata({ workspaceIds, tombstonesDeletedBefore })
const result = await pruneLargeValueMetadata({
workspaceIds,
tombstonesDeletedBefore,
dbClient: cleanupDb,
})
logger.info(
`[${label}/execution_large_value_metadata] Pruned ${result.referencesDeleted} stale references, ${result.dependenciesDeleted} dependencies, ${result.tombstonesDeleted} tombstones`
)
Expand All @@ -377,8 +384,9 @@ async function cleanupWorkflowExecutionLogs(
tableDef: workflowExecutionLogs,
workspaceIds,
tableName: `${label}/workflow_execution_logs`,
dbClient: cleanupDb,
selectChunk: (chunkIds, limit) =>
db
cleanupDb
.select({
id: workflowExecutionLogs.id,
files: workflowExecutionLogs.files,
Expand Down Expand Up @@ -465,6 +473,7 @@ export async function runCleanupLogs(payload: CleanupJobPayload): Promise<void>
workspaceIds,
retentionDate,
tableName: `${label}/job_execution_logs`,
dbClient: cleanupDb,
})

if (runGlobalHousekeeping && plan === 'free') {
Expand Down
13 changes: 9 additions & 4 deletions apps/sim/background/cleanup-soft-deletes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,18 @@ const {
}
})

vi.mock('@sim/db', () => ({
db: {
vi.mock('@sim/db', () => {
const db = {
delete: mockDelete,
select: mockSelect,
transaction: mockTransaction,
},
}))
}
return {
db,
// Cleanup-pool client shares the instance so the seeded chains still apply.
dbFor: () => db,
}
})

vi.mock('@sim/db/schema', () => {
const table = (cols: string[]) =>
Expand Down
34 changes: 22 additions & 12 deletions apps/sim/background/cleanup-soft-deletes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { db } from '@sim/db'
import { db, dbFor } from '@sim/db'
import {
copilotChats,
document,
Expand Down Expand Up @@ -37,6 +37,13 @@ import { deleteFileMetadata } from '@/lib/uploads/server/metadata'

const logger = createLogger('CleanupSoftDeletes')

/**
* Cleanup queries run on the dedicated cleanup pool. The one exception is the
* billable-file transaction below, which couples row deletion with a storage
* billing decrement — billing writes stay on the default client.
*/
const cleanupDb = dbFor('cleanup')

const KB_ORPHAN_BINDING_BATCH_SIZE = 500
const KB_ORPHAN_BINDING_TOTAL_LIMIT = 5_000
/**
Expand Down Expand Up @@ -81,7 +88,7 @@ async function selectExpiredWorkspaceFiles(
): Promise<WorkspaceFileScope> {
const [legacyRows, multiContextRows] = await Promise.all([
selectRowsByIdChunks(workspaceIds, (chunkIds, chunkLimit) =>
db
cleanupDb
.select({
id: workspaceFile.id,
key: workspaceFile.key,
Expand All @@ -98,7 +105,7 @@ async function selectExpiredWorkspaceFiles(
.limit(chunkLimit)
),
selectRowsByIdChunks(workspaceIds, (chunkIds, chunkLimit) =>
db
cleanupDb
.select({
id: workspaceFiles.id,
key: workspaceFiles.key,
Expand Down Expand Up @@ -200,7 +207,7 @@ async function deleteExpiredLegacyWorkspaceFileRows(
const result = { deleted: 0, failed: 0 }
for (const batch of chunkArray(rows, DEFAULT_DELETE_CHUNK_SIZE)) {
try {
const deleted = await db
const deleted = await cleanupDb
.delete(workspaceFile)
.where(
and(
Expand Down Expand Up @@ -240,7 +247,7 @@ async function deleteExpiredUnbilledWorkspaceFileRows(
for (const [context, contextRows] of rowsByContext) {
for (const batch of chunkArray(contextRows, DEFAULT_DELETE_CHUNK_SIZE)) {
try {
const deleted = await db
const deleted = await cleanupDb
.delete(workspaceFiles)
.where(
and(
Expand Down Expand Up @@ -343,7 +350,7 @@ async function hardDeleteKnowledgeBaseDocuments(
label: string
): Promise<void> {
for (let batch = 0; batch < KB_DOCUMENT_DELETE_MAX_BATCHES; batch++) {
const documentRows = await db
const documentRows = await cleanupDb
.select({ id: document.id })
.from(document)
.where(inArray(document.knowledgeBaseId, knowledgeBaseIds))
Expand All @@ -358,7 +365,7 @@ async function hardDeleteKnowledgeBaseDocuments(
}
}

const remaining = await db
const remaining = await cleanupDb
.select({ id: document.id })
.from(document)
.where(inArray(document.knowledgeBaseId, knowledgeBaseIds))
Expand All @@ -378,8 +385,9 @@ async function cleanupExpiredKnowledgeBases(
workspaceIds,
tableName: `${label}/knowledgeBase`,
batchSize: KB_RETENTION_BATCH_SIZE,
dbClient: cleanupDb,
selectChunk: (chunkIds, limit) =>
db
cleanupDb
.select({ id: knowledgeBase.id })
.from(knowledgeBase)
.where(
Expand Down Expand Up @@ -458,7 +466,7 @@ async function cleanupOrphanedKnowledgeBaseBindings(
KB_ORPHAN_BINDING_BATCH_SIZE,
KB_ORPHAN_BINDING_TOTAL_LIMIT - attempted
)
const rows = await db
const rows = await cleanupDb
.select({ key: workspaceFiles.key })
.from(workspaceFiles)
.where(
Expand Down Expand Up @@ -535,7 +543,7 @@ export async function runCleanupSoftDeletes(payload: CleanupJobPayload): Promise
// different subsets above the LIMIT cap and orphan or prematurely purge data.
const [doomedWorkflows, fileScope] = await Promise.all([
selectRowsByIdChunks(workspaceIds, (chunkIds, chunkLimit) =>
db
cleanupDb
.select({ id: workflow.id })
.from(workflow)
.where(
Expand All @@ -555,7 +563,7 @@ export async function runCleanupSoftDeletes(payload: CleanupJobPayload): Promise

if (doomedWorkflowIds.length > 0) {
const doomedChats = await selectRowsByIdChunks(doomedWorkflowIds, (chunkIds, chunkLimit) =>
db
cleanupDb
.select({ id: copilotChats.id })
.from(copilotChats)
.where(inArray(copilotChats.workflowId, chunkIds))
Expand All @@ -577,7 +585,8 @@ export async function runCleanupSoftDeletes(payload: CleanupJobPayload): Promise
workflow,
workflow.id,
doomedWorkflowIds,
`${label}/workflow`
`${label}/workflow`,
cleanupDb
)
totalDeleted += workflowResult.deleted

Expand Down Expand Up @@ -614,6 +623,7 @@ export async function runCleanupSoftDeletes(payload: CleanupJobPayload): Promise
retentionDate,
tableName: `${label}/${target.name}`,
requireTimestampNotNull: true,
dbClient: cleanupDb,
})
totalDeleted += result.deleted
}
Expand Down
21 changes: 15 additions & 6 deletions apps/sim/background/cleanup-tasks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { db } from '@sim/db'
import { dbFor } from '@sim/db'
import {
copilotAsyncToolCalls,
copilotChats,
Expand All @@ -21,6 +21,9 @@ import { prepareChatCleanup } from '@/lib/cleanup/chat-cleanup'

const logger = createLogger('CleanupTasks')

/** All cleanup queries run on the dedicated cleanup pool. */
const cleanupDb = dbFor('cleanup')
Comment thread
TheodoreSpeaks marked this conversation as resolved.

/**
* Delete copilot run checkpoints and async tool calls via join through copilotRuns.
* These tables don't have a direct workspaceId — we find qualifying run IDs first.
Expand All @@ -46,7 +49,7 @@ async function cleanupRunChildren(
if (workspaceIds.length === 0) return []

const runIds = await selectRowsByIdChunks(workspaceIds, (chunkIds, chunkLimit) =>
db
cleanupDb
.select({ id: copilotRuns.id })
.from(copilotRuns)
.where(
Expand All @@ -62,7 +65,9 @@ async function cleanupRunChildren(
const ids = runIds.map((r) => r.id)

return Promise.all(
RUN_CHILD_TABLES.map((t) => deleteRowsById(t.table, t.runIdCol, ids, `${label}/${t.name}`))
RUN_CHILD_TABLES.map((t) =>
deleteRowsById(t.table, t.runIdCol, ids, `${label}/${t.name}`, cleanupDb)
)
)
}

Expand All @@ -81,7 +86,7 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise<void>
)

const doomedChats = await selectRowsByIdChunks(workspaceIds, (chunkIds, chunkLimit) =>
db
cleanupDb
.select({ id: copilotChats.id })
.from(copilotChats)
.where(
Expand All @@ -106,7 +111,8 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise<void>
copilotFeedback,
copilotFeedback.chatId,
doomedChatIds,
`${label}/copilotFeedback`
`${label}/copilotFeedback`,
cleanupDb
)

// Delete copilot runs (has workspaceId directly, cascades checkpoints)
Expand All @@ -117,6 +123,7 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise<void>
workspaceIds,
retentionDate,
tableName: `${label}/copilotRuns`,
dbClient: cleanupDb,
})

// Delete copilot chats using the exact IDs collected above so the chat
Expand All @@ -125,7 +132,8 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise<void>
copilotChats,
copilotChats.id,
doomedChatIds,
`${label}/copilotChats`
`${label}/copilotChats`,
cleanupDb
)

// Delete mothership inbox tasks (has workspaceId directly)
Expand All @@ -136,6 +144,7 @@ export async function runCleanupTasks(payload: CleanupJobPayload): Promise<void>
workspaceIds,
retentionDate,
tableName: `${label}/mothershipInboxTask`,
dbClient: cleanupDb,
})

const totalDeleted =
Expand Down
Loading
Loading