| 1 | /** |
| 2 | * Repro rig for the "green destroy but leaked state" race (leaked |
| 3 | * rpchttpteststack-* / rpcworkerbindingstack-* workers in the 2026-08-05 |
| 4 | * full-suite run): a `destroy(Stack)` whose plan printed "no changes" |
| 5 | * even though the localState stage dir had committed rows on disk. |
| 6 | * |
| 7 | * Mirrors the Test/Alchemy harness topology exactly: each simulated "file" |
| 8 | * loops deploy -> destroy of its own `Alchemy.Stack` with `localState()`, |
| 9 | * through Core.deploy / Core.destroy / Core.toEffect with a shared scope, |
| 10 | * with K files running concurrently to simulate full-suite load. |
| 11 | * |
| 12 | * An anomaly is any iteration where, after a successful destroy, the |
| 13 | * stage directory still exists with state rows in it. |
| 14 | * |
| 15 | * Run from packages/alchemy: |
| 16 | * bun test/State/destroy-consistency-repro.ts [files] [iterations] |
| 17 | */ |
| 18 | import * as Alchemy from "@/index.ts"; |
| 19 | import * as Core from "@/Test/Core.ts"; |
| 20 | import * as Effect from "effect/Effect"; |
| 21 | import * as Exit from "effect/Exit"; |
| 22 | import * as Scope from "effect/Scope"; |
| 23 | import { existsSync, readdirSync } from "node:fs"; |
| 24 | import * as path from "node:path"; |
| 25 | import { Bucket, TestLayers } from "../test.resources.ts"; |
| 26 | |
| 27 | const FILES = Number(process.argv[2] ?? 16); |
| 28 | const ITERATIONS = Number(process.argv[3] ?? 50); |
| 29 | |
| 30 | const stateRoot = path.join(process.cwd(), ".alchemy", "state"); |
| 31 | |
| 32 | let anomalies = 0; |
| 33 | let completed = 0; |
| 34 | |
| 35 | const simulateFile = async (fileIdx: number) => { |
| 36 | const stackName = `DestroyRaceRepro${fileIdx}`; |
| 37 | const stack = Alchemy.Stack( |
| 38 | stackName, |
| 39 | { |
| 40 | providers: TestLayers(), |
| 41 | state: Alchemy.localState(), |
| 42 | }, |
| 43 | Effect.gen(function* () { |
| 44 | const bucket = yield* Bucket(`bucket`, { name: `repro-${fileIdx}` }); |
| 45 | return { arn: bucket.bucketArn.as<string>() }; |
| 46 | }), |
| 47 | ); |
| 48 | const options: Core.MakeOptions = { providers: TestLayers() }; |
| 49 | |
| 50 | for (let i = 0; i < ITERATIONS; i++) { |
| 51 | // fresh scope per iteration, like a fresh test file |
| 52 | const scope = Scope.makeUnsafe("sequential"); |
| 53 | const run = <A>(eff: Core.TestEffect<A>) => |
| 54 | Effect.runPromise(Core.toEffect(eff, options, scope) as Effect.Effect<A>); |
| 55 | try { |
| 56 | await run(Core.deploy(options, stack as any, { scope })); |
| 57 | const stageDir = path.join(stateRoot, stackName, "test"); |
| 58 | if (!existsSync(stageDir) || readdirSync(stageDir).length === 0) { |
| 59 | anomalies++; |
| 60 | console.error( |
| 61 | `ANOMALY[deploy] iter=${i} file=${fileIdx}: state dir missing/empty after deploy`, |
| 62 | ); |
| 63 | } |
| 64 | await run(Core.destroy(options, stack as any, { scope })); |
| 65 | if (existsSync(stageDir)) { |
| 66 | const rows = readdirSync(stageDir); |
| 67 | anomalies++; |
| 68 | console.error( |
| 69 | `ANOMALY[destroy] iter=${i} file=${fileIdx}: stage dir survived destroy: ${JSON.stringify(rows)}`, |
| 70 | ); |
| 71 | } |
| 72 | } finally { |
| 73 | await Effect.runPromise( |
| 74 | Effect.suspend(() => Scope.close(scope, Exit.void)).pipe(Effect.ignore), |
| 75 | ); |
| 76 | } |
| 77 | completed++; |
| 78 | if (completed % 100 === 0) { |
| 79 | console.log( |
| 80 | `progress: ${completed}/${FILES * ITERATIONS} anomalies=${anomalies}`, |
| 81 | ); |
| 82 | } |
| 83 | } |
| 84 | }; |
| 85 | |
| 86 | const t0 = Date.now(); |
| 87 | await Promise.all(Array.from({ length: FILES }, (_, i) => simulateFile(i))); |
| 88 | console.log( |
| 89 | `done: ${FILES * ITERATIONS} deploy/destroy cycles in ${((Date.now() - t0) / 1000).toFixed(1)}s, anomalies=${anomalies}`, |
| 90 | ); |
| 91 | process.exit(anomalies > 0 ? 1 : 0); |