From bbb80165db3d22cf6a6319189e2469aa25d1c02f Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:45:33 +0300 Subject: [PATCH 1/5] test: stabilise the Miniflare D1 suites (#190) Both CI flakes in #190 were one Miniflare 4.20260730.0 bug: the synchronous binding proxy (`D1Database#prepare`/`bind`) waits on a shared 0/1 flag, so a late notify from the previous reply wakes the next call before its reply exists. The assert at fetch-sync.ts:147 fails and every later call reads the previous reply (the 1-3 ms cascade in roles.test.ts). The list-values "statement budget" failure has the same stack. Every async proxy call also went through dispatchFetch with undici's `reset`, one TIME_WAIT port per D1 query (2,000 queries left 2,002), which ran macOS out of ephemeral ports under parallel D1 files. No newer Miniflare 4.x exists, so patch the pin with the upstream fixes merged for Miniflare 5 only: workers-sdk#15552 (generation handshake) and #15781 (keep-alive dispatchFetch, 1 s idle), plus the same 1 s keep-alive on the sync proxy's pool (#16445 A). No retries, D1 files stay parallel. - patches/miniflare@4.20260730.0.patch via patchedDependencies - packages/storage/test/miniflare-patch.test.ts fails without the patch - .github/workflows/d1-stress.yml runs the Miniflare halves 20 times - ADR-0012 addendum, AGENTS.md note Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .github/workflows/d1-stress.yml | 61 +++++++++++++ AGENTS.md | 4 +- docs/adr/0012-cloudflare-deployment.md | 10 +++ packages/storage/test/miniflare-patch.test.ts | 46 ++++++++++ patches/miniflare@4.20260730.0.patch | 85 +++++++++++++++++++ pnpm-lock.yaml | 15 ++-- pnpm-workspace.yaml | 6 ++ 7 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/d1-stress.yml create mode 100644 packages/storage/test/miniflare-patch.test.ts create mode 100644 patches/miniflare@4.20260730.0.patch diff --git a/.github/workflows/d1-stress.yml b/.github/workflows/d1-stress.yml new file mode 100644 index 00000000..f7d9116c --- /dev/null +++ b/.github/workflows/d1-stress.yml @@ -0,0 +1,61 @@ +name: D1 stress + +# #190: runs the Miniflare halves (D1, Durable Object SQLite, R2) of every package that tests with +# the pinned Miniflare, many times on fresh runners, under the same load as CI +# (`turbo --concurrency=2`). Run it by hand after a Miniflare bump or a change to its patch +# (ADR-0012, addendum 2026-11-10); pull requests that touch the patch run it on their own. + +on: + workflow_dispatch: + inputs: + runs: + description: Runs per Node version + type: number + default: 10 + pull_request: + paths: + - patches/miniflare@*.patch + - .github/workflows/d1-stress.yml + +permissions: + contents: read + +concurrency: + group: d1-stress-${{ github.ref }} + cancel-in-progress: true + +jobs: + plan: + runs-on: ubuntu-latest + outputs: + runs: ${{ steps.runs.outputs.runs }} + steps: + - id: runs + env: + RUNS: ${{ inputs.runs || 10 }} + run: echo "runs=$(node -e 'console.log(JSON.stringify(Array.from({ length: Math.min(Number(process.env.RUNS), 50) }, (_, i) => i + 1)))')" >> "$GITHUB_OUTPUT" + + d1: + name: D1 halves (Node ${{ matrix.node }}, run ${{ matrix.run }}) + needs: plan + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + node: [24, 26] + run: ${{ fromJSON(needs.plan.outputs.runs) }} + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node }} + cache: pnpm + - run: pnpm install --frozen-lockfile + # Same scheduler load as CI's test step; `-t` keeps only the suites labelled "(Miniflare)". + - run: >- + pnpm turbo run test --concurrency=2 + --filter=@cloudhal/storage --filter=@cloudhal/core-ts --filter=@cloudhal/api + --filter=@cloudhal/schedules --filter=@cloudhal/comments --filter=@cloudhal/analytics + -- -t '\(Miniflare\)' diff --git a/AGENTS.md b/AGENTS.md index eb10029d..65236a84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ pnpm i18n:export / pnpm i18n:import [--locale xx] # translations as nested ICU - `packages/api` — Hono app (`createApi`), Better Auth (`src/auth.ts`), error mapping; shared suite `src/testing.ts` on sqlite + D1. Build chat is `build-turn.ts`, its model eval `build-eval.ts`; `design-pass.ts` is RFC-0021 21c (Build chat looks at its render, the eval's vision-judged score). - `packages/ui` — `export * from "@cloudflare/kumo"` + blocks; `styles.css` is the only Tailwind entry for admin UIs. Heavy kits have lazy subpaths: `./charts` (ECharts), `./editor` (the Tiptap markdown body editor, RFC-0014). - `packages/messages` — RFC-0032 / ADR-0032: the ICU MessageFormat 1 subset of Studio's catalogs (`plural`, `select`, `selectordinal`, `number`). `./compile` (build-time only: the Vite plugin `./vite`, the type generator, tests; `bundle-budget.ts` keeps it out of every chunk) and `./format` (Worker-safe runtime, cached `Intl.PluralRules`/`NumberFormat`), which is also Studio's `Intl` layer: `formatNumber`, `formatDate`, `formatRelative`, `formatList` and cached `numberFormat`/`dateTimeFormat`/`relativeTimeFormat`/`displayNames`/`collator` — screens and blocks never build `Intl` formatters themselves (lint `i18n-format`). Catalogs compile to data, never code. -- `patches/` — `pnpm patch` files (`patchedDependencies` in `pnpm-workspace.yaml`). `@cloudflare__kumo@2.14.0.patch` is **generated** by `node scripts/kumo-patch.ts`: the hand-written changes in `patches/kumo/*.patch` (`tooltip-timestamp.patch` adds `TimeseriesChart`'s `tooltipTimestampFormat`, DESIGN.md §2v "Kumo locale") plus every physical utility in Kumo's `dist` made logical (ADR-0033 A1). Never edit it; on a Kumo upgrade rename it and the `patchedDependencies` entry, re-run the script, `pnpm install`, re-check the gaps in `docs/design/kumo-upstream-notes.md`. `packages/ui/test/kumo-rtl.test.tsx` fails when the installed Kumo is not patched. +- `patches/` — `pnpm patch` files (`patchedDependencies` in `pnpm-workspace.yaml`). `@cloudflare__kumo@2.14.0.patch` is **generated** by `node scripts/kumo-patch.ts`: the hand-written changes in `patches/kumo/*.patch` (`tooltip-timestamp.patch` adds `TimeseriesChart`'s `tooltipTimestampFormat`, DESIGN.md §2v "Kumo locale") plus every physical utility in Kumo's `dist` made logical (ADR-0033 A1). Never edit it; on a Kumo upgrade rename it and the `patchedDependencies` entry, re-run the script, `pnpm install`, re-check the gaps in `docs/design/kumo-upstream-notes.md`. `packages/ui/test/kumo-rtl.test.tsx` fails when the installed Kumo is not patched. `miniflare@4.20260730.0.patch` backports Miniflare 5's binding-proxy fixes (workers-sdk#15552, #15781) that the D1 harness needs (#190, ADR-0012); `packages/storage/test/miniflare-patch.test.ts` fails without it. - `apps/studio` — SPA; routes in `src/router.tsx`, screens in `src/routes`, typed API client `src/lib/api.ts` (a barrel over `src/lib/api/.ts`), i18n in `src/i18n` (messages are ICU MF1, compiled at build time, with parameter types generated into `src/i18n/generated/params.ts` by `pnpm gen` — never edit it; languages registered in `locales.ts`; add keys to `en.ts` and every `.ts`, or to a lazy namespace `src/i18n/lazy/..ts` (the list and how to add one: `src/i18n/namespaces.ts`; DESIGN.md §1); only the active language of the core catalogs loads, `tr.ts` through `loadLocale`, and each other language's lazy-namespace loaders come with it from the generated `src/i18n/loaders/.ts`; Arabic and Persian format with Western digits through the registry's `intl` tag). - `docs/adr/` decisions (with sources) · `docs/rfcs/` proposals · `docs/design/DESIGN.md` the binding admin design system (Kumo). - `.agents/skills/` — skills for you: `contract-change`, `add-capability`, `write-rfc`, `studio-ui`, `translate`, and Cloudflare's official **`kumo-design`** (installed from `cloudflare/kumo`, tracked in `skills-lock.json`; update with `npx skills update`). Any admin-UI change loads `kumo-design` + `studio-ui` and follows `docs/design/DESIGN.md`. @@ -59,7 +59,7 @@ pnpm i18n:export / pnpm i18n:import [--locale xx] # translations as nested ICU 13. **Regexes over external input** (request paths and bodies, agent or editor markdown, uploads, fetched feeds) must be linear (#168, #174): no unanchored or end-anchored quantified runs (`/-+$/`, `/\s+$/`, `/\/+$/`), no lazy scans that rescan to the end when the closer is missing (`/[\s\S]*?<\/x>/`, `/<[^>]*>/`), no adjacent quantifiers over overlapping classes (`\s+[^:]+`, `\s*(.*)`). Use `trimChar`/`trimEndChar` (`@cloudhal/core-ts/names`), `trimEnd()`, `indexOf`/`lastIndexOf` scanners, and check lengths before patterns. A rewrite gets an equivalence test against the old regex and a 100k-character adversarial case. ## Versions (pinned via pnpm catalog / Cargo workspace) -Node 24 · pnpm 12 · TypeScript 7.0 · Biome 2.6 · Vitest 5 · Turborepo 2.11 · Rust 2024 edition · json-schema-to-typescript 16.0.0 · cargo-typify 0.8.0 · ajv 8.20 · jsonschema 0.60 · better-sqlite3 13 · miniflare 4.20260730 (last stable 4.x; its sharp overridden to 0.35.5; see ADR-0012) · wrangler 4.158 · @astrojs/cloudflare 14.4 · Astro 7.4 · Tailwind 4.3 · marked 18 · Hono 4.13 · Better Auth 1.7.12 · Kumo 2.14 · React 19.3 · TanStack Router 1.170 / Query 5.104 · Vite 8.3. +Node 24 · pnpm 12 · TypeScript 7.0 · Biome 2.6 · Vitest 5 · Turborepo 2.11 · Rust 2024 edition · json-schema-to-typescript 16.0.0 · cargo-typify 0.8.0 · ajv 8.20 · jsonschema 0.60 · better-sqlite3 13 · miniflare 4.20260730 (last stable 4.x, patched for #190; its sharp overridden to 0.35.5; see ADR-0012) · wrangler 4.158 · @astrojs/cloudflare 14.4 · Astro 7.4 · Tailwind 4.3 · marked 18 · Hono 4.13 · Better Auth 1.7.12 · Kumo 2.14 · React 19.3 · TanStack Router 1.170 / Query 5.104 · Vite 8.3. ## When unsure Read `docs/adr/README.md` and the relevant ADR; each ADR links its primary sources. If the change alters a contract or a public interface, write an RFC first (`.agents/skills/write-rfc`). diff --git a/docs/adr/0012-cloudflare-deployment.md b/docs/adr/0012-cloudflare-deployment.md index a3bfe8d7..6458533d 100644 --- a/docs/adr/0012-cloudflare-deployment.md +++ b/docs/adr/0012-cloudflare-deployment.md @@ -53,3 +53,13 @@ Decision 6 pins the direct `miniflare` devDependency (the D1, R2 and Durable Obj - **undici.** The undici versions Miniflare pins (7.28.0 in 4.20260730.0, 7.29.1 in the 5.x alpha) are accepted for local development and tests: Miniflare uses undici to reach its own workerd on localhost and, outside tests, to fetch Cloudflare's `cf.json` from workers.cloudflare.com, a trusted endpoint (set `CLOUDFLARE_CF_FETCH_ENABLED=false` to turn that off). The fixed 7.30.1 is a minor move inside `dispatchFetch`, which the D1 harness and Miniflare's binding proxy depend on (the Durable Object harness bypasses it since #116), so an override waits for its own serial test run. *Update 2026-11-08:* that run happened. #159 (Dependabot alerts #5–#32) added the override `"undici@<7.30.1": 7.30.2` to `pnpm-workspace.yaml`, so Miniflare's pinned 7.28.0 resolves to 7.30.2 in monorepo installs; the D1 and Durable Object harnesses passed on it. Like the sharp override it is not published, and it goes when Miniflare 4 is no longer pinned. Sources: https://github.com/advisories/GHSA-wq5f-xc86-pv6w · https://github.com/advisories/GHSA-rgj7-g3m4-5g8c · https://pnpm.io/settings#overrides · `npm view miniflare dist-tags`, `npm view sharp time`, `npm view wrangler time` (checked 2026-10-25) · Miniflare 4.20260730.0 `dist/src/index.js` (`imagesLocalFetcher`, `cfImageLocalFetcher`, `setupCf`). + +## Addendum (2026-11-10): patching the pinned Miniflare's binding proxy (#190) + +The D1 suites failed now and then on CI, never on sqlite or Durable Object SQLite, and passed on rerun. Both failures in #190 were the same assertion, `message?.id === id` at `miniflare/src/plugins/core/proxy/fetch-sync.ts:147`, reached from `D1Database#prepare`/`bind`, which Miniflare's binding proxy runs as synchronous calls. The "statement budget" test in `core-ts` was not a counting or timing failure: its stack ends in the same assertion. + +- **Cause.** `SynchronousFetcher` blocks the caller on a shared 0/1 flag: the worker thread posts the reply, stores 1 and notifies. If the worker is preempted between its store and its notify while the caller has already read the reply (the flag was 1 when it reached `Atomics.wait`), the late notify wakes the *next* call before its reply exists. `receiveMessageOnPort` returns `undefined`, the assert throws, and every later call reads the previous call's reply, which is the cascade of 1–3 ms failures in `roles.test.ts`. A loaded runner makes the preemption likely: `turbo --concurrency=2` runs two packages of up to three Vitest workers each (two in `@cloudhal/api`), and every D1 or Durable Object file runs its own workerd process plus the proxy's worker thread, on four vCPUs. The race reproduces in a standalone model of the handshake with a stall between the store and the notify (the shipped protocol desyncs at call 3, the fixed one runs 5,000 calls). Upstream: cloudflare/workers-sdk#15552, merged 2026-09-16 into Miniflare 5 only. +- **Ports.** Every asynchronous proxy call (`first`, `all`, `run`, `batch`) goes through `dispatchFetch`, which set undici's `reset` on every request: one new connection and one port in `TIME_WAIT` per query. 2,000 queries left 2,002 `TIME_WAIT` sockets on macOS, where `TIME_WAIT` lasts 30 s, so D1-heavy files in parallel ran out of ephemeral ports locally. Upstream: cloudflare/workers-sdk#15781 (fixes #15716), merged 2026-10-05 into Miniflare 5 only. The Durable Object harness already bypassed `dispatchFetch` for this reason (#116). +- **Decision.** There is no newer Miniflare 4.x (4.20260730.0 is still `latest`; 5.x is beta), so the pin stays and `patches/miniflare@4.20260730.0.patch` backports both fixes, plus a 1 s keep-alive on the synchronous proxy's undici pool so an idle socket closes before workerd's 5 s idle timeout (#16445, part A; the same setting #15781 gives the runtime pool). The patch is applied through `patchedDependencies` in `pnpm-workspace.yaml`, so it covers monorepo installs only. Projects that use `@cloudhal/storage/testing` with their own Miniflare 4 keep these bugs unless they apply the same patch. `packages/storage/test/miniflare-patch.test.ts` fails if the patch is missing: it checks the generation handshake in Miniflare's code and counts the connections that 200 D1 queries open (200 unpatched, 1 patched). No retries were added and D1 files still run in parallel. #16445 part B (a `FREE` racing an in-flight `batch([prepare()])`) is not patched: it needs a garbage collection during a workerd stall of over 100 ms, and it has not been seen in CI. When Miniflare 5 is stable, remove the patch together with the sharp and undici overrides. + +Sources: https://github.com/cloudflare/workers-sdk/pull/15552 · https://github.com/cloudflare/workers-sdk/pull/15781 · https://github.com/cloudflare/workers-sdk/issues/16445 · `npm view miniflare dist-tags` (checked 2026-11-10) · Miniflare 4.20260730.0 and 5.20261006.0-alpha `dist/src/index.js` (`SynchronousFetcher`, `DispatchFetchDispatcher`) · CI runs 42789152142 and 42793831919 (first attempts). diff --git a/packages/storage/test/miniflare-patch.test.ts b/packages/storage/test/miniflare-patch.test.ts new file mode 100644 index 00000000..39858a2d --- /dev/null +++ b/packages/storage/test/miniflare-patch.test.ts @@ -0,0 +1,46 @@ +/** + * #190: `patches/miniflare@4.20260730.0.patch` backports three upstream Miniflare fixes that the D1 + * harness depends on (ADR-0012, addendum 2026-11-10). This file fails when a Miniflare change + * arrives without them, or the patch did not apply: re-check the patch against the new release + * (drop it once the release carries cloudflare/workers-sdk#15552 and #15781) and `pnpm install`. + */ +import { subscribe, unsubscribe } from "node:diagnostics_channel"; +import { readFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { afterAll, describe, expect, it } from "vitest"; +import { createMiniflareD1 } from "../src/testing.ts"; + +const require = createRequire(import.meta.url); + +describe("Miniflare carries the #190 fixes (ADR-0012)", () => { + let d1: Awaited> | undefined; + afterAll(async () => { + await d1?.dispose(); + }); + + it("the synchronous proxy waits for its own reply's generation (workers-sdk#15552)", async () => { + // The race needs a preempted worker thread, so it cannot be forced from here: check the code. + const source = await readFile(require.resolve("miniflare"), "utf8"); + expect(source).toContain("/* generation */ (id + 1) | 0"); + expect(source).toContain("seen !== generation"); + expect(source).not.toMatch(/Atomics\.store\(\s*this\.#notifyHandle,\s*\/\* index \*\/\s*0,/); + }); + + it("async D1 calls reuse kept-alive connections (workers-sdk#15781)", async () => { + d1 = await createMiniflareD1(); + await d1.db.prepare("SELECT 1").first(); + // Unpatched, `dispatchFetch` closes the connection after every call: one new socket (and one + // port in TIME_WAIT) per query. Each idle socket closes after 1 s, so a few may reconnect. + let connected = 0; + const onConnect = () => { + connected++; + }; + subscribe("undici:client:connected", onConnect); + try { + for (let i = 0; i < 200; i++) await d1.db.prepare("SELECT ?1 AS n").bind(i).first(); + } finally { + unsubscribe("undici:client:connected", onConnect); + } + expect(connected).toBeLessThan(10); + }); +}); diff --git a/patches/miniflare@4.20260730.0.patch b/patches/miniflare@4.20260730.0.patch new file mode 100644 index 00000000..4e52fe7e --- /dev/null +++ b/patches/miniflare@4.20260730.0.patch @@ -0,0 +1,85 @@ +diff --git a/dist/src/index.js b/dist/src/index.js +index ae57410a970d7d47f9f1810567529df69e9d8df1..e46464411db851fafb5c2b4a582e1497e8e0da2f 100644 +--- a/dist/src/index.js ++++ b/dist/src/index.js +@@ -72975,7 +72975,10 @@ var DispatchFetchDispatcher = class extends undici.Dispatcher { + const headers = convertUndiciHeadersToStandard(options.headers ?? {}); + this.addHeaders(headers, path69); + options.headers = headers; +- options.reset = true; ++ // Cloudhal patch (cloudflare/workers-sdk#15781): reuse connections instead of one ++ // TIME_WAIT port per dispatch; never replay a request that may have reached the Worker. ++ options.idempotent = false; ++ options.reset = false; + return this.runtimeDispatcher.dispatch(options, handler); + } else { + return this.globalDispatcher.dispatch(options, handler); +@@ -92215,6 +92218,10 @@ port.addEventListener("message", async (event) => { + dispatcherUrl = url; + dispatcher = new Pool(new URL(url).origin, { + connect: { rejectUnauthorized: false }, ++ // Cloudhal patch (cloudflare/workers-sdk#16445): close idle sockets before ++ // workerd's 5s idle timeout, as the runtime dispatcher does since #15781. ++ keepAliveTimeout: 1000, ++ keepAliveMaxTimeout: 1000, + // Disable timeouts for local dev \u2014 long-running responses (streaming, + // slow uploads, long-polling) should not be killed by undici defaults. + headersTimeout: 0, +@@ -92247,7 +92254,10 @@ port.addEventListener("message", async (event) => { + port.postMessage({ id, error: new Error(String(error)) }); + } + } finally { +- Atomics.store(notifyHandle, /* index */ 0, /* value */ 1); ++ // Cloudhal patch (cloudflare/workers-sdk#15552): publish THIS request's generation ++ // (never 0), not a bare flag, so a late notify from the previous reply cannot wake the ++ // next call before its reply is queued. ++ Atomics.store(notifyHandle, /* index */ 0, /* generation */ (id + 1) | 0); + Atomics.notify(notifyHandle, /* index */ 0); + } + }); +@@ -92278,13 +92288,6 @@ var SynchronousFetcher = class { + } + fetch(url44, init2) { + this.#ensureWorker(); +- Atomics.store( +- this.#notifyHandle, +- /* index */ +- 0, +- /* value */ +- 0 +- ); + const id = this.#nextId++; + this.#channel.port1.postMessage({ + id, +@@ -92293,13 +92296,13 @@ var SynchronousFetcher = class { + headers: init2.headers, + body: init2.body + }); +- Atomics.wait( +- this.#notifyHandle, +- /* index */ +- 0, +- /* value */ +- 0 +- ); ++ // Cloudhal patch (cloudflare/workers-sdk#15552): a stale notify from the previous ++ // reply wakes the wait early; re-check the generation and sleep again. The worker ++ // stores the generation after posting, so seeing it means the reply is queued. ++ const generation = (id + 1) | 0; ++ for (let seen = Atomics.load(this.#notifyHandle, 0); seen !== generation; seen = Atomics.load(this.#notifyHandle, 0)) { ++ Atomics.wait(this.#notifyHandle, 0, seen); ++ } + const message = (0, import_node_worker_threads.receiveMessageOnPort)( + this.#channel.port1 + )?.message; +@@ -104369,6 +104372,10 @@ See https://developers.cloudflare.com/cloudflare-one/access-controls/service-cre + }); + this.#runtimeDispatcher = new import_undici9.Pool(this.#runtimeEntryURL, { + connect: { rejectUnauthorized: false }, ++ // Cloudhal patch (cloudflare/workers-sdk#15781): close idle client sockets ++ // before workerd's 5s idle timeout. ++ keepAliveTimeout: 1e3, ++ keepAliveMaxTimeout: 1e3, + // Disable timeouts for local dev — long-running responses (streaming, + // slow uploads, long-polling) should not be killed by undici defaults. + headersTimeout: 0, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cefe6306..bc12f192 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,6 +384,7 @@ overrides: patchedDependencies: '@cloudflare/kumo@2.14.0': 198f9be784beeb608a5d59b175d6085d7b4f0291567d57b089a85da1fef5f9d7 + miniflare@4.20260730.0: 78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96 importers: @@ -582,7 +583,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) typescript: specifier: 'catalog:' version: 7.0.2 @@ -670,7 +671,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) typescript: specifier: 'catalog:' version: 7.0.2 @@ -869,7 +870,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) typescript: specifier: 'catalog:' version: 7.0.2 @@ -934,7 +935,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) typescript: specifier: 'catalog:' version: 7.0.2 @@ -1045,7 +1046,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) rrule-temporal: specifier: 'catalog:' version: 2.2.8 @@ -1076,7 +1077,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) tsx: specifier: 'catalog:' version: 4.23.15 @@ -7920,7 +7921,7 @@ snapshots: min-indent@1.0.1: {} - miniflare@4.20260730.0(@types/node@26.6.5): + miniflare@4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5): dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.35.5(@types/node@26.6.5) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 289ad5ba..5eead922 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -108,5 +108,11 @@ overrides: "sharp@<0.35.5": 0.35.5 "undici@<7.30.1": 7.30.2 +# The Miniflare patch backports cloudflare/workers-sdk#15552 (the sync binding proxy's off-by-one +# desync, the `fetch-sync.ts:147` assertion), #15781 (one TIME_WAIT port per dispatchFetch) and the +# sync pool's keep-alive below workerd's 5 s idle timeout (#16445 A); both PRs are merged for +# Miniflare 5 only (#190, ADR-0012 addendum 2026-11-10). Not published, like the overrides. +# `packages/storage/test/miniflare-patch.test.ts` fails when it is missing. Remove with the 4.x pin. patchedDependencies: '@cloudflare/kumo@2.14.0': patches/@cloudflare__kumo@2.14.0.patch + miniflare@4.20260730.0: patches/miniflare@4.20260730.0.patch From 8928cccf8a3057c70d37a338461a4094e62462e3 Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:47:23 +0300 Subject: [PATCH 2/5] ci(d1-stress): fix the plan step's YAML Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .github/workflows/d1-stress.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/d1-stress.yml b/.github/workflows/d1-stress.yml index f7d9116c..b59f6bc0 100644 --- a/.github/workflows/d1-stress.yml +++ b/.github/workflows/d1-stress.yml @@ -33,7 +33,8 @@ jobs: - id: runs env: RUNS: ${{ inputs.runs || 10 }} - run: echo "runs=$(node -e 'console.log(JSON.stringify(Array.from({ length: Math.min(Number(process.env.RUNS), 50) }, (_, i) => i + 1)))')" >> "$GITHUB_OUTPUT" + run: | + echo "runs=$(jq -nc --argjson n "$RUNS" '[range(1; ([$n, 50] | min) + 1)]')" >> "$GITHUB_OUTPUT" d1: name: D1 halves (Node ${{ matrix.node }}, run ${{ matrix.run }}) From d62e9cfd4a50211da14bf8d651bcb343cb136eb0 Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:16:52 +0300 Subject: [PATCH 3/5] test: keep Miniflare's dispatchFetch as shipped; stress on a busy runner The #15781 backport made async proxy calls reuse sockets, but the timers that close idle ones run on the test's main thread. A stall past workerd's 5 s idle timeout then reuses a closed socket: ECONNRESET, not safely retryable (one local roles.test.ts run in 20; reproduced with a 5.5 s stall). Drop it and keep the shipped `reset`; the patch is now #15552 plus the sync pool's 1 s keep-alive, whose timers run on the proxy's own thread. The stress workflow adds one busy loop per vCPU: 20 unpatched runs at CI's own load passed, so it did not tell the builds apart. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- .github/workflows/d1-stress.yml | 23 ++++++++--- AGENTS.md | 2 +- docs/adr/0012-cloudflare-deployment.md | 5 ++- packages/storage/test/miniflare-patch.test.ts | 38 ++++--------------- patches/miniflare@4.20260730.0.patch | 37 ++++-------------- pnpm-lock.yaml | 16 ++++---- pnpm-workspace.yaml | 6 +-- 7 files changed, 46 insertions(+), 81 deletions(-) diff --git a/.github/workflows/d1-stress.yml b/.github/workflows/d1-stress.yml index b59f6bc0..443be835 100644 --- a/.github/workflows/d1-stress.yml +++ b/.github/workflows/d1-stress.yml @@ -12,6 +12,10 @@ on: description: Runs per Node version type: number default: 10 + hogs: + description: Busy loops beside the tests (0 = CI's own load) + type: number + default: 4 pull_request: paths: - patches/miniflare@*.patch @@ -54,9 +58,16 @@ jobs: node-version: ${{ matrix.node }} cache: pnpm - run: pnpm install --frozen-lockfile - # Same scheduler load as CI's test step; `-t` keeps only the suites labelled "(Miniflare)". - - run: >- - pnpm turbo run test --concurrency=2 - --filter=@cloudhal/storage --filter=@cloudhal/core-ts --filter=@cloudhal/api - --filter=@cloudhal/schedules --filter=@cloudhal/comments --filter=@cloudhal/analytics - -- -t '\(Miniflare\)' + # CI's test step (`turbo --concurrency=2`), plus one busy loop per vCPU: the proxy race needs a + # preempted thread, and an oversubscribed runner preempts far more often than a quiet one. + # `-t` keeps only the suites labelled "(Miniflare)". + - name: Miniflare halves on an oversubscribed runner + env: + HOGS: ${{ inputs.hogs || 4 }} + run: | + for i in $(seq "$HOGS"); do sh -c 'while :; do :; done' & done + trap 'kill $(jobs -p) 2>/dev/null' EXIT + pnpm turbo run test --concurrency=2 \ + --filter=@cloudhal/storage --filter=@cloudhal/core-ts --filter=@cloudhal/api \ + --filter=@cloudhal/schedules --filter=@cloudhal/comments --filter=@cloudhal/analytics \ + -- -t '\(Miniflare\)' diff --git a/AGENTS.md b/AGENTS.md index 65236a84..653be5ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ pnpm i18n:export / pnpm i18n:import [--locale xx] # translations as nested ICU - `packages/api` — Hono app (`createApi`), Better Auth (`src/auth.ts`), error mapping; shared suite `src/testing.ts` on sqlite + D1. Build chat is `build-turn.ts`, its model eval `build-eval.ts`; `design-pass.ts` is RFC-0021 21c (Build chat looks at its render, the eval's vision-judged score). - `packages/ui` — `export * from "@cloudflare/kumo"` + blocks; `styles.css` is the only Tailwind entry for admin UIs. Heavy kits have lazy subpaths: `./charts` (ECharts), `./editor` (the Tiptap markdown body editor, RFC-0014). - `packages/messages` — RFC-0032 / ADR-0032: the ICU MessageFormat 1 subset of Studio's catalogs (`plural`, `select`, `selectordinal`, `number`). `./compile` (build-time only: the Vite plugin `./vite`, the type generator, tests; `bundle-budget.ts` keeps it out of every chunk) and `./format` (Worker-safe runtime, cached `Intl.PluralRules`/`NumberFormat`), which is also Studio's `Intl` layer: `formatNumber`, `formatDate`, `formatRelative`, `formatList` and cached `numberFormat`/`dateTimeFormat`/`relativeTimeFormat`/`displayNames`/`collator` — screens and blocks never build `Intl` formatters themselves (lint `i18n-format`). Catalogs compile to data, never code. -- `patches/` — `pnpm patch` files (`patchedDependencies` in `pnpm-workspace.yaml`). `@cloudflare__kumo@2.14.0.patch` is **generated** by `node scripts/kumo-patch.ts`: the hand-written changes in `patches/kumo/*.patch` (`tooltip-timestamp.patch` adds `TimeseriesChart`'s `tooltipTimestampFormat`, DESIGN.md §2v "Kumo locale") plus every physical utility in Kumo's `dist` made logical (ADR-0033 A1). Never edit it; on a Kumo upgrade rename it and the `patchedDependencies` entry, re-run the script, `pnpm install`, re-check the gaps in `docs/design/kumo-upstream-notes.md`. `packages/ui/test/kumo-rtl.test.tsx` fails when the installed Kumo is not patched. `miniflare@4.20260730.0.patch` backports Miniflare 5's binding-proxy fixes (workers-sdk#15552, #15781) that the D1 harness needs (#190, ADR-0012); `packages/storage/test/miniflare-patch.test.ts` fails without it. +- `patches/` — `pnpm patch` files (`patchedDependencies` in `pnpm-workspace.yaml`). `@cloudflare__kumo@2.14.0.patch` is **generated** by `node scripts/kumo-patch.ts`: the hand-written changes in `patches/kumo/*.patch` (`tooltip-timestamp.patch` adds `TimeseriesChart`'s `tooltipTimestampFormat`, DESIGN.md §2v "Kumo locale") plus every physical utility in Kumo's `dist` made logical (ADR-0033 A1). Never edit it; on a Kumo upgrade rename it and the `patchedDependencies` entry, re-run the script, `pnpm install`, re-check the gaps in `docs/design/kumo-upstream-notes.md`. `packages/ui/test/kumo-rtl.test.tsx` fails when the installed Kumo is not patched. `miniflare@4.20260730.0.patch` backports Miniflare 5's sync binding-proxy fix (workers-sdk#15552) that the D1 harness needs (#190, ADR-0012); `packages/storage/test/miniflare-patch.test.ts` fails without it. - `apps/studio` — SPA; routes in `src/router.tsx`, screens in `src/routes`, typed API client `src/lib/api.ts` (a barrel over `src/lib/api/.ts`), i18n in `src/i18n` (messages are ICU MF1, compiled at build time, with parameter types generated into `src/i18n/generated/params.ts` by `pnpm gen` — never edit it; languages registered in `locales.ts`; add keys to `en.ts` and every `.ts`, or to a lazy namespace `src/i18n/lazy/..ts` (the list and how to add one: `src/i18n/namespaces.ts`; DESIGN.md §1); only the active language of the core catalogs loads, `tr.ts` through `loadLocale`, and each other language's lazy-namespace loaders come with it from the generated `src/i18n/loaders/.ts`; Arabic and Persian format with Western digits through the registry's `intl` tag). - `docs/adr/` decisions (with sources) · `docs/rfcs/` proposals · `docs/design/DESIGN.md` the binding admin design system (Kumo). - `.agents/skills/` — skills for you: `contract-change`, `add-capability`, `write-rfc`, `studio-ui`, `translate`, and Cloudflare's official **`kumo-design`** (installed from `cloudflare/kumo`, tracked in `skills-lock.json`; update with `npx skills update`). Any admin-UI change loads `kumo-design` + `studio-ui` and follows `docs/design/DESIGN.md`. diff --git a/docs/adr/0012-cloudflare-deployment.md b/docs/adr/0012-cloudflare-deployment.md index 6458533d..b6183df8 100644 --- a/docs/adr/0012-cloudflare-deployment.md +++ b/docs/adr/0012-cloudflare-deployment.md @@ -59,7 +59,8 @@ Sources: https://github.com/advisories/GHSA-wq5f-xc86-pv6w · https://github.com The D1 suites failed now and then on CI, never on sqlite or Durable Object SQLite, and passed on rerun. Both failures in #190 were the same assertion, `message?.id === id` at `miniflare/src/plugins/core/proxy/fetch-sync.ts:147`, reached from `D1Database#prepare`/`bind`, which Miniflare's binding proxy runs as synchronous calls. The "statement budget" test in `core-ts` was not a counting or timing failure: its stack ends in the same assertion. - **Cause.** `SynchronousFetcher` blocks the caller on a shared 0/1 flag: the worker thread posts the reply, stores 1 and notifies. If the worker is preempted between its store and its notify while the caller has already read the reply (the flag was 1 when it reached `Atomics.wait`), the late notify wakes the *next* call before its reply exists. `receiveMessageOnPort` returns `undefined`, the assert throws, and every later call reads the previous call's reply, which is the cascade of 1–3 ms failures in `roles.test.ts`. A loaded runner makes the preemption likely: `turbo --concurrency=2` runs two packages of up to three Vitest workers each (two in `@cloudhal/api`), and every D1 or Durable Object file runs its own workerd process plus the proxy's worker thread, on four vCPUs. The race reproduces in a standalone model of the handshake with a stall between the store and the notify (the shipped protocol desyncs at call 3, the fixed one runs 5,000 calls). Upstream: cloudflare/workers-sdk#15552, merged 2026-09-16 into Miniflare 5 only. -- **Ports.** Every asynchronous proxy call (`first`, `all`, `run`, `batch`) goes through `dispatchFetch`, which set undici's `reset` on every request: one new connection and one port in `TIME_WAIT` per query. 2,000 queries left 2,002 `TIME_WAIT` sockets on macOS, where `TIME_WAIT` lasts 30 s, so D1-heavy files in parallel ran out of ephemeral ports locally. Upstream: cloudflare/workers-sdk#15781 (fixes #15716), merged 2026-10-05 into Miniflare 5 only. The Durable Object harness already bypassed `dispatchFetch` for this reason (#116). -- **Decision.** There is no newer Miniflare 4.x (4.20260730.0 is still `latest`; 5.x is beta), so the pin stays and `patches/miniflare@4.20260730.0.patch` backports both fixes, plus a 1 s keep-alive on the synchronous proxy's undici pool so an idle socket closes before workerd's 5 s idle timeout (#16445, part A; the same setting #15781 gives the runtime pool). The patch is applied through `patchedDependencies` in `pnpm-workspace.yaml`, so it covers monorepo installs only. Projects that use `@cloudhal/storage/testing` with their own Miniflare 4 keep these bugs unless they apply the same patch. `packages/storage/test/miniflare-patch.test.ts` fails if the patch is missing: it checks the generation handshake in Miniflare's code and counts the connections that 200 D1 queries open (200 unpatched, 1 patched). No retries were added and D1 files still run in parallel. #16445 part B (a `FREE` racing an in-flight `batch([prepare()])`) is not patched: it needs a garbage collection during a workerd stall of over 100 ms, and it has not been seen in CI. When Miniflare 5 is stable, remove the patch together with the sharp and undici overrides. +- **Decision.** There is no newer Miniflare 4.x (4.20260730.0 is still `latest`; 5.x is beta), so the pin stays and `patches/miniflare@4.20260730.0.patch` backports #15552. It also closes the synchronous proxy's idle sockets after 1 s instead of undici's 4 s, well before workerd's 5 s idle timeout (#16445, part A). That pool lives on the proxy's own worker thread, which only waits for requests, so its timers fire even while a test blocks the main thread. The patch is applied through `patchedDependencies` in `pnpm-workspace.yaml`, so it covers monorepo installs only. Projects that use `@cloudhal/storage/testing` with their own Miniflare 4 keep the bug unless they apply the same patch. `packages/storage/test/miniflare-patch.test.ts` fails if the patch is missing. No retries were added and D1 files still run in parallel. +- **Not taken: keep-alive `dispatchFetch`.** Every asynchronous proxy call (`first`, `all`, `run`, `batch`) goes through `dispatchFetch`, which sets undici's `reset`: one connection and one port in `TIME_WAIT` per query (2,000 queries left 2,002 `TIME_WAIT` sockets on macOS, where they last 30 s). That is why D1-heavy files in parallel can run a Mac out of ephemeral ports; Linux CI has not shown it. cloudflare/workers-sdk#15781 keeps those connections alive in Miniflare 5, and it was backported here first, but the timers that close idle sockets run on the test's main thread. When that thread stalls past workerd's 5 s idle timeout, the next call reuses a socket workerd has closed and fails with `ECONNRESET`, which cannot be retried safely because a write may have run (reproduced: a 5.5 s stall, then one query). One local `roles.test.ts` run out of 20 failed that way, so the backport was dropped: a port shortage on a loaded Mac is a local inconvenience, a non-retryable reset is a CI flake. Run test-heavy packages one at a time locally. +- **Not patched:** #16445 part B (a `FREE` racing an in-flight `batch([prepare()])`). It needs a garbage collection during a workerd stall of over 100 ms, and it has not been seen in CI. When Miniflare 5 is stable, remove the patch together with the sharp and undici overrides. Sources: https://github.com/cloudflare/workers-sdk/pull/15552 · https://github.com/cloudflare/workers-sdk/pull/15781 · https://github.com/cloudflare/workers-sdk/issues/16445 · `npm view miniflare dist-tags` (checked 2026-11-10) · Miniflare 4.20260730.0 and 5.20261006.0-alpha `dist/src/index.js` (`SynchronousFetcher`, `DispatchFetchDispatcher`) · CI runs 42789152142 and 42793831919 (first attempts). diff --git a/packages/storage/test/miniflare-patch.test.ts b/packages/storage/test/miniflare-patch.test.ts index 39858a2d..c19b2c91 100644 --- a/packages/storage/test/miniflare-patch.test.ts +++ b/packages/storage/test/miniflare-patch.test.ts @@ -1,23 +1,17 @@ /** - * #190: `patches/miniflare@4.20260730.0.patch` backports three upstream Miniflare fixes that the D1 - * harness depends on (ADR-0012, addendum 2026-11-10). This file fails when a Miniflare change - * arrives without them, or the patch did not apply: re-check the patch against the new release - * (drop it once the release carries cloudflare/workers-sdk#15552 and #15781) and `pnpm install`. + * #190: `patches/miniflare@4.20260730.0.patch` backports cloudflare/workers-sdk#15552, the fix for + * the synchronous binding proxy's desync that the D1 harness hit on CI (ADR-0012, addendum + * 2026-11-10). This file fails when a Miniflare change arrives without it, or the patch did not + * apply: re-check the patch against the new release (drop it once the release carries #15552) + * and `pnpm install`. */ -import { subscribe, unsubscribe } from "node:diagnostics_channel"; import { readFile } from "node:fs/promises"; import { createRequire } from "node:module"; -import { afterAll, describe, expect, it } from "vitest"; -import { createMiniflareD1 } from "../src/testing.ts"; +import { describe, expect, it } from "vitest"; const require = createRequire(import.meta.url); -describe("Miniflare carries the #190 fixes (ADR-0012)", () => { - let d1: Awaited> | undefined; - afterAll(async () => { - await d1?.dispose(); - }); - +describe("Miniflare carries the #190 fix (ADR-0012)", () => { it("the synchronous proxy waits for its own reply's generation (workers-sdk#15552)", async () => { // The race needs a preempted worker thread, so it cannot be forced from here: check the code. const source = await readFile(require.resolve("miniflare"), "utf8"); @@ -25,22 +19,4 @@ describe("Miniflare carries the #190 fixes (ADR-0012)", () => { expect(source).toContain("seen !== generation"); expect(source).not.toMatch(/Atomics\.store\(\s*this\.#notifyHandle,\s*\/\* index \*\/\s*0,/); }); - - it("async D1 calls reuse kept-alive connections (workers-sdk#15781)", async () => { - d1 = await createMiniflareD1(); - await d1.db.prepare("SELECT 1").first(); - // Unpatched, `dispatchFetch` closes the connection after every call: one new socket (and one - // port in TIME_WAIT) per query. Each idle socket closes after 1 s, so a few may reconnect. - let connected = 0; - const onConnect = () => { - connected++; - }; - subscribe("undici:client:connected", onConnect); - try { - for (let i = 0; i < 200; i++) await d1.db.prepare("SELECT ?1 AS n").bind(i).first(); - } finally { - unsubscribe("undici:client:connected", onConnect); - } - expect(connected).toBeLessThan(10); - }); }); diff --git a/patches/miniflare@4.20260730.0.patch b/patches/miniflare@4.20260730.0.patch index 4e52fe7e..4232bf86 100644 --- a/patches/miniflare@4.20260730.0.patch +++ b/patches/miniflare@4.20260730.0.patch @@ -1,31 +1,19 @@ diff --git a/dist/src/index.js b/dist/src/index.js -index ae57410a970d7d47f9f1810567529df69e9d8df1..e46464411db851fafb5c2b4a582e1497e8e0da2f 100644 +index ae57410a970d7d47f9f1810567529df69e9d8df1..31275022fef212b8bacc9a42929bcd9ccf0e34ca 100644 --- a/dist/src/index.js +++ b/dist/src/index.js -@@ -72975,7 +72975,10 @@ var DispatchFetchDispatcher = class extends undici.Dispatcher { - const headers = convertUndiciHeadersToStandard(options.headers ?? {}); - this.addHeaders(headers, path69); - options.headers = headers; -- options.reset = true; -+ // Cloudhal patch (cloudflare/workers-sdk#15781): reuse connections instead of one -+ // TIME_WAIT port per dispatch; never replay a request that may have reached the Worker. -+ options.idempotent = false; -+ options.reset = false; - return this.runtimeDispatcher.dispatch(options, handler); - } else { - return this.globalDispatcher.dispatch(options, handler); -@@ -92215,6 +92218,10 @@ port.addEventListener("message", async (event) => { +@@ -92215,6 +92215,10 @@ port.addEventListener("message", async (event) => { dispatcherUrl = url; dispatcher = new Pool(new URL(url).origin, { connect: { rejectUnauthorized: false }, -+ // Cloudhal patch (cloudflare/workers-sdk#16445): close idle sockets before -+ // workerd's 5s idle timeout, as the runtime dispatcher does since #15781. ++ // Cloudhal patch (cloudflare/workers-sdk#16445 A): close idle sockets well before ++ // workerd's 5s idle timeout. This thread only waits for requests, so its timers run. + keepAliveTimeout: 1000, + keepAliveMaxTimeout: 1000, // Disable timeouts for local dev \u2014 long-running responses (streaming, // slow uploads, long-polling) should not be killed by undici defaults. headersTimeout: 0, -@@ -92247,7 +92254,10 @@ port.addEventListener("message", async (event) => { +@@ -92247,7 +92251,10 @@ port.addEventListener("message", async (event) => { port.postMessage({ id, error: new Error(String(error)) }); } } finally { @@ -37,7 +25,7 @@ index ae57410a970d7d47f9f1810567529df69e9d8df1..e46464411db851fafb5c2b4a582e1497 Atomics.notify(notifyHandle, /* index */ 0); } }); -@@ -92278,13 +92288,6 @@ var SynchronousFetcher = class { +@@ -92278,13 +92285,6 @@ var SynchronousFetcher = class { } fetch(url44, init2) { this.#ensureWorker(); @@ -51,7 +39,7 @@ index ae57410a970d7d47f9f1810567529df69e9d8df1..e46464411db851fafb5c2b4a582e1497 const id = this.#nextId++; this.#channel.port1.postMessage({ id, -@@ -92293,13 +92296,13 @@ var SynchronousFetcher = class { +@@ -92293,13 +92293,13 @@ var SynchronousFetcher = class { headers: init2.headers, body: init2.body }); @@ -72,14 +60,3 @@ index ae57410a970d7d47f9f1810567529df69e9d8df1..e46464411db851fafb5c2b4a582e1497 const message = (0, import_node_worker_threads.receiveMessageOnPort)( this.#channel.port1 )?.message; -@@ -104369,6 +104372,10 @@ See https://developers.cloudflare.com/cloudflare-one/access-controls/service-cre - }); - this.#runtimeDispatcher = new import_undici9.Pool(this.#runtimeEntryURL, { - connect: { rejectUnauthorized: false }, -+ // Cloudhal patch (cloudflare/workers-sdk#15781): close idle client sockets -+ // before workerd's 5s idle timeout. -+ keepAliveTimeout: 1e3, -+ keepAliveMaxTimeout: 1e3, - // Disable timeouts for local dev — long-running responses (streaming, - // slow uploads, long-polling) should not be killed by undici defaults. - headersTimeout: 0, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc12f192..2ff31598 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -384,7 +384,7 @@ overrides: patchedDependencies: '@cloudflare/kumo@2.14.0': 198f9be784beeb608a5d59b175d6085d7b4f0291567d57b089a85da1fef5f9d7 - miniflare@4.20260730.0: 78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96 + miniflare@4.20260730.0: 6d7c8031fbf8682db97d0bab3227b4bb2c8c4bc8e77fc8b69019ca2dafd5bd46 importers: @@ -583,7 +583,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=6d7c8031fbf8682db97d0bab3227b4bb2c8c4bc8e77fc8b69019ca2dafd5bd46)(@types/node@26.6.5) typescript: specifier: 'catalog:' version: 7.0.2 @@ -671,7 +671,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=6d7c8031fbf8682db97d0bab3227b4bb2c8c4bc8e77fc8b69019ca2dafd5bd46)(@types/node@26.6.5) typescript: specifier: 'catalog:' version: 7.0.2 @@ -870,7 +870,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=6d7c8031fbf8682db97d0bab3227b4bb2c8c4bc8e77fc8b69019ca2dafd5bd46)(@types/node@26.6.5) typescript: specifier: 'catalog:' version: 7.0.2 @@ -935,7 +935,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=6d7c8031fbf8682db97d0bab3227b4bb2c8c4bc8e77fc8b69019ca2dafd5bd46)(@types/node@26.6.5) typescript: specifier: 'catalog:' version: 7.0.2 @@ -1046,7 +1046,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=6d7c8031fbf8682db97d0bab3227b4bb2c8c4bc8e77fc8b69019ca2dafd5bd46)(@types/node@26.6.5) rrule-temporal: specifier: 'catalog:' version: 2.2.8 @@ -1077,7 +1077,7 @@ importers: version: 13.0.3 miniflare: specifier: 'catalog:' - version: 4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5) + version: 4.20260730.0(patch_hash=6d7c8031fbf8682db97d0bab3227b4bb2c8c4bc8e77fc8b69019ca2dafd5bd46)(@types/node@26.6.5) tsx: specifier: 'catalog:' version: 4.23.15 @@ -7921,7 +7921,7 @@ snapshots: min-indent@1.0.1: {} - miniflare@4.20260730.0(patch_hash=78c0f2a29369e6677c63eca67cf1258639b873dfd8dbadc0fbbbb764970eaa96)(@types/node@26.6.5): + miniflare@4.20260730.0(patch_hash=6d7c8031fbf8682db97d0bab3227b4bb2c8c4bc8e77fc8b69019ca2dafd5bd46)(@types/node@26.6.5): dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.35.5(@types/node@26.6.5) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5eead922..bbaea4c8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -109,9 +109,9 @@ overrides: "undici@<7.30.1": 7.30.2 # The Miniflare patch backports cloudflare/workers-sdk#15552 (the sync binding proxy's off-by-one -# desync, the `fetch-sync.ts:147` assertion), #15781 (one TIME_WAIT port per dispatchFetch) and the -# sync pool's keep-alive below workerd's 5 s idle timeout (#16445 A); both PRs are merged for -# Miniflare 5 only (#190, ADR-0012 addendum 2026-11-10). Not published, like the overrides. +# desync, the `fetch-sync.ts:147` assertion; merged for Miniflare 5 only) and closes the sync +# pool's idle sockets after 1 s, before workerd's 5 s idle timeout (#16445 A). #190, ADR-0012 +# addendum 2026-11-10. Not published, like the overrides. # `packages/storage/test/miniflare-patch.test.ts` fails when it is missing. Remove with the 4.x pin. patchedDependencies: '@cloudflare/kumo@2.14.0': patches/@cloudflare__kumo@2.14.0.patch From 49c8160699e1c6cfa7728aa6ee52bf032f097053 Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:53:31 +0300 Subject: [PATCH 4/5] test: reproduce the Miniflare sync-proxy race in the #190 guard The guard now forces the race on the real Miniflare: a `node -r` preload (Node loads it in worker threads too) stalls the proxy thread between its store and notify and the caller before its wait, on every other call. Unpatched, the first stalled call fails the CI assertion; patched, 300 prepare/bind calls stay in step. ADR-0012: record the stress results (unpatched also passes 20 CI runs without the forced stalls) and the local port exhaustion that stays open. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- docs/adr/0012-cloudflare-deployment.md | 6 ++--- .../test/fixtures/preempt-sync-proxy.cjs | 26 +++++++++++++++++++ .../test/fixtures/sync-proxy-calls.cjs | 24 +++++++++++++++++ packages/storage/test/miniflare-patch.test.ts | 22 +++++++++------- 4 files changed, 66 insertions(+), 12 deletions(-) create mode 100644 packages/storage/test/fixtures/preempt-sync-proxy.cjs create mode 100644 packages/storage/test/fixtures/sync-proxy-calls.cjs diff --git a/docs/adr/0012-cloudflare-deployment.md b/docs/adr/0012-cloudflare-deployment.md index b6183df8..636bf1b0 100644 --- a/docs/adr/0012-cloudflare-deployment.md +++ b/docs/adr/0012-cloudflare-deployment.md @@ -58,9 +58,9 @@ Sources: https://github.com/advisories/GHSA-wq5f-xc86-pv6w · https://github.com The D1 suites failed now and then on CI, never on sqlite or Durable Object SQLite, and passed on rerun. Both failures in #190 were the same assertion, `message?.id === id` at `miniflare/src/plugins/core/proxy/fetch-sync.ts:147`, reached from `D1Database#prepare`/`bind`, which Miniflare's binding proxy runs as synchronous calls. The "statement budget" test in `core-ts` was not a counting or timing failure: its stack ends in the same assertion. -- **Cause.** `SynchronousFetcher` blocks the caller on a shared 0/1 flag: the worker thread posts the reply, stores 1 and notifies. If the worker is preempted between its store and its notify while the caller has already read the reply (the flag was 1 when it reached `Atomics.wait`), the late notify wakes the *next* call before its reply exists. `receiveMessageOnPort` returns `undefined`, the assert throws, and every later call reads the previous call's reply, which is the cascade of 1–3 ms failures in `roles.test.ts`. A loaded runner makes the preemption likely: `turbo --concurrency=2` runs two packages of up to three Vitest workers each (two in `@cloudhal/api`), and every D1 or Durable Object file runs its own workerd process plus the proxy's worker thread, on four vCPUs. The race reproduces in a standalone model of the handshake with a stall between the store and the notify (the shipped protocol desyncs at call 3, the fixed one runs 5,000 calls). Upstream: cloudflare/workers-sdk#15552, merged 2026-09-16 into Miniflare 5 only. -- **Decision.** There is no newer Miniflare 4.x (4.20260730.0 is still `latest`; 5.x is beta), so the pin stays and `patches/miniflare@4.20260730.0.patch` backports #15552. It also closes the synchronous proxy's idle sockets after 1 s instead of undici's 4 s, well before workerd's 5 s idle timeout (#16445, part A). That pool lives on the proxy's own worker thread, which only waits for requests, so its timers fire even while a test blocks the main thread. The patch is applied through `patchedDependencies` in `pnpm-workspace.yaml`, so it covers monorepo installs only. Projects that use `@cloudhal/storage/testing` with their own Miniflare 4 keep the bug unless they apply the same patch. `packages/storage/test/miniflare-patch.test.ts` fails if the patch is missing. No retries were added and D1 files still run in parallel. -- **Not taken: keep-alive `dispatchFetch`.** Every asynchronous proxy call (`first`, `all`, `run`, `batch`) goes through `dispatchFetch`, which sets undici's `reset`: one connection and one port in `TIME_WAIT` per query (2,000 queries left 2,002 `TIME_WAIT` sockets on macOS, where they last 30 s). That is why D1-heavy files in parallel can run a Mac out of ephemeral ports; Linux CI has not shown it. cloudflare/workers-sdk#15781 keeps those connections alive in Miniflare 5, and it was backported here first, but the timers that close idle sockets run on the test's main thread. When that thread stalls past workerd's 5 s idle timeout, the next call reuses a socket workerd has closed and fails with `ECONNRESET`, which cannot be retried safely because a write may have run (reproduced: a 5.5 s stall, then one query). One local `roles.test.ts` run out of 20 failed that way, so the backport was dropped: a port shortage on a loaded Mac is a local inconvenience, a non-retryable reset is a CI flake. Run test-heavy packages one at a time locally. +- **Cause.** `SynchronousFetcher` blocks the caller on a shared 0/1 flag: the worker thread posts the reply, stores 1 and notifies. If the worker is preempted between its store and its notify while the caller has already read the reply (the flag was 1 when it reached `Atomics.wait`), the late notify wakes the *next* call before its reply exists. `receiveMessageOnPort` returns `undefined`, the assert throws, and every later call reads the previous call's reply, which is the cascade of 1–3 ms failures in `roles.test.ts`. A loaded runner makes the preemption likely: `turbo --concurrency=2` runs two packages of up to three Vitest workers each (two in `@cloudhal/api`), and every D1 or Durable Object file runs its own workerd process plus the proxy's worker thread, on four vCPUs. The race reproduces on the real Miniflare once both preemptions are forced (see the test below), and in a standalone model of the handshake (the shipped protocol desyncs at call 3, the fixed one runs 5,000 calls). It does not show up on demand without them: 20 unpatched runs of the D1 halves passed on CI's own load, and 20 more with one busy loop per vCPU. Upstream: cloudflare/workers-sdk#15552, merged 2026-09-16 into Miniflare 5 only. +- **Decision.** There is no newer Miniflare 4.x (4.20260730.0 is still `latest`; 5.x is beta), so the pin stays and `patches/miniflare@4.20260730.0.patch` backports #15552. It also closes the synchronous proxy's idle sockets after 1 s instead of undici's 4 s, well before workerd's 5 s idle timeout (#16445, part A). That pool lives on the proxy's own worker thread, which only waits for requests, so its timers fire even while a test blocks the main thread. The patch is applied through `patchedDependencies` in `pnpm-workspace.yaml`, so it covers monorepo installs only. Projects that use `@cloudhal/storage/testing` with their own Miniflare 4 keep the bug unless they apply the same patch. `packages/storage/test/miniflare-patch.test.ts` fails if the patch is missing: a `node -r` preload (Node loads it in worker threads too) stalls the proxy thread between its store and its notify and the caller before its wait, on every other call, and 300 `prepare`/`bind` calls must stay in step. Unpatched, the first stalled call fails the CI assertion; patched, all pass. No retries were added and D1 files still run in parallel. +- **Not taken: keep-alive `dispatchFetch`.** Every asynchronous proxy call (`first`, `all`, `run`, `batch`) goes through `dispatchFetch`, which sets undici's `reset`: one connection and one port in `TIME_WAIT` per query (2,000 queries left 2,002 `TIME_WAIT` sockets on macOS, where they last 30 s). On a Mac a single `@cloudhal/core-ts` run fills the whole ephemeral range (16,340 of 16,384 ports in `TIME_WAIT`, also at half the workers) and fails with `EADDRNOTAVAIL`; Linux CI has not shown it. cloudflare/workers-sdk#15781 keeps those connections alive in Miniflare 5, and it was backported here first: core-ts then passed locally, but one `roles.test.ts` run in 20 failed with `ECONNRESET` on two kept-alive sockets in a row, which cannot be retried safely because a write may have run. A main-thread stall past workerd's 5 s idle timeout reproduces that error (the client's idle timers cannot fire during it), but a normal run stalls at most about 0.5 s, so the cause is not settled. The backport was dropped from #190, which is about CI; the local port shortage is older than this change and is a follow-up. - **Not patched:** #16445 part B (a `FREE` racing an in-flight `batch([prepare()])`). It needs a garbage collection during a workerd stall of over 100 ms, and it has not been seen in CI. When Miniflare 5 is stable, remove the patch together with the sharp and undici overrides. Sources: https://github.com/cloudflare/workers-sdk/pull/15552 · https://github.com/cloudflare/workers-sdk/pull/15781 · https://github.com/cloudflare/workers-sdk/issues/16445 · `npm view miniflare dist-tags` (checked 2026-11-10) · Miniflare 4.20260730.0 and 5.20261006.0-alpha `dist/src/index.js` (`SynchronousFetcher`, `DispatchFetchDispatcher`) · CI runs 42789152142 and 42793831919 (first attempts). diff --git a/packages/storage/test/fixtures/preempt-sync-proxy.cjs b/packages/storage/test/fixtures/preempt-sync-proxy.cjs new file mode 100644 index 00000000..dde16b19 --- /dev/null +++ b/packages/storage/test/fixtures/preempt-sync-proxy.cjs @@ -0,0 +1,26 @@ +// Preloaded with `node -r` (#190, test/miniflare-patch.test.ts). Node loads `-r` modules in worker +// threads too, so this reaches Miniflare's synchronous binding proxy thread. On every other call +// it stalls the caller between posting its request and waiting, and the proxy thread between +// storing its "done" flag and notifying: the two preemptions behind cloudflare/workers-sdk#15552. +const { isMainThread, MessagePort } = require("node:worker_threads"); + +const spin = (ms) => { + const end = process.hrtime.bigint() + BigInt(ms * 1_000_000); + while (process.hrtime.bigint() < end); +}; + +if (isMainThread) { + const post = MessagePort.prototype.postMessage; + MessagePort.prototype.postMessage = function (message, ...rest) { + const result = post.call(this, message, ...rest); + if (process.env.PREEMPT && typeof message?.id === "number" && message.id % 2 === 0) spin(3); + return result; + }; +} else { + const notify = Atomics.notify; + let calls = 0; + Atomics.notify = (...args) => { + if (calls++ % 2 === 0) spin(6); + return notify(...args); + }; +} diff --git a/packages/storage/test/fixtures/sync-proxy-calls.cjs b/packages/storage/test/fixtures/sync-proxy-calls.cjs new file mode 100644 index 00000000..1ea0d66a --- /dev/null +++ b/packages/storage/test/fixtures/sync-proxy-calls.cjs @@ -0,0 +1,24 @@ +// Run with `node -r ./preempt-sync-proxy.cjs` (#190, test/miniflare-patch.test.ts): 300 +// synchronous D1 proxy calls (`prepare`/`bind`) under the simulated preemption. Prints "ok", or +// the first failure. +const { Miniflare } = require("miniflare"); + +(async () => { + const mf = new Miniflare({ + modules: true, + script: "export default { fetch() { return new Response('ok'); } }", + d1Databases: ["DB"], + }); + const db = await mf.getD1Database("DB"); + process.env.PREEMPT = "1"; + let i = 0; + try { + for (; i < 300; i++) db.prepare("SELECT ?1").bind(i); + console.log("ok"); + } catch (e) { + console.log(`call ${i}: ${String(e?.message).split("\n").slice(0, 3).join(" ")}`); + } finally { + delete process.env.PREEMPT; + await mf.dispose(); + } +})(); diff --git a/packages/storage/test/miniflare-patch.test.ts b/packages/storage/test/miniflare-patch.test.ts index c19b2c91..83c49443 100644 --- a/packages/storage/test/miniflare-patch.test.ts +++ b/packages/storage/test/miniflare-patch.test.ts @@ -5,18 +5,22 @@ * apply: re-check the patch against the new release (drop it once the release carries #15552) * and `pnpm install`. */ -import { readFile } from "node:fs/promises"; -import { createRequire } from "node:module"; +import { execFile } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; -const require = createRequire(import.meta.url); +const fixture = (name: string) => fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)); describe("Miniflare carries the #190 fix (ADR-0012)", () => { - it("the synchronous proxy waits for its own reply's generation (workers-sdk#15552)", async () => { - // The race needs a preempted worker thread, so it cannot be forced from here: check the code. - const source = await readFile(require.resolve("miniflare"), "utf8"); - expect(source).toContain("/* generation */ (id + 1) | 0"); - expect(source).toContain("seen !== generation"); - expect(source).not.toMatch(/Atomics\.store\(\s*this\.#notifyHandle,\s*\/\* index \*\/\s*0,/); + it("synchronous D1 calls stay in step when the proxy thread is preempted (workers-sdk#15552)", async () => { + // A preload stalls both sides of the handshake on every other call, as a busy runner does + // now and then. Unpatched, the first stalled call fails `message?.id === id`. + const { stdout } = await promisify(execFile)( + process.execPath, + ["-r", fixture("preempt-sync-proxy.cjs"), fixture("sync-proxy-calls.cjs")], + { timeout: 60_000 }, + ); + expect(stdout.trim()).toBe("ok"); }); }); From 5f854918620ee2eb2dd6ac6ccebb25fb9c45286e Mon Sep 17 00:00:00 2001 From: Madraka <8898640+Madraka@users.noreply.github.com> Date: Tue, 10 Nov 2026 16:06:34 +0300 Subject: [PATCH 5/5] test(storage): pass the guard's fixture paths as strings `fileURLToPath(new URL(...))` failed typecheck (DOM vs Node `URL`); build the paths with `path.join` like the other storage tests. ADR-0012 gains the recipe for regenerating the Miniflare patch on an upgrade. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Madraka <8898640+Madraka@users.noreply.github.com> --- docs/adr/0012-cloudflare-deployment.md | 1 + packages/storage/test/miniflare-patch.test.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/adr/0012-cloudflare-deployment.md b/docs/adr/0012-cloudflare-deployment.md index 636bf1b0..7f854d2d 100644 --- a/docs/adr/0012-cloudflare-deployment.md +++ b/docs/adr/0012-cloudflare-deployment.md @@ -60,6 +60,7 @@ The D1 suites failed now and then on CI, never on sqlite or Durable Object SQLit - **Cause.** `SynchronousFetcher` blocks the caller on a shared 0/1 flag: the worker thread posts the reply, stores 1 and notifies. If the worker is preempted between its store and its notify while the caller has already read the reply (the flag was 1 when it reached `Atomics.wait`), the late notify wakes the *next* call before its reply exists. `receiveMessageOnPort` returns `undefined`, the assert throws, and every later call reads the previous call's reply, which is the cascade of 1–3 ms failures in `roles.test.ts`. A loaded runner makes the preemption likely: `turbo --concurrency=2` runs two packages of up to three Vitest workers each (two in `@cloudhal/api`), and every D1 or Durable Object file runs its own workerd process plus the proxy's worker thread, on four vCPUs. The race reproduces on the real Miniflare once both preemptions are forced (see the test below), and in a standalone model of the handshake (the shipped protocol desyncs at call 3, the fixed one runs 5,000 calls). It does not show up on demand without them: 20 unpatched runs of the D1 halves passed on CI's own load, and 20 more with one busy loop per vCPU. Upstream: cloudflare/workers-sdk#15552, merged 2026-09-16 into Miniflare 5 only. - **Decision.** There is no newer Miniflare 4.x (4.20260730.0 is still `latest`; 5.x is beta), so the pin stays and `patches/miniflare@4.20260730.0.patch` backports #15552. It also closes the synchronous proxy's idle sockets after 1 s instead of undici's 4 s, well before workerd's 5 s idle timeout (#16445, part A). That pool lives on the proxy's own worker thread, which only waits for requests, so its timers fire even while a test blocks the main thread. The patch is applied through `patchedDependencies` in `pnpm-workspace.yaml`, so it covers monorepo installs only. Projects that use `@cloudhal/storage/testing` with their own Miniflare 4 keep the bug unless they apply the same patch. `packages/storage/test/miniflare-patch.test.ts` fails if the patch is missing: a `node -r` preload (Node loads it in worker threads too) stalls the proxy thread between its store and its notify and the caller before its wait, on every other call, and 300 `prepare`/`bind` calls must stay in step. Unpatched, the first stalled call fails the CI assertion; patched, all pass. No retries were added and D1 files still run in parallel. +- **On a Miniflare upgrade.** The patch is hand-written against `dist/src/index.js` (no generator, unlike Kumo's). If the new release carries #15552, delete the patch and its `patchedDependencies` entry. Otherwise `pnpm patch miniflare@`, re-apply the two changes in the edit directory (in `SynchronousFetcher`'s worker script: store `(id + 1) | 0` instead of 1 and set `keepAliveTimeout`/`keepAliveMaxTimeout` to 1000 on its `Pool`; in `fetch()`: drop the reset to 0 and loop `Atomics.wait` until the generation is seen), `pnpm patch-commit`, rename the `patchedDependencies` entry, and run `packages/storage/test/miniflare-patch.test.ts`, which fails until the patch applies. - **Not taken: keep-alive `dispatchFetch`.** Every asynchronous proxy call (`first`, `all`, `run`, `batch`) goes through `dispatchFetch`, which sets undici's `reset`: one connection and one port in `TIME_WAIT` per query (2,000 queries left 2,002 `TIME_WAIT` sockets on macOS, where they last 30 s). On a Mac a single `@cloudhal/core-ts` run fills the whole ephemeral range (16,340 of 16,384 ports in `TIME_WAIT`, also at half the workers) and fails with `EADDRNOTAVAIL`; Linux CI has not shown it. cloudflare/workers-sdk#15781 keeps those connections alive in Miniflare 5, and it was backported here first: core-ts then passed locally, but one `roles.test.ts` run in 20 failed with `ECONNRESET` on two kept-alive sockets in a row, which cannot be retried safely because a write may have run. A main-thread stall past workerd's 5 s idle timeout reproduces that error (the client's idle timers cannot fire during it), but a normal run stalls at most about 0.5 s, so the cause is not settled. The backport was dropped from #190, which is about CI; the local port shortage is older than this change and is a follow-up. - **Not patched:** #16445 part B (a `FREE` racing an in-flight `batch([prepare()])`). It needs a garbage collection during a workerd stall of over 100 ms, and it has not been seen in CI. When Miniflare 5 is stable, remove the patch together with the sharp and undici overrides. diff --git a/packages/storage/test/miniflare-patch.test.ts b/packages/storage/test/miniflare-patch.test.ts index 83c49443..920f1a8c 100644 --- a/packages/storage/test/miniflare-patch.test.ts +++ b/packages/storage/test/miniflare-patch.test.ts @@ -6,11 +6,13 @@ * and `pnpm install`. */ import { execFile } from "node:child_process"; +import path from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; -const fixture = (name: string) => fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)); +const here = path.dirname(fileURLToPath(import.meta.url)); +const fixture = (name: string) => path.join(here, "fixtures", name); describe("Miniflare carries the #190 fix (ADR-0012)", () => { it("synchronous D1 calls stay in step when the proxy thread is preempted (workers-sdk#15552)", async () => {