Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 5c5b0ad

Browse files
CraigglesOmrbbot
andauthored
DO Alarms patch (#294)
* fix setup * fix ava * adjust to latest setAlarm * fix ava * better delete checking; fixed comment clarity * better comments * error message; add test case * Update packages/durable-objects/src/alarms.ts Co-authored-by: MrBBot <[email protected]> * fix throw alarm check Co-authored-by: MrBBot <[email protected]>
1 parent 318aba4 commit 5c5b0ad

File tree

3 files changed

+156
-9
lines changed

3 files changed

+156
-9
lines changed

packages/durable-objects/src/alarms.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class AlarmStore {
2727
// 'objectName:hexId' -> DurableObjectAlarm [pulled from plugin.getObject]
2828
#alarms: Map<string, DurableObjectAlarm> = new Map();
2929
#alarmTimeout?: NodeJS.Timeout;
30+
#callback?: (objectKey: string) => Promise<void>;
3031

3132
// build a map of all alarms from file storage if persist
3233
async setupStore(storage: StorageFactory, persist?: boolean | string) {
@@ -38,20 +39,33 @@ export class AlarmStore {
3839
}
3940
}
4041

42+
#setAlarmTimeout(
43+
now: number,
44+
objectKey: string,
45+
doAlarm: DurableObjectAlarm
46+
) {
47+
// if timeout was already created, delete alarm incase scheduledTime changed
48+
if (doAlarm.timeout) clearTimeout(doAlarm.timeout);
49+
// set alarm
50+
doAlarm.timeout = setTimeout(() => {
51+
this.#deleteAlarm(objectKey, doAlarm);
52+
this.#callback?.(objectKey);
53+
}, Math.max(doAlarm.scheduledTime - now, 0));
54+
}
55+
4156
// any alarms 30 seconds in the future or sooner are returned
42-
async setupAlarms(callback: (objectKey: string) => Promise<void>) {
57+
async setupAlarms(callback?: (objectKey: string) => Promise<void>) {
58+
if (typeof callback === "function") this.#callback = callback;
4359
if (this.#alarmTimeout) return;
4460
const now = Date.now();
4561

4662
// iterate the store. For every alarm within 30 seconds of now,
4763
// setup a timeout and run the callback and then delete the alarm
4864
for (const [objectKey, doAlarm] of this.#alarms) {
4965
const { scheduledTime } = doAlarm;
66+
// if the alarm is within the next 30 seconds, set a timeout
5067
if (scheduledTime < now + 30_000) {
51-
doAlarm.timeout = setTimeout(() => {
52-
this.#deleteAlarm(objectKey, doAlarm);
53-
callback(objectKey);
54-
}, Math.max(scheduledTime - now, 0));
68+
this.#setAlarmTimeout(now, objectKey, doAlarm);
5569
}
5670
}
5771

@@ -60,7 +74,7 @@ export class AlarmStore {
6074
// prior to our next check.
6175
this.#alarmTimeout = setTimeout(() => {
6276
this.#alarmTimeout = undefined;
63-
this.setupAlarms(callback);
77+
this.setupAlarms();
6478
}, 30_000);
6579
this.#alarmTimeout.unref();
6680
}
@@ -74,10 +88,25 @@ export class AlarmStore {
7488
}
7589

7690
async setAlarm(objectKey: string, scheduledTime: number | Date) {
91+
const now = Date.now();
7792
if (typeof scheduledTime !== "number")
7893
scheduledTime = scheduledTime.getTime();
94+
if (scheduledTime <= 0) {
95+
throw TypeError("setAlarm() cannot be called with an alarm time <= 0");
96+
}
97+
// pull in the alarm or create a new one if it does not exist
98+
const doAlarm: DurableObjectAlarm = this.#alarms.get(objectKey) ?? {
99+
scheduledTime,
100+
};
101+
// update scheduledTime incase old alarm existed
102+
doAlarm.scheduledTime = scheduledTime;
103+
// if the alarm is within the next 31 seconds, set a timeout immediately
104+
// add a second to ensure healthy overlap between alarm checks
105+
if (scheduledTime < now + 31_000) {
106+
this.#setAlarmTimeout(now, objectKey, doAlarm);
107+
}
79108
// set the alarm in the store
80-
this.#alarms.set(objectKey, { scheduledTime });
109+
this.#alarms.set(objectKey, doAlarm);
81110
// store the alarm in storage
82111
assert(this.#store);
83112
await this.#store.put(objectKey, {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import assert from "assert";
2+
import { MemoryStorageFactory } from "@miniflare/shared-test";
3+
import anyTest, { TestInterface } from "ava";
4+
import { AlarmStore } from "../src/alarms";
5+
6+
interface Context {
7+
alarmStore: AlarmStore;
8+
}
9+
10+
const test = anyTest as TestInterface<Context>;
11+
12+
test.beforeEach((t) => {
13+
const factory = new MemoryStorageFactory();
14+
const alarmStore = new AlarmStore();
15+
alarmStore.setupStore(factory);
16+
t.context = { alarmStore };
17+
});
18+
19+
test.afterEach((t) => {
20+
const { alarmStore } = t.context;
21+
alarmStore.dispose();
22+
});
23+
24+
test("Alarms: check that a bridge is created", (t) => {
25+
const { alarmStore } = t.context;
26+
const bridge = alarmStore.buildBridge("test");
27+
assert(bridge);
28+
t.is(typeof bridge.setAlarm, "function");
29+
t.is(typeof bridge.deleteAlarm, "function");
30+
});
31+
32+
test("Alarms: setupAlarms and call setAlarm immediately", async (t) => {
33+
t.plan(1);
34+
const { alarmStore } = t.context;
35+
await new Promise<null>((resolve) => {
36+
alarmStore.setupAlarms(async (objectKey) => {
37+
t.is(objectKey, "test");
38+
resolve(null);
39+
});
40+
alarmStore.setAlarm("test", 1);
41+
});
42+
});
43+
44+
test("Alarms: wait a second before updating value", async (t) => {
45+
t.plan(3);
46+
const { alarmStore } = t.context;
47+
let value = 3;
48+
const promise = new Promise<null>((resolve) => {
49+
alarmStore.setupAlarms(async (objectKey) => {
50+
t.is(objectKey, "update");
51+
value++;
52+
resolve(null);
53+
});
54+
alarmStore.setAlarm("update", Date.now() + 1_000);
55+
});
56+
t.is(value, 3);
57+
await promise;
58+
t.is(value, 4);
59+
});
60+
61+
test("Alarms: setAlarm returns undefined; deleteAlarm", async (t) => {
62+
const { alarmStore } = t.context;
63+
const alarm = await alarmStore.setAlarm("toDelete", Date.now() + 50_000);
64+
t.is(alarm, undefined);
65+
const deleted = await alarmStore.deleteAlarm("toDelete");
66+
t.is(deleted, undefined);
67+
t.pass();
68+
});
69+
test("Alarms: check delete worked via a wait period", async (t) => {
70+
t.plan(1);
71+
const { alarmStore } = t.context;
72+
alarmStore.setupAlarms(async () => {
73+
t.fail();
74+
});
75+
// set first alarm 1 second from now
76+
await alarmStore.setAlarm("test", Date.now() + 1_000);
77+
// delete said alarm
78+
await alarmStore.deleteAlarm("test");
79+
// wait an appropriate amount of time
80+
await new Promise((resolve) => setTimeout(resolve, 2_000));
81+
t.pass();
82+
});
83+
84+
test("Alarms: setupAlarms and call setAlarm through the bridge", async (t) => {
85+
t.plan(1);
86+
const { alarmStore } = t.context;
87+
const bridge = alarmStore.buildBridge("test");
88+
await new Promise<null>((resolve) => {
89+
alarmStore.setupAlarms(async (objectKey) => {
90+
t.is(objectKey, "test");
91+
resolve(null);
92+
});
93+
bridge.setAlarm(1);
94+
});
95+
});
96+
97+
test("Alarms: setupAlarms and call setAlarm twice. The second one should trigger", async (t) => {
98+
t.plan(1);
99+
const { alarmStore } = t.context;
100+
const now = Date.now();
101+
await new Promise<null>((resolve) => {
102+
alarmStore.setupAlarms(async () => {
103+
t.true(Date.now() - now > 2_000);
104+
resolve(null);
105+
});
106+
// set first alarm 1 second from now
107+
alarmStore.setAlarm("test", Date.now() + 1_000);
108+
// set the second 5 seconds from now
109+
alarmStore.setAlarm("test", Date.now() + 3_000);
110+
});
111+
});
112+
113+
test("Alarms: setTimeout of 0 throws", async (t) => {
114+
const { alarmStore } = t.context;
115+
await t.throwsAsync(async () => {
116+
await alarmStore.setAlarm("test", 0);
117+
});
118+
});

packages/durable-objects/test/storage.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -992,8 +992,8 @@ test("setAlarm: backing returns inputed number", async (t) => {
992992
test("setAlarm: overide alarm", async (t) => {
993993
const { storage } = t.context;
994994
await storage.setAlarm(testNumber);
995-
await storage.setAlarm(0);
996-
t.is(await storage.getAlarm(), 0);
995+
await storage.setAlarm(5);
996+
t.is(await storage.getAlarm(), 5);
997997
});
998998
test("setAlarm: closes input gate unless allowConcurrency", async (t) => {
999999
const { storage } = t.context;

0 commit comments

Comments
 (0)