Skip to content

[pull] canary from vercel:canary - #1407

Merged
pull[bot] merged 3 commits into
code:canaryfrom
vercel:canary
Sep 18, 2026
Merged

pull[bot] merged 3 commits into
code:canaryfrom
vercel:canary

Conversation

@pull

@pull pull Bot commented Sep 18, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

mischnic and others added 3 commits September 18, 2026 09:29
### What?

Adds a CI job that runs selected Rust unit tests under
[Miri](https://github.com/rust-lang/miri) to detect undefined behavior
in unsafe code.

The job currently covers the low-level crates that are compatible with
Miri:

- `turbo-persistence`
- `turbo-rcstr`
- `turbo-tasks-malloc`

It also makes those crates Miri-compatible by:

- using provenance-free tagged-pointer operations in `turbo-rcstr`;
- disabling mimalloc and native compression under Miri;
- making mmap support a default-enabled Cargo feature that is disabled
for Miri and wasm;
- running persistence through file I/O when mmap is disabled;
- skipping only tests measured to exceed the Miri time budget;
- removing leaked test arenas from the analyzer predicate tests.

### Why?

Miri can detect invalid memory access and other undefined behavior that
normal Rust tests may not expose. Running it in CI gives low-level
unsafe code an additional correctness check.

The job uses an explicit package allowlist because Turbo Tasks builds
its generated registry from linker sections, and Miri does not support
the linker-defined section symbols required by that registry.

### How?

- Installs the `miri` Rust component in CI and the development
container.
- Adds a dedicated `test-cargo-unit-miri` task and reusable workflow
configuration.
- Makes `memmap2` optional behind the default `mmap` feature and
disables that feature in Miri CI and the wasm dependency graph.
- Uses structural `cfg(miri)` branches for allocator and compression
paths Miri cannot execute.
- Re-enables three persistence compaction tests after measuring them
successfully under Miri.
- Keeps normal native behavior unchanged and documents measured Miri
exclusions at the affected tests.

Verification included focused normal and Miri tests for persistence,
rcstr, allocation accounting, compression, and the refactored leak-free
predicate cases.

<!-- NEXT_JS_LLM -->


<!-- fleet fb42942f-173a-484c-b4de-4e18354834c6 -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
If upgrade handles the Node version check, upgrade from Next version
that allowed Node v18 -> Next version that require Node v20 can fail the
upgrade. Next handles compatible Node version so proceed upgrade and let
agent handle it afterwards.
…dis, and cross-page tag invalidation (#98716)

Three runtime problems in `examples/cache-handler-redis`.

### 1. One new Redis connection per request, never closed

Next.js constructs the singular `cacheHandler` class once per request
(`new CurCacheHandler(...)` in `IncrementalCache`, which
`route-module.ts` creates per request). The example's constructor calls
`createClient()` + `connect()`, so every request opens a connection that
is never closed.

```
$ redis-cli client list | wc -l      # before
102
$ for i in $(seq 1 10); do curl -s -o /dev/null localhost:3000/cet; done
$ redis-cli client list | wc -l
132                                  # +3 per request
```

With Redis' default `maxclients 10000`, a single instance runs out after
a few thousand requests.

### 2. Requests hang while Redis is unavailable

The README says the handlers "degrade gracefully when Redis is
unavailable, so the app still builds and runs, just without a shared
cache". At runtime they don't: with Redis stopped, every request on
every instance blocks until Redis comes back.

```
$ docker stop cache-handler-redis
$ curl -s -o /dev/null -m 60 -w "%{http_code} %{time_total}s\n" localhost:3000/cet
000 60.007305s                       # (uncapped: 188s, returned the moment Redis was started again)
```

Same when the app is started while Redis is down. Cause: node-redis
keeps retrying in the background and `client.connect()` does not settle
until a connection succeeds. Isolated:

```js
const c = createClient({ url: "redis://localhost:6379" }); c.on("error", () => {});
await Promise.race([c.connect(), new Promise(r => setTimeout(() => r("pending"), 15000))]);
// -> "pending" after 15s, isOpen=true isReady=false   (redis@6.2.1)
```

Because each request built a new handler, each request awaited a fresh,
never-settling `connect()` in `getClient()`. `remote-cache-handler.js`
has one module-level client, but its `getClient()` awaits the same
promise, so it hangs the same way if the app starts while Redis is down.

### 3. `updateTag` never reaches other pages' remote entries

The remote handler's `get` never checks the entry's own tags. Next.js
only passes soft tags to `getExpiration`, and the `"use cache"` wrapper
only knows about tags revalidated in the current request, so
`updateTag("time-data")` from `/cet` left the `/gmt` entry stale on
every instance, including the one that ran it, until it expired
(`cacheLife` `expire: 3600`). The [`cacheHandlers`
docs](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheHandlers#get)
say `get` should report an entry whose tag was invalidated as missing or
stale.

### Fix

- Hoist the client in `cache-handler.js` to module scope (the pattern
`remote-cache-handler.js` already uses).
- `getClient()` awaits the connect promise raced against a 1s
`unref()`'d timer (suggested in review), then returns the client when
`isReady` and `null` otherwise. Requests during startup still wait for
the connection, and a down Redis costs one bounded wait instead of
blocking every request. The client keeps retrying and `isReady` flips
back on its own.
- `disableOfflineQueue: true`, so a command issued while the connection
is down rejects immediately (`ClientOfflineError`) instead of being
queued for the 5s command timeout.
- Entry `get` / `set` catch the Redis call only and degrade to a miss.
- Remote `get` compares the entry's tags against their revalidation
timestamps (one `MGET`) and misses when any is newer, the same
comparison as the built-in handler. `getExpiration` returns `Date.now()`
when Redis can't answer, so the entry is discarded rather than served.
- `revalidateTag` / `updateTags` throw when Redis isn't ready, so an
invalidation that never reached Redis surfaces as an error instead of a
success whose entries come back once Redis does.

### After

Two `next start` instances on one Redis, same script on `next@16.3.5`
and `16.4.0-canary.35` (identical results), canary's handlers vs this
PR:

| scenario | before | after |
| --- | --- | --- |
| `updateTag` from `/cet`, read `/gmt` on both instances | stale |
refreshed |
| `revalidateTag("time-data", "max")` | neither page refreshed | both
refreshed |
| tag lookup fails | 500 | 200, regenerated |
| request while Redis is stopped | no response | 200, uncached |
| `updateTag` while Redis is stopped | no response | 500, error logged |
| app started without Redis | no response | first ISR read waits ≤1.1s
once |
| Redis connections, 200 requests | 173 → 773 | 7 → 7 |
| `/cet` under load (autocannon, 10 connections × 10s, `16.3.5`) | 332
req/s, hits Redis `maxclients` | 1,106 req/s median, p99 20ms, 3
connections |
| 10 readers during 20 `updateTag`s: reads served a value from before a
completed invalidation | 3,089 of 3,089 | 0 of 33,899 |
| Redis down ~4s under load | timeouts, caching doesn't come back | 0
errors, caching resumes |

The tag check costs one Redis round trip per remote hit (−9 to −13% on a
route that only reads one remote entry). Handler-level cases, full
tables and the benchmark breakdown are in [this
comment](#98716 (comment)).
@pull pull Bot locked and limited conversation to collaborators Sep 18, 2026
@pull pull Bot added the ⤵️ pull label Sep 18, 2026
@pull
pull Bot merged commit 579ff60 into code:canary Sep 18, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants