From 87a9de2eb569ae941cb990198d12371719243192 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Mon, 24 Aug 2026 10:01:37 +0100 Subject: [PATCH] refactor(webapp): drop the mint-shard ceiling env var before it ships RUN_OPS_MINT_SHARDS was added on this branch and never deployed, so there is nothing to keep compatible. It duplicated information the shard descriptors will own: a descriptor names every key this deployment can route, so a second hand-maintained list only gives the two a way to disagree. Its only job was to stop the list naming a key with no configured database. Nothing here mints, so that cannot happen yet, and by the time it can the descriptors exist and are the right source. Bounding the list belongs with them. The list flag alone is now the gate. Unset or empty means today's behaviour, which is the state of every deployment that has not set it, so this stays inert on merge. env.server.ts is untouched by this branch again. Dependency this creates: the change that carries a shard key into an id must not land before the descriptors bound the list, or it must bound the list itself. --- apps/webapp/app/env.server.ts | 25 ------ .../runOpsMintShard.server.test.ts | 81 +++---------------- .../runOpsMigration/runOpsMintShard.server.ts | 33 ++------ 3 files changed, 14 insertions(+), 125 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 851454acb..c91793061 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -4,7 +4,6 @@ import { BoolEnv } from "./utils/boolEnv"; import { isValidDatabaseUrl } from "./utils/db"; import { isValidRegex } from "./utils/regex"; import { isValidDuration } from "./services/realtime/duration.server"; -import { parseShardCsv } from "./v3/runOpsMigration/mintShardGrace"; // `z.string()` constrained to a `parseDuration`-parseable string (e.g. // `7d`, `1h`). Validated at boot so a typo'd duration fails fast. @@ -42,23 +41,6 @@ const parseMachinePresetCsv = (raw: string, ctx: z.RefinementCtx): MachinePreset return out; }; -// A CSV of gen-2 mint shard keys, validated at boot by parseShardCsv. Kept as the raw string: -// the resolution is built once in runOpsMintShard.server.ts, and this only has to fail fast. -const shardCsvString = () => - z - .string() - .default("") - .superRefine((raw, ctx) => { - try { - parseShardCsv(raw); - } catch (error) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: error instanceof Error ? error.message : "invalid shard key CSV", - }); - } - }); - const GithubAppEnvSchema = z.preprocess( (val) => { const obj = val as any; @@ -2016,13 +1998,6 @@ const EnvironmentSchema = z // (stale or fresh) resolves to the same kind for the whole window. See mintFlipGrace.ts. RUN_OPS_MINT_FLIP_GRACE_MS: z.coerce.number().int().default(90_000), - // Gen-2 mint shards — CSV of single-char [a-z0-9] keys this deployment can mint roots into. - // Unset or empty means no gen-2 minting, which is today's behaviour. Validated at boot: an - // invalid key would mint an id that cannot be routed. This is a CEILING, not the live list: - // it changes only by deploy, and the runOpsMintShardSet flag selects from it at runtime. - // A rolling deploy runs two values of this var at once, so it must never be the ramp lever. - RUN_OPS_MINT_SHARDS: shardCsvString(), - // Session replication (Postgres → ClickHouse sessions_v1). Shares Redis // with the runs replicator for leader locking but has its own slot and // publication so the two consume independently. diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.ts index 322c1322c..771e52487 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.test.ts @@ -21,15 +21,12 @@ function envIds(count: number): string[] { return ids; } -const ALL_KEYS = "abcdefghijklmnopqrstuvwxyz0123456789".split(""); - function deps( resolution: MintShardSetResolution, overrides: Partial = {} ): MintShardDeps { return { resolution, - ceiling: ALL_KEYS, nowMs: T + GRACE_MS + 1, graceMs: GRACE_MS, orgFeatureFlags: undefined, @@ -54,23 +51,9 @@ describe("computeMintShard — the no-shards answer", () => { expect(computeMintShard({ id: "env_1" }, deps({ set: [] }))).toBe("new"); }); - it("returns new when the deployment configures no ceiling", () => { - // An unconfigured deployment is an unconditional kill switch, whatever the stored list says. - const resolution: MintShardSetResolution = { set: ["a", "b"] }; - expect(computeMintShard({ id: "env_1" }, deps(resolution, { ceiling: [] }))).toBe("new"); - }); - - it("returns new when the stored list names nothing this deployment can route", () => { - const resolution: MintShardSetResolution = { set: ["z"] }; - expect(computeMintShard({ id: "env_1" }, deps(resolution, { ceiling: ["a"] }))).toBe("new"); - }); - - it("returns new when the ceiling is empty even with a stale stamp present", () => { - const resolution: MintShardSetResolution = { set: [], prevSet: ["a"], flippedAtMs: T }; - // The ceiling gate MUST run before the grace, so no stored value can reopen a closed switch. - expect(computeMintShard({ id: "env_1" }, deps(resolution, { nowMs: T + 1, ceiling: [] }))).toBe( - "new" - ); + it("returns new when the grace serves an empty list", () => { + const resolution: MintShardSetResolution = { set: ["a"], prevSet: [], flippedAtMs: T }; + expect(computeMintShard({ id: "env_1" }, deps(resolution, { nowMs: T + 1 }))).toBe("new"); }); it("returns new when the grace serves an empty prevSet", () => { @@ -269,47 +252,11 @@ describe("computeMintShard — rendezvous properties", () => { }); }); -describe("computeMintShard — the ceiling bounds the stored list", () => { - it("mints only into keys the deployment can route", () => { - const resolution: MintShardSetResolution = { set: ["a", "b", "c"] }; - const ids = envIds(300); - for (const id of ids) { - const shard = computeMintShard({ id }, deps(resolution, { ceiling: ["a", "b"] })); - expect(["a", "b"]).toContain(shard); - } - }); - - it("ignores a pin to a key outside the ceiling", () => { - const resolution: MintShardSetResolution = { set: ["a", "c"] }; - const rejected: string[] = []; - const shard = computeMintShard( - { id: "env_1" }, - deps(resolution, { - ceiling: ["a"], - orgFeatureFlags: { runOpsMintShard: "c" }, - onPinRejected: (info) => rejected.push(info.pin), - }) - ); - expect(shard).toBe("a"); - expect(rejected).toEqual(["c"]); - }); - - it("still honours a gen-1 pin when the ceiling is narrower than the stored list", () => { - const resolution: MintShardSetResolution = { set: ["a", "b"] }; - const shard = computeMintShard( - { id: "env_1" }, - deps(resolution, { ceiling: ["a"], orgFeatureFlags: { runOpsMintShard: "new" } }) - ); - expect(shard).toBe("new"); - }); -}); - describe("resolveMintShardWith — cache, ceiling short-circuit and fail-safe", () => { function wrapperDeps( overrides: Partial = {} ): ResolveMintShardDeps & { reads: number } { const state = { - ceiling: ["a", "b"], readFlags: async () => ({ runOpsMintShardSet: "a,b" }), cache: { current: undefined as MintShardCache }, nowMs: T, @@ -327,12 +274,6 @@ describe("resolveMintShardWith — cache, ceiling short-circuit and fail-safe", return state; } - it("never reads the list when the deployment configures no ceiling", async () => { - const deps = wrapperDeps({ ceiling: [] }); - expect(await resolveMintShardWith({ id: "env_1" }, deps)).toBe("new"); - expect(deps.reads).toBe(0); - }); - it("reads once, then serves the cache until the TTL expires", async () => { const deps = wrapperDeps(); await resolveMintShardWith({ id: "env_1" }, deps); @@ -368,11 +309,8 @@ describe("resolveMintShardWith — cache, ceiling short-circuit and fail-safe", expect(["a", "b"]).toContain(await resolveMintShardWith({ id: "env_1" }, deps)); }); - it("returns gen-1 when the stored list names nothing inside the ceiling", async () => { - const deps = wrapperDeps({ - ceiling: ["a"], - readFlags: async () => ({ runOpsMintShardSet: "z" }), - }); + it("returns gen-1 when the stored list is empty", async () => { + const deps = wrapperDeps({ readFlags: async () => ({ runOpsMintShardSet: "" }) }); expect(await resolveMintShardWith({ id: "env_1" }, deps)).toBe("new"); }); @@ -383,7 +321,6 @@ describe("resolveMintShardWith — cache, ceiling short-circuit and fail-safe", { id: "env_1" }, { resolution: { set: ["a", "b"] }, - ceiling: ["a", "b"], nowMs: T, graceMs: GRACE_MS, orgFeatureFlags: undefined, @@ -451,9 +388,9 @@ describe("computeMintShard — the global override wins the complete cutover", ( } }); - it("cannot resurrect minting when the ceiling is empty", () => { - expect( - computeMintShard({ id: "env_1" }, deps(resolution, { globalOverride: "b", ceiling: [] })) - ).toBe("new"); + it("cannot resurrect minting when the list is empty", () => { + expect(computeMintShard({ id: "env_1" }, deps({ set: [] }, { globalOverride: "b" }))).toBe( + "new" + ); }); }); diff --git a/apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts b/apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts index 6f2ea5a59..e7b9e2af8 100644 --- a/apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts +++ b/apps/webapp/app/v3/runOpsMigration/runOpsMintShard.server.ts @@ -8,7 +8,6 @@ import { effectiveMintShardSet, GEN_1_PIN_VALUE, isValidPinValue, - parseShardCsv, readMintShardSetResolution, type MintShardSetResolution, } from "./mintShardGrace"; @@ -16,8 +15,6 @@ import { export type MintShardDeps = { // The live list, from the control-plane database. resolution: MintShardSetResolution; - // The keys this deployment can route, from the environment. Bounds the live list. - ceiling: string[]; // Fleet-wide pin that beats every per-org and per-env pin. The complete-cutover lever. globalOverride?: unknown; nowMs: number; @@ -89,20 +86,15 @@ function hrwSelect(environmentId: string, activeSet: string[]): string { // PURE CORE — no env, no clock, no I/O; tests drive this directly. Deterministic for fixed // deps, which is what lets run minting and token minting agree on one answer. // -// The ceiling gate runs BEFORE the grace, so an unconfigured deployment is an unconditional -// kill switch that no stored value can reopen. The live list is then intersected with the -// ceiling, so a stored key this deployment cannot route is never minted into. +// An empty list is the off state, and it is the state of every deployment that has not set the +// flag. Bounding the list against the shard keys this deployment can actually route belongs with +// the shard descriptors, which own that information; nothing here mints, so nothing can misroute. // // A pin outside the active set falls through to the hash rather than throwing: honouring it // would leak the drain the active list performs, and throwing would fail customer triggers // whenever a pinned shard drains. export function computeMintShard(environment: { id: string }, deps: MintShardDeps): ShardKey { - if (deps.ceiling.length === 0) { - return "new"; - } - - const live = effectiveMintShardSet(deps.resolution, deps.nowMs, deps.graceMs); - const activeSet = live.filter((key) => deps.ceiling.includes(key)); + const activeSet = effectiveMintShardSet(deps.resolution, deps.nowMs, deps.graceMs); if (activeSet.length === 0) { return "new"; } @@ -134,13 +126,6 @@ export function computeMintShard(environment: { id: string }, deps: MintShardDep return hrwSelect(environment.id, activeSet); } -// ENV-BOUND wrapper — the only place env is read. The ceiling is parsed once at boot; it is a -// deploy-time value, so re-parsing per mint would burn CPU on the hottest path in the system. -// -// SEAM: this is the one place the ceiling is sourced. Once shard descriptors are configured, the -// ceiling becomes their keys and RUN_OPS_MINT_SHARDS is deleted. Two hand-kept lists would drift. -const ceiling: string[] = parseShardCsv(env.RUN_OPS_MINT_SHARDS); - // Read together so the override costs no extra query beyond the list it is bounded by. const GLOBAL_SHARD_KEYS = [ FEATURE_FLAG.runOpsMintShardSet, @@ -154,8 +139,7 @@ type GlobalShardConfig = { resolution: MintShardSetResolution; override: unknown export type MintShardCache = { value: GlobalShardConfig; expiresAt: number } | undefined; export type ResolveMintShardDeps = { - ceiling: string[]; - // Reads the three list rows. Injected so the cache and the fail-safe are testable without a + // Reads the list rows. Injected so the cache and the fail-safe are testable without a // database, the same way computeRunIdMintKind takes its flag reader. readFlags: () => Promise>; cache: { current: MintShardCache }; @@ -177,11 +161,6 @@ export async function resolveMintShardWith( environment: { id: string; orgFeatureFlags?: unknown }, deps: ResolveMintShardDeps ): Promise { - // No ceiling means no gen-2 minting, so skip the read entirely. - if (deps.ceiling.length === 0) { - return "new"; - } - let config: GlobalShardConfig; const cached = deps.cache.current; if (cached && cached.expiresAt > deps.nowMs) { @@ -202,7 +181,6 @@ export async function resolveMintShardWith( return computeMintShard(environment, { resolution: config.resolution, - ceiling: deps.ceiling, globalOverride: config.override, nowMs: deps.nowMs, graceMs: deps.graceMs, @@ -251,7 +229,6 @@ export async function resolveMintShard(environment: { orgFeatureFlags?: unknown; }): Promise { return resolveMintShardWith(environment, { - ceiling, readFlags: readSetFlags, cache: liveCache, nowMs: Date.now(),