mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(durable): arm before resume in the Durable Object example; timer wording
The README example and the test worker arm the recovery alarm before resume() in the constructor and in alarm(). Timers should not fire early, and callers recheck; nativeTimer no longer needs a non-null assertion.
This commit is contained in:
parent
ec1be23eb9
commit
00bf7193d6
6 changed files with 28 additions and 24 deletions
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
- `TaskRuntime.sleep()` is removed: it waited inside a phase, which holds an invocation and, after a restart, reruns the phase and restarts the wait. A task now ends its phase asleep with `runtime.sleepUntil(until, checkpoint, context)` or by returning `{ status: "sleeping", until, checkpoint }` from `runtime.commit()`; move the work after the wait into the next phase and carry what it needs in the checkpoint. `TaskRuntime.setTimer()` paces work inside a phase without waiting for it.
|
||||
- Tasks have a `sleeping` status, so code that handles every `TaskState`, `TaskGraphState`, or `TaskInspection` state needs a case for it.
|
||||
- Opening a SQLite storage migrates it to schema version 2 automatically, rebuilding its task table. A migrated database cannot be opened by 1.3.0 or earlier, so a deployment cannot roll back past this release. JSONL storage needs no migration, but 1.3.0 or earlier never resumes a sleeping task it finds, so its conversation stays busy.
|
||||
- Opening a SQLite storage migrates it to schema version 2 automatically, rebuilding its task table. A migrated database cannot be opened by 1.3.0 or earlier, so a deployment cannot roll back past this release. JSONL storage needs no migration, but 1.3.0 or earlier never resumes a sleeping task it finds (a run's conversation stays busy).
|
||||
- `TaskRuntime` has `sleepUntil()` and `setTimer()`, and `ToolExecutionApi` has `now()`: code that builds either object by hand, such as a test fake or a tool wrapper that does not spread the original, must add them.
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -165,13 +165,15 @@ export class Session extends DurableObject<Env> {
|
|||
// A failed alarm write resets the object; it arms again when it next starts.
|
||||
void this.#arm(wake).catch(() => ctx.abort("Alarm write failed"));
|
||||
this.#harness = await Harness.open(storage, { models, registry, onWake }, context);
|
||||
this.#harness.resume();
|
||||
// Armed before `resume()` runs anything, as for any call that can start work.
|
||||
await this.#arm(this.#harness.wake());
|
||||
this.#harness.resume(); // work a lost instance left, and due sleeps
|
||||
});
|
||||
}
|
||||
|
||||
override async alarm(): Promise<void> {
|
||||
this.#harness.resume(); // resumes due sleeps and work a lost process left
|
||||
await this.#arm(this.#harness.wake());
|
||||
await this.#arm({ ...this.#harness.wake(), active: true });
|
||||
this.#harness.resume(); // due sleeps and work a lost instance left
|
||||
}
|
||||
|
||||
// Arm before any call that can start work (submit, compact, abortTask, abort, a registry change): `onWake` runs
|
||||
|
|
|
|||
|
|
@ -2363,11 +2363,10 @@ when `nodejs_compat` gives it an `unref()`, and wherever timers cannot be
|
|||
unreferenced. Foreground timers pace or resume work: throttled
|
||||
progress commits through `TaskRuntime.setTimer()`, and, without `onWake`, the
|
||||
timer for the earliest sleep while scheduling runs. That one keeps a Node.js
|
||||
process alive, since a sleeping task is work left to do. A timer must not fire
|
||||
before its delay and may fire late; the default splits a delay beyond what
|
||||
`setTimeout` supports itself. A host that throws on or declines a foreground
|
||||
process alive, since a sleeping task is work left to do. A timer should not fire
|
||||
early and may fire late; every caller checks its clock again when it fires. The
|
||||
default waits out a delay beyond what `setTimeout` supports in steps. A host that throws on or declines a foreground
|
||||
timer has it armed with `setTimeout` instead, and the first failure is reported.
|
||||
Callers still check their clock when a timer fires.
|
||||
|
||||
**Wakeups.** `Harness.wake()` says, from memory and also while scheduling is
|
||||
paused, when the Session next needs a running process:
|
||||
|
|
|
|||
|
|
@ -10,26 +10,26 @@ const MAX_TIMER_DELAY = 2_147_483_647;
|
|||
const inWorkers = typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers";
|
||||
|
||||
/**
|
||||
* The default `HarnessOptions.setTimer`: `setTimeout`, never firing early, also for delays beyond its limit. A
|
||||
* background timer is unreferenced, so it never keeps the process alive. It is declined in Cloudflare Workers and
|
||||
* wherever timers cannot be unreferenced.
|
||||
* The default `HarnessOptions.setTimer`: `setTimeout`, waiting out delays beyond its limit in steps. A background timer
|
||||
* is unreferenced, so it never keeps the process alive. It is declined in Cloudflare Workers and wherever timers cannot
|
||||
* be unreferenced.
|
||||
*/
|
||||
export const nativeTimer: SetTimer = (callback, delayMs, options) => {
|
||||
if (options.background && inWorkers) return undefined;
|
||||
let handle: ReturnType<typeof setTimeout>;
|
||||
const arm = (remaining: number): boolean => {
|
||||
handle = setTimeout(
|
||||
() => (remaining > MAX_TIMER_DELAY ? arm(remaining - MAX_TIMER_DELAY) : callback()),
|
||||
const arm = (remaining: number): ReturnType<typeof setTimeout> => {
|
||||
const next = setTimeout(
|
||||
() => {
|
||||
if (remaining > MAX_TIMER_DELAY) handle = arm(remaining - MAX_TIMER_DELAY);
|
||||
else callback();
|
||||
},
|
||||
Math.min(remaining, MAX_TIMER_DELAY),
|
||||
);
|
||||
if (!options.background) return true;
|
||||
const unref = (handle as { unref?: unknown }).unref;
|
||||
if (typeof unref !== "function") return false;
|
||||
unref.call(handle);
|
||||
return true;
|
||||
if (options.background) (next as { unref?: () => void }).unref?.();
|
||||
return next;
|
||||
};
|
||||
if (!arm(Math.max(0, delayMs))) {
|
||||
clearTimeout(handle!);
|
||||
let handle = arm(Math.max(0, delayMs));
|
||||
if (options.background && typeof (handle as { unref?: unknown }).unref !== "function") {
|
||||
clearTimeout(handle);
|
||||
return undefined;
|
||||
}
|
||||
return () => clearTimeout(handle);
|
||||
|
|
|
|||
|
|
@ -134,10 +134,11 @@ export type TaskDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocF
|
|||
>;
|
||||
|
||||
/**
|
||||
* A host timer, given as `HarnessOptions.setTimer`: call `callback` once, no sooner than `delayMs` from now, and return
|
||||
* A host timer, given as `HarnessOptions.setTimer`: call `callback` once, about `delayMs` from now, and return
|
||||
* a function that disarms it. `background` timers only free memory, and a host may decline one by returning
|
||||
* `undefined`, for example where a pending timer keeps the process from hibernating; the Harness then frees that memory
|
||||
* at later commits. Other timers pace or resume work: one the host throws on or declines is armed with `setTimeout`.
|
||||
* A timer should not fire early; callers check their clock again when it fires.
|
||||
*/
|
||||
export type SetTimer = (
|
||||
callback: () => void,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ export class TestSession extends DurableObject {
|
|||
},
|
||||
context,
|
||||
);
|
||||
// Armed before `resume()` runs anything, as for any call that can start work.
|
||||
await this.arm(this.#harness.wake());
|
||||
this.#harness.resume();
|
||||
});
|
||||
}
|
||||
|
|
@ -94,8 +96,8 @@ export class TestSession extends DurableObject {
|
|||
}
|
||||
|
||||
async alarm(): Promise<void> {
|
||||
await this.arm({ ...this.#harness.wake(), active: true });
|
||||
this.#harness.resume();
|
||||
await this.arm(this.#harness.wake());
|
||||
}
|
||||
|
||||
/** The alarm a host arms for `wake`: the earliest sleep, or a recovery heartbeat while work can run. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue