Skip to content

Commit c93b290

Browse files
myftijaTrigger.dev RepoOps
authored andcommitted
feat(run-engine): per-environment worker queue routing overrides
Admins can override worker queue routing per environment. A new org feature flag, `workerQueueEnvironmentOverrides`, maps an environment type (`PRODUCTION`, `STAGING`, `PREVIEW`) or a specific environment id to a worker queue compatibility and channel, so every staging environment in an org can be born onto the compute lane while production stays on containers. Compat and channel each resolve from the environment id entry, then the environment type entry, then the org-wide `workerQueueCompatibility` and `workerQueueChannel` flags, then the existing default. Development environments are never routed. An environment compat of `compute` also moves that environment onto its region's compute backing worker group when the region has one, and `container` stops it from being moved there, ahead of the org's compute migration setting. The org feature flags dialog gets a table to edit these overrides. Mono-RevId: c8a73661232d8df52694856c514ecaecf1c1e714
1 parent 3804d5a commit c93b290

11 files changed

Lines changed: 984 additions & 22 deletions

‎apps/webapp/app/components/admin/FeatureFlagsDialog.tsx‎

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ import {
2121
NumberControl,
2222
StringControl,
2323
WorkerGroupControl,
24+
WorkerQueueEnvironmentOverridesControl,
2425
type WorkerGroup,
2526
} from "./FlagControls";
27+
import {
28+
incompleteOverrideScopes,
29+
type OverrideEnvironment,
30+
type WorkerQueueEnvironmentOverrides,
31+
} from "./workerQueueEnvironmentOverrides";
2632

