Skip to content

Commit 32f4477

Browse files
committed
fix(settings): release superseded reconnect ownership
1 parent 7a44821 commit 32f4477

2 files changed

Lines changed: 96 additions & 9 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.test.tsx‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const {
1111
mockCancel,
1212
mockAdoptScopedExecution,
1313
mockBeginScopedExecution,
14+
mockClearExecutionPointer,
1415
mockEndScopedExecution,
1516
mockExecute,
1617
mockExecuteFromBlock,
@@ -91,6 +92,7 @@ const {
9192
mockCancel: vi.fn(),
9293
mockAdoptScopedExecution: vi.fn(),
9394
mockBeginScopedExecution: vi.fn(() => ({})),
95+
mockClearExecutionPointer: vi.fn(),
9496
mockEndScopedExecution: vi.fn(() => true),
9597
mockExecute: vi.fn(),
9698
mockExecuteFromBlock: vi.fn(),
@@ -245,7 +247,7 @@ vi.mock('@/stores/execution', () => ({
245247
}))
246248

247249
vi.mock('@/stores/terminal', () => ({
248-
clearExecutionPointer: vi.fn(),
250+
clearExecutionPointer: mockClearExecutionPointer,
249251
consolePersistence: {
250252
adoptScopedExecution: mockAdoptScopedExecution,
251253
beginScopedExecution: mockBeginScopedExecution,
@@ -629,6 +631,54 @@ describe('useWorkflowExecution attachment uploads', () => {
629631
unmount()
630632
})
631633

634+
it('releases only its persistence ownership when a reconnect retry is superseded', async () => {
635+
const persistenceExecution = {}
636+
terminalStoreState._hasHydrated = true
637+
executionStoreState.getWorkflowExecution.mockReturnValue({
638+
...executionStoreState.getWorkflowExecution(),
639+
status: 'running',
640+
isExecuting: true,
641+
currentExecutionId: 'execution-1',
642+
})
643+
executionStoreState.getCurrentExecutionId.mockReturnValue('execution-1')
644+
mockLoadExecutionPointer.mockResolvedValue({
645+
workflowId: 'workflow-1',
646+
executionId: 'execution-1',
647+
lastEventId: 0,
648+
})
649+
mockAdoptScopedExecution.mockReturnValue(persistenceExecution)
650+
mockReconnect.mockImplementationOnce(async ({ callbacks }) => {
651+
callbacks.onBlockStarted({
652+
blockId: 'start',
653+
blockName: 'Start',
654+
blockType: 'starter',
655+
executionOrder: 1,
656+
})
657+
executionStoreState.getWorkflowExecution.mockReturnValue({
658+
...executionStoreState.getWorkflowExecution(),
659+
status: 'running',
660+
isExecuting: true,
661+
currentExecutionId: 'execution-2',
662+
})
663+
executionStoreState.getCurrentExecutionId.mockReturnValue('execution-2')
664+
throw new Error('Reconnect failed after replacement started')
665+
})
666+
667+
const { unmount } = renderWorkflowExecutionHook()
668+
await act(async () => {
669+
await Promise.resolve()
670+
await Promise.resolve()
671+
})
672+
673+
expect(mockEndScopedExecution).toHaveBeenCalledWith('workflow-1', persistenceExecution)
674+
expect(executionStoreState.setCurrentExecutionId).not.toHaveBeenCalledWith('workflow-1', null)
675+
expect(executionStoreState.setIsExecuting).not.toHaveBeenCalledWith('workflow-1', false)
676+
expect(executionStoreState.setActiveBlocks).not.toHaveBeenCalled()
677+
expect(mockClearExecutionPointer).not.toHaveBeenCalled()
678+
679+
unmount()
680+
})
681+
632682
it('uses only projected live thinking without changing normal settle behavior', async () => {
633683
mockExecute.mockImplementationOnce(async (options) => {
634684
options.onExecutionId?.('execution-1')

‎apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts‎

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2425,6 +2425,13 @@ export function useWorkflowExecution() {
24252425
let activated = false
24262426
let activationOwnsPersistence = false
24272427
let reconnectPersistenceExecution: ConsolePersistenceExecution | undefined
2428+
const releaseReconnectPersistenceOwnership = () => {
2429+
const persistenceExecution = reconnectPersistenceExecution
2430+
reconnectPersistenceExecution = undefined
2431+
activationOwnsPersistence = false
2432+
if (!persistenceExecution) return
2433+
consolePersistence.endScopedExecution(reconnectWorkflowId, persistenceExecution)
2434+
}
24282435
const isReconnectStillCurrent = canReconnectClaimWorkflow
24292436
const finishReconnectExecution = () => {
24302437
if (reconnectPersistenceExecution) {
@@ -2440,12 +2447,16 @@ export function useWorkflowExecution() {
24402447
if (ownedReconnectExecutionId) {
24412448
executionStream.cancelReconnect(reconnectWorkflowId, ownedReconnectExecutionId)
24422449
}
2450+
releaseReconnectPersistenceOwnership()
24432451
releaseReconnectOwnership()
24442452
}
24452453
const releaseActivatedReconnectState = () => {
24462454
if (!activated) return
24472455
const currentId = useExecutionStore.getState().getCurrentExecutionId(reconnectWorkflowId)
2448-
if (currentId !== capturedExecutionId) return
2456+
if (currentId !== capturedExecutionId) {
2457+
releaseReconnectPersistenceOwnership()
2458+
return
2459+
}
24492460
setCurrentExecutionId(reconnectWorkflowId, null)
24502461
if (activationOwnsPersistence) finishReconnectExecution()
24512462
else rawSetIsExecuting(reconnectWorkflowId, false)
@@ -2456,7 +2467,10 @@ export function useWorkflowExecution() {
24562467
.getState()
24572468
.getWorkflowExecution(reconnectWorkflowId)
24582469
const currentId = executionState?.currentExecutionId ?? null
2459-
if (currentId && currentId !== capturedExecutionId) return
2470+
if (currentId && currentId !== capturedExecutionId) {
2471+
releaseReconnectPersistenceOwnership()
2472+
return
2473+
}
24602474
handleExecutionErrorConsole({
24612475
workflowId: reconnectWorkflowId,
24622476
executionId: capturedExecutionId,
@@ -2470,9 +2484,12 @@ export function useWorkflowExecution() {
24702484
const scheduleRetryableReconnect = () => {
24712485
releaseReconnectOwnership()
24722486
retryTimeoutId = setTimeout(() => {
2473-
if (!cleanupRan && !reconnectionComplete) {
2474-
setReconnectAttemptNonce((nonce) => nonce + 1)
2487+
if (cleanupRan || reconnectionComplete) return
2488+
if (!isReconnectStillCurrent()) {
2489+
stopStaleReconnect()
2490+
return
24752491
}
2492+
setReconnectAttemptNonce((nonce) => nonce + 1)
24762493
}, MAX_DELAY_MS)
24772494
}
24782495
const ensureActivated = () => {
@@ -2509,6 +2526,10 @@ export function useWorkflowExecution() {
25092526
const delay = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS)
25102527
await sleep(delay)
25112528
if (cleanupRan || reconnectionComplete) return
2529+
if (!isReconnectStillCurrent()) {
2530+
stopStaleReconnect()
2531+
return
2532+
}
25122533
}
25132534

25142535
try {
@@ -2543,7 +2564,10 @@ export function useWorkflowExecution() {
25432564
const currentId = useExecutionStore
25442565
.getState()
25452566
.getCurrentExecutionId(reconnectWorkflowId)
2546-
if (currentId !== capturedExecutionId) return
2567+
if (currentId !== capturedExecutionId) {
2568+
releaseReconnectPersistenceOwnership()
2569+
return
2570+
}
25472571
reconcileFinalBlockLogs(
25482572
updateConsole,
25492573
reconnectWorkflowId,
@@ -2562,7 +2586,10 @@ export function useWorkflowExecution() {
25622586
const currentId = useExecutionStore
25632587
.getState()
25642588
.getCurrentExecutionId(reconnectWorkflowId)
2565-
if (currentId !== capturedExecutionId) return
2589+
if (currentId !== capturedExecutionId) {
2590+
releaseReconnectPersistenceOwnership()
2591+
return
2592+
}
25662593
reconcileFinalBlockLogs(
25672594
updateConsole,
25682595
reconnectWorkflowId,
@@ -2591,7 +2618,10 @@ export function useWorkflowExecution() {
25912618
const currentId = useExecutionStore
25922619
.getState()
25932620
.getCurrentExecutionId(reconnectWorkflowId)
2594-
if (currentId !== capturedExecutionId) return
2621+
if (currentId !== capturedExecutionId) {
2622+
releaseReconnectPersistenceOwnership()
2623+
return
2624+
}
25952625
handleExecutionErrorConsole({
25962626
workflowId: reconnectWorkflowId,
25972627
executionId: capturedExecutionId,
@@ -2611,7 +2641,10 @@ export function useWorkflowExecution() {
26112641
const currentId = useExecutionStore
26122642
.getState()
26132643
.getCurrentExecutionId(reconnectWorkflowId)
2614-
if (currentId !== capturedExecutionId) return
2644+
if (currentId !== capturedExecutionId) {
2645+
releaseReconnectPersistenceOwnership()
2646+
return
2647+
}
26152648
handleExecutionCancelledConsole({
26162649
workflowId: reconnectWorkflowId,
26172650
executionId: capturedExecutionId,
@@ -2625,6 +2658,10 @@ export function useWorkflowExecution() {
26252658
},
26262659
})
26272660
} catch (error) {
2661+
if (!isReconnectStillCurrent()) {
2662+
stopStaleReconnect()
2663+
return
2664+
}
26282665
if (isReconnectNonRetryable(error)) {
26292666
logger.info('Reconnection skipped; run buffer no longer exists', {
26302667
executionId: capturedExecutionId,

0 commit comments

Comments
 (0)