Skip to content

Commit fcd9a8d

Browse files
committed
fix(webapp): move error_fingerprint and machine_preset filters to WHERE
Both reflect execution/outcome and change across a run versions, so they are unsafe in PREWHERE (evaluated before FINAL): error_fingerprint is derived from status per snapshot and is cleared when a run recovers to a non-error status, and machine_preset can escalate to a larger machine on an out-of-memory retry. In either case an earlier version matches the filter while the winning version does not, so PREWHERE could keep the stale version and drop the winner. Only trigger-time identity columns and append-only arrays stay in PREWHERE.
1 parent 6a835fa commit fcd9a8d

1 file changed

Lines changed: 25 additions & 21 deletions

File tree

apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -414,15 +414,19 @@ export class ClickHouseRunsRepository implements IRunsRepository {
414414
/**
415415
* Builds the shared filter clauses for the runs list against `task_runs_v2 FINAL`.
416416
*
417-
* Immutable / additive-only columns go in PREWHERE so ClickHouse filters (and, for `tags`, uses
418-
* the skip index) before FINAL reconciles versions and before materialising the wide columns,
419-
* which is what bounds memory on these scans. A filter stays in WHERE (post-FINAL) when its truth
420-
* value can flip across a run's versions, since PREWHERE could then keep a stale version and drop
421-
* the winner: `status` (lifecycle-mutable), and `regions` (its `if(region != '', region,
422-
* worker_queue)` expression yields the worker_queue before dequeue and the region after, two
423-
* different non-empty values). The `(organization_id, project_id, environment_id)` primary-key
424-
* prefix and the `created_at` range stay in WHERE so they keep driving primary-key and partition
425-
* pruning.
417+
* A filter may go in PREWHERE only if its truth value can never flip true->false across a run's
418+
* versions, because PREWHERE is evaluated before FINAL reconciles versions and would otherwise keep
419+
* a stale matching version and drop the winning one. That holds for trigger-time identity columns
420+
* that never change (task_identifier, task_version, schedule_id, is_test, root_run_id, batch_id,
421+
* friendly_id, queue, task_kind) and for append-only arrays under `hasAny`/`hasAll` (tags,
422+
* bulk_action_group_ids), so those go in PREWHERE to filter (and, for tags, use the skip index)
423+
* before FINAL and before materialising the wide columns, which is what bounds memory on these
424+
* scans. Columns that reflect execution/outcome and change as a run runs stay in WHERE (post-FINAL):
425+
* `status`, `machine_preset` (can escalate on OOM retry), `error_fingerprint` (set/cleared with
426+
* status), and the `regions` expression (`if(region != '', region, worker_queue)` yields the
427+
* worker_queue before dequeue and the region after). The `(organization_id, project_id,
428+
* environment_id)` primary-key prefix and the `created_at` range also stay in WHERE so they keep
429+
* driving primary-key and partition pruning.
426430
*/
427431
function applyRunFiltersToQueryBuilder<T>(
428432
queryBuilder: ClickhouseQueryBuilder<T>,
@@ -449,6 +453,18 @@ function applyRunFiltersToQueryBuilder<T>(
449453
});
450454
}
451455

456+
if (options.machines && options.machines.length > 0) {
457+
queryBuilder.where("machine_preset IN {machines: Array(String)}", {
458+
machines: options.machines,
459+
});
460+
}
461+
462+
if (options.errorId) {
463+
queryBuilder.where("error_fingerprint = {errorFingerprint: String}", {
464+
errorFingerprint: ErrorId.toId(options.errorId),
465+
});
466+
}
467+
452468
// Period is a number of milliseconds duration
453469
if (options.period) {
454470
queryBuilder.where("created_at >= fromUnixTimestamp64Milli({period: Int64})", {
@@ -517,18 +533,6 @@ function applyRunFiltersToQueryBuilder<T>(
517533
queryBuilder.prewhere("queue IN {queues: Array(String)}", { queues: options.queues });
518534
}
519535

520-
if (options.machines && options.machines.length > 0) {
521-
queryBuilder.prewhere("machine_preset IN {machines: Array(String)}", {
522-
machines: options.machines,
523-
});
524-
}
525-
526-
if (options.errorId) {
527-
queryBuilder.prewhere("error_fingerprint = {errorFingerprint: String}", {
528-
errorFingerprint: ErrorId.toId(options.errorId),
529-
});
530-
}
531-
532536
if (options.taskKinds && options.taskKinds.length > 0) {
533537
const includesStandard = options.taskKinds.includes("STANDARD");
534538
// Include empty string when filtering for STANDARD (default value for pre-existing runs)

0 commit comments

Comments
 (0)