Skip to content

Commit 294b2fd

Browse files
committed
perf(webapp,database): index RuntimeEnvironment.pauseSource and push billing reconcile distinct into the DB
The billingLimit.reconcileTick worker runs getOrgIdsWithBillingPauseSource every ~90s. It seq-scanned the whole RuntimeEnvironment table (no index on pauseSource) and used Prisma distinct, which fetches every paused row before deduping to a handful of org ids. Add a partial index on (pauseSource, organizationId) WHERE pauseSource IS NOT NULL and switch to groupBy so the DB returns only the distinct orgs. The index is tiny since nearly all rows have pauseSource = null.
1 parent 7b7d489 commit 294b2fd

4 files changed

Lines changed: 85 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Reduced recurring background database load from the billing-limit recovery check, so paused environments are reconciled with less overhead.

‎apps/webapp/app/v3/services/billingLimit/billingLimitReconciliation.server.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EnvironmentPauseSource } from "@trigger.dev/database";
2+
import type { PrismaClient } from "@trigger.dev/database";
23
import pMap from "p-map";
34
import { prisma } from "~/db.server";
45
import type { BillingLimitResult } from "~/services/billingLimit.schemas";
@@ -47,15 +48,14 @@ export function resolveReconcileTargetFromBillingLimit(
4748
return resolveConvergeTargetFromBillingLimit(billingLimit);
4849
}
4950

50-
export async function getOrgIdsWithBillingPauseSource(): Promise<string[]> {
51-
const rows = await prisma.runtimeEnvironment.findMany({
51+
export async function getOrgIdsWithBillingPauseSource(
52+
db: PrismaClient = prisma
53+
): Promise<string[]> {
54+
const rows = await db.runtimeEnvironment.groupBy({
55+
by: ["organizationId"],
5256
where: {
5357
pauseSource: EnvironmentPauseSource.BILLING_LIMIT,
5458
},
55-
select: {
56-
organizationId: true,
57-
},
58-
distinct: ["organizationId"],
5959
});
6060

6161
return rows.map((row) => row.organizationId);

‎apps/webapp/test/billingLimitReconciliation.test.ts‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { postgresTest } from "@internal/testcontainers";
2+
import type { PrismaClient } from "@trigger.dev/database";
13
import { describe, expect, it } from "vitest";
24
import type { BillingLimitResult } from "~/services/billingLimit.schemas";
35
import {
46
collectOrgIdsNeedingBillingLimitLookup,
7+
getOrgIdsWithBillingPauseSource,
58
resolveConvergeTargetFromBillingLimit,
69
resolveReconcileTargetFromBillingLimit,
710
resolveReconcileTargetsForOrgLookups,
@@ -97,3 +100,70 @@ describe("billingLimitReconciliation", () => {
97100
expect(new Set(lookedUpOrgIds)).toEqual(new Set(["org_ok", "org_fail", "org_grace"]));
98101
});
99102
});
103+
104+
let envSeedCounter = 0;
105+
106+
async function seedEnvironment(
107+
prisma: PrismaClient,
108+
opts: { organizationId: string; projectId: string; pauseSource: "BILLING_LIMIT" | null }
109+
) {
110+
const n = envSeedCounter++;
111+
return prisma.runtimeEnvironment.create({
112+
data: {
113+
slug: `env-${n}`,
114+
type: "PRODUCTION",
115+
projectId: opts.projectId,
116+
organizationId: opts.organizationId,
117+
apiKey: `api-${n}`,
118+
pkApiKey: `pk-${n}`,
119+
shortcode: `sc-${n}`,
120+
pauseSource: opts.pauseSource,
121+
},
122+
});
123+
}
124+
125+
describe("getOrgIdsWithBillingPauseSource", () => {
126+
postgresTest(
127+
"returns each org once and ignores envs without the billing-limit pause source",
128+
async ({ prisma }) => {
129+
const seed: Record<string, Array<"BILLING_LIMIT" | null>> = {
130+
org_a: ["BILLING_LIMIT", "BILLING_LIMIT"],
131+
org_b: ["BILLING_LIMIT"],
132+
org_c: [null],
133+
};
134+
135+
const orgIdBySlug = new Map<string, string>();
136+
137+
for (const [slug, pauseSources] of Object.entries(seed)) {
138+
const organization = await prisma.organization.create({
139+
data: { title: slug, slug: `${slug}-${envSeedCounter}` },
140+
});
141+
const project = await prisma.project.create({
142+
data: {
143+
name: slug,
144+
slug: `proj-${slug}-${envSeedCounter}`,
145+
organizationId: organization.id,
146+
externalRef: `ext-${slug}-${envSeedCounter}`,
147+
},
148+
});
149+
orgIdBySlug.set(slug, organization.id);
150+
151+
for (const pauseSource of pauseSources) {
152+
await seedEnvironment(prisma, {
153+
organizationId: organization.id,
154+
projectId: project.id,
155+
pauseSource,
156+
});
157+
}
158+
}
159+
160+
const orgIds = await getOrgIdsWithBillingPauseSource(prisma);
161+
162+
expect(orgIds.length).toBe(new Set(orgIds).size);
163+
expect([...orgIds].sort()).toEqual(
164+
[orgIdBySlug.get("org_a")!, orgIdBySlug.get("org_b")!].sort()
165+
);
166+
},
167+
30_000
168+
);
169+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "RuntimeEnvironment_pauseSource_organizationId_idx"
2+
ON "RuntimeEnvironment" ("pauseSource", "organizationId")
3+
WHERE "pauseSource" IS NOT NULL;

0 commit comments

Comments
 (0)