@mcp-b/do-runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +110 -0
  3. package/LICENSE.workerd +176 -0
  4. package/NOTICE +7 -0
  5. package/README.md +282 -0
  6. package/dist/backends/node-sqlite.d.ts +38 -0
  7. package/dist/backends/node-sqlite.js +335 -0
  8. package/dist/backends/node-sqlite.js.map +1 -0
  9. package/dist/backends/sqlite-wasm.d.ts +130 -0
  10. package/dist/backends/sqlite-wasm.js +259 -0
  11. package/dist/backends/sqlite-wasm.js.map +1 -0
  12. package/dist/chunks/sqlite-DFg92Tgt.js +498 -0
  13. package/dist/chunks/sqlite-DFg92Tgt.js.map +1 -0
  14. package/dist/cloudflare-workers.js +351 -0
  15. package/dist/cloudflare-workers.js.map +1 -0
  16. package/dist/conformance/host.d.ts +58 -0
  17. package/dist/conformance.js +18 -0
  18. package/dist/conformance.js.map +1 -0
  19. package/dist/index.js +7184 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/server/alarm-scheduler.js +513 -0
  22. package/dist/server/alarm-scheduler.js.map +1 -0
  23. package/dist/src/api/actor-state.d.ts +396 -0
  24. package/dist/src/api/actor.d.ts +306 -0
  25. package/dist/src/api/cloudflare-workers.d.ts +259 -0
  26. package/dist/src/api/export-loopback.d.ts +264 -0
  27. package/dist/src/api/global-scope.d.ts +262 -0
  28. package/dist/src/api/http.d.ts +52 -0
  29. package/dist/src/api/sql.d.ts +188 -0
  30. package/dist/src/api/sync-kv.d.ts +51 -0
  31. package/dist/src/api/web-socket.d.ts +93 -0
  32. package/dist/src/api/worker-loader.d.ts +354 -0
  33. package/dist/src/index.d.ts +130 -0
  34. package/dist/src/io/actor-cache.d.ts +203 -0
  35. package/dist/src/io/actor-id.d.ts +74 -0
  36. package/dist/src/io/actor-sqlite.d.ts +298 -0
  37. package/dist/src/io/io-channels.d.ts +191 -0
  38. package/dist/src/io/io-context.d.ts +451 -0
  39. package/dist/src/io/io-gate.d.ts +298 -0
  40. package/dist/src/io/worker-source.d.ts +108 -0
  41. package/dist/src/io/worker.d.ts +88 -0
  42. package/dist/src/server/actor-container.d.ts +525 -0
  43. package/dist/src/server/actor-id-impl.d.ts +118 -0
  44. package/dist/src/server/alarm-scheduler.d.ts +201 -0
  45. package/dist/src/server/facet-deletion.d.ts +156 -0
  46. package/dist/src/server/facet-tree-index.d.ts +94 -0
  47. package/dist/src/server/sha256.d.ts +39 -0
  48. package/dist/src/transport/rpc-session.d.ts +34 -0
  49. package/dist/src/util/sqlite-kv.d.ts +98 -0
  50. package/dist/src/util/sqlite-metadata.d.ts +46 -0
  51. package/dist/src/util/sqlite.d.ts +291 -0
  52. package/package.json +111 -0
