Skip to content

Commit 6273c96

Browse files
committed
Persist a __ps_name fallback so cold-alarm wakes recover this.name
Reintroduces a one-time-per-DO `__ps_name` storage record written during initialization whenever `ctx.id.name` is observed. This restores the safety net 0.5.0 dropped: when an alarm fires on a cold DO instance after a dev-server restart, workerd does not propagate the name into `ctx.id.name`, so without a fallback `this.name` throws inside `onStart()` and frameworks like Cloudflare Agents surface the error reported in #390. Adds a self-contained reproduction at `fixtures/alarm-restart-e2e/` that runs three Durable Objects side-by-side under `vite dev` + `@cloudflare/vite-plugin`: a raw `DurableObject`, `partyserver@0.5.3` (aliased as `partyserver-stock`), and the workspace partyserver. The fixture's README walks through the cold-alarm-after-restart procedure and shows that stock partyserver throws while the workspace version recovers via the fallback record. Also adds a "Raw runtime contract" describe block in the partyserver test suite that pins workerd's behavior for `ctx.id.name` in `alarm()` on a warm DO under `compatibility_date < 2026-03-15`. Made-with: Cursor
1 parent e2eab6f commit 6273c96

16 files changed

Lines changed: 985 additions & 51 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"partyserver": patch
3+
---
4+
5+
Persist a `__ps_name` fallback for name-based Durable Objects during initialization. This lets alarm handlers recover `this.name` even when firing on a stale on-disk alarm record that was scheduled by an older workerd version that didn't yet persist `name` into the alarm record. See cloudflare/partykit#390.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# `alarm-restart-e2e`
2+
3+
Reproducer for the runtime contract that motivates partyserver's
4+
`__ps_name` fallback record. Pins down behavior reported in
5+
[cloudflare/partykit#390](https://github.com/cloudflare/partykit/issues/390)
6+
across three Durable Objects in the same Worker:
7+
8+
| DO | Class | Extends |
9+
| ------------ | --------------------------------- | -------------------------------------------------------------------------- |
10+
| `RawAlarm` | `RawAlarm` | `DurableObject` (no PartyServer) |
11+
| `StockAlarm` | `StockAlarm` (built from a mixin) | `Server` from `partyserver@0.5.3` (aliased as `partyserver-stock`) |
12+
| `FixedAlarm` | `FixedAlarm` (built from a mixin) | `Server` from this workspace's local `partyserver` (with the fallback fix) |
13+
14+
Each DO records an observation (`{source, ctxIdName, storedPsName,
15+
partyName, partyNameError, at}`) to its own SQLite-backed storage on
16+
every entry through `fetch()` or `alarm()`. Observations accumulate
17+
across dev-server restarts.
18+
19+
## Run the experiment
20+
21+
```bash
22+
npm install
23+
npm run start
24+
```
25+
26+
In a second shell, schedule an alarm into a fresh room and observe:
27+
28+
```bash
29+
ROOM="cold-strict-$(date +%s)"
30+
31+
# Session A: schedule into a fresh room. This is the only entry into
32+
# the DO instances during session A. After this, the alarm record on
33+
# disk is what carries the DO across the restart.
34+
curl -s "http://localhost:5173/raw/$ROOM?schedule=45"
35+
curl -s "http://localhost:5173/parties/stock-alarm/$ROOM?schedule=45"
36+
curl -s "http://localhost:5173/parties/fixed-alarm/$ROOM?schedule=45"
37+
```
38+
39+
Then kill `vite dev` (Ctrl-C), restart it (`npm run start`), and
40+
**don't touch the room** until well past the 45-second mark. Then:
41+
42+
```bash
43+
curl -s "http://localhost:5173/raw/$ROOM?snapshot=1" | jq
44+
curl -s -i "http://localhost:5173/parties/stock-alarm/$ROOM?snapshot=1" | head -n 12
45+
curl -s "http://localhost:5173/parties/fixed-alarm/$ROOM?snapshot=1" | jq
46+
```
47+
48+
Observed behavior on `workerd@1.20260424.1`,
49+
`compatibility_date: "2026-01-28"`:
50+
51+
- `RawAlarm`: alarm observation has no `ctxIdName` (i.e. `ctx.id.name`
52+
is `undefined`). Subsequent fetches via `idFromName(...)` ALSO see
53+
`ctx.id.name === undefined` for the lifetime of that DO instance —
54+
the instance is "born nameless" and stays that way.
55+
56+
- `StockAlarm`: `Server.fetch` returns 500 with the "Cannot determine
57+
the name" error. Reproduces the failure reported in cloudflare/partykit#390.
58+
59+
- `FixedAlarm`: `alarm()` runs successfully. `ctx.id.name` is
60+
`undefined` in the observation, but `this.name` resolves from the
61+
on-disk `__ps_name` record that PartyServer wrote during session
62+
A's fetch. `partyserver` recovers the name; the DO continues
63+
working normally.
64+
65+
## Why three DOs
66+
67+
`RawAlarm` pins down what workerd actually does, free of any
68+
framework. `StockAlarm` reproduces the user-reported bug under
69+
`partyserver@0.5.3`. `FixedAlarm` validates that the workspace fix
70+
restores normal operation under the same conditions.
71+
72+
## Critical: don't warm the DOs before the alarm fires
73+
74+
Any HTTP fetch or websocket message sent to a DO between session B
75+
startup and the alarm firing time will wake the DO via that entry
76+
point first. workerd captures `ctx.id.name` from the first entry
77+
point and that value persists for the instance's lifetime. So a
78+
pre-alarm fetch silently warms `ctx.id.name` and masks the bug. The
79+
critical window is from `vite dev` starting back up until the
80+
expected alarm fire time. Don't open the page in a browser, don't
81+
curl `?snapshot`, don't let any client reconnect to the room. Just
82+
wait.
83+
84+
The frontend `index.html` exists for manual exploration but is
85+
deliberately separate from the cold-DO experiment so a developer
86+
running the page won't accidentally warm a different room. To run
87+
the cold experiment, drive everything from `curl` against rooms the
88+
frontend isn't subscribed to.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/* eslint-disable */
2+
// Generated by Wrangler by running `wrangler types env.d.ts --include-runtime false` (hash: 9d5eb238d4dbfdedf1bf7b0674d6a12c)
3+
declare namespace Cloudflare {
4+
interface Env {
5+
RawAlarm: DurableObjectNamespace /* RawAlarm */;
6+
StockAlarm: DurableObjectNamespace /* StockAlarm */;
7+
FixedAlarm: DurableObjectNamespace /* FixedAlarm */;
8+
}
9+
}
10+
interface Env extends Cloudflare.Env {}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<title>partyserver alarm-restart e2e</title>
6+
<style>
7+
body {
8+
font-family:
9+
ui-monospace,
10+
SFMono-Regular,
11+
SF Mono,
12+
Menlo,
13+
monospace;
14+
margin: 24px;
15+
background: #0b0b0e;
16+
color: #e6e6ea;
17+
}
18+
h1 {
19+
font-size: 16px;
20+
margin: 0 0 16px;
21+
}
22+
.grid {
23+
display: grid;
24+
grid-template-columns: repeat(3, minmax(0, 1fr));
25+
gap: 16px;
26+
}
27+
.card {
28+
background: #14141a;
29+
border: 1px solid #2a2a33;
30+
border-radius: 6px;
31+
padding: 12px;
32+
font-size: 12px;
33+
}
34+
.card h2 {
35+
font-size: 13px;
36+
margin: 0 0 8px;
37+
}
38+
.row {
39+
display: flex;
40+
align-items: center;
41+
gap: 8px;
42+
margin-bottom: 6px;
43+
}
44+
.badge {
45+
display: inline-block;
46+
padding: 2px 6px;
47+
border-radius: 4px;
48+
font-size: 11px;
49+
}
50+
.ok {
51+
background: #133b1f;
52+
color: #8be9a3;
53+
}
54+
.warn {
55+
background: #4a2a08;
56+
color: #f5b061;
57+
}
58+
.err {
59+
background: #4a1818;
60+
color: #ef9494;
61+
}
62+
pre {
63+
margin: 0;
64+
white-space: pre-wrap;
65+
word-break: break-all;
66+
background: #0a0a0e;
67+
border: 1px solid #1f1f27;
68+
padding: 8px;
69+
border-radius: 4px;
70+
font-size: 11px;
71+
max-height: 320px;
72+
overflow: auto;
73+
}
74+
input,
75+
button {
76+
background: #1f1f27;
77+
border: 1px solid #2f2f3a;
78+
color: #e6e6ea;
79+
padding: 4px 8px;
80+
border-radius: 4px;
81+
font: inherit;
82+
}
83+
button {
84+
cursor: pointer;
85+
}
86+
button:hover {
87+
background: #292935;
88+
}
89+
.controls {
90+
display: flex;
91+
gap: 8px;
92+
margin-bottom: 12px;
93+
}
94+
</style>
95+
</head>
96+
<body>
97+
<h1>partyserver alarm-restart e2e</h1>
98+
<div class="controls">
99+
Room name:
100+
<input id="room" value="default-room" />
101+
Schedule alarm in
102+
<input id="seconds" type="number" value="20" style="width: 64px" />s
103+
<button id="schedule-all">Schedule on all 3</button>
104+
<button id="snapshot-all">Snapshot all 3</button>
105+
</div>
106+
<div class="grid">
107+
<section class="card" id="card-raw">
108+
<h2>RawAlarm <span class="badge" data-status>?</span></h2>
109+
<div class="row">
110+
<small data-summary></small>
111+
</div>
112+
<pre data-log>(no events yet)</pre>
113+
</section>
114+
<section class="card" id="card-stock">
115+
<h2>
116+
StockAlarm (partyserver@0.5.3)
117+
<span class="badge" data-status>?</span>
118+
</h2>
119+
<div class="row">
120+
<small data-summary></small>
121+
</div>
122+
<pre data-log>(no events yet)</pre>
123+
</section>
124+
<section class="card" id="card-fixed">
125+
<h2>
126+
FixedAlarm (workspace partyserver)
127+
<span class="badge" data-status>?</span>
128+
</h2>
129+
<div class="row">
130+
<small data-summary></small>
131+
</div>
132+
<pre data-log>(no events yet)</pre>
133+
</section>
134+
</div>
135+
<script src="/src/client.ts" type="module"></script>
136+
</body>
137+
</html>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@partyserver/fixture-alarm-restart-e2e",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"start": "vite dev",
8+
"types": "wrangler types env.d.ts --include-runtime false"
9+
},
10+
"dependencies": {
11+
"partyserver": "*",
12+
"partyserver-stock": "npm:partyserver@0.5.3",
13+
"partysocket": "^1.1.18"
14+
},
15+
"devDependencies": {
16+
"@cloudflare/vite-plugin": "^1.33.2",
17+
"@cloudflare/workers-types": "^4.20260424.1",
18+
"vite": "^8.0.10",
19+
"wrangler": "^4.85.0"
20+
}
21+
}

0 commit comments

Comments
 (0)