Skip to content

Commit 23de76b

Browse files
committed
feat(webapp): remove auto-balance from the allocation view
The allocation view keeps manual limit edits, the review dialog, and bulk apply. The one-shot auto-balance button is removed (and the row locks whose only purpose was protecting queues from it); a policy-driven approach can replace it if rebalancing returns.
1 parent 279db19 commit 23de76b

1 file changed

Lines changed: 3 additions & 119 deletions

File tree

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/AllocationView.tsx

Lines changed: 3 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { LockClosedIcon, LockOpenIcon, ScaleIcon } from "@heroicons/react/20/solid";
21
import { Form, useNavigation } from "@remix-run/react";
32
import { type ReactNode, useEffect, useMemo, useState } from "react";
43
import { BigNumber } from "~/components/metrics/BigNumber";
@@ -28,64 +27,6 @@ import { cn } from "~/utils/cn";
2827

2928
type Drafts = Record<string, number>;
3029

31-
/**
32-
* Distribute the env budget across unlocked queues, weighted by current load
33-
* (running + queued), largest-remainder rounding, min 1 when the budget allows.
34-
* Locked queues keep their current value and are subtracted from the budget first.
35-
*/
36-
export function computeAutoBalance(
37-
queues: QueueAllocationItem[],
38-
envLimit: number,
39-
locked: Set<string>,
40-
draftLimit: (queue: QueueAllocationItem) => number | null
41-
): Drafts {
42-
const unlocked = queues.filter((q) => !locked.has(q.id));
43-
if (unlocked.length === 0) return {};
44-
45-
const lockedSum = queues
46-
.filter((q) => locked.has(q.id))
47-
.reduce((sum, q) => sum + (draftLimit(q) ?? 0), 0);
48-
const budget = Math.max(0, envLimit - lockedSum);
49-
50-
const weights = unlocked.map((q) => q.running + q.queued);
51-
const totalWeight = weights.reduce((a, b) => a + b, 0);
52-
const raw = unlocked.map((_, i) =>
53-
totalWeight > 0 ? (budget * weights[i]) / totalWeight : budget / unlocked.length
54-
);
55-
56-
const shares = raw.map(Math.floor);
57-
let remainder = budget - shares.reduce((a, b) => a + b, 0);
58-
const byFraction = raw
59-
.map((value, i) => ({ i, fraction: value - Math.floor(value) }))
60-
.sort((a, b) => b.fraction - a.fraction);
61-
for (const { i } of byFraction) {
62-
if (remainder <= 0) break;
63-
shares[i]++;
64-
remainder--;
65-
}
66-
67-
// Every unlocked queue gets at least 1 when the budget can afford it.
68-
if (budget >= unlocked.length) {
69-
for (let i = 0; i < shares.length; i++) {
70-
while (shares[i] < 1) {
71-
let donor = -1;
72-
for (let j = 0; j < shares.length; j++) {
73-
if (j !== i && shares[j] > 1 && (donor === -1 || shares[j] > shares[donor])) donor = j;
74-
}
75-
if (donor === -1) break;
76-
shares[donor]--;
77-
shares[i]++;
78-
}
79-
}
80-
}
81-
82-
const result: Drafts = {};
83-
unlocked.forEach((q, i) => {
84-
result[q.id] = Math.min(Math.max(shares[i], 0), envLimit);
85-
});
86-
return result;
87-
}
88-
8930
export function AllocationView({
9031
allocation,
9132
environment,
@@ -94,7 +35,6 @@ export function AllocationView({
9435
environment: Environment;
9536
}) {
9637
const [drafts, setDrafts] = useState<Drafts>({});
97-
const [locked, setLocked] = useState<Set<string>>(new Set());
9838
const [reviewOpen, setReviewOpen] = useState(false);
9939
const navigation = useNavigation();
10040
const isSubmitting = navigation.state !== "idle";
@@ -154,29 +94,6 @@ export function AllocationView({
15494
});
15595
};
15696

157-
const toggleLock = (id: string) => {
158-
setLocked((prev) => {
159-
const next = new Set(prev);
160-
if (next.has(id)) next.delete(id);
161-
else next.add(id);
162-
return next;
163-
});
164-
};
165-
166-
const autoBalance = () => {
167-
const balanced = computeAutoBalance(allocation.queues, envLimit, locked, draftLimit);
168-
setDrafts((prev) => {
169-
const next = { ...prev };
170-
for (const queue of allocation.queues) {
171-
const value = balanced[queue.id];
172-
if (value === undefined) continue;
173-
if (value === queue.limit) delete next[queue.id];
174-
else next[queue.id] = value;
175-
}
176-
return next;
177-
});
178-
};
179-
18097
const changesPayload = useMemo(
18198
() =>
18299
JSON.stringify(changes.map((queue) => ({ friendlyId: queue.id, limit: drafts[queue.id] }))),
@@ -237,8 +154,8 @@ export function AllocationView({
237154
{overAllocated && (
238155
<Callout variant="warning">
239156
The queue limits add up to more than the environment limit, so queues will compete for
240-
concurrency when the environment saturates. Reduce limits (or use Auto-balance) to
241-
guarantee each queue its allocation.
157+
concurrency when the environment saturates. Reduce limits to guarantee each queue its
158+
allocation.
242159
</Callout>
243160
)}
244161

@@ -250,16 +167,6 @@ export function AllocationView({
250167
)}
251168

252169
<div className="flex items-center gap-2">
253-
<Button
254-
type="button"
255-
variant="secondary/small"
256-
LeadingIcon={ScaleIcon}
257-
onClick={autoBalance}
258-
disabled={isSubmitting}
259-
tooltip="Distribute the environment limit across unlocked queues, weighted by current load"
260-
>
261-
Auto-balance
262-
</Button>
263170
<Button
264171
type="button"
265172
variant="minimal/small"
@@ -332,14 +239,10 @@ export function AllocationView({
332239
>
333240
Limit
334241
</TableHeaderCell>
335-
<TableHeaderCell className="w-[1%]">
336-
<span className="sr-only">Lock</span>
337-
</TableHeaderCell>
338242
</TableRow>
339243
</TableHeader>
340244
<TableBody>
341245
{tableQueues.map((queue) => {
342-
const isLocked = locked.has(queue.id);
343246
const changed = drafts[queue.id] !== undefined && drafts[queue.id] !== queue.limit;
344247
return (
345248
<TableRow key={queue.id}>
@@ -377,31 +280,12 @@ export function AllocationView({
377280
value={drafts[queue.id] ?? queue.limit ?? ""}
378281
placeholder={String(envLimit)}
379282
onChange={(e) => setDraft(queue, e.target.value)}
380-
disabled={isLocked || isSubmitting}
283+
disabled={isSubmitting}
381284
className="w-24"
382285
variant="small"
383286
/>
384287
</span>
385288
</TableCell>
386-
<TableCell>
387-
<SimpleTooltip
388-
button={
389-
<Button
390-
type="button"
391-
variant="minimal/small"
392-
LeadingIcon={isLocked ? LockClosedIcon : LockOpenIcon}
393-
leadingIconClassName={isLocked ? "text-text-bright" : "text-text-dimmed"}
394-
onClick={() => toggleLock(queue.id)}
395-
aria-label={isLocked ? "Unlock queue" : "Lock queue"}
396-
/>
397-
}
398-
content={
399-
isLocked
400-
? "Locked: auto-balance keeps this queue's limit"
401-
: "Unlocked: auto-balance can change this queue's limit"
402-
}
403-
/>
404-
</TableCell>
405289
</TableRow>
406290
);
407291
})}

0 commit comments

Comments
 (0)