aca234d1c3
## 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.
151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
import { containerTest } from "@internal/testcontainers";
|
|
import { boundedIn, type PrismaClient } from "@trigger.dev/database";
|
|
import { describe, expect, vi } from "vitest";
|
|
import { resolveProjectScopedEnvironments } from "~/v3/services/resolveProjectScopedEnvironments";
|
|
|
|
vi.setConfig({ testTimeout: 60_000 });
|
|
|
|
async function seedProjectWithEnv(prisma: PrismaClient, slugBase: string) {
|
|
const slug = `${slugBase}_${Math.random().toString(36).slice(2, 10)}`;
|
|
const organization = await prisma.organization.create({ data: { title: slug, slug } });
|
|
const project = await prisma.project.create({
|
|
data: { name: slug, slug, organizationId: organization.id, externalRef: slug },
|
|
});
|
|
const environment = await prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: `${slug}-prod`,
|
|
type: "PRODUCTION",
|
|
projectId: project.id,
|
|
organizationId: organization.id,
|
|
apiKey: `tr_prod_${slug}`,
|
|
pkApiKey: `pk_prod_${slug}`,
|
|
shortcode: slug.slice(0, 6),
|
|
},
|
|
});
|
|
return { organization, project, environment };
|
|
}
|
|
|
|
async function seedBranchEnv(
|
|
prisma: PrismaClient,
|
|
project: { id: string; organizationId: string },
|
|
slugBase: string,
|
|
{ archived }: { archived: boolean }
|
|
) {
|
|
const slug = `${slugBase}_${Math.random().toString(36).slice(2, 10)}`;
|
|
return prisma.runtimeEnvironment.create({
|
|
data: {
|
|
slug: `${slug}-branch`,
|
|
type: "PREVIEW",
|
|
branchName: slug,
|
|
projectId: project.id,
|
|
organizationId: project.organizationId,
|
|
apiKey: `tr_preview_${slug}`,
|
|
pkApiKey: `pk_preview_${slug}`,
|
|
shortcode: Math.random().toString(36).slice(2, 10),
|
|
archivedAt: archived ? new Date() : null,
|
|
},
|
|
});
|
|
}
|
|
|
|
function projectEnvironments(prisma: PrismaClient, projectId: string) {
|
|
return prisma.runtimeEnvironment.findMany({ where: { projectId }, select: { id: true } });
|
|
}
|
|
|
|
function loadScopedEnvironments(prisma: PrismaClient, projectId: string, environmentIds: string[]) {
|
|
return prisma.project
|
|
.findFirst({
|
|
where: { id: projectId },
|
|
select: {
|
|
organizationId: true,
|
|
environments: {
|
|
where: { id: { in: boundedIn(environmentIds) } },
|
|
select: { id: true, type: true, archivedAt: true },
|
|
},
|
|
},
|
|
})
|
|
.then((project) => project?.environments ?? []);
|
|
}
|
|
|
|
describe("resolveProjectScopedEnvironments (schedule env scoping)", () => {
|
|
containerTest("rejects an environment id that belongs to another project", async ({ prisma }) => {
|
|
const a = await seedProjectWithEnv(prisma, "orga");
|
|
const b = await seedProjectWithEnv(prisma, "orgb");
|
|
|
|
const result = resolveProjectScopedEnvironments(
|
|
[a.environment.id, b.environment.id],
|
|
await projectEnvironments(prisma, a.project.id)
|
|
);
|
|
|
|
expect(result.kind).toBe("foreign");
|
|
expect(result).toMatchObject({ foreignEnvironmentId: b.environment.id });
|
|
});
|
|
|
|
containerTest("accepts environment ids that belong to the project", async ({ prisma }) => {
|
|
const a = await seedProjectWithEnv(prisma, "orga");
|
|
|
|
const result = resolveProjectScopedEnvironments(
|
|
[a.environment.id],
|
|
await projectEnvironments(prisma, a.project.id)
|
|
);
|
|
|
|
expect(result.kind).toBe("ok");
|
|
});
|
|
});
|
|
|
|
describe("CheckScheduleService bounded environments load", () => {
|
|
containerTest(
|
|
"loads only the requested environments, not every project environment",
|
|
async ({ prisma }) => {
|
|
const a = await seedProjectWithEnv(prisma, "orga");
|
|
for (let i = 0; i < 8; i++) {
|
|
await seedBranchEnv(prisma, a.project, `branch${i}`, { archived: true });
|
|
}
|
|
await seedBranchEnv(prisma, a.project, "active", { archived: false });
|
|
|
|
const all = await projectEnvironments(prisma, a.project.id);
|
|
expect(all.length).toBe(10);
|
|
|
|
const scoped = await loadScopedEnvironments(prisma, a.project.id, [a.environment.id]);
|
|
expect(scoped.length).toBe(1);
|
|
expect(scoped[0]?.id).toBe(a.environment.id);
|
|
|
|
const result = resolveProjectScopedEnvironments([a.environment.id], scoped);
|
|
expect(result.kind).toBe("ok");
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"still rejects a foreign environment id when the load is bounded",
|
|
async ({ prisma }) => {
|
|
const a = await seedProjectWithEnv(prisma, "orga");
|
|
const b = await seedProjectWithEnv(prisma, "orgb");
|
|
await seedBranchEnv(prisma, a.project, "branch", { archived: true });
|
|
|
|
const scoped = await loadScopedEnvironments(prisma, a.project.id, [
|
|
a.environment.id,
|
|
b.environment.id,
|
|
]);
|
|
expect(scoped.length).toBe(1);
|
|
|
|
const result = resolveProjectScopedEnvironments([a.environment.id, b.environment.id], scoped);
|
|
expect(result.kind).toBe("foreign");
|
|
expect(result).toMatchObject({ foreignEnvironmentId: b.environment.id });
|
|
}
|
|
);
|
|
|
|
containerTest(
|
|
"still surfaces an archived branch env when it is the requested one",
|
|
async ({ prisma }) => {
|
|
const a = await seedProjectWithEnv(prisma, "orga");
|
|
const archivedBranch = await seedBranchEnv(prisma, a.project, "branch", { archived: true });
|
|
|
|
const scoped = await loadScopedEnvironments(prisma, a.project.id, [archivedBranch.id]);
|
|
expect(scoped.length).toBe(1);
|
|
|
|
const result = resolveProjectScopedEnvironments([archivedBranch.id], scoped);
|
|
expect(result.kind).toBe("ok");
|
|
expect(result.kind === "ok" && result.environments.some((env) => env.archivedAt)).toBe(true);
|
|
}
|
|
);
|
|
});
|