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(),