Skip to content

Commit 515c3e4

Browse files
committed
fix(webapp): retirement removes engine keys before nulling bounds, retired rows leave the dashboard
Engine cleanup runs first and a failure leaves the row bounded so the next deploy retries, instead of stranding an invisible stale engine limit (worst case a pause-by-zero) after Postgres already cleared. The Concurrency page mirrors the limits surface's namespace scoping: boundless anonymous limit/task rows are retired and hidden while boundless named limits stay visible as real uncapped rows.
1 parent d220661 commit 515c3e4

2 files changed

Lines changed: 55 additions & 20 deletions

File tree

‎apps/webapp/app/presenters/v3/QueueListPresenter.server.ts‎

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,22 +87,44 @@ function buildQueueListWhere(
8787
): Prisma.TaskQueueWhereInput {
8888
const trimmedQuery = query?.trim();
8989

90-
return {
90+
const common = {
9191
runtimeEnvironmentId: environmentId,
92-
/** Only the dashboard interleaves named limits, and the type filter names queue
93-
* shapes, so either condition scopes the list to queue rows; the public queues
94-
* API always stays queue-only. */
95-
role:
96-
includeLimits && !type ? { in: ["QUEUE" as const, "LIMIT" as const] } : ("QUEUE" as const),
97-
version: "V2",
92+
version: "V2" as const,
9893
name: trimmedQuery
9994
? {
10095
contains: trimmedQuery,
101-
mode: "insensitive",
96+
mode: "insensitive" as const,
10297
}
10398
: undefined,
10499
type: type ? typeToDBQueueType[type] : undefined,
105100
};
101+
102+
/** Only the dashboard interleaves named limits, and the type filter names queue
103+
* shapes, so either condition scopes the list to queue rows; the public queues
104+
* API always stays queue-only. Boundless rows in the anonymous limit/task/
105+
* namespace are retired (their inline limit moved onto the task's own queue)
106+
* and stay hidden; boundless NAMED limits are real uncapped rows and show. */
107+
if (includeLimits && !type) {
108+
return {
109+
...common,
110+
OR: [
111+
{ role: "QUEUE" as const },
112+
{
113+
role: "LIMIT" as const,
114+
OR: [
115+
{ name: { not: { startsWith: "limit/task/" } } },
116+
{ concurrencyLimit: { not: null } },
117+
{ totalConcurrencyLimit: { not: null } },
118+
],
119+
},
120+
],
121+
};
122+
}
123+
124+
return {
125+
...common,
126+
role: "QUEUE" as const,
127+
};
106128
}
107129

108130
export class QueueListPresenter extends BasePresenter {

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -426,13 +426,32 @@ async function retireStaleAnonymousConcurrencyLimitRows(
426426
return;
427427
}
428428

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. */
429+
/** Engine keys go first, and the row's bounds are only nulled once both
430+
* removals succeeded: a failed removal leaves the row bounded, so the next
431+
* deploy retries the retirement instead of stranding a stale engine limit
432+
* (worst case a pause-by-zero) that nothing can see or clear. Each null is
433+
* guarded on the row's read updatedAt, so a concurrent writer (an operator
434+
* override, or another deploy re-creating the limit) wins and its own engine
435+
* sync governs; a racing limits-surface sync converges via its freshness
436+
* re-check against the nulled row. */
434437
for (const row of staleRows) {
435-
const retired = await prisma.taskQueue.updateMany({
438+
try {
439+
await Promise.all([
440+
removeQueueConcurrencyLimits(environment, row.name),
441+
removeQueueTotalConcurrencyLimits(environment, row.name),
442+
]);
443+
} catch (error) {
444+
logger.error(
445+
"retireStaleAnonymousConcurrencyLimitRows: engine cleanup failed, retrying next deploy",
446+
{
447+
environmentId: environment.id,
448+
queueName: row.name,
449+
error,
450+
}
451+
);
452+
continue;
453+
}
454+
await prisma.taskQueue.updateMany({
436455
where: { id: row.id, updatedAt: row.updatedAt },
437456
data: {
438457
concurrencyLimit: null,
@@ -446,12 +465,6 @@ async function retireStaleAnonymousConcurrencyLimitRows(
446465
totalConcurrencyLimitOverriddenBy: null,
447466
},
448467
});
449-
if (retired.count > 0) {
450-
await Promise.allSettled([
451-
removeQueueConcurrencyLimits(environment, row.name),
452-
removeQueueTotalConcurrencyLimits(environment, row.name),
453-
]);
454-
}
455468
}
456469
}
457470

0 commit comments

Comments
 (0)