Commit Graph

2 Commits

Author SHA1 Message Date
Eric Allam aca234d1c3 perf(webapp): bound checkSchedule environment load to the requested ids (#4598)
## What

`CheckScheduleService.call` loaded **every** environment of a project
(`{ id, type, archivedAt }`, no filter) and then immediately narrowed to
just the requested `environmentIds` via
`resolveProjectScopedEnvironments`. It only ever uses the requested envs
(to reject foreign env ids and reject archived branches). On a
preview-heavy project that meant loading hundreds of archived branch
rows to validate one, on a path called in a per-scheduled-task loop on
the deploy path (`createBackgroundWorker` -> `syncDeclarativeSchedules`)
and from `upsertTaskSchedule`.

The query is index-backed and individually fast (rows_read/returned = 1
per predicate), so this is about result-set width / egress and wasted
work at scale (~580k calls/24h observed via Insights), not a slow plan.

## Change

Bound the `environments` relation load to `boundedIn(environmentIds)`:

```ts
environments: {
  where: { id: { in: boundedIn(environmentIds) } },
  select: { id: true, type: true, archivedAt: true },
}
```

Returns `<=` the number of requested envs (usually 1) instead of the
whole project. Both existing behaviors are preserved:

- **Foreign-id rejection**: the relation is still scoped to the project,
so a requested id belonging to another project never comes back and
`resolveProjectScopedEnvironments` reports it as `foreign` (a missing
requested id is already treated as foreign).
- **Archived-branch rejection**: a requested id that is an archived
branch still comes back with `archivedAt` set, so the downstream `Can't
add or edit a schedule for an archived branch` check still fires.

`archivedAt` is kept in the select deliberately, so this bounds by id
rather than filtering archived rows out.

## Evidence (isolated stack, seeded 1 prod env + 40 archived branch
envs)

Local `EXPLAIN (ANALYZE)` of the exact environments sub-select:

| | rows returned | buffers |
|---|---|---|
| before (unbounded) | **41** | shared hit=12 |
| after (`id IN (requested)`) | **1** (`Rows Removed by Filter: 40`) |
shared hit=4 |

Same `RuntimeEnvironment_projectId_idx`, no plan change. Rows to the
client drop to `len(environmentIds)`, which is the point.

**Unit (vitest, testcontainers, real Postgres):**
`apps/webapp/test/checkSchedule.test.ts` extended to prove, on real
rows, that the bounded load returns only the requested env (1 of 10),
still reports a foreign id as foreign, and still surfaces an archived
branch when it is the requested one. 5/5 pass.

**Full e2e (both execution modes, real stack):** a purpose-built project
with two declarative `schedules.task`s.
- `trigger dev`: dev worker created, both schedules synced through the
edited `checkSchedule` loop, no errors.
- `trigger deploy` (managed deployment): PRODUCTION worker registered,
both schedules synced against the **prod** environment through the same
loop, prod + dev schedule instances active, no errors.

`typecheck --filter webapp` clean.

## Rollout / rollback

Straight deploy, no flag, no migration. Rollback is revert-only
(read-path narrowing, no data change). Old and in-flight rows read
correctly under both the old and new code.

## Out of scope

The two lower-priority sibling reads in the ticket (the Query/metrics
env id->slug map and the env-var repository fan-out) are left for
follow-ups; they need caching / per-method scoping rather than this
single bound.
2026-08-13 07:36:07 +01:00
Chris Arderne 6997aeb05e fix: security release 2026-07-08 (#4316)
⚒️ Publish Worker (v4) / build (supervisor) (push) Has been cancelled
2026-07-21 12:00:58 +01:00