chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,618 @@
|
||||
import { db } from '@sim/db'
|
||||
import {
|
||||
executionLargeValueDependencies,
|
||||
executionLargeValueReferences,
|
||||
executionLargeValues,
|
||||
pausedExecutions,
|
||||
workflowExecutionLogs,
|
||||
} from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, inArray, notInArray, sql } from 'drizzle-orm'
|
||||
import { chunkArray } from '@/lib/cleanup/batch-delete'
|
||||
import { collectLargeValueKeys } from '@/lib/execution/payloads/large-execution-value'
|
||||
|
||||
const logger = createLogger('LargeValueMetadata')
|
||||
|
||||
type LargeValueMetadataClient = typeof db | Parameters<Parameters<typeof db.transaction>[0]>[0]
|
||||
|
||||
export const MAX_LARGE_VALUE_REFERENCES_PER_SCOPE = 5_000
|
||||
const LARGE_VALUE_METADATA_WRITE_CHUNK_SIZE = 500
|
||||
const LARGE_VALUE_METADATA_WORKSPACE_CHUNK_SIZE = 50
|
||||
const LARGE_VALUE_METADATA_PRUNE_BATCH_SIZE = 1_000
|
||||
const LARGE_VALUE_METADATA_PRUNE_MAX_ROWS_PER_TABLE = 5_000
|
||||
export const LIVE_PAUSED_REFERENCE_STATUSES = ['paused', 'partially_resumed', 'cancelling'] as const
|
||||
|
||||
export interface LargeValueOwner {
|
||||
key: string
|
||||
workspaceId: string
|
||||
workflowId: string
|
||||
executionId: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface LargeValueReferenceScope {
|
||||
workspaceId?: string
|
||||
workflowId?: string | null
|
||||
executionId?: string
|
||||
source: 'execution_log' | 'paused_snapshot'
|
||||
}
|
||||
|
||||
interface LargeValueStorageKeyParts {
|
||||
workspaceId: string
|
||||
workflowId: string
|
||||
executionId: string
|
||||
}
|
||||
|
||||
export interface LargeValueMetadataPruneResult {
|
||||
referencesDeleted: number
|
||||
dependenciesDeleted: number
|
||||
tombstonesDeleted: number
|
||||
}
|
||||
|
||||
interface PruneLargeValueMetadataOptions {
|
||||
workspaceIds: string[]
|
||||
tombstonesDeletedBefore: Date
|
||||
batchSize?: number
|
||||
maxRowsPerTable?: number
|
||||
}
|
||||
|
||||
function parseLargeValueStorageKey(key: string): LargeValueStorageKeyParts | null {
|
||||
const parts = key.split('/')
|
||||
if (
|
||||
parts.length !== 5 ||
|
||||
parts[0] !== 'execution' ||
|
||||
!parts[1] ||
|
||||
!parts[2] ||
|
||||
!parts[3] ||
|
||||
!/^large-value-lv_[A-Za-z0-9_-]{12}\.json$/.test(parts[4])
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: parts[1],
|
||||
workflowId: parts[2],
|
||||
executionId: parts[3],
|
||||
}
|
||||
}
|
||||
|
||||
function getBoundedUniqueKeys(keys: string[], label: string): string[] {
|
||||
const uniqueKeys = Array.from(new Set(keys))
|
||||
if (uniqueKeys.length > MAX_LARGE_VALUE_REFERENCES_PER_SCOPE) {
|
||||
throw new Error(
|
||||
`${label} contains ${uniqueKeys.length} large value references, exceeding the limit of ${MAX_LARGE_VALUE_REFERENCES_PER_SCOPE}`
|
||||
)
|
||||
}
|
||||
return uniqueKeys
|
||||
}
|
||||
|
||||
function getCount(rows: unknown): number {
|
||||
const [row] = Array.isArray(rows) ? rows : []
|
||||
if (!row || typeof row !== 'object' || !('count' in row)) {
|
||||
return 0
|
||||
}
|
||||
return Number((row as { count: unknown }).count) || 0
|
||||
}
|
||||
|
||||
export function collectLargeValueReferenceKeys(value: unknown, workspaceId?: string): string[] {
|
||||
return getBoundedUniqueKeys(
|
||||
collectLargeValueKeys(value).filter((key) => {
|
||||
const parsed = parseLargeValueStorageKey(key)
|
||||
return workspaceId ? parsed?.workspaceId === workspaceId : Boolean(parsed)
|
||||
}),
|
||||
'Large value reference set'
|
||||
)
|
||||
}
|
||||
|
||||
async function getDependencyClosure(
|
||||
client: LargeValueMetadataClient,
|
||||
ownerKey: string,
|
||||
workspaceId: string,
|
||||
referencedKeys: string[]
|
||||
): Promise<string[]> {
|
||||
const directKeys = getBoundedUniqueKeys(
|
||||
referencedKeys.filter((key) => {
|
||||
const parsed = parseLargeValueStorageKey(key)
|
||||
return parsed?.workspaceId === workspaceId && key !== ownerKey
|
||||
}),
|
||||
'Large value dependency set'
|
||||
)
|
||||
|
||||
if (directKeys.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const closureKeys = new Set(directKeys)
|
||||
let frontier = directKeys
|
||||
|
||||
while (frontier.length > 0) {
|
||||
const nextFrontier: string[] = []
|
||||
|
||||
for (const keyChunk of chunkArray(frontier, LARGE_VALUE_METADATA_WRITE_CHUNK_SIZE)) {
|
||||
const remainingBudget = MAX_LARGE_VALUE_REFERENCES_PER_SCOPE - closureKeys.size
|
||||
const rows = await client
|
||||
.selectDistinct({ childKey: executionLargeValueDependencies.childKey })
|
||||
.from(executionLargeValueDependencies)
|
||||
.where(
|
||||
and(
|
||||
eq(executionLargeValueDependencies.workspaceId, workspaceId),
|
||||
inArray(executionLargeValueDependencies.parentKey, keyChunk),
|
||||
notInArray(executionLargeValueDependencies.childKey, Array.from(closureKeys))
|
||||
)
|
||||
)
|
||||
.limit(remainingBudget + 1)
|
||||
|
||||
for (const row of rows) {
|
||||
if (closureKeys.has(row.childKey)) {
|
||||
continue
|
||||
}
|
||||
closureKeys.add(row.childKey)
|
||||
nextFrontier.push(row.childKey)
|
||||
if (closureKeys.size > MAX_LARGE_VALUE_REFERENCES_PER_SCOPE) {
|
||||
throw new Error(
|
||||
`Large value dependency closure exceeds the limit of ${MAX_LARGE_VALUE_REFERENCES_PER_SCOPE}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
frontier = nextFrontier
|
||||
}
|
||||
|
||||
return Array.from(closureKeys)
|
||||
}
|
||||
|
||||
export async function registerLargeValueOwner(
|
||||
owner: LargeValueOwner,
|
||||
referencedKeys: string[] = []
|
||||
): Promise<boolean> {
|
||||
if (!Number.isFinite(owner.size) || owner.size <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const parsed = parseLargeValueStorageKey(owner.key)
|
||||
if (
|
||||
!parsed ||
|
||||
parsed.workspaceId !== owner.workspaceId ||
|
||||
parsed.workflowId !== owner.workflowId ||
|
||||
parsed.executionId !== owner.executionId
|
||||
) {
|
||||
logger.warn('Skipping large value owner registration for malformed storage key', {
|
||||
key: owner.key,
|
||||
workspaceId: owner.workspaceId,
|
||||
workflowId: owner.workflowId,
|
||||
executionId: owner.executionId,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.insert(executionLargeValues)
|
||||
.values({
|
||||
key: owner.key,
|
||||
workspaceId: owner.workspaceId,
|
||||
workflowId: owner.workflowId,
|
||||
ownerExecutionId: owner.executionId,
|
||||
size: Math.ceil(owner.size),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
|
||||
const dependencyKeys = await getDependencyClosure(
|
||||
tx,
|
||||
owner.key,
|
||||
owner.workspaceId,
|
||||
referencedKeys
|
||||
)
|
||||
if (dependencyKeys.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const keyChunk of chunkArray(dependencyKeys, LARGE_VALUE_METADATA_WRITE_CHUNK_SIZE)) {
|
||||
await tx
|
||||
.insert(executionLargeValueDependencies)
|
||||
.values(
|
||||
keyChunk.map((childKey) => ({
|
||||
parentKey: owner.key,
|
||||
childKey,
|
||||
workspaceId: owner.workspaceId,
|
||||
}))
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function replaceLargeValueReferencesWithClient(
|
||||
client: LargeValueMetadataClient,
|
||||
scope: LargeValueReferenceScope,
|
||||
value: unknown
|
||||
): Promise<void> {
|
||||
if (!scope.workspaceId || !scope.executionId) {
|
||||
return
|
||||
}
|
||||
|
||||
await replaceLargeValueReferenceKeysWithClient(
|
||||
client,
|
||||
scope,
|
||||
collectLargeValueReferenceKeys(value, scope.workspaceId)
|
||||
)
|
||||
}
|
||||
|
||||
export async function replaceLargeValueReferenceKeysWithClient(
|
||||
client: LargeValueMetadataClient,
|
||||
scope: LargeValueReferenceScope,
|
||||
referenceKeys: string[]
|
||||
): Promise<void> {
|
||||
const { workspaceId, workflowId, executionId, source } = scope
|
||||
if (!workspaceId || !executionId) {
|
||||
return
|
||||
}
|
||||
|
||||
const keys = getBoundedUniqueKeys(
|
||||
referenceKeys.filter((key) => {
|
||||
const parsed = parseLargeValueStorageKey(key)
|
||||
return parsed?.workspaceId === workspaceId
|
||||
}),
|
||||
'Large value reference set'
|
||||
)
|
||||
|
||||
await client
|
||||
.delete(executionLargeValueReferences)
|
||||
.where(
|
||||
and(
|
||||
eq(executionLargeValueReferences.workspaceId, workspaceId),
|
||||
eq(executionLargeValueReferences.executionId, executionId),
|
||||
eq(executionLargeValueReferences.source, source)
|
||||
)
|
||||
)
|
||||
|
||||
if (keys.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const keyChunk of chunkArray(keys, LARGE_VALUE_METADATA_WRITE_CHUNK_SIZE)) {
|
||||
await client
|
||||
.insert(executionLargeValueReferences)
|
||||
.values(
|
||||
keyChunk.map((key) => ({
|
||||
key,
|
||||
workspaceId,
|
||||
workflowId: workflowId ?? null,
|
||||
executionId,
|
||||
source,
|
||||
}))
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
}
|
||||
}
|
||||
|
||||
export async function addLargeValueReference(
|
||||
scope: LargeValueReferenceScope,
|
||||
key: string
|
||||
): Promise<void> {
|
||||
const { workspaceId, workflowId, executionId, source } = scope
|
||||
if (!workspaceId || !executionId) {
|
||||
return
|
||||
}
|
||||
|
||||
const [boundedKey] = getBoundedUniqueKeys(
|
||||
[key].filter((candidate) => {
|
||||
const parsed = parseLargeValueStorageKey(candidate)
|
||||
return parsed?.workspaceId === workspaceId
|
||||
}),
|
||||
'Large value reference set'
|
||||
)
|
||||
if (!boundedKey) {
|
||||
return
|
||||
}
|
||||
|
||||
const [existingRef] = await db
|
||||
.select({ key: executionLargeValueReferences.key })
|
||||
.from(executionLargeValueReferences)
|
||||
.where(
|
||||
and(
|
||||
eq(executionLargeValueReferences.workspaceId, workspaceId),
|
||||
eq(executionLargeValueReferences.executionId, executionId),
|
||||
eq(executionLargeValueReferences.source, source),
|
||||
eq(executionLargeValueReferences.key, boundedKey)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existingRef) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingRefs = await db
|
||||
.select({ key: executionLargeValueReferences.key })
|
||||
.from(executionLargeValueReferences)
|
||||
.where(
|
||||
and(
|
||||
eq(executionLargeValueReferences.workspaceId, workspaceId),
|
||||
eq(executionLargeValueReferences.executionId, executionId),
|
||||
eq(executionLargeValueReferences.source, source)
|
||||
)
|
||||
)
|
||||
.limit(MAX_LARGE_VALUE_REFERENCES_PER_SCOPE + 1)
|
||||
|
||||
if (existingRefs.length >= MAX_LARGE_VALUE_REFERENCES_PER_SCOPE) {
|
||||
throw new Error(
|
||||
`Large value reference set contains at least ${existingRefs.length} references, exceeding the limit of ${MAX_LARGE_VALUE_REFERENCES_PER_SCOPE}`
|
||||
)
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(executionLargeValueReferences)
|
||||
.values({
|
||||
key: boundedKey,
|
||||
workspaceId,
|
||||
workflowId: workflowId ?? null,
|
||||
executionId,
|
||||
source,
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
}
|
||||
|
||||
export async function replaceLargeValueReferences(
|
||||
scope: LargeValueReferenceScope,
|
||||
value: unknown
|
||||
): Promise<void> {
|
||||
const referenceKeys = scope.workspaceId
|
||||
? collectLargeValueReferenceKeys(value, scope.workspaceId)
|
||||
: []
|
||||
await db.transaction(async (tx) => {
|
||||
await replaceLargeValueReferenceKeysWithClient(tx, scope, referenceKeys)
|
||||
})
|
||||
}
|
||||
|
||||
export async function markLargeValuesDeleted(keys: string[]): Promise<void> {
|
||||
if (keys.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
await db
|
||||
.update(executionLargeValues)
|
||||
.set({ deletedAt: new Date() })
|
||||
.where(inArray(executionLargeValues.key, keys))
|
||||
}
|
||||
|
||||
async function pruneStaleReferences(workspaceIds: string[], batchSize: number): Promise<number> {
|
||||
const rows = await db.execute<{ count: number }>(sql`
|
||||
WITH deleted AS (
|
||||
DELETE FROM ${executionLargeValueReferences} AS ref
|
||||
WHERE ref.ctid IN (
|
||||
SELECT ref.ctid
|
||||
FROM ${executionLargeValueReferences} AS ref
|
||||
WHERE ref.workspace_id = ANY(${workspaceIds}::text[])
|
||||
AND (
|
||||
(
|
||||
ref.source = 'execution_log'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${workflowExecutionLogs} AS wel
|
||||
WHERE wel.execution_id = ref.execution_id
|
||||
)
|
||||
)
|
||||
OR (
|
||||
ref.source = 'paused_snapshot'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${pausedExecutions} AS pe
|
||||
WHERE pe.execution_id = ref.execution_id
|
||||
AND pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
|
||||
)
|
||||
)
|
||||
OR ref.source NOT IN ('execution_log', 'paused_snapshot')
|
||||
)
|
||||
LIMIT ${batchSize}
|
||||
)
|
||||
RETURNING ref.key
|
||||
)
|
||||
SELECT count(*)::int AS count FROM deleted
|
||||
`)
|
||||
return getCount(rows)
|
||||
}
|
||||
|
||||
async function pruneDeletedParentDependencies(
|
||||
workspaceIds: string[],
|
||||
batchSize: number
|
||||
): Promise<number> {
|
||||
const rows = await db.execute<{ count: number }>(sql`
|
||||
WITH deleted AS (
|
||||
DELETE FROM ${executionLargeValueDependencies} AS dependency
|
||||
WHERE dependency.ctid IN (
|
||||
SELECT dependency.ctid
|
||||
FROM ${executionLargeValueDependencies} AS dependency
|
||||
WHERE dependency.workspace_id = ANY(${workspaceIds}::text[])
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM ${executionLargeValues} AS parent_value
|
||||
WHERE parent_value.key = dependency.parent_key
|
||||
AND parent_value.deleted_at IS NOT NULL
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${executionLargeValues} AS parent_value
|
||||
WHERE parent_value.key = dependency.parent_key
|
||||
)
|
||||
)
|
||||
LIMIT ${batchSize}
|
||||
)
|
||||
RETURNING dependency.parent_key
|
||||
)
|
||||
SELECT count(*)::int AS count FROM deleted
|
||||
`)
|
||||
return getCount(rows)
|
||||
}
|
||||
|
||||
async function pruneDeletedLargeValueTombstones(
|
||||
workspaceIds: string[],
|
||||
deletedBefore: Date,
|
||||
batchSize: number
|
||||
): Promise<number> {
|
||||
const rows = await db.execute<{ count: number }>(sql`
|
||||
WITH deleted AS (
|
||||
DELETE FROM ${executionLargeValues} AS value
|
||||
WHERE value.ctid IN (
|
||||
SELECT value.ctid
|
||||
FROM ${executionLargeValues} AS value
|
||||
WHERE value.workspace_id = ANY(${workspaceIds}::text[])
|
||||
AND value.deleted_at IS NOT NULL
|
||||
AND value.deleted_at < ${deletedBefore}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${executionLargeValueDependencies} AS dependency
|
||||
WHERE dependency.parent_key = value.key
|
||||
)
|
||||
LIMIT ${batchSize}
|
||||
)
|
||||
RETURNING value.key
|
||||
)
|
||||
SELECT count(*)::int AS count FROM deleted
|
||||
`)
|
||||
return getCount(rows)
|
||||
}
|
||||
|
||||
export async function pruneLargeValueMetadata({
|
||||
workspaceIds,
|
||||
tombstonesDeletedBefore,
|
||||
batchSize = LARGE_VALUE_METADATA_PRUNE_BATCH_SIZE,
|
||||
maxRowsPerTable = LARGE_VALUE_METADATA_PRUNE_MAX_ROWS_PER_TABLE,
|
||||
}: PruneLargeValueMetadataOptions): Promise<LargeValueMetadataPruneResult> {
|
||||
const result: LargeValueMetadataPruneResult = {
|
||||
referencesDeleted: 0,
|
||||
dependenciesDeleted: 0,
|
||||
tombstonesDeleted: 0,
|
||||
}
|
||||
if (workspaceIds.length === 0) return result
|
||||
|
||||
for (const workspaceChunk of chunkArray(
|
||||
workspaceIds,
|
||||
LARGE_VALUE_METADATA_WORKSPACE_CHUNK_SIZE
|
||||
)) {
|
||||
const referencesRemaining = maxRowsPerTable - result.referencesDeleted
|
||||
if (referencesRemaining > 0) {
|
||||
result.referencesDeleted += await pruneStaleReferences(
|
||||
workspaceChunk,
|
||||
Math.min(batchSize, referencesRemaining)
|
||||
)
|
||||
}
|
||||
|
||||
const dependenciesRemaining = maxRowsPerTable - result.dependenciesDeleted
|
||||
if (dependenciesRemaining > 0) {
|
||||
result.dependenciesDeleted += await pruneDeletedParentDependencies(
|
||||
workspaceChunk,
|
||||
Math.min(batchSize, dependenciesRemaining)
|
||||
)
|
||||
}
|
||||
|
||||
const tombstonesRemaining = maxRowsPerTable - result.tombstonesDeleted
|
||||
if (tombstonesRemaining > 0) {
|
||||
result.tombstonesDeleted += await pruneDeletedLargeValueTombstones(
|
||||
workspaceChunk,
|
||||
tombstonesDeletedBefore,
|
||||
Math.min(batchSize, tombstonesRemaining)
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
result.referencesDeleted >= maxRowsPerTable &&
|
||||
result.dependenciesDeleted >= maxRowsPerTable &&
|
||||
result.tombstonesDeleted >= maxRowsPerTable
|
||||
) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function unreferencedLargeValuePredicate() {
|
||||
return sql`
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${executionLargeValueReferences} AS elvr
|
||||
WHERE elvr.key = ${executionLargeValues.key}
|
||||
AND (
|
||||
(
|
||||
elvr.source = 'execution_log'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM ${workflowExecutionLogs} AS wel
|
||||
WHERE wel.execution_id = elvr.execution_id
|
||||
)
|
||||
)
|
||||
OR (
|
||||
elvr.source = 'paused_snapshot'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM ${pausedExecutions} AS pe
|
||||
WHERE pe.execution_id = elvr.execution_id
|
||||
AND pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${workflowExecutionLogs} AS owner_wel
|
||||
WHERE owner_wel.execution_id = ${executionLargeValues.ownerExecutionId}
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${pausedExecutions} AS owner_pe
|
||||
WHERE owner_pe.execution_id = ${executionLargeValues.ownerExecutionId}
|
||||
AND owner_pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${executionLargeValueDependencies} AS dependency
|
||||
INNER JOIN ${executionLargeValues} AS parent_value
|
||||
ON parent_value.key = dependency.parent_key
|
||||
AND parent_value.deleted_at IS NULL
|
||||
WHERE dependency.workspace_id = ${executionLargeValues.workspaceId}
|
||||
AND dependency.child_key = ${executionLargeValues.key}
|
||||
AND (
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM ${workflowExecutionLogs} AS parent_owner_wel
|
||||
WHERE parent_owner_wel.execution_id = parent_value.owner_execution_id
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM ${pausedExecutions} AS parent_owner_pe
|
||||
WHERE parent_owner_pe.execution_id = parent_value.owner_execution_id
|
||||
AND parent_owner_pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM ${executionLargeValueReferences} AS parent_ref
|
||||
WHERE parent_ref.key = parent_value.key
|
||||
AND (
|
||||
(
|
||||
parent_ref.source = 'execution_log'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM ${workflowExecutionLogs} AS parent_ref_wel
|
||||
WHERE parent_ref_wel.execution_id = parent_ref.execution_id
|
||||
)
|
||||
)
|
||||
OR (
|
||||
parent_ref.source = 'paused_snapshot'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM ${pausedExecutions} AS parent_ref_pe
|
||||
WHERE parent_ref_pe.execution_id = parent_ref.execution_id
|
||||
AND parent_ref_pe.status = ANY(${LIVE_PAUSED_REFERENCE_STATUSES}::text[])
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
}
|
||||
Reference in New Issue
Block a user