Skip to content

Commit 910ccb2

Browse files
committed
fix(webapp): boundless named limits stay cappable, retirement is race-guarded
The retired-row bounds filter applies only to the anonymous limit/task/ namespace: a boundless named limit is a real, deliberately uncapped row (referenced without a declaration) that stays visible and cappable, while a boundless anonymous row is retired and falls through to the live queue row. Each retirement nulls its row guarded on the read updatedAt, so a concurrent operator override or re-creating deploy wins and keeps its own engine sync; engine keys are removed only for rows this deploy actually retired.
1 parent 3745a8a commit 910ccb2

3 files changed

Lines changed: 83 additions & 29 deletions

File tree

‎apps/webapp/app/v3/services/concurrencyLimitsSystem.server.ts‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,20 +190,29 @@ function concurrencyLimitNameFromRow(row: Pick<TaskQueue, "name">): string {
190190
* queue compiles onto that V2 QUEUE row (the design's zero-gate-cost case). Its
191191
* derived `task/<id>` name resolves here too, so every declared limit is
192192
* retrievable and overridable through this one surface. V1 queue rows never
193-
* match: their limit is queue surface, managed through the queues API.
193+
* match: their limit is queue surface, managed through the queues API. Only the
194+
* anonymous `limit/task/` namespace requires bounds — a boundless row there is
195+
* retired (its inline limit moved onto the task's own queue) and must fall
196+
* through to the live queue row — while a boundless NAMED limit is a real,
197+
* deliberately uncapped row (referenced without a declaration) that stays
198+
* visible and cappable.
194199
*/
195200
function limitRowsWhere(environment: AuthenticatedEnvironment) {
196-
const hasBounds = {
197-
OR: [{ concurrencyLimit: { not: null } }, { totalConcurrencyLimit: { not: null } }],
198-
};
199201
return {
200202
runtimeEnvironmentId: environment.id,
201203
OR: [
202-
{ role: "LIMIT" as const, ...hasBounds },
204+
{
205+
role: "LIMIT" as const,
206+
OR: [
207+
{ name: { not: { startsWith: `${LIMIT_QUEUE_PREFIX}task/` } } },
208+
{ concurrencyLimit: { not: null } },
209+
{ totalConcurrencyLimit: { not: null } },
210+
],
211+
},
203212
{
204213
role: "QUEUE" as const,
205214
concurrencyVersion: "V2" as const,
206-
...hasBounds,
215+
OR: [{ concurrencyLimit: { not: null } }, { totalConcurrencyLimit: { not: null } }],
207216
},
208217
],
209218
};
@@ -224,7 +233,11 @@ function findLimitByName(
224233
runtimeEnvironmentId: environment.id,
225234
name: `${LIMIT_QUEUE_PREFIX}${name}`,
226235
role: "LIMIT",
227-
OR: [{ concurrencyLimit: { not: null } }, { totalConcurrencyLimit: { not: null } }],
236+
...(name.startsWith("task/")
237+
? {
238+
OR: [{ concurrencyLimit: { not: null } }, { totalConcurrencyLimit: { not: null } }],
239+
}
240+
: {}),
228241
},
229242
}),
230243
(error) => ({ type: "other" as const, cause: error })

‎apps/webapp/app/v3/services/createBackgroundWorker.server.ts‎

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -420,33 +420,39 @@ async function retireStaleAnonymousConcurrencyLimitRows(
420420
name: { in: boundedIn(candidateNames) },
421421
OR: [{ concurrencyLimit: { not: null } }, { totalConcurrencyLimit: { not: null } }],
422422
},
423-
select: { id: true, name: true },
423+
select: { id: true, name: true, updatedAt: true },
424424
});
425425
if (staleRows.length === 0) {
426426
return;
427427
}
428428