2733
type LoaderData = {
2834
org: { id: string; title: string; slug: string };
@@ -31,6 +37,7 @@ type LoaderData = {
3137
controlTypes: Record<string, FlagControlType>;
3238
workerGroupName?: string;
3339
workerGroups?: WorkerGroup[];
40+
environments?: OverrideEnvironment[];
3441
isManagedCloud?: boolean;
3542
};
3643

@@ -95,6 +102,9 @@ export function FeatureFlagsDialog({
95102
}, [saveFetcher.data]);
96103

97104
const isDirty = stableStringify(overrides) !== stableStringify(initialOverrides);
105+
const environmentOverrides = (overrides[FEATURE_FLAG.workerQueueEnvironmentOverrides] ??
106+
{}) as WorkerQueueEnvironmentOverrides;
107+
const hasIncompleteOverrides = incompleteOverrideScopes(environmentOverrides).length > 0;
98108

99109
const setFlagValue = (key: string, value: unknown) => {
100110
setOverrides((prev) => ({ ...prev, [key]: value }));
@@ -187,6 +197,61 @@ export function FeatureFlagsDialog({
187197

188198
const isOverridden = key in overrides;
189199

200+
if (key === FEATURE_FLAG.workerQueueEnvironmentOverrides) {
201+
const enumOptions = (flag: string) => {
202+
const flagControl = data.controlTypes[flag];
203+
return flagControl?.type === "enum" ? flagControl.options : [];
204+
};
205+
return (
206+
<div
207+
key={key}
208+
className={cn(
209+
"flex flex-col gap-2 rounded-md border px-3 py-2.5",
210+
isOverridden
211+
? "border-indigo-500/20 bg-indigo-500/5"
212+
: "border-transparent bg-background-hover"
213+
)}
214+
>
215+
<div className="flex items-start justify-between gap-2">
216+
<div className="min-w-0">
217+
<div
218+
className={cn(
219+
"truncate text-sm",
220+
isOverridden ? "text-text-bright" : "text-text-dimmed"
221+
)}
222+
>
223+
{key}
224+
</div>
225+
<div className="text-xs text-text-dimmed">
226+
Per field: environment id, then type, then the org flags above. A compat
227+
of compute or container here also decides the compute backing swap.
228+
</div>
229+
</div>
230+
<Button
231+
variant="minimal/small"
232+
onClick={() => unsetFlag(key)}
233+
className={cn(!isOverridden && "invisible")}
234+
>
235+
unset
236+
</Button>
237+
</div>
238+
<WorkerQueueEnvironmentOverridesControl
239+
value={environmentOverrides}
240+
environments={data.environments ?? []}
241+
compatOptions={enumOptions(FEATURE_FLAG.workerQueueCompatibility)}
242+
channelOptions={enumOptions(FEATURE_FLAG.workerQueueChannel)}
243+
onChange={(val) => {
244+
if (val === undefined) {
245+
unsetFlag(key);
246+
} else {
247+
setFlagValue(key, val);
248+
}
249+
}}
250+
/>
251+
</div>
252+
);
253+
}
254+
190255
return (
191256
<div
192257
key={key}
@@ -296,13 +361,21 @@ export function FeatureFlagsDialog({
296361
</details>
297362
)}
298363

364+
{hasIncompleteOverrides && (
365+
<Callout variant="error">Each environment override needs a compat or a channel.</Callout>
366+
)}
367+
299368
{saveError && <Callout variant="error">{saveError}</Callout>}
300369

301370
<DialogFooter>
302371
<Button variant="tertiary/small" onClick={() => onOpenChange(false)}>
303372
Cancel
304373
</Button>
305-
<Button variant="primary/small" onClick={handleSave} disabled={!isDirty || isSaving}>
374+
<Button
375+
variant="primary/small"
376+
onClick={handleSave}
377+
disabled={!isDirty || isSaving || hasIncompleteOverrides}
378+
>
306379
{isSaving ? "Saving..." : "Save changes"}
307380
</Button>
308381
</DialogFooter>

‎apps/webapp/app/components/admin/FlagControls.tsx‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
import { useState } from "react";
12
import { Switch } from "~/components/primitives/Switch";
23
import { Select, SelectItem } from "~/components/primitives/Select";
34
import { Input } from "~/components/primitives/Input";
5+
import { Button } from "~/components/primitives/Buttons";
46
import { cn } from "~/utils/cn";
7+
import type { WorkerQueueEnvironmentOverride } from "~/v3/featureFlags";
8+
import {
9+
availableOverrideScopes,
10+
orderedOverrideScopes,
11+
overrideScopeLabel,
12+
removeOverride,
13+
setOverrideField,
14+
type OverrideEnvironment,
15+
type WorkerQueueEnvironmentOverrides,
16+
} from "./workerQueueEnvironmentOverrides";
517

618
export const UNSET_VALUE = "__unset__";
719

@@ -153,3 +165,130 @@ export function StringControl({
153165
/>
154166
);
155167
}
168+
169+
type Compat = WorkerQueueEnvironmentOverride["compat"];
170+
type Channel = WorkerQueueEnvironmentOverride["channel"];
171+
172+
function fromEnumValue<T extends string>(val: string): T | undefined {
173+
return val === UNSET_VALUE ? undefined : (val as T);
174+
}
175+
176+
export function WorkerQueueEnvironmentOverridesControl({
177+
value,
178+
environments,
179+
compatOptions,
180+
channelOptions,
181+
onChange,
182+
}: {
183+
value: WorkerQueueEnvironmentOverrides;
184+
environments: OverrideEnvironment[];
185+
compatOptions: string[];
186+
channelOptions: string[];
187+
onChange: (val: WorkerQueueEnvironmentOverrides | undefined) => void;
188+
}) {
189+
const [draftScope, setDraftScope] = useState(UNSET_VALUE);
190+
const [draftCompat, setDraftCompat] = useState<Compat>(undefined);
191+
const [draftChannel, setDraftChannel] = useState<Channel>(undefined);
192+
193+
const label = (scope: string) => overrideScopeLabel(scope, environments);
194+
const availableScopes = availableOverrideScopes(value, environments);
195+
const scopeItems = [UNSET_VALUE, ...availableScopes];
196+
const canAdd =
197+
availableScopes.includes(draftScope) &&
198+
(draftCompat !== undefined || draftChannel !== undefined);
199+
200+
const addOverride = () => {
201+
if (!canAdd) return;
202+
const entry: WorkerQueueEnvironmentOverride = {};
203+
if (draftCompat !== undefined) entry.compat = draftCompat;
204+
if (draftChannel !== undefined) entry.channel = draftChannel;
205+
onChange({ ...value, [draftScope]: entry });
206+
setDraftScope(UNSET_VALUE);
207+
setDraftCompat(undefined);
208+
setDraftChannel(undefined);
209+
};
210+
211+
return (
212+
<div className="grid grid-cols-[minmax(0,1fr)_auto_auto_auto] items-center gap-x-2 gap-y-1.5">
213+
<div className="text-xs text-text-dimmed">scope</div>
214+
<div className="text-xs text-text-dimmed">compat</div>
215+
<div className="text-xs text-text-dimmed">channel</div>
216+
<div />
217+
218+
{orderedOverrideScopes(value, environments).map((scope) => {
219+
const entry = value[scope];
220+
const incomplete = entry.compat === undefined && entry.channel === undefined;
221+
return (
222+
<div key={scope} className="contents">
223+
<div
224+
className={cn("truncate text-sm", incomplete ? "text-error" : "text-text-bright")}
225+
title={incomplete ? "Set compat or channel, or remove this row" : scope}
226+
>
227+
{label(scope)}
228+
</div>
229+
<EnumControl
230+
value={entry.compat}
231+
options={compatOptions}
232+
onChange={(val) =>
233+
onChange(
234+
setOverrideField(value, scope, "compat", fromEnumValue<NonNullable<Compat>>(val))
235+
)
236+
}
237+
dimmed={false}
238+
/>
239+
<EnumControl
240+
value={entry.channel}
241+
options={channelOptions}
242+
onChange={(val) =>
243+
onChange(
244+
setOverrideField(
245+
value,
246+
scope,
247+
"channel",
248+
fromEnumValue<NonNullable<Channel>>(val)
249+
)
250+
)
251+
}
252+
dimmed={false}
253+
/>
254+
<Button variant="minimal/small" onClick={() => onChange(removeOverride(value, scope))}>
255+
remove
256+
</Button>
257+
</div>
258+
);
259+
})}
260+
261+
<Select
262+
variant="tertiary/small"
263+
value={draftScope}
264+
setValue={setDraftScope}
265+
items={scopeItems}
266+
filter={(item, search) => label(item).toLowerCase().includes(search.toLowerCase())}
267+
text={(val) => (val === UNSET_VALUE ? "add environment..." : label(val))}
268+
>
269+
{(items) =>
270+
items.map((item) => (
271+
<SelectItem key={item} value={item}>
272+
{item === UNSET_VALUE ? "none" : label(item)}
273+
</SelectItem>
274+
))
275+
}
276+
</Select>
277+
<EnumControl
278+
value={draftCompat}
279+
options={compatOptions}
280+
onChange={(val) => setDraftCompat(fromEnumValue<NonNullable<Compat>>(val))}
281+
dimmed={draftScope === UNSET_VALUE}
282+
/>
283+
<EnumControl
284+
value={draftChannel}
285+
options={channelOptions}
286+
onChange={(val) => setDraftChannel(fromEnumValue<NonNullable<Channel>>(val))}
287+
dimmed={draftScope === UNSET_VALUE}
288+
/>
289+
<Button variant="secondary/small" onClick={addOverride} disabled={!canAdd}>
290+
add
291+
</Button>
292+
</div>
293+
);
294+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {
2+
WORKER_QUEUE_OVERRIDE_ENVIRONMENT_TYPES,
3+
type WorkerQueueEnvironmentOverride,
4+
} from "~/v3/featureFlags";
5+
6+
export type WorkerQueueEnvironmentOverrides = Record<string, WorkerQueueEnvironmentOverride>;
7+
8+
export type OverrideEnvironment = {
9+
id: string;
10+
type: string;
11+
branchName: string | null;
12+
projectName: string;
13+
};
14+
15+
const ENVIRONMENT_TYPE_LABELS: Record<string, string> = {
16+
PRODUCTION: "Production",
17+
STAGING: "Staging",
18+
PREVIEW: "Preview",
19+
};
20+
21+
export function overrideScopeLabel(
22+
scope: string,
23+
environments: readonly OverrideEnvironment[]
24+
): string {
25+
const typeLabel = ENVIRONMENT_TYPE_LABELS[scope];
26+
if (typeLabel) {
27+
return `All ${typeLabel.toLowerCase()}`;
28+
}
29+
30+
const environment = environments.find((environment) => environment.id === scope);
31+
if (!environment) {
32+
return `Unknown environment (${scope})`;
33+
}
34+
35+
const name = ENVIRONMENT_TYPE_LABELS[environment.type] ?? environment.type;
36+
return environment.branchName
37+
? `${environment.projectName} / ${name} (${environment.branchName})`
38+
: `${environment.projectName} / ${name}`;
39+
}
40+
41+
export function orderedOverrideScopes(
42+
overrides: WorkerQueueEnvironmentOverrides,
43+
environments: readonly OverrideEnvironment[]
44+
): string[] {
45+
const known = [
46+
...WORKER_QUEUE_OVERRIDE_ENVIRONMENT_TYPES,
47+
...environments.map((environment) => environment.id),
48+
];
49+
const scopes = Object.keys(overrides);
50+
return [
51+
...known.filter((scope) => scopes.includes(scope)),
52+
...scopes.filter((scope) => !known.includes(scope)).sort(),
53+
];
54+
}
55+
56+
export function availableOverrideScopes(
57+
overrides: WorkerQueueEnvironmentOverrides,
58+
environments: readonly OverrideEnvironment[]
59+
): string[] {
60+
return [
61+
...WORKER_QUEUE_OVERRIDE_ENVIRONMENT_TYPES,
62+
...environments.map((environment) => environment.id),
63+
].filter((scope) => !(scope in overrides));
64+
}
65+
66+
export function setOverrideField<K extends keyof WorkerQueueEnvironmentOverride>(
67+
overrides: WorkerQueueEnvironmentOverrides,
68+
scope: string,
69+
field: K,
70+
value: WorkerQueueEnvironmentOverride[K] | undefined
71+
): WorkerQueueEnvironmentOverrides {
72+
const entry = { ...overrides[scope] };
73+
if (value === undefined) {
74+
delete entry[field];
75+
} else {
76+
entry[field] = value;
77+
}
78+
return { ...overrides, [scope]: entry };
79+
}
80+
81+
export function removeOverride(
82+
overrides: WorkerQueueEnvironmentOverrides,
83+
scope: string
84+
): WorkerQueueEnvironmentOverrides | undefined {
85+
const next = { ...overrides };
86+
delete next[scope];
87+
return Object.keys(next).length === 0 ? undefined : next;
88+
}
89+
90+
export function incompleteOverrideScopes(overrides: WorkerQueueEnvironmentOverrides): string[] {
91+
return Object.entries(overrides)
92+
.filter(([, entry]) => entry.compat === undefined && entry.channel === undefined)
93+
.map(([scope]) => scope);
94+
}

0 commit comments

Comments
 (0)