Skip to content

Commit d3d8f9c

Browse files
authored
fix(logs,workspace): prevent cancelled status overwrite on race and move impersonation banner (#4617)
- Guard completeWithError against overwriting a cancelled execution status — cancel route writes cancelled to DB optimistically, but a block error racing the 500ms Redis check could finalize with failed before the engine detects cancellation - Add tests covering the guard: cancelled DB status skips the write, non-cancelled proceeds normally, DB failure falls through to cost-only fallback, and subsequent attempts are deduped after guard marks session complete - Move ImpersonationBanner from workspace root into components/ folder
1 parent c9118e7 commit d3d8f9c

4 files changed

Lines changed: 87 additions & 1 deletion

File tree

apps/sim/app/workspace/[workspaceId]/impersonation-banner.tsx renamed to apps/sim/app/workspace/[workspaceId]/components/impersonation-banner.tsx

File renamed without changes.

apps/sim/app/workspace/[workspaceId]/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { redirect } from 'next/navigation'
22
import { ToastProvider } from '@/components/emcn'
33
import { getSession } from '@/lib/auth'
4+
import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/components/impersonation-banner'
45
import { NavTour } from '@/app/workspace/[workspaceId]/components/product-tour'
5-
import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/impersonation-banner'
66
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
77
import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'
88
import { SettingsLoader } from '@/app/workspace/[workspaceId]/providers/settings-loader'

apps/sim/lib/logs/execution/logging-session.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,75 @@ describe('LoggingSession completion retries', () => {
453453
})
454454
})
455455

456+
describe('completeWithError cancelled-status guard', () => {
457+
beforeEach(() => {
458+
vi.clearAllMocks()
459+
dbMocks.updateWhere.mockResolvedValue(undefined)
460+
dbMocks.execute.mockResolvedValue(undefined)
461+
})
462+
463+
it('skips writing failed and marks session complete when DB status is already cancelled', async () => {
464+
dbMocks.selectLimit.mockResolvedValue([{ status: 'cancelled' }])
465+
const session = new LoggingSession('workflow-1', 'execution-1', 'api', 'req-1')
466+
467+
await session.safeCompleteWithError({ error: { message: 'block errored mid-cancel' } })
468+
469+
expect(completeWorkflowExecutionMock).not.toHaveBeenCalled()
470+
expect(session.hasCompleted()).toBe(true)
471+
})
472+
473+
it('writes failed when DB status is running (no cancel in flight)', async () => {
474+
dbMocks.selectLimit.mockResolvedValue([{ status: 'running' }])
475+
completeWorkflowExecutionMock.mockResolvedValue({})
476+
const session = new LoggingSession('workflow-1', 'execution-1', 'api', 'req-1')
477+
478+
await session.safeCompleteWithError({ error: { message: 'genuine block failure' } })
479+
480+
expect(completeWorkflowExecutionMock).toHaveBeenCalledWith(
481+
expect.objectContaining({ status: 'failed' })
482+
)
483+
expect(session.hasCompleted()).toBe(true)
484+
})
485+
486+
it('writes failed when no execution log exists yet', async () => {
487+
dbMocks.selectLimit.mockResolvedValue([])
488+
completeWorkflowExecutionMock.mockResolvedValue({})
489+
const session = new LoggingSession('workflow-1', 'execution-1', 'api', 'req-1')
490+
491+
await session.safeCompleteWithError({ error: { message: 'pre-log error' } })
492+
493+
expect(completeWorkflowExecutionMock).toHaveBeenCalledWith(
494+
expect.objectContaining({ status: 'failed' })
495+
)
496+
})
497+
498+
it('deduplicates all subsequent completion attempts after guard early-return', async () => {
499+
dbMocks.selectLimit.mockResolvedValue([{ status: 'cancelled' }])
500+
completeWorkflowExecutionMock.mockResolvedValue({})
501+
const session = new LoggingSession('workflow-1', 'execution-1', 'api', 'req-1')
502+
503+
await session.safeCompleteWithError({ error: { message: 'error 1' } })
504+
await session.safeCompleteWithError({ error: { message: 'error 2' } })
505+
await session.safeComplete({ finalOutput: { ok: true } })
506+
507+
expect(completeWorkflowExecutionMock).not.toHaveBeenCalled()
508+
expect(session.hasCompleted()).toBe(true)
509+
})
510+
511+
it('falls through to cost-only fallback when the DB check itself throws', async () => {
512+
dbMocks.selectLimit.mockRejectedValueOnce(new Error('DB connection lost'))
513+
completeWorkflowExecutionMock.mockResolvedValue({})
514+
const session = new LoggingSession('workflow-1', 'execution-1', 'api', 'req-1')
515+
516+
await session.safeCompleteWithError({ error: { message: 'block failed' } })
517+
518+
expect(completeWorkflowExecutionMock).toHaveBeenCalledWith(
519+
expect.objectContaining({ finalizationPath: 'force_failed' })
520+
)
521+
expect(session.hasCompleted()).toBe(true)
522+
})
523+
})
524+
456525
describe('LoggingSession.markExecutionAsFailed workflowId scoping', () => {
457526
beforeEach(() => {
458527
vi.clearAllMocks()

apps/sim/lib/logs/execution/logging-session.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,23 @@ export class LoggingSession {
544544
this.completing = true
545545

546546
try {
547+
const currentLog = await db
548+
.select({ status: workflowExecutionLogs.status })
549+
.from(workflowExecutionLogs)
550+
.where(
551+
and(
552+
eq(workflowExecutionLogs.workflowId, this.workflowId),
553+
eq(workflowExecutionLogs.executionId, this.executionId)
554+
)
555+
)
556+
.limit(1)
557+
.then((rows) => rows[0])
558+
559+
if (currentLog?.status === 'cancelled') {
560+
this.completed = true
561+
return
562+
}
563+
547564
const { endedAt, totalDurationMs, error, traceSpans, skipCost } = params
548565

549566
const endTime = endedAt ? new Date(endedAt) : new Date()

0 commit comments

Comments
 (0)