Skip to content

Commit 480b304

Browse files
authored
fix(executor): retry transient connection failures when binding a tool call's delegation (#8174)
1 parent 87863dd commit 480b304

2 files changed

Lines changed: 89 additions & 21 deletions

File tree

apps/sim/lib/auth/internal-delegation.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const { mockResolveWorkflow, mockResolveRun, mockResolveExecution, mockResolveDe
1111
mockResolveDeploymentVersion: vi.fn(),
1212
}))
1313

14+
vi.mock('@sim/utils/helpers', () => ({ sleep: vi.fn().mockResolvedValue(undefined) }))
15+
1416
vi.mock('@/lib/workflows/application/context', () => ({
1517
resolveActiveWorkflowApplicationContext: mockResolveWorkflow,
1618
resolveActiveWorkflowRunApplicationContext: mockResolveRun,
@@ -323,5 +325,44 @@ describe('bindInternalExecutorDelegation', () => {
323325
await expect(
324326
bindInternalExecutorDelegation(claims, { audience: 'sim:workspace-files' })
325327
).rejects.toBe(infrastructureError)
328+
expect(mockResolveWorkflow).toHaveBeenCalledTimes(1)
329+
})
330+
331+
it('retries a canonical load that failed on a reset database connection', async () => {
332+
const connectionReset = Object.assign(new Error('Failed query'), {
333+
cause: Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }),
334+
})
335+
mockResolveRun.mockRejectedValueOnce(connectionReset)
336+
337+
const principal = await bindInternalExecutorDelegation(
338+
{ ...claims, executionId: 'execution-1' },
339+
{ audience: 'sim:function-execute' }
340+
)
341+
342+
expect(principal.workspaceId).toBe('workspace-1')
343+
expect(mockResolveRun).toHaveBeenCalledTimes(2)
344+
})
345+
346+
it('retries a current-workflow load that failed on a reset database connection', async () => {
347+
const connectionReset = Object.assign(new Error('Failed query'), {
348+
cause: Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }),
349+
})
350+
mockResolveDeploymentVersion.mockRejectedValueOnce(connectionReset)
351+
352+
const principal = await bindInternalExecutorDelegation(
353+
{
354+
...claims,
355+
executionId: 'execution-1',
356+
currentWorkflow: {
357+
workflowId: 'child-workflow',
358+
mode: 'deployment',
359+
deploymentVersionId: 'deployment-version-1',
360+
},
361+
},
362+
{ audience: 'sim:credential-groups' }
363+
)
364+
365+
expect(principal.workspaceId).toBe('workspace-1')
366+
expect(mockResolveDeploymentVersion).toHaveBeenCalledTimes(2)
326367
})
327368
})

apps/sim/lib/auth/internal-delegation.ts

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
} from '@sim/auth/principal'
55
import type { VerifiedInternalDelegation } from '@/lib/auth/internal'
66
import { asOrchestrationError } from '@/lib/core/orchestration/types'
7+
import { withDatabaseReadRetry } from '@/lib/db/read-retry'
78
import {
89
type ActiveWorkflowApplicationContext,
910
resolveActiveWorkflowApplicationContext,
@@ -25,7 +26,15 @@ export class InvalidInternalDelegationBindingError extends Error {
2526
}
2627
}
2728

28-
/** Binds signed executor claims to the workflow's canonical active workspace. */
29+
const CANONICAL_LOAD_RETRY = { label: 'internal delegation canonical load' } as const
30+
31+
/**
32+
* Binds signed executor claims to the workflow's canonical active workspace.
33+
*
34+
* Every tool call a workflow run delegates (Function, MCP, files, knowledge) binds here first, and
35+
* nothing above it retries. The canonical loads are independent reads, so a transient connection
36+
* failure such as a reset pooled socket is retried instead of failing the block.
37+
*/
2938
export async function bindInternalExecutorDelegation(
3039
claims: VerifiedInternalDelegation,
3140
options: BindInternalExecutorDelegationOptions
@@ -43,19 +52,32 @@ export async function bindInternalExecutorDelegation(
4352
try {
4453
if (claims.currentWorkflow) {
4554
if (!claims.executionId) throw new InvalidInternalDelegationBindingError()
46-
const executionContext = await resolveActiveWorkflowExecutionApplicationContext({
47-
runId: claims.executionId,
48-
assertedWorkflowId: claims.workflowId,
49-
})
55+
const runId = claims.executionId
56+
const executionContext = await withDatabaseReadRetry(
57+
() =>
58+
resolveActiveWorkflowExecutionApplicationContext({
59+
runId,
60+
assertedWorkflowId: claims.workflowId,
61+
}),
62+
CANONICAL_LOAD_RETRY
63+
)
5064
context = executionContext
5165
rootDeploymentVersionId = executionContext.deploymentVersionId
5266
} else if (claims.executionId) {
53-
context = await resolveActiveWorkflowRunApplicationContext({
54-
runId: claims.executionId,
55-
assertedWorkflowId: claims.workflowId,
56-
})
67+
const runId = claims.executionId
68+
context = await withDatabaseReadRetry(
69+
() =>
70+
resolveActiveWorkflowRunApplicationContext({
71+
runId,
72+
assertedWorkflowId: claims.workflowId,
73+
}),
74+
CANONICAL_LOAD_RETRY
75+
)
5776
} else {
58-
context = await resolveActiveWorkflowApplicationContext({ workflowId: claims.workflowId })
77+
context = await withDatabaseReadRetry(
78+
() => resolveActiveWorkflowApplicationContext({ workflowId: claims.workflowId }),
79+
CANONICAL_LOAD_RETRY
80+
)
5981
}
6082
} catch (error) {
6183
if (asOrchestrationError(error)?.code === 'not_found') {
@@ -74,18 +96,23 @@ export async function bindInternalExecutorDelegation(
7496
throw new InvalidInternalDelegationBindingError()
7597
}
7698
} else {
99+
const currentWorkflow = claims.currentWorkflow
100+
const workspaceId = context.workspaceId
77101
try {
78-
const currentContext =
79-
claims.currentWorkflow.mode === 'deployment'
80-
? await resolveActiveWorkflowDeploymentVersionApplicationContext({
81-
workflowId: claims.currentWorkflow.workflowId,
82-
deploymentVersionId: claims.currentWorkflow.deploymentVersionId,
83-
assertedWorkspaceId: context.workspaceId,
84-
})
85-
: await resolveActiveWorkflowApplicationContext({
86-
workflowId: claims.currentWorkflow.workflowId,
87-
assertedWorkspaceId: context.workspaceId,
88-
})
102+
const currentContext = await withDatabaseReadRetry(
103+
() =>
104+
currentWorkflow.mode === 'deployment'
105+
? resolveActiveWorkflowDeploymentVersionApplicationContext({
106+
workflowId: currentWorkflow.workflowId,
107+
deploymentVersionId: currentWorkflow.deploymentVersionId,
108+
assertedWorkspaceId: workspaceId,
109+
})
110+
: resolveActiveWorkflowApplicationContext({
111+
workflowId: currentWorkflow.workflowId,
112+
assertedWorkspaceId: workspaceId,
113+
}),
114+
CANONICAL_LOAD_RETRY
115+
)
89116
if (currentContext.workspaceId !== context.workspaceId) {
90117
throw new InvalidInternalDelegationBindingError()
91118
}

0 commit comments

Comments
 (0)