429-
await prisma.taskQueue.updateMany({
430-
where: { id: { in: boundedIn(staleRows.map((row) => row.id)) } },
431-
data: {
432-
concurrencyLimit: null,
433-
concurrencyLimitBase: null,
434-
concurrencyLimitOverriddenAt: null,
435-
concurrencyLimitOverriddenBy: null,
436-
concurrencyLimitOverridePercent: null,
437-
totalConcurrencyLimit: null,
438-
totalConcurrencyLimitBase: null,
439-
totalConcurrencyLimitOverriddenAt: null,
440-
totalConcurrencyLimitOverriddenBy: null,
441-
},
442-
});
443-
444-
await Promise.allSettled(
445-
staleRows.flatMap((row) => [
446-
removeQueueConcurrencyLimits(environment, row.name),
447-
removeQueueTotalConcurrencyLimits(environment, row.name),
448-
])
449-
);
429+
/** Each null is guarded on the row's read updatedAt, so a concurrent writer
430+
* (an operator override, or another deploy re-creating the limit) wins and
431+
* that row is left alone — its own engine sync governs. Engine keys are only
432+
* removed for rows this deploy actually retired; a racing limits-surface sync
433+
* converges via its freshness re-check against the nulled row. */
434+
for (const row of staleRows) {
435+
const retired = await prisma.taskQueue.updateMany({
436+
where: { id: row.id, updatedAt: row.updatedAt },
437+
data: {
438+
concurrencyLimit: null,
439+
concurrencyLimitBase: null,
440+
concurrencyLimitOverriddenAt: null,
441+
concurrencyLimitOverriddenBy: null,
442+
concurrencyLimitOverridePercent: null,
443+
totalConcurrencyLimit: null,
444+
totalConcurrencyLimitBase: null,
445+
totalConcurrencyLimitOverriddenAt: null,
446+
totalConcurrencyLimitOverriddenBy: null,
447+
},
448+
});
449+
if (retired.count > 0) {
450+
await Promise.allSettled([
451+
removeQueueConcurrencyLimits(environment, row.name),
452+
removeQueueTotalConcurrencyLimits(environment, row.name),
453+
]);
454+
}
455+
}
450456
}
451457

452458
async function createWorkerTask(

‎apps/webapp/test/concurrencyLimitsSystem.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,41 @@ describe("ConcurrencyLimitsSystem", () => {
465465
expect(updated.concurrencyLimitOverridePercent).toBeNull();
466466
});
467467

468+
postgresTest("an uncapped named limit stays visible and cappable", async ({ prisma }) => {
469+
const { authEnv, system, environment } = await seedEnvAndLimit(prisma, { total: 25 });
470+
471+
/** Deploys materialize referenced-but-undeclared names as uncapped LIMIT
472+
* rows; only the anonymous limit/task/ namespace treats boundless as
473+
* retired. */
474+
await prisma.taskQueue.create({
475+
data: {
476+
friendlyId: `queue_un${environment.slug}`,
477+
name: "limit/acme-api",
478+
orderableName: "acme-api",
479+
projectId: environment.projectId,
480+
runtimeEnvironmentId: environment.id,
481+
role: "LIMIT",
482+
concurrencyVersion: "V2",
483+
},
484+
});
485+
486+
const retrieved = await system.limits.retrieve(authEnv, "acme-api");
487+
expect(retrieved.isOk()).toBe(true);
488+
if (retrieved.isOk()) {
489+
expect(retrieved.value.total.current).toBeNull();
490+
}
491+
492+
const capped = await system.limits.override(authEnv, "acme-api", { total: 5 });
493+
expect(capped.isOk()).toBe(true);
494+
expect(totalSyncMock).toHaveBeenCalledWith(authEnv, "limit/acme-api", 5);
495+
496+
const listed = await system.limits.list(authEnv, { page: 1, perPage: 50 });
497+
expect(listed.isOk()).toBe(true);
498+
if (listed.isOk()) {
499+
expect(listed.value.map((item) => item.name)).toContain("acme-api");
500+
}
501+
});
502+
468503
postgresTest("retrieve misses queue-role rows and unknown names", async ({ prisma }) => {
469504
const { authEnv, system, environment } = await seedEnvAndLimit(prisma, { total: 25 });
470505

0 commit comments

Comments
 (0)