@@ -0,0 +1,513 @@
1
+ import { a as getInt64, c as isNull, o as getText, r as SqliteDatabase, s as hasCurrentSqliteTable } from "../chunks/sqlite-DFg92Tgt.js";
2
+ //#region src/server/alarm-scheduler.ts
3
+ /**
4
+ * ← `WorkerInterface::ALARM_RETRY_START_SECONDS` (`io/worker-interface.h:130`),
5
+ * re-declared as `AlarmScheduler::RETRY_START_SECONDS` (`alarm-scheduler.h:42`).
6
+ *
7
+ * "not a duration so we can left shift it" — upstream's own comment, and the
8
+ * reason the ladder below is a shift rather than a table.
9
+ */
10
+ var ALARM_RETRY_START_SECONDS = 2;
11
+ /**
12
+ * ← `WorkerInterface::ALARM_RETRY_MAX_TRIES` (`io/worker-interface.h:131`) /
13
+ * `AlarmScheduler::RETRY_MAX_TRIES` (`alarm-scheduler.h:45`).
14
+ *
15
+ * "Max number of 'valid' retry attempts, i.e the worker returned an error."
16
+ * It bounds `countedRetry`, NOT `backoff`: a run of failures that do not count
17
+ * against the limit is retried forever, and its delay is bounded by
18
+ * `RETRY_BACKOFF_MAX` instead.
19
+ */
20
+ var ALARM_RETRY_MAX_TRIES = 6;
21
+ /**
22
+ * ← `AlarmScheduler::RETRY_BACKOFF_MAX` (`alarm-scheduler.h:50`).
23
+ *
24
+ * "Bound for exponential backoff when RETRY_MAX_TRIES is exceeded due to
25
+ * internal errors. 2 << 9 is 1024 seconds, about 17 minutes. Total time spent in
26
+ * retries once the backoff limit is reached is over 30 minutes."
27
+ */
28
+ var RETRY_BACKOFF_MAX = 9;
29
+ /**
30
+ * ← `AlarmScheduler::RETRY_JITTER_FACTOR` (`alarm-scheduler.h:54`).
31
+ *
32
+ * "How much jitter should be applied to retry times to avoid bundled retries
33
+ * overloading some common dependency between a set of failed alarms."
34
+ */
35
+ var RETRY_JITTER_FACTOR = .25;
36
+ /**
37
+ * ← `(AlarmScheduler::RETRY_START_SECONDS << backoff) * kj::SECONDS`
38
+ * (`alarm-scheduler.c++:270`), before jitter.
39
+ *
40
+ * It refuses a backoff outside `[0, RETRY_BACKOFF_MAX]` rather than shifting it,
41
+ * because JS's `<<` is a 32-bit operator: an unclamped counter would wrap to a
42
+ * zero or negative delay — a hot retry loop — instead of saturating. The ladder
43
+ * clamps immediately above its own call, exactly where upstream does; this is
44
+ * what makes that clamp load bearing rather than decorative.
45
+ */
46
+ function alarmRetryDelayMs(backoff) {
47
+ if (!Number.isInteger(backoff) || backoff < 0 || backoff > 9) throw new Error(`Alarm retry backoff ${backoff} is outside [0, 9].`);
48
+ return (2 << backoff) * 1e3;
49
+ }
50
+ /**
51
+ * ← `_cf_ALARM` (`alarm-scheduler.c++:54-59`), plus the two prepared statements
52
+ * (`alarm-scheduler.h:117-123`).
53
+ *
54
+ * Prepared statements are not part of the backend seam, so they survive as SQL
55
+ * text under upstream's own member names — the treatment `util/sqlite-kv.ts`'s
56
+ * `STMT` already records.
57
+ *
58
+ * `scheduled_time` holds **milliseconds** where upstream holds nanoseconds
59
+ * (`:72`, `:102`). Same reason as `_cf_METADATA`'s alarm column: a JS number
60
+ * runs out of integer precision 104 days into the epoch at nanosecond scale, so
61
+ * storing what upstream stores would silently round every alarm.
62
+ *
63
+ * **Five columns upstream does not have, and they are this section's
64
+ * divergence.** Upstream stores `(actor_id, scheduled_time)` and keeps the whole
65
+ * retry ladder — `backoff`, `countedRetry`, `previousRetryCountedAgainstLimit`
66
+ * and the fact that a delivery is in flight — in the `ScheduledAlarm` struct,
67
+ * because a workerd process lives for hours and `loadAlarmsFromDb` runs once.
68
+ * An MV3 service worker is evicted after seconds, so a scheduler rebuilt per
69
+ * worker lifetime never accumulates any of them: `countedRetry` cannot reach
70
+ * `ALARM_RETRY_MAX_TRIES`, so `#abandon` is unreachable and a permanently
71
+ * failing alarm is never given up on, and `backoff` never leaves its first rung,
72
+ * so that alarm wakes the browser every two seconds forever. Persisting the four
73
+ * makes the ladder a property of the alarm rather than of the process that
74
+ * happened to be running when it failed. See the README's divergence table.
75
+ *
76
+ * `retry_time` is the wake, which upstream never needs to store: its retries
77
+ * live in the timer and the row's `scheduled_time` — which stays the alarm's own
78
+ * time, because that is the identity `deliverAlarm` and `abandonAlarm` are told
79
+ * — is by then in the past. Reloading without it re-arms every pending retry for
80
+ * immediately, so the counters would climb while the delay was ignored.
81
+ *
82
+ * `running` is written before a delivery and cleared by whatever the delivery
83
+ * turns into. A row still marked running at load is a delivery no result ever
84
+ * came back from, which on this substrate is the ordinary case rather than a
85
+ * crash: Chrome evicted the worker mid-alarm. Without it that alarm is
86
+ * redelivered on every restart with no counter moved and no delay applied, which
87
+ * is the hot loop above in its worst form, because nothing about it is bounded.
88
+ */
89
+ var STMT = {
90
+ createTable: `
91
+ CREATE TABLE IF NOT EXISTS _cf_ALARM (
92
+ actor_id TEXT PRIMARY KEY,
93
+ scheduled_time INTEGER,
94
+ retry_time INTEGER,
95
+ backoff INTEGER NOT NULL,
96
+ counted_retry INTEGER NOT NULL,
97
+ previous_retry_counted INTEGER NOT NULL,
98
+ running INTEGER NOT NULL
99
+ ) WITHOUT ROWID
100
+ `,
101
+ loadAlarms: `
102
+ SELECT actor_id, scheduled_time, retry_time, backoff, counted_retry,
103
+ previous_retry_counted, running
104
+ FROM _cf_ALARM
105
+ `,
106
+ setAlarm: `
107
+ INSERT INTO _cf_ALARM VALUES(?, ?, NULL, 0, 0, 0, 0)
108
+ ON CONFLICT DO UPDATE SET
109
+ scheduled_time = excluded.scheduled_time,
110
+ retry_time = NULL,
111
+ backoff = 0,
112
+ counted_retry = 0,
113
+ previous_retry_counted = 0
114
+ `,
115
+ markRunning: `
116
+ UPDATE _cf_ALARM SET running = 1 WHERE actor_id = ?
117
+ `,
118
+ saveRetry: `
119
+ UPDATE _cf_ALARM
120
+ SET retry_time = ?, backoff = ?, counted_retry = ?,
121
+ previous_retry_counted = ?, running = 0
122
+ WHERE actor_id = ?
123
+ `,
124
+ clearRunning: `
125
+ UPDATE _cf_ALARM SET running = 0 WHERE actor_id = ?
126
+ `,
127
+ deleteAlarm: `
128
+ DELETE FROM _cf_ALARM WHERE actor_id = ?
129
+ `,
130
+ deleteAll: `
131
+ DELETE FROM _cf_ALARM
132
+ `
133
+ };
134
+ /**
135
+ * Allows scheduling alarm executions at specific times, returning a promise
136
+ * representing the completion of the alarm event.
137
+ */
138
+ var AlarmScheduler = class {
139
+ #timer;
140
+ #random;
141
+ #getActor;
142
+ #projectWake;
143
+ #db;
144
+ /** ← `kj::HashMap<ActorKey, ScheduledAlarm> alarms`, whose key is one string. */
145
+ #alarms = /* @__PURE__ */ new Map();
146
+ /** ← `kj::TaskSet tasks`, which holds a task that has outlived its entry. */
147
+ #tasks = /* @__PURE__ */ new Set();
148
+ #taskFailure;
149
+ #projection = Promise.resolve();
150
+ constructor(options) {
151
+ this.#timer = options.timer;
152
+ this.#random = options.random ?? Math.random;
153
+ this.#getActor = options.getActor;
154
+ this.#projectWake = options.projectWake;
155
+ this.#db = new SqliteDatabase(options.db);
156
+ ensureInitialized(this.#db);
157
+ this.#loadAlarmsFromDb();
158
+ this.#projectNextWake();
159
+ }
160
+ /**
161
+ * ← `getAlarm` (`alarm-scheduler.c++:84-99`), including its TODO: "Might be
162
+ * able to simplify AlarmScheduler somewhat, now that ActorSqlite no longer
163
+ * relies on it for getAlarm()?"
164
+ */
165
+ getAlarm(actorId) {
166
+ const alarm = this.#alarms.get(actorId);
167
+ if (alarm === void 0) return null;
168
+ if (alarm.status === "STARTED") return alarm.queuedAlarm;
169
+ return alarm.scheduledTime;
170
+ }
171
+ /**
172
+ * ← `setAlarm` (`alarm-scheduler.c++:101-127`).
173
+ *
174
+ * The `boolean` is upstream's `query.changeCount() > 0`, and it is constant
175
+ * true against SQLite's semantics: an `INSERT … ON CONFLICT DO UPDATE` always
176
+ * reports one changed row, even when the value is unchanged. No caller reads it
177
+ * — `ActorSqliteHooks::scheduleRun` discards it — so it is kept as upstream's
178
+ * shape rather than as a signal.
179
+ */
180
+ setAlarm(actorId, scheduledTime) {
181
+ const query = this.#db.run(STMT.setAlarm, actorId, scheduledTime);
182
+ const entry = this.#alarms.get(actorId);
183
+ if (entry === void 0) this.#alarms.set(actorId, this.#scheduleAlarm(this.#timer.now(), actorId, scheduledTime));
184
+ else if (entry.status !== "WAITING") entry.queuedAlarm = scheduledTime;
185
+ else this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, scheduledTime));
186
+ this.#projectNextWake();
187
+ return query.rowsWritten > 0;
188
+ }
189
+ /** ← `deleteAll` (`alarm-scheduler.c++:129-134`). */
190
+ deleteAll() {
191
+ for (const entry of this.#alarms.values()) entry.cancel.abort();
192
+ this.#alarms.clear();
193
+ this.#db.run(STMT.deleteAll);
194
+ this.#projectNextWake();
195
+ }
196
+ /** ← `deleteAlarm` (`alarm-scheduler.c++:136-156`). */
197
+ deleteAlarm(actorId) {
198
+ const query = this.#db.run(STMT.deleteAlarm, actorId);
199
+ const entry = this.#alarms.get(actorId);
200
+ if (entry !== void 0) {
201
+ const queued = entry.queuedAlarm;
202
+ if (queued !== null) {
203
+ if (entry.status === "STARTED") entry.queuedAlarm = null;
204
+ else this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, queued));
205
+ } else if (entry.status !== "STARTED") {
206
+ entry.cancel.abort();
207
+ this.#alarms.delete(actorId);
208
+ }
209
+ }
210
+ this.#projectNextWake();
211
+ return query.rowsWritten > 0;
212
+ }
213
+ /**
214
+ * ← `ActorSqliteHooks` (`server.c++:3199-3219`), which is the whole of how an
215
+ * actor's storage engine reaches a scheduler: one adapter per actor, holding
216
+ * that actor's key, turning a time into `setAlarm` or `deleteAlarm`.
217
+ */
218
+ hooks(actorId) {
219
+ return { scheduleRun: (scheduledTime, _priorTask) => {
220
+ if (scheduledTime !== null) this.setAlarm(actorId, scheduledTime);
221
+ else this.deleteAlarm(actorId);
222
+ return this.#projection;
223
+ } };
224
+ }
225
+ /**
226
+ * ← `taskFailed`'s `KJ_LOG(WARNING, e)` (`alarm-scheduler.c++:289-291`), and
227
+ * the two other log sites in `makeAlarmTask` that report a failure and carry
228
+ * on (`:202`, `:285`).
229
+ *
230
+ * This package has no logger, so the exception is kept instead of written —
231
+ * the same treatment divergence 154 records for `waitUntilStatus()`, and for
232
+ * the same reason: a background failure that is neither logged nor readable is
233
+ * one nothing can notice.
234
+ */
235
+ taskFailure() {
236
+ return this.#taskFailure?.exception;
237
+ }
238
+ /**
239
+ * ← `loadAlarmsFromDb` (`alarm-scheduler.c++:62-82`), plus the retry state
240
+ * upstream has no columns for.
241
+ *
242
+ * This is the whole of the divergence's read side. Upstream's loop rebuilds
243
+ * every entry with zeroed counters, which is what makes a per-worker-lifetime
244
+ * scheduler forget; here the counters come off the row, and a row that says a
245
+ * delivery was in flight is recovered before its entry is built.
246
+ */
247
+ #loadAlarmsFromDb() {
248
+ const now = this.#timer.now();
249
+ for (const row of this.#db.run(STMT.loadAlarms).rawRows) {
250
+ const actorId = getText(row, 0);
251
+ const scheduledTime = getInt64(row, 1);
252
+ const persisted = readPersistedAlarm(actorId, row);
253
+ const resumed = persisted.running ? this.#recoverInterruptedDelivery(actorId, now, persisted) : persisted;
254
+ this.#alarms.set(actorId, this.#scheduleAlarm(now, actorId, scheduledTime, resumed));
255
+ }
256
+ }
257
+ /**
258
+ * A delivery that started and never reported an outcome, turned into an
259
+ * **uncounted** retry: `backoff` climbs so the next attempt is further away,
260
+ * and `countedRetry` does not move, so no number of them can abandon the
261
+ * alarm.
262
+ *
263
+ * That split is decision 6's ranking applied to the one failure this substrate
264
+ * produces routinely. Chrome evicting the worker mid-alarm is an
265
+ * infrastructure failure, which is exactly the category upstream retries
266
+ * forever and bounds by `RETRY_BACKOFF_MAX` rather than by
267
+ * `ALARM_RETRY_MAX_TRIES` — "a default of user error would abandon precisely
268
+ * the alarms that most need keeping". So a handler that reliably kills its
269
+ * worker settles at one attempt every 1024 seconds and is never dropped. The
270
+ * The browser host preserves that classification by reconstructing this
271
+ * scheduler over the same namespace database. A row left `running` is the
272
+ * durable evidence of the interrupted delivery; the Chrome-side watchdog
273
+ * merely recreates the worker so this recovery path can read it.
274
+ */
275
+ #recoverInterruptedDelivery(actorId, now, persisted) {
276
+ const backoff = Math.min(9, persisted.backoff);
277
+ let delay = alarmRetryDelayMs(backoff);
278
+ delay += this.#jitterMsForDelay(delay);
279
+ const resumed = {
280
+ retryTime: now + delay,
281
+ backoff: backoff + 1,
282
+ countedRetry: persisted.countedRetry,
283
+ previousRetryCountedAgainstLimit: false,
284
+ running: false
285
+ };
286
+ this.#db.run(STMT.saveRetry, resumed.retryTime, resumed.backoff, resumed.countedRetry, 0, actorId);
287
+ return resumed;
288
+ }
289
+ /** ← `scheduleAlarm` (`alarm-scheduler.c++:166-171`). */
290
+ #scheduleAlarm(now, actorId, scheduledTime, resumed) {
291
+ const entry = {
292
+ actorId,
293
+ scheduledTime,
294
+ wakeTime: null,
295
+ task: void 0,
296
+ cancel: new AbortController(),
297
+ queuedAlarm: null,
298
+ status: "WAITING",
299
+ previousRetryCountedAgainstLimit: resumed?.previousRetryCountedAgainstLimit ?? false,
300
+ backoff: resumed?.backoff ?? 0,
301
+ countedRetry: resumed?.countedRetry ?? 0
302
+ };
303
+ const wake = Math.max(scheduledTime, resumed?.retryTime ?? scheduledTime);
304
+ entry.wakeTime = wake;
305
+ entry.task = this.#makeAlarmTask(wake - now, entry, scheduledTime);
306
+ return entry;
307
+ }
308
+ /** ← `entry.value = scheduleAlarm(...)`, whose assignment destroys the old task. */
309
+ #replace(previous, next) {
310
+ previous.cancel.abort();
311
+ this.#alarms.set(next.actorId, next);
312
+ }
313
+ /** ← `checkTimestamp` (`alarm-scheduler.c++:173-185`), as a loop rather than tail recursion. */
314
+ async #checkTimestamp(delay, scheduledTime, signal) {
315
+ let remaining = delay;
316
+ for (;;) {
317
+ await this.#timer.afterDelay(remaining, signal);
318
+ const now = this.#timer.now();
319
+ if (now >= scheduledTime) return;
320
+ remaining = scheduledTime - now;
321
+ }
322
+ }
323
+ /** ← `runAlarm` (`alarm-scheduler.c++:158-164`). */
324
+ async #runAlarm(actorId, scheduledTime, retryCount) {
325
+ const result = await this.#getActor(actorId).deliverAlarm(scheduledTime, retryCount);
326
+ return {
327
+ retry: result.outcome !== "ok" && result.retry,
328
+ retryCountsAgainstLimit: result.retryCountsAgainstLimit
329
+ };
330
+ }
331
+ /** ← the try/catch lambda around `runAlarm` (`alarm-scheduler.c++:197-211`). */
332
+ async #runAlarmGuarded(actorId, scheduledTime, retryCount) {
333
+ try {
334
+ return await this.#runAlarm(actorId, scheduledTime, retryCount);
335
+ } catch (exception) {
336
+ this.#taskFailed(exception);
337
+ return {
338
+ retry: true,
339
+ retryCountsAgainstLimit: false
340
+ };
341
+ }
342
+ }
343
+ /** ← `makeAlarmTask` (`alarm-scheduler.c++:187-287`). */
344
+ async #makeAlarmTask(delay, entry, scheduledTime) {
345
+ const actorId = entry.actorId;
346
+ await this.#checkTimestamp(delay, scheduledTime, entry.cancel.signal);
347
+ if (this.#alarms.get(actorId) !== entry) return;
348
+ try {
349
+ this.#db.run(STMT.markRunning, actorId);
350
+ } catch (exception) {
351
+ this.#taskFailed(exception);
352
+ return;
353
+ }
354
+ entry.status = "STARTED";
355
+ entry.wakeTime = null;
356
+ this.#projectNextWake();
357
+ const retryCount = entry.countedRetry;
358
+ const retryInfo = await this.#runAlarmGuarded(actorId, scheduledTime, retryCount);
359
+ try {
360
+ if (this.#alarms.get(actorId) !== entry) return;
361
+ const task = entry.task;
362
+ if (task === void 0) throw new Error("An alarm task ran before it was recorded.");
363
+ this.#addTask(task);
364
+ entry.task = void 0;
365
+ const queued = entry.queuedAlarm;
366
+ if (queued !== null) {
367
+ this.#db.run(STMT.clearRunning, actorId);
368
+ this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, queued));
369
+ this.#projectNextWake();
370
+ return;
371
+ }
372
+ entry.status = "FINISHED";
373
+ if (retryInfo.retry) {
374
+ if (entry.countedRetry >= 6) {
375
+ await this.#abandon(entry, scheduledTime);
376
+ return;
377
+ }
378
+ if (retryInfo.retryCountsAgainstLimit) {
379
+ entry.countedRetry += 1;
380
+ if (!entry.previousRetryCountedAgainstLimit) entry.backoff = 0;
381
+ }
382
+ entry.previousRetryCountedAgainstLimit = retryInfo.retryCountsAgainstLimit;
383
+ entry.backoff = Math.min(9, entry.backoff);
384
+ let retryDelay = alarmRetryDelayMs(entry.backoff);
385
+ retryDelay += this.#jitterMsForDelay(retryDelay);
386
+ entry.backoff += 1;
387
+ const retryTime = this.#timer.now() + retryDelay;
388
+ this.#db.run(STMT.saveRetry, retryTime, entry.backoff, entry.countedRetry, entry.previousRetryCountedAgainstLimit ? 1 : 0, actorId);
389
+ entry.wakeTime = retryTime;
390
+ entry.task = this.#makeAlarmTask(retryDelay, entry, scheduledTime);
391
+ this.#projectNextWake();
392
+ } else {
393
+ if (entry.queuedAlarm !== null) throw new Error("An alarm that will not retry still has an alarm queued behind it.");
394
+ this.deleteAlarm(actorId);
395
+ }
396
+ } catch (exception) {
397
+ this.#taskFailed(exception);
398
+ }
399
+ }
400
+ /**
401
+ * ← the `countedRetry >= RETRY_MAX_TRIES` block (`alarm-scheduler.c++:237-253`).
402
+ *
403
+ * Its comment, verbatim, because the second half is the whole point: "Notify
404
+ * the actor to clear its in-memory alarm state so getAlarm() reflects the
405
+ * deletion. We ignore the returned remaining time — the workerd-local alarm
406
+ * scheduler already has visibility into the actor's alarm state via its SQLite
407
+ * hooks. If the notification fails, we keep the alarm in the scheduler so it is
408
+ * not silently lost."
409
+ *
410
+ * **Divergence: the returned time is not ignored** (upstream's
411
+ * `.ignoreResult()`, `:244`). Upstream is right that the newer alarm normally
412
+ * arrives on its own — `ActorSqlite` reports it through `scheduleRun`, and it
413
+ * lands in `queuedAlarm`, which `deleteAlarm` below then reschedules for. But
414
+ * that is a race, not an invariant: `abandonAlarm` reads the actor's committed
415
+ * metadata, and there is a window in which the actor's alarm is newer than
416
+ * anything the scheduler has been told about. In that window upstream's
417
+ * unconditional `deleteAlarm` removes both the row and the entry, and the alarm
418
+ * only comes back if a later commit happens to re-announce it. Re-registering
419
+ * what `abandonAlarm` reports closes the window, is a no-op whenever the
420
+ * queued alarm already covered it, and takes the side that preserves the alarm.
421
+ */
422
+ async #abandon(entry, scheduledTime) {
423
+ const actorId = entry.actorId;
424
+ let newerAlarm;
425
+ try {
426
+ newerAlarm = await this.#getActor(actorId).abandonAlarm(scheduledTime);
427
+ } catch (exception) {
428
+ this.#taskFailed(exception);
429
+ return;
430
+ }
431
+ this.deleteAlarm(actorId);
432
+ if (newerAlarm !== null && !this.#alarms.has(actorId)) this.setAlarm(actorId, newerAlarm);
433
+ }
434
+ /**
435
+ * ← `maxJitterMsForDelay` (`alarm-scheduler.c++:13-16`) drawn through
436
+ * `std::uniform_int_distribution<>(0, max)` (`:272-273`), whose range is
437
+ * inclusive at both ends.
438
+ */
439
+ #jitterMsForDelay(delayMs) {
440
+ const max = Math.floor(RETRY_JITTER_FACTOR * delayMs);
441
+ return Math.min(max, Math.floor(this.#random() * (max + 1)));
442
+ }
443
+ /** The earliest wake a browser watchdog must keep alive across process death. */
444
+ #projectNextWake() {
445
+ if (this.#projectWake === void 0) return;
446
+ let earliest = null;
447
+ for (const entry of this.#alarms.values()) {
448
+ const wake = entry.status === "STARTED" ? entry.queuedAlarm : entry.wakeTime;
449
+ if (wake !== null && (earliest === null || wake < earliest)) earliest = wake;
450
+ }
451
+ this.#projection = Promise.resolve(this.#projectWake(earliest));
452
+ this.#projection.catch((exception) => this.#taskFailed(exception));
453
+ }
454
+ /** ← `tasks.add`, whose failures reach `taskFailed`. */
455
+ #addTask(task) {
456
+ const tracked = task.then(() => {
457
+ this.#tasks.delete(tracked);
458
+ }, (exception) => {
459
+ this.#tasks.delete(tracked);
460
+ this.#taskFailed(exception);
461
+ });
462
+ this.#tasks.add(tracked);
463
+ }
464
+ /** ← `taskFailed` (`alarm-scheduler.c++:289-291`). */
465
+ #taskFailed(exception) {
466
+ this.#taskFailure ??= { exception };
467
+ }
468
+ };
469
+ /**
470
+ * Reads one `_cf_ALARM` row's retry state, refusing anything this scheduler
471
+ * could not have written.
472
+ *
473
+ * No upstream twin — upstream reads two columns and neither can be out of range.
474
+ * It fails closed rather than clamping because every value here decides how long
475
+ * an alarm waits or whether it is given up on, and a repaired counter is a
476
+ * silent behaviour change on the one path that has nobody watching it. There is
477
+ * no reader for any older shape: a database written before these columns existed
478
+ * fails at the SELECT, which is what pre-production means here.
479
+ *
480
+ * The ranges are the ladder's own. `backoff` reaches `RETRY_BACKOFF_MAX + 1`
481
+ * because the clamp is applied before the shift and the increment after it
482
+ * (`alarm-scheduler.c++:269-275`), and `counted_retry` reaches
483
+ * `ALARM_RETRY_MAX_TRIES` because the limit is checked before the increment
484
+ * (`:237`).
485
+ */
486
+ function readPersistedAlarm(actorId, row) {
487
+ const retryTime = isNull(row, 2) ? null : getInt64(row, 2);
488
+ const backoff = requireRange(actorId, "backoff", getInt64(row, 3), 10);
489
+ const countedRetry = requireRange(actorId, "counted_retry", getInt64(row, 4), 6);
490
+ const previous = requireRange(actorId, "previous_retry_counted", getInt64(row, 5), 1);
491
+ const running = requireRange(actorId, "running", getInt64(row, 6), 1);
492
+ return {
493
+ retryTime,
494
+ backoff,
495
+ countedRetry,
496
+ previousRetryCountedAgainstLimit: previous === 1,
497
+ running: running === 1
498
+ };
499
+ }
500
+ function requireRange(actorId, column, value, max) {
501
+ if (value < 0 || value > max) throw new Error(`Alarm ${column} ${value} for actor ${actorId} is outside [0, ${max}]; _cf_ALARM holds a state this scheduler cannot have written.`);
502
+ return value;
503
+ }
504
+ /** ← `ensureInitialized` (`alarm-scheduler.c++:50-60`). */
505
+ function ensureInitialized(db) {
506
+ hasCurrentSqliteTable(db, "_cf_ALARM", STMT.createTable);
507
+ db.run("PRAGMA journal_mode=WAL");
508
+ db.run(STMT.createTable);
509
+ }
510
+ //#endregion
511
+ export { ALARM_RETRY_MAX_TRIES, ALARM_RETRY_START_SECONDS, AlarmScheduler, RETRY_BACKOFF_MAX, RETRY_JITTER_FACTOR, alarmRetryDelayMs };
512
+
513
+ //# sourceMappingURL=alarm-scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alarm-scheduler.js","names":["#timer","#random","#getActor","#projectWake","#db","#alarms","#tasks","#loadAlarmsFromDb","#projectNextWake","#scheduleAlarm","#replace","#projection","#taskFailure","#recoverInterruptedDelivery","#jitterMsForDelay","#makeAlarmTask","#checkTimestamp","#runAlarm","#runAlarmGuarded","#taskFailed","#addTask","#abandon"],"sources":["../../src/server/alarm-scheduler.ts"],"sourcesContent":["/**\n * ← workerd `src/workerd/server/alarm-scheduler.{h,c++}`\n *\n * Delivery and retry: the `_cf_ALARM` table, the watchdog arming, the queued\n * alarm, and the retry ladder. Measured on real workerd: an alarm re-armed for\n * `Date.now()` from inside a running handler does NOT re-enter — delivery is\n * serialised (`enter:1, exit:1, enter:2, exit:2`). That property is load\n * bearing; `_cf_executingScheduleRowId` upstream is safe only because of it\n * (§2.3). Here it falls out of the queued alarm: a `setAlarm` that arrives\n * while a handler runs is stored on the entry and started only after the run\n * finishes (`alarm-scheduler.c++:116-124`, `:220-227`).\n *\n * **This is runtime-internal, and it is what a host puts behind\n * `ActorPorts.alarms`.** Upstream wires it the same way: `ActorSqliteHooks`\n * (`server.c++:3199-3219`) is a three-line adapter whose `scheduleRun` is\n * `setAlarm`/`deleteAlarm` on the scheduler, and the scheduler is built once per\n * namespace (`server.c++:2325-2350`) rather than once per actor. `hooks(actorId)`\n * below is that adapter, so a host composes the two instead of writing its own\n * ladder.\n *\n * **One deliberate divergence, and it is the table's shape.** Upstream keeps the\n * retry ladder in memory and reloads every alarm with its counters at zero,\n * which is right for a process that lives for hours and is a regression on a\n * service worker Chrome evicts after seconds — see `_cf_ALARM` below and the\n * README's divergence table. Everything else here is upstream's, line for line.\n *\n * Spec: §1.8, §2.6, decisions 6, 11 and 16 in\n * docs/decisions.md.\n */\n\nimport type { AlarmOutlet } from \"../io/actor-sqlite\";\nimport type { Timer } from \"../io/io-context\";\nimport {\n getInt64,\n getText,\n hasCurrentSqliteTable,\n isNull,\n type SqlDatabase,\n SqliteDatabase,\n} from \"../util/sqlite\";\n\n// =======================================================================================\n// Constants\n\n/**\n * ← `WorkerInterface::ALARM_RETRY_START_SECONDS` (`io/worker-interface.h:130`),\n * re-declared as `AlarmScheduler::RETRY_START_SECONDS` (`alarm-scheduler.h:42`).\n *\n * \"not a duration so we can left shift it\" — upstream's own comment, and the\n * reason the ladder below is a shift rather than a table.\n */\nexport const ALARM_RETRY_START_SECONDS = 2;\n\n/**\n * ← `WorkerInterface::ALARM_RETRY_MAX_TRIES` (`io/worker-interface.h:131`) /\n * `AlarmScheduler::RETRY_MAX_TRIES` (`alarm-scheduler.h:45`).\n *\n * \"Max number of 'valid' retry attempts, i.e the worker returned an error.\"\n * It bounds `countedRetry`, NOT `backoff`: a run of failures that do not count\n * against the limit is retried forever, and its delay is bounded by\n * `RETRY_BACKOFF_MAX` instead.\n */\nexport const ALARM_RETRY_MAX_TRIES = 6;\n\n/**\n * ← `AlarmScheduler::RETRY_BACKOFF_MAX` (`alarm-scheduler.h:50`).\n *\n * \"Bound for exponential backoff when RETRY_MAX_TRIES is exceeded due to\n * internal errors. 2 << 9 is 1024 seconds, about 17 minutes. Total time spent in\n * retries once the backoff limit is reached is over 30 minutes.\"\n */\nexport const RETRY_BACKOFF_MAX = 9;\n\n/**\n * ← `AlarmScheduler::RETRY_JITTER_FACTOR` (`alarm-scheduler.h:54`).\n *\n * \"How much jitter should be applied to retry times to avoid bundled retries\n * overloading some common dependency between a set of failed alarms.\"\n */\nexport const RETRY_JITTER_FACTOR = 0.25;\n\n/**\n * ← `(AlarmScheduler::RETRY_START_SECONDS << backoff) * kj::SECONDS`\n * (`alarm-scheduler.c++:270`), before jitter.\n *\n * It refuses a backoff outside `[0, RETRY_BACKOFF_MAX]` rather than shifting it,\n * because JS's `<<` is a 32-bit operator: an unclamped counter would wrap to a\n * zero or negative delay — a hot retry loop — instead of saturating. The ladder\n * clamps immediately above its own call, exactly where upstream does; this is\n * what makes that clamp load bearing rather than decorative.\n */\nexport function alarmRetryDelayMs(backoff: number): number {\n if (!Number.isInteger(backoff) || backoff < 0 || backoff > RETRY_BACKOFF_MAX) {\n throw new Error(`Alarm retry backoff ${backoff} is outside [0, ${RETRY_BACKOFF_MAX}].`);\n }\n return (ALARM_RETRY_START_SECONDS << backoff) * 1_000;\n}\n\n/**\n * ← `_cf_ALARM` (`alarm-scheduler.c++:54-59`), plus the two prepared statements\n * (`alarm-scheduler.h:117-123`).\n *\n * Prepared statements are not part of the backend seam, so they survive as SQL\n * text under upstream's own member names — the treatment `util/sqlite-kv.ts`'s\n * `STMT` already records.\n *\n * `scheduled_time` holds **milliseconds** where upstream holds nanoseconds\n * (`:72`, `:102`). Same reason as `_cf_METADATA`'s alarm column: a JS number\n * runs out of integer precision 104 days into the epoch at nanosecond scale, so\n * storing what upstream stores would silently round every alarm.\n *\n * **Five columns upstream does not have, and they are this section's\n * divergence.** Upstream stores `(actor_id, scheduled_time)` and keeps the whole\n * retry ladder — `backoff`, `countedRetry`, `previousRetryCountedAgainstLimit`\n * and the fact that a delivery is in flight — in the `ScheduledAlarm` struct,\n * because a workerd process lives for hours and `loadAlarmsFromDb` runs once.\n * An MV3 service worker is evicted after seconds, so a scheduler rebuilt per\n * worker lifetime never accumulates any of them: `countedRetry` cannot reach\n * `ALARM_RETRY_MAX_TRIES`, so `#abandon` is unreachable and a permanently\n * failing alarm is never given up on, and `backoff` never leaves its first rung,\n * so that alarm wakes the browser every two seconds forever. Persisting the four\n * makes the ladder a property of the alarm rather than of the process that\n * happened to be running when it failed. See the README's divergence table.\n *\n * `retry_time` is the wake, which upstream never needs to store: its retries\n * live in the timer and the row's `scheduled_time` — which stays the alarm's own\n * time, because that is the identity `deliverAlarm` and `abandonAlarm` are told\n * — is by then in the past. Reloading without it re-arms every pending retry for\n * immediately, so the counters would climb while the delay was ignored.\n *\n * `running` is written before a delivery and cleared by whatever the delivery\n * turns into. A row still marked running at load is a delivery no result ever\n * came back from, which on this substrate is the ordinary case rather than a\n * crash: Chrome evicted the worker mid-alarm. Without it that alarm is\n * redelivered on every restart with no counter moved and no delay applied, which\n * is the hot loop above in its worst form, because nothing about it is bounded.\n */\nconst STMT = {\n createTable: `\n CREATE TABLE IF NOT EXISTS _cf_ALARM (\n actor_id TEXT PRIMARY KEY,\n scheduled_time INTEGER,\n retry_time INTEGER,\n backoff INTEGER NOT NULL,\n counted_retry INTEGER NOT NULL,\n previous_retry_counted INTEGER NOT NULL,\n running INTEGER NOT NULL\n ) WITHOUT ROWID\n `,\n loadAlarms: `\n SELECT actor_id, scheduled_time, retry_time, backoff, counted_retry,\n previous_retry_counted, running\n FROM _cf_ALARM\n `,\n // A new alarm time is new work, so it carries a fresh retry budget — which is\n // upstream's behaviour too, since every path that changes an entry's scheduled\n // time replaces the whole `ScheduledAlarm` and zeroes its counters with it.\n // `running` is deliberately NOT in the SET list: a delivery that is in flight\n // is still in flight, and clearing the mark here would erase the only evidence\n // that it never finished.\n setAlarm: `\n INSERT INTO _cf_ALARM VALUES(?, ?, NULL, 0, 0, 0, 0)\n ON CONFLICT DO UPDATE SET\n scheduled_time = excluded.scheduled_time,\n retry_time = NULL,\n backoff = 0,\n counted_retry = 0,\n previous_retry_counted = 0\n `,\n markRunning: `\n UPDATE _cf_ALARM SET running = 1 WHERE actor_id = ?\n `,\n // One statement, because the retry state and the end of the delivery have to\n // land together: a restart between them would resume the alarm with a stale\n // wake and a ladder one rung behind.\n saveRetry: `\n UPDATE _cf_ALARM\n SET retry_time = ?, backoff = ?, counted_retry = ?,\n previous_retry_counted = ?, running = 0\n WHERE actor_id = ?\n `,\n clearRunning: `\n UPDATE _cf_ALARM SET running = 0 WHERE actor_id = ?\n `,\n deleteAlarm: `\n DELETE FROM _cf_ALARM WHERE actor_id = ?\n `,\n deleteAll: `\n DELETE FROM _cf_ALARM\n `,\n} as const;\n\n// =======================================================================================\n// The result a delivery reports back\n\n/**\n * ← `EventOutcome` (`io/outcome.capnp`), restricted to the values the alarm path\n * can produce.\n *\n * The whole enum is a metrics type with no port — divergence 154 records that\n * for `waitUntilStatus()` — but `runAlarm` reads one bit of it\n * (`alarm-scheduler.c++:162`, `result.outcome != EventOutcome::OK`), so the\n * values `ServiceWorkerGlobalScope::runAlarm` actually returns are named here\n * and the rest are not.\n */\nexport type EventOutcome =\n | \"ok\"\n | \"canceled\"\n | \"script-not-found\"\n | \"exception\"\n | \"exceeded-cpu\"\n | \"unknown\";\n\n/**\n * ← `WorkerInterface::AlarmResult` (`io/worker-interface.h:71-81`).\n *\n * Upstream defaults all three fields; here every one is required, because the\n * producer is `server/actor-container.ts` rather than a capnp wire default and a\n * silently-defaulted `retryCountsAgainstLimit` is the difference between an\n * alarm that survives a broken actor and one that is abandoned.\n */\nexport type AlarmResult = {\n readonly retry: boolean;\n readonly retryCountsAgainstLimit: boolean;\n readonly outcome: EventOutcome;\n readonly errorDescription?: string;\n};\n\n/**\n * ← the `WorkerInterface` `GetActorFn` hands back\n * (`alarm-scheduler.c++:160`, `:244`), restricted to its two alarm members.\n *\n * `WorkerInterface` itself has no port — it is capnp dispatch, and divergence\n * 176 records it collapsing into the stub the transport returns — so the seam is\n * the two methods the scheduler calls. `ActorContainer` satisfies it\n * structurally; `deliverAlarm` is upstream's `runAlarm` under the name Section\n * 6b already gave it.\n */\nexport interface AlarmTarget {\n deliverAlarm(scheduledTime: number, retryCount: number): Promise<AlarmResult>;\n /**\n * ← `WorkerInterface::abandonAlarm` (`io/worker-interface.h:114`): \"Returns the\n * actor's stored alarm time if it differs from scheduledTime (i.e. the user\n * set a new alarm), or null if the alarm was cleared or no alarm was stored.\"\n */\n abandonAlarm(scheduledTime: number): Promise<number | null>;\n}\n\n/** ← `AlarmScheduler::GetActorFn` (`alarm-scheduler.h:56`). */\nexport type GetActorFn = (actorId: string) => AlarmTarget;\n\nexport type AlarmSchedulerOptions = {\n /**\n * ← the `const kj::Clock&` and the `kj::Timer&` upstream takes separately\n * (`alarm-scheduler.h:58-59`). One object here because `Timer.now()` is\n * already wall-clock milliseconds — `IoContext::now()` reads the same one —\n * so nothing distinguishes the two. `checkTimestamp`'s re-check loop stays,\n * because a JS timer really can fire a fraction of a millisecond early\n * relative to the clock it is compared against.\n */\n timer: Timer;\n /**\n * The database `_cf_ALARM` lives in — upstream's `metadata.sqlite`, one per\n * namespace beside the per-actor files (`server.c++:2336-2346`).\n *\n * Already open, where upstream's constructor opens it from a vfs and a path:\n * `SqlDatabaseProvider.open` is asynchronous and a constructor cannot await,\n * which is the same reason `createActorContainer` is a promise.\n */\n db: SqlDatabase;\n getActor: GetActorFn;\n /**\n * Browser hosts can mirror the earliest durable wake onto a platform watchdog\n * such as `chrome.alarms`. Workerd needs no such seam because its process owns\n * the scheduler timer.\n */\n projectWake?: (scheduledTime: number | null) => Promise<void> | void;\n /**\n * ← `std::default_random_engine`, seeded from the monotonic clock\n * (`alarm-scheduler.c++:20-27`). A test seam on a runtime-internal class, not\n * a substrate port: the jitter is the one part of the ladder that is\n * deliberately not a function of its inputs.\n */\n random?: () => number;\n};\n\n// =======================================================================================\n// The scheduler\n\n/** ← `AlarmScheduler::AlarmStatus` (`alarm-scheduler.h:72`). */\ntype AlarmStatus = \"WAITING\" | \"STARTED\" | \"FINISHED\";\n\n/** ← `AlarmScheduler::ScheduledAlarm` (`alarm-scheduler.h:80-99`). */\ntype ScheduledAlarm = {\n readonly actorId: string;\n readonly scheduledTime: number;\n /** The timer's actual wake, including persisted retry delay; null while running. */\n wakeTime: number | null;\n /**\n * ← `kj::Promise<void> task`. It exists upstream so the entry OWNS the task and\n * destroying the entry cancels it; JS has no such destruction, so the two\n * halves of that are explicit here — `cancel` stops the pending wake, and every\n * resumption re-reads the map to see whether it is still the live entry.\n */\n task: Promise<void> | undefined;\n /** The half of kj's cancel-by-drop that stops the timer. Divergence 147's shape. */\n readonly cancel: AbortController;\n /** Once started, an alarm can have a single alarm queued behind it. */\n queuedAlarm: number | null;\n status: AlarmStatus;\n previousRetryCountedAgainstLimit: boolean;\n /**\n * Counter for calculating backoff -- separate from retry, so we can reset\n * backoff without losing the total count of retry attempts\n */\n backoff: number;\n /** Counter for retry attempts that apply to the retry limit. */\n countedRetry: number;\n};\n\n/** ← `AlarmScheduler::RetryInfo` (`alarm-scheduler.h:103-106`). */\ntype RetryInfo = {\n readonly retry: boolean;\n readonly retryCountsAgainstLimit: boolean;\n};\n\n/**\n * The half of a `ScheduledAlarm` that outlives the process holding it: one row\n * of `_cf_ALARM` past `scheduled_time`, validated.\n */\ntype PersistedAlarm = {\n /** When the next attempt is due, or null while the alarm waits for its own time. */\n readonly retryTime: number | null;\n readonly backoff: number;\n readonly countedRetry: number;\n readonly previousRetryCountedAgainstLimit: boolean;\n /** A delivery started and nothing recorded how it ended. */\n readonly running: boolean;\n};\n\n/**\n * Allows scheduling alarm executions at specific times, returning a promise\n * representing the completion of the alarm event.\n */\nexport class AlarmScheduler {\n readonly #timer: Timer;\n readonly #random: () => number;\n readonly #getActor: GetActorFn;\n readonly #projectWake: ((scheduledTime: number | null) => Promise<void> | void) | undefined;\n readonly #db: SqliteDatabase;\n /** ← `kj::HashMap<ActorKey, ScheduledAlarm> alarms`, whose key is one string. */\n readonly #alarms = new Map<string, ScheduledAlarm>();\n /** ← `kj::TaskSet tasks`, which holds a task that has outlived its entry. */\n readonly #tasks = new Set<Promise<void>>();\n #taskFailure: { readonly exception: unknown } | undefined;\n #projection: Promise<void> = Promise.resolve();\n\n constructor(options: AlarmSchedulerOptions) {\n this.#timer = options.timer;\n this.#random = options.random ?? Math.random;\n this.#getActor = options.getActor;\n this.#projectWake = options.projectWake;\n this.#db = new SqliteDatabase(options.db);\n ensureInitialized(this.#db);\n this.#loadAlarmsFromDb();\n this.#projectNextWake();\n }\n\n /**\n * ← `getAlarm` (`alarm-scheduler.c++:84-99`), including its TODO: \"Might be\n * able to simplify AlarmScheduler somewhat, now that ActorSqlite no longer\n * relies on it for getAlarm()?\"\n */\n getAlarm(actorId: string): number | null {\n const alarm = this.#alarms.get(actorId);\n if (alarm === undefined) {\n // We currently retain the entire set of queued alarms in memory, no need to hit sqlite\n return null;\n }\n if (alarm.status === \"STARTED\") {\n // getAlarm() when the alarm handler is running should return null, unless an alarm is queued;\n return alarm.queuedAlarm;\n }\n return alarm.scheduledTime;\n }\n\n /**\n * ← `setAlarm` (`alarm-scheduler.c++:101-127`).\n *\n * The `boolean` is upstream's `query.changeCount() > 0`, and it is constant\n * true against SQLite's semantics: an `INSERT … ON CONFLICT DO UPDATE` always\n * reports one changed row, even when the value is unchanged. No caller reads it\n * — `ActorSqliteHooks::scheduleRun` discards it — so it is kept as upstream's\n * shape rather than as a signal.\n */\n setAlarm(actorId: string, scheduledTime: number): boolean {\n const query = this.#db.run(STMT.setAlarm, actorId, scheduledTime);\n\n const entry = this.#alarms.get(actorId);\n if (entry === undefined) {\n this.#alarms.set(actorId, this.#scheduleAlarm(this.#timer.now(), actorId, scheduledTime));\n } else if (entry.status !== \"WAITING\") {\n // We queue any new alarm after the existing alarm even if the new alarm has the same scheduled\n // time, as receiving a notification directly maps to a write for that time in the actor.\n entry.queuedAlarm = scheduledTime;\n } else {\n this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, scheduledTime));\n }\n\n this.#projectNextWake();\n\n return query.rowsWritten > 0;\n }\n\n /** ← `deleteAll` (`alarm-scheduler.c++:129-134`). */\n deleteAll(): void {\n // Cancel all in-memory alarm tasks. Upstream's `alarms.clear()` destroys every task with its\n // entry; here the abort is that destruction's timer half, and a task that has already passed\n // its wake finds itself unmapped and returns.\n for (const entry of this.#alarms.values()) entry.cancel.abort();\n this.#alarms.clear();\n // Wipe the persistent store.\n this.#db.run(STMT.deleteAll);\n this.#projectNextWake();\n }\n\n /** ← `deleteAlarm` (`alarm-scheduler.c++:136-156`). */\n deleteAlarm(actorId: string): boolean {\n const query = this.#db.run(STMT.deleteAlarm, actorId);\n\n const entry = this.#alarms.get(actorId);\n if (entry !== undefined) {\n const queued = entry.queuedAlarm;\n if (queued !== null) {\n if (entry.status === \"STARTED\") {\n // If we are currently running an alarm, we want to delete the queued instead of current.\n entry.queuedAlarm = null;\n } else {\n this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, queued));\n }\n } else if (entry.status !== \"STARTED\") {\n // We can't remove running alarms.\n entry.cancel.abort();\n this.#alarms.delete(actorId);\n }\n }\n\n this.#projectNextWake();\n\n return query.rowsWritten > 0;\n }\n\n /**\n * ← `ActorSqliteHooks` (`server.c++:3199-3219`), which is the whole of how an\n * actor's storage engine reaches a scheduler: one adapter per actor, holding\n * that actor's key, turning a time into `setAlarm` or `deleteAlarm`.\n */\n hooks(actorId: string): AlarmOutlet {\n return {\n // Deliberately not `async`: `AlarmOutlet.scheduleRun` may throw synchronously and\n // `ActorSqlite` relies on it, because a scheduling failure has to reach the caller before\n // the local database commits. `priorTask` is ignored for upstream's reason — \"We ignore the\n // priorTask in workerd because everything should run synchronously.\"\n scheduleRun: (scheduledTime: number | null, _priorTask: Promise<void>): Promise<void> => {\n if (scheduledTime !== null) this.setAlarm(actorId, scheduledTime);\n else this.deleteAlarm(actorId);\n return this.#projection;\n },\n };\n }\n\n /**\n * ← `taskFailed`'s `KJ_LOG(WARNING, e)` (`alarm-scheduler.c++:289-291`), and\n * the two other log sites in `makeAlarmTask` that report a failure and carry\n * on (`:202`, `:285`).\n *\n * This package has no logger, so the exception is kept instead of written —\n * the same treatment divergence 154 records for `waitUntilStatus()`, and for\n * the same reason: a background failure that is neither logged nor readable is\n * one nothing can notice.\n */\n taskFailure(): unknown {\n return this.#taskFailure?.exception;\n }\n\n // -----------------------------------------------------------------\n\n /**\n * ← `loadAlarmsFromDb` (`alarm-scheduler.c++:62-82`), plus the retry state\n * upstream has no columns for.\n *\n * This is the whole of the divergence's read side. Upstream's loop rebuilds\n * every entry with zeroed counters, which is what makes a per-worker-lifetime\n * scheduler forget; here the counters come off the row, and a row that says a\n * delivery was in flight is recovered before its entry is built.\n */\n #loadAlarmsFromDb(): void {\n const now = this.#timer.now();\n\n // TODO(someday): don't maintain the entire alarm set in memory -- right now for the usecase of\n // local development, doing so is sufficient.\n for (const row of this.#db.run(STMT.loadAlarms).rawRows) {\n const actorId = getText(row, 0);\n const scheduledTime = getInt64(row, 1);\n const persisted = readPersistedAlarm(actorId, row);\n const resumed = persisted.running\n ? this.#recoverInterruptedDelivery(actorId, now, persisted)\n : persisted;\n this.#alarms.set(actorId, this.#scheduleAlarm(now, actorId, scheduledTime, resumed));\n }\n }\n\n /**\n * A delivery that started and never reported an outcome, turned into an\n * **uncounted** retry: `backoff` climbs so the next attempt is further away,\n * and `countedRetry` does not move, so no number of them can abandon the\n * alarm.\n *\n * That split is decision 6's ranking applied to the one failure this substrate\n * produces routinely. Chrome evicting the worker mid-alarm is an\n * infrastructure failure, which is exactly the category upstream retries\n * forever and bounds by `RETRY_BACKOFF_MAX` rather than by\n * `ALARM_RETRY_MAX_TRIES` — \"a default of user error would abandon precisely\n * the alarms that most need keeping\". So a handler that reliably kills its\n * worker settles at one attempt every 1024 seconds and is never dropped. The\n * The browser host preserves that classification by reconstructing this\n * scheduler over the same namespace database. A row left `running` is the\n * durable evidence of the interrupted delivery; the Chrome-side watchdog\n * merely recreates the worker so this recovery path can read it.\n */\n #recoverInterruptedDelivery(\n actorId: string,\n now: number,\n persisted: PersistedAlarm,\n ): PersistedAlarm {\n const backoff = Math.min(RETRY_BACKOFF_MAX, persisted.backoff);\n let delay = alarmRetryDelayMs(backoff);\n delay += this.#jitterMsForDelay(delay);\n\n const resumed: PersistedAlarm = {\n retryTime: now + delay,\n backoff: backoff + 1,\n countedRetry: persisted.countedRetry,\n // Uncounted, which is what makes the next counted failure reset the\n // backoff — upstream's own rule for a user error arriving after an\n // internal one (`alarm-scheduler.c++:257-265`).\n previousRetryCountedAgainstLimit: false,\n running: false,\n };\n this.#db.run(\n STMT.saveRetry,\n resumed.retryTime,\n resumed.backoff,\n resumed.countedRetry,\n 0,\n actorId,\n );\n return resumed;\n }\n\n /** ← `scheduleAlarm` (`alarm-scheduler.c++:166-171`). */\n #scheduleAlarm(\n now: number,\n actorId: string,\n scheduledTime: number,\n resumed?: PersistedAlarm,\n ): ScheduledAlarm {\n // The entry exists before its task, where upstream's task exists before the entry that owns it:\n // the task has to be able to ask whether it is still the live entry, which is what stands in\n // for kj cancelling it when the entry it lives on is destroyed. Nothing observes the ordering,\n // because `makeAlarmTask` awaits its wake before touching anything.\n const entry: ScheduledAlarm = {\n actorId,\n scheduledTime,\n wakeTime: null,\n task: undefined,\n cancel: new AbortController(),\n queuedAlarm: null,\n status: \"WAITING\",\n previousRetryCountedAgainstLimit: resumed?.previousRetryCountedAgainstLimit ?? false,\n backoff: resumed?.backoff ?? 0,\n countedRetry: resumed?.countedRetry ?? 0,\n };\n // A pending retry can only DELAY the wake, never pull it before the alarm's own time. The two\n // disagree when a delivery was interrupted and the actor had already asked for a later alarm:\n // the row then holds a future `scheduled_time` and a retry due seconds from now, and honouring\n // the retry would fire the newer alarm early.\n const wake = Math.max(scheduledTime, resumed?.retryTime ?? scheduledTime);\n entry.wakeTime = wake;\n entry.task = this.#makeAlarmTask(wake - now, entry, scheduledTime);\n return entry;\n }\n\n /** ← `entry.value = scheduleAlarm(...)`, whose assignment destroys the old task. */\n #replace(previous: ScheduledAlarm, next: ScheduledAlarm): void {\n previous.cancel.abort();\n this.#alarms.set(next.actorId, next);\n }\n\n /** ← `checkTimestamp` (`alarm-scheduler.c++:173-185`), as a loop rather than tail recursion. */\n async #checkTimestamp(delay: number, scheduledTime: number, signal: AbortSignal): Promise<void> {\n let remaining = delay;\n for (;;) {\n await this.#timer.afterDelay(remaining, signal);\n\n // Since we are waiting on timer.afterDelay, it's possible that timer.now() was behind\n // the real time by a few ms, leading to premature alarm() execution. This checks it the current\n // time is >= than scheduledTime to ensure we run alarms only on or after their scheduled time.\n const now = this.#timer.now();\n if (now >= scheduledTime) return;\n // If it's not yet time to trigger the alarm, we shall wait a while longer until we can\n // trigger it. This repeats until it's time for the alarm to run.\n remaining = scheduledTime - now;\n }\n }\n\n /** ← `runAlarm` (`alarm-scheduler.c++:158-164`). */\n async #runAlarm(actorId: string, scheduledTime: number, retryCount: number): Promise<RetryInfo> {\n const result = await this.#getActor(actorId).deliverAlarm(scheduledTime, retryCount);\n return {\n retry: result.outcome !== \"ok\" && result.retry,\n retryCountsAgainstLimit: result.retryCountsAgainstLimit,\n };\n }\n\n /** ← the try/catch lambda around `runAlarm` (`alarm-scheduler.c++:197-211`). */\n async #runAlarmGuarded(\n actorId: string,\n scheduledTime: number,\n retryCount: number,\n ): Promise<RetryInfo> {\n try {\n return await this.#runAlarm(actorId, scheduledTime, retryCount);\n } catch (exception) {\n this.#taskFailed(exception);\n return {\n retry: true,\n // An exception here is \"weird\", they should normally be turned into AlarmResult statuses in\n // the sandbox for any user-caused error. Let's not count this retry attempt against the\n // limit.\n retryCountsAgainstLimit: false,\n };\n }\n }\n\n /** ← `makeAlarmTask` (`alarm-scheduler.c++:187-287`). */\n async #makeAlarmTask(delay: number, entry: ScheduledAlarm, scheduledTime: number): Promise<void> {\n const actorId = entry.actorId;\n await this.#checkTimestamp(delay, scheduledTime, entry.cancel.signal);\n\n // ← `KJ_ASSERT_NONNULL(alarms.findEntry(actorRef))` (`:192`). Upstream can assert here because\n // dropping the entry destroyed this task before it could resume; this is that cancellation.\n if (this.#alarms.get(actorId) !== entry) return;\n\n // Before the delivery, so that a worker that dies during it leaves the mark behind. A failure\n // to write it refuses the delivery rather than running one nothing can notice the end of: the\n // row is untouched, so the alarm is still due and a later scheduler picks it up unchanged. The\n // entry is left WAITING with no task, which a `setAlarm` re-arms; a metadata database this\n // scheduler cannot write is already failing every `setAlarm` too.\n try {\n this.#db.run(STMT.markRunning, actorId);\n } catch (exception) {\n this.#taskFailed(exception);\n return;\n }\n\n entry.status = \"STARTED\";\n entry.wakeTime = null;\n this.#projectNextWake();\n const retryCount = entry.countedRetry;\n\n const retryInfo = await this.#runAlarmGuarded(actorId, scheduledTime, retryCount);\n\n try {\n // ← `:214`'s second `KJ_ASSERT_NONNULL`, which upstream reaches by way of its outer catch when\n // `deleteAll()` cleared the map during the run.\n if (this.#alarms.get(actorId) !== entry) return;\n\n // We can't overwrite our entry before moving ourselves out of it, as a promise cannot\n // delete itself.\n const task = entry.task;\n if (task === undefined) throw new Error(\"An alarm task ran before it was recorded.\");\n this.#addTask(task);\n entry.task = undefined;\n\n // If an alarm is queued, there's no point in retrying the current one -- proceed\n // to running the queued alarm instead.\n const queued = entry.queuedAlarm;\n if (queued !== null) {\n // The delivery is over, and the row already describes the queued alarm — `setAlarm` wrote\n // its time and zeroed the retry state when it arrived — so the mark is all that is left to\n // clear.\n this.#db.run(STMT.clearRunning, actorId);\n // creating a new alarm and overwriting the old one will reset\n // `status` to WAITING and `queuedAlarm` to null\n this.#replace(entry, this.#scheduleAlarm(this.#timer.now(), actorId, queued));\n this.#projectNextWake();\n return;\n }\n\n // When we reach this block of code and alarm has either succeeded or failed and may (or may\n // not) retry. Setting the status of an alarm as FINISHED here, will allow deletion of alarms\n // between retries. If there's a retry, `makeAlarmTask` is called, setting status as RUNNING\n // again.\n entry.status = \"FINISHED\";\n\n if (retryInfo.retry) {\n // recreate the task, running after a delay determined using the retry factor\n if (entry.countedRetry >= ALARM_RETRY_MAX_TRIES) {\n await this.#abandon(entry, scheduledTime);\n return;\n }\n if (retryInfo.retryCountsAgainstLimit) {\n entry.countedRetry += 1;\n\n if (!entry.previousRetryCountedAgainstLimit) {\n // The last retry didn't count against the limit, indicating it was due to some internal\n // error. However, this retry does, meaning it's due to an error in user code,\n // most likely a different error. We should reset the retry counter used for\n // calculating backoff, so user-caused retries don't have an unnecessarily high backoff\n // time if they come after internal-caused retries.\n\n entry.backoff = 0;\n }\n }\n entry.previousRetryCountedAgainstLimit = retryInfo.retryCountsAgainstLimit;\n\n entry.backoff = Math.min(RETRY_BACKOFF_MAX, entry.backoff);\n let retryDelay = alarmRetryDelayMs(entry.backoff);\n\n retryDelay += this.#jitterMsForDelay(retryDelay);\n\n entry.backoff += 1;\n // Persisted before the task is armed, and it also clears `running`, so the two facts a\n // restart needs — that this delivery ended, and where the ladder now stands — are one\n // write. If it throws, the outer catch records it and the mark stays set, which a later\n // scheduler reads as an interrupted delivery: the alarm keeps its counters and is retried,\n // rather than being armed here in memory the process is about to lose.\n const retryTime = this.#timer.now() + retryDelay;\n this.#db.run(\n STMT.saveRetry,\n retryTime,\n entry.backoff,\n entry.countedRetry,\n entry.previousRetryCountedAgainstLimit ? 1 : 0,\n actorId,\n );\n\n entry.wakeTime = retryTime;\n entry.task = this.#makeAlarmTask(retryDelay, entry, scheduledTime);\n this.#projectNextWake();\n } else {\n if (entry.queuedAlarm !== null) {\n throw new Error(\"An alarm that will not retry still has an alarm queued behind it.\");\n }\n this.deleteAlarm(actorId);\n }\n } catch (exception) {\n // ← `KJ_LOG(ERROR, \"Failed to run alarm and was unable to schedule a retry\", exception)`.\n this.#taskFailed(exception);\n }\n }\n\n /**\n * ← the `countedRetry >= RETRY_MAX_TRIES` block (`alarm-scheduler.c++:237-253`).\n *\n * Its comment, verbatim, because the second half is the whole point: \"Notify\n * the actor to clear its in-memory alarm state so getAlarm() reflects the\n * deletion. We ignore the returned remaining time — the workerd-local alarm\n * scheduler already has visibility into the actor's alarm state via its SQLite\n * hooks. If the notification fails, we keep the alarm in the scheduler so it is\n * not silently lost.\"\n *\n * **Divergence: the returned time is not ignored** (upstream's\n * `.ignoreResult()`, `:244`). Upstream is right that the newer alarm normally\n * arrives on its own — `ActorSqlite` reports it through `scheduleRun`, and it\n * lands in `queuedAlarm`, which `deleteAlarm` below then reschedules for. But\n * that is a race, not an invariant: `abandonAlarm` reads the actor's committed\n * metadata, and there is a window in which the actor's alarm is newer than\n * anything the scheduler has been told about. In that window upstream's\n * unconditional `deleteAlarm` removes both the row and the entry, and the alarm\n * only comes back if a later commit happens to re-announce it. Re-registering\n * what `abandonAlarm` reports closes the window, is a no-op whenever the\n * queued alarm already covered it, and takes the side that preserves the alarm.\n */\n async #abandon(entry: ScheduledAlarm, scheduledTime: number): Promise<void> {\n const actorId = entry.actorId;\n let newerAlarm: number | null;\n try {\n newerAlarm = await this.#getActor(actorId).abandonAlarm(scheduledTime);\n } catch (exception) {\n this.#taskFailed(exception);\n return;\n }\n this.deleteAlarm(actorId);\n if (newerAlarm !== null && !this.#alarms.has(actorId)) {\n this.setAlarm(actorId, newerAlarm);\n }\n }\n\n /**\n * ← `maxJitterMsForDelay` (`alarm-scheduler.c++:13-16`) drawn through\n * `std::uniform_int_distribution<>(0, max)` (`:272-273`), whose range is\n * inclusive at both ends.\n */\n #jitterMsForDelay(delayMs: number): number {\n const max = Math.floor(RETRY_JITTER_FACTOR * delayMs);\n return Math.min(max, Math.floor(this.#random() * (max + 1)));\n }\n\n /** The earliest wake a browser watchdog must keep alive across process death. */\n #projectNextWake(): void {\n if (this.#projectWake === undefined) return;\n let earliest: number | null = null;\n for (const entry of this.#alarms.values()) {\n const wake = entry.status === \"STARTED\" ? entry.queuedAlarm : entry.wakeTime;\n if (wake !== null && (earliest === null || wake < earliest)) earliest = wake;\n }\n this.#projection = Promise.resolve(this.#projectWake(earliest));\n void this.#projection.catch((exception: unknown) => this.#taskFailed(exception));\n }\n\n /** ← `tasks.add`, whose failures reach `taskFailed`. */\n #addTask(task: Promise<void>): void {\n const tracked = task.then(\n () => {\n this.#tasks.delete(tracked);\n },\n (exception: unknown) => {\n this.#tasks.delete(tracked);\n this.#taskFailed(exception);\n },\n );\n this.#tasks.add(tracked);\n }\n\n /** ← `taskFailed` (`alarm-scheduler.c++:289-291`). */\n #taskFailed(exception: unknown): void {\n this.#taskFailure ??= { exception };\n }\n}\n\n/**\n * Reads one `_cf_ALARM` row's retry state, refusing anything this scheduler\n * could not have written.\n *\n * No upstream twin — upstream reads two columns and neither can be out of range.\n * It fails closed rather than clamping because every value here decides how long\n * an alarm waits or whether it is given up on, and a repaired counter is a\n * silent behaviour change on the one path that has nobody watching it. There is\n * no reader for any older shape: a database written before these columns existed\n * fails at the SELECT, which is what pre-production means here.\n *\n * The ranges are the ladder's own. `backoff` reaches `RETRY_BACKOFF_MAX + 1`\n * because the clamp is applied before the shift and the increment after it\n * (`alarm-scheduler.c++:269-275`), and `counted_retry` reaches\n * `ALARM_RETRY_MAX_TRIES` because the limit is checked before the increment\n * (`:237`).\n */\nfunction readPersistedAlarm(actorId: string, row: readonly unknown[]): PersistedAlarm {\n const retryTime = isNull(row, 2) ? null : getInt64(row, 2);\n const backoff = requireRange(actorId, \"backoff\", getInt64(row, 3), RETRY_BACKOFF_MAX + 1);\n const countedRetry = requireRange(\n actorId,\n \"counted_retry\",\n getInt64(row, 4),\n ALARM_RETRY_MAX_TRIES,\n );\n const previous = requireRange(actorId, \"previous_retry_counted\", getInt64(row, 5), 1);\n const running = requireRange(actorId, \"running\", getInt64(row, 6), 1);\n return {\n retryTime,\n backoff,\n countedRetry,\n previousRetryCountedAgainstLimit: previous === 1,\n running: running === 1,\n };\n}\n\nfunction requireRange(actorId: string, column: string, value: number, max: number): number {\n if (value < 0 || value > max) {\n throw new Error(\n `Alarm ${column} ${value} for actor ${actorId} is outside [0, ${max}]; ` +\n `_cf_ALARM holds a state this scheduler cannot have written.`,\n );\n }\n return value;\n}\n\n/** ← `ensureInitialized` (`alarm-scheduler.c++:50-60`). */\nfunction ensureInitialized(db: SqliteDatabase): void {\n hasCurrentSqliteTable(db, \"_cf_ALARM\", STMT.createTable);\n // TODO(sqlite): Do this automatically at a lower layer?\n db.run(\"PRAGMA journal_mode=WAL\");\n\n db.run(STMT.createTable);\n}\n"],"mappings":"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"}