Skip to content

Commit 6649d8a

Browse files
authored
fix(api): match pause points with jsonb_path_exists so a status-filtered run list stays fast (#9971)
1 parent 85b731a commit 6649d8a

3 files changed

Lines changed: 28 additions & 26 deletions

File tree

‎apps/sim/lib/workflows/executor/execution-queries.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
22
import { pausedExecutions, workflowExecutionLogs } from '@sim/db/schema'
33
import { and, asc, desc, eq, gt, gte, lt, lte, or, sql } from 'drizzle-orm'
4-
import { PAUSE_POINT_ROWS } from '@/lib/workflows/executor/pause-point-sql'
4+
import { somePausePointMatches } from '@/lib/workflows/executor/pause-point-sql'
55

66
export type WorkflowExecutionStatus =
77
| 'pending'
@@ -27,6 +27,10 @@ export interface ListWorkflowExecutionsInput {
2727
order: 'asc' | 'desc'
2828
}
2929

30+
/** A point a resume can still act on: any `resumeStatus` but `resumed` or `failed`, absent included. */
31+
const POINT_UNSETTLED =
32+
'!(@.resumeStatus.type() == "string" && (@.resumeStatus == "resumed" || @.resumeStatus == "failed"))'
33+
3034
/**
3135
* An open pause record reads `paused` unless the log has finished and no point is left to resume,
3236
* the same rule `readWorkflowExecutionStatus` applies to a single run.
@@ -35,10 +39,7 @@ const executionStatus = sql<string>`CASE
3539
WHEN ${pausedExecutions.status} IN ('paused', 'partially_resumed')
3640
AND NOT (
3741
${workflowExecutionLogs.status} IN ('completed', 'failed', 'cancelled')
38-
AND NOT EXISTS (
39-
select 1 from ${PAUSE_POINT_ROWS}
40-
where coalesce(point.value ->> 'resumeStatus', 'paused') NOT IN ('resumed', 'failed')
41-
)
42+
AND NOT ${somePausePointMatches(POINT_UNSETTLED)}
4243
)
4344
THEN 'paused'
4445
ELSE ${workflowExecutionLogs.status}
Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
import { pausedExecutions } from '@sim/db/schema'
2-
import { sql } from 'drizzle-orm'
2+
import { type SQL, sql } from 'drizzle-orm'
33

44
/**
5-
* Every pause point of a `paused_executions` row, as the derived table `point(value)`.
5+
* Whether any pause point of a `paused_executions` row matches the jsonpath `filter`, which
6+
* reads the point as `@`.
67
*
78
* `pause_points` is an object keyed by context id, or an array on legacy rows; both shapes are
8-
* read, as `normalizePausePoints` reads them, and each set function only ever receives the shape
9-
* it accepts.
9+
* read, as `normalizePausePoints` reads them. A `jsonb_path_exists` call per row keeps this cheap
10+
* enough to evaluate in a `WHERE` across every run of a workflow, which a correlated
11+
* set-returning subquery is not.
1012
*/
11-
export const PAUSE_POINT_ROWS = sql`(
12-
select value from jsonb_each(
13-
case when jsonb_typeof(${pausedExecutions.pausePoints}) = 'object'
14-
then ${pausedExecutions.pausePoints} else '{}'::jsonb end
15-
)
16-
union all
17-
select value from jsonb_array_elements(
18-
case when jsonb_typeof(${pausedExecutions.pausePoints}) = 'array'
19-
then ${pausedExecutions.pausePoints} else '[]'::jsonb end
20-
)
21-
) as point(value)`
13+
export function somePausePointMatches(filter: string): SQL {
14+
return sql`(case jsonb_typeof(${pausedExecutions.pausePoints})
15+
when 'object' then jsonb_path_exists(${pausedExecutions.pausePoints}, ${`$.* ? (${filter})`}::jsonpath)
16+
when 'array' then jsonb_path_exists(${pausedExecutions.pausePoints}, ${`$[*] ? (${filter})`}::jsonpath)
17+
else false
18+
end)`
19+
}

‎apps/sim/lib/workflows/executor/paused-runs.ts‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { db } from '@sim/db'
22
import { pausedExecutions, workflow } from '@sim/db/schema'
3-
import { and, eq, inArray, isNull, sql } from 'drizzle-orm'
3+
import { and, eq, inArray, isNull } from 'drizzle-orm'
44
import {
55
type CursorKey,
66
encodeKeyset,
@@ -14,7 +14,7 @@ import {
1414
resumeKeyset,
1515
textKey,
1616
} from '@/lib/api/list-query'
17-
import { PAUSE_POINT_ROWS } from '@/lib/workflows/executor/pause-point-sql'
17+
import { somePausePointMatches } from '@/lib/workflows/executor/pause-point-sql'
1818

1919
/** A paused run of an active workflow, with the raw `pause_points` its detail is built from. */
2020
interface WorkspacePausedRunRow {
@@ -32,12 +32,15 @@ const PAUSED_RUN_KEYS: readonly KeysetKey<WorkspacePausedRunRow>[] = [
3232
textKey(pausedExecutions.id, (row) => row.id),
3333
]
3434

35-
/** Whether `point.value` is a pause point still waiting on a human, with `mapPausePoints`' defaults. */
36-
const POINT_WAITS_ON_HUMAN = sql`coalesce(point.value ->> 'pauseKind', 'human') <> 'time'
37-
and coalesce(point.value ->> 'resumeStatus', 'paused') = 'paused'`
35+
/**
36+
* A pause point still waiting on a human, with `mapPausePoints`' defaults: a missing or null
37+
* `pauseKind` reads `human` and a missing or null `resumeStatus` reads `paused`.
38+
*/
39+
const POINT_WAITS_ON_HUMAN =
40+
'!(@.pauseKind.type() == "string" && @.pauseKind == "time") && (!exists(@.resumeStatus) || @.resumeStatus == null || (@.resumeStatus.type() == "string" && @.resumeStatus == "paused"))'
3841

3942
/** A paused run with a pause point still waiting on a human. */
40-
const WAITS_ON_HUMAN = sql`exists (select 1 from ${PAUSE_POINT_ROWS} where ${POINT_WAITS_ON_HUMAN})`
43+
const WAITS_ON_HUMAN = somePausePointMatches(POINT_WAITS_ON_HUMAN)
4144

4245
/** The cursor that resumes the list after `row`. */
4346
export function pausedRunCursorKeys(row: WorkspacePausedRunRow): CursorKey[] {

0 commit comments

Comments
 (0)