Skip to content

Commit 634b19e

Browse files
committed
fix(workflows): return not found when import loses archive race
1 parent 0b7eed6 commit 634b19e

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

‎apps/sim/app/api/superuser/import-workflow/route.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server'
77
import { importWorkflowAsSuperuserContract } from '@/lib/api/contracts/workflows'
88
import { parseRequest } from '@/lib/api/server'
99
import { getSession } from '@/lib/auth'
10+
import { OrchestrationError } from '@/lib/core/orchestration/types'
1011
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1112
import { loadCopilotChatMessages } from '@/lib/mothership/chat/lifecycle'
1213
import { appendCopilotChatMessages } from '@/lib/mothership/chat/messages-store'
@@ -227,6 +228,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
227228
copilotChatsImported,
228229
})
229230
} catch (error) {
231+
if (error instanceof OrchestrationError && error.code === 'not_found') {
232+
return NextResponse.json({ error: 'Target workspace not found' }, { status: 404 })
233+
}
230234
logger.error('Error importing workflow', error)
231235
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
232236
}

‎apps/sim/app/api/v1/admin/workflows/import/route.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { and, eq, isNull } from 'drizzle-orm'
2828
import { NextResponse } from 'next/server'
2929
import { adminV1ImportWorkflowContract } from '@/lib/api/contracts/v1/admin'
3030
import { parseRequest } from '@/lib/api/server'
31+
import { OrchestrationError } from '@/lib/core/orchestration/types'
3132
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
3233
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
3334
import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
@@ -182,6 +183,9 @@ export const POST = withRouteHandler(
182183

183184
return NextResponse.json(response)
184185
} catch (error) {
186+
if (error instanceof OrchestrationError && error.code === 'not_found') {
187+
return notFoundResponse('Workspace')
188+
}
185189
if (error instanceof FolderNotFoundError) {
186190
return badRequestResponse(error.message)
187191
}

‎apps/sim/lib/projects/__integration__/foundation.integration.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors'
2727
import { sleep } from '@sim/utils/helpers'
2828
import { generateId } from '@sim/utils/id'
2929
import { and, eq, inArray, sql } from 'drizzle-orm'
30+
import { NextRequest } from 'next/server'
3031
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
3132
import { removeUserFromOrganization } from '@/lib/billing/organizations/membership'
3233
import { prepareProjectsForAccountDeletion } from '@/lib/projects/account-deletion'
@@ -55,9 +56,14 @@ import { archiveWorkspace } from '@/lib/workspaces/lifecycle'
5556
import { detachOrganizationWorkspacesTx } from '@/lib/workspaces/organization-workspaces'
5657
import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils'
5758
import { getWorkspaceCreationPolicy } from '@/lib/workspaces/policy'
59+
import { POST as importAdminWorkflow } from '@/app/api/v1/admin/workflows/import/route'
5860
import { createFork } from '@/ee/workspace-forking/lib/create-fork'
5961
import { unlinkForkEdge } from '@/ee/workspace-forking/lib/lineage/unlink'
6062

63+
vi.hoisted(() => {
64+
process.env.ADMIN_API_KEY = 'project-fixture-admin-key'
65+
})
66+
6167
beforeEach(() => {
6268
vi.stubEnv('PROJECT_API_ENABLED', 'true')
6369
})
@@ -842,6 +848,55 @@ describe('Project foundation at the database and application boundary', () => {
842848
}
843849
)
844850

851+
check(
852+
'admin import returns not found when a concurrent archive wins the workspace lock',
853+
async () => {
854+
const f = await fixture(false, 1)
855+
const held = createDeferred<number>()
856+
const release = createDeferred<void>()
857+
const archive = db.transaction(async (tx) => {
858+
const [connection] = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`)
859+
await archiveProjectInTransaction(tx, f.projectId)
860+
held.resolve(connection.pid)
861+
await release.promise
862+
})
863+
const blocker = await held.promise
864+
const imported = importAdminWorkflow(
865+
new NextRequest('http://localhost:3000/api/v1/admin/workflows/import', {
866+
method: 'POST',
867+
headers: {
868+
'content-type': 'application/json',
869+
'x-admin-key': 'project-fixture-admin-key',
870+
},
871+
body: JSON.stringify({
872+
workspaceId: f.ids[0],
873+
workflow: { blocks: {}, edges: [], loops: {}, parallels: {} },
874+
}),
875+
}),
876+
{}
877+
)
878+
try {
879+
let waiting = false
880+
for (let attempt = 0; attempt < 100; attempt++) {
881+
const rows = await db.execute(
882+
sql`SELECT 1 FROM pg_stat_activity WHERE ${blocker} = ANY(pg_blocking_pids(pid))`
883+
)
884+
if (rows.length) {
885+
waiting = true
886+
break
887+
}
888+
await sleep(10)
889+
}
890+
expect(waiting).toBe(true)
891+
} finally {
892+
release.resolve()
893+
await archive
894+
}
895+
expect((await imported).status).toBe(404)
896+
expect(await db.select().from(workflow).where(eq(workflow.workspaceId, f.ids[0]))).toEqual([])
897+
}
898+
)
899+
845900
check('workflow restore cannot overtake a concurrent Project archive', async () => {
846901
const f = await fixture()
847902
const workflowId = await addWorkflow(f.ids[0], f.ownerId)

0 commit comments

Comments
 (0)