Skip to content

perf(server): stop loading every job definition to describe schedules - #19

Merged
semics-tech merged 3 commits into
mainfrom
perf/schedule-grouping-payload
Jul 30, 2026
Merged

perf(server): stop loading every job definition to describe schedules#19
semics-tech merged 3 commits into
mainfrom
perf/schedule-grouping-payload

Conversation

@semics-tech

Copy link
Copy Markdown
Owner

What's wrong

Grouping jobs by schedule needs each job's schedules array. It was getting there by selecting the whole definition:

.select({ instanceId, jobUuid, definition: jobVersions.definition })

A JobDefinition.v1 contains every step's full T-SQL body. On the reference estate — 50 instances, ~50 jobs each — that is tens of megabytes pulled across the wire and parsed in Node on every request to /api/jobs/groups?by=schedule, to read one small array from each row.

Fix

Extract it in Postgres:

schedules: sql<unknown[] | null>`${jobVersions.definition} -> 'schedules'`

Same result, a fraction of the payload, and step bodies — which routinely contain connection strings — never leave the database for this query at all.

Also: the silent truncation

groupJobs capped at 5,000 rows and said nothing. The failure mode is quiet and bad: a group renders as "all healthy" because the failing instances fell off the end of the limit.

  • Cap raised to 20,000 (the reference estate is ~2,500).
  • The API now returns truncated, and the Jobs page shows a notice saying the grouping is incomplete and to narrow the filter.

A bounded result is fine. A bounded result presented as complete is not.

Verification

80 server tests pass; lint, typecheck and dashboard build clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj

Grouping by schedule read `job_versions.definition` for every current job in the
estate, then used one small array from each. That definition contains every
step's full T-SQL body: on a 50-instance estate with 50 jobs apiece, tens of
megabytes crossed the wire and were parsed in Node on every request, to read
`schedules`.

Extracts `definition -> 'schedules'` in Postgres instead. Same result, a
fraction of the payload, and the step bodies never leave the database.

Also surfaces the row cap. It was silently truncating at 5,000 jobs, which
presented a partial estate as though it were the whole one — the failure mode
being a group that looks healthy because the failing instances fell off the end.
The cap is now 20,000, and the API reports `truncated` so the dashboard can say
the grouping is incomplete and to narrow the filter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj
@semics-tech semics-tech changed the title Stop loading every job definition to describe schedules perf(server): stop loading every job definition to describe schedules Jul 30, 2026
@semics-tech
semics-tech merged commit 94c0246 into main Jul 30, 2026
9 checks passed
@semics-tech
semics-tech deleted the perf/schedule-grouping-payload branch July 30, 2026 11:25
semics-tech added a commit that referenced this pull request Jul 30, 2026
#19 changed groupJobs from returning an array to returning
{ groups, truncated } so the endpoint can say when it capped the result.
This branch's test still asserted length on the return value.

Git merged the two cleanly and TypeScript accepted it, because the
assertion was toHaveLength on an untyped await — so the only thing that
caught it was running the test. The one other caller, api/app.ts, already
destructures the new shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj
semics-tech added a commit that referenced this pull request Jul 30, 2026
## What's wrong

Removing an instance configuration deletes the config and stops the
worker monitoring it — the worker correctly logs `Instance removed in
the dashboard; disconnecting`.

But the mirrored `instances` row was never touched. It kept appearing
in:

- the estate grid and the sidebar tree
- overview totals (`instances`, `jobs`, worker `instanceCount`)
- cross-estate job grouping

frozen at whatever it last reported, forever. Nothing in the UI could
remove it.

Found while cleaning up after testing the onboarding flow — the test
instance had to be deleted from Postgres by hand.

## Why not just delete the row

`instances` cascades to `jobs`, `job_versions`, `job_history`,
`job_activity` and `agent_log_entries`. Deleting would destroy the run
history and version timeline, which are most of the reason to run this
product. That is far too much data loss to sit behind a one-click
**Remove** with no warning.

## Fix

New `instances.detached_at`. On config removal the instance is marked
rather than deleted:

- excluded from `getEstateOverview`, `getRunningJobs`,
`getRecentFailures`, `getWorkerHealth`, the overview totals and
`groupJobs`
- history stays queryable
- configuring the instance again clears the mark, so it comes back
**with its history** instead of starting over

Migration `0003_fair_whizzer.sql`, nullable column, no backfill needed.

## Tests

Six cases in a new file. The important one is that *all four*
estate-wide views agree — a detached instance leaking into one view but
not another is worse than it showing in all of them, because the numbers
stop reconciling. Also covers re-attachment preserving history, and not
disturbing sibling instances on the same worker.

86 server tests pass.

## Note on merge order

Trivially conflicts with #19, which changes `groupJobs` to return `{
groups, truncated }`. The assertion in `instance-detach.test.ts` uses
this branch's array signature; whichever merges second needs `.groups`
added or removed on two lines.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01AyYg2j8FVkLjiaVcj5HCkj

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant