Skip to content

Commit e4b045d

Browse files
committed
fix!(swingset): overhaul vat-timer, durability, API, and tests
vat-timer is now fully virtualized, durablized, and upgradeable. RAM usage should be O(N) in the number of: * pending Promise wakeups (`wakeAt`, `delay`) * active Notifier promises (`makeNotifier`) * active Iterator promises (`makeNotifier()[Symbol.asyncIterator]`) Pending promises will be disconnected (rejected) during upgrade, as usual. All handlers and Promises will fire with the most recent timestamp available, which (under load) may be somewhat later than the scheduled wakeup time. Until cancellation, Notifiers will always report a scheduled time (i.e. `start` plus some multiple of the interval). The opaque `updateCount` used in Notifier updates is a counter starting from 1n. When a Notifier is cancelled, the final/"finish" value is the timestamp of cancellation, which may or may not be a multiple of the interval (and might be a duplicate of the last non-final value). Once in the cancelled state, `getUpdateSince(anything)` yields `{ value: cancellationTimestamp, updateCount: undefined }`, and the corresponding `iterator.next()` resolves to `{ value: cancellationTimestamp, done: true }`. Neither will ever reject their Promises (except due to upgrade). Asking for a wakeup in the past or present will fire immediately. Most API calls will accept an arbitrary Far object as a CancelToken, which can be used to cancel the wakeup/repeater. `makeRepeater` is the exception. This does not change the device-timer API or implementation, however vat-timer now only uses a single device-side wakeup, and only exposes a single handler object, to minimize the memory usage and object retention by the device (since devices do not participate in GC). This introduces a `Clock` which can return time values without also providing scheduling authority, and a `TimerBrand` which can validate time values without providing clock or scheduling authority. Timestamps are not yet Branded, but the scaffolding is in place. `packages/SwingSet/tools/manual-timer.js` offers a manually-driven timer service, which can help with unit tests. closes #5668 closes #5709 closes #4282 refs #4286 closes #4296 closes #5616 closes #5709 refs #5798
1 parent 46e34f6 commit e4b045d

11 files changed

+2702
-110
lines changed

packages/SwingSet/docs/timer.md

+66-22
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
There's documentation elsewhere about [how devices fit into the SwingSet
44
architecture](devices.md). In order to install a Timer device, you first build
5-
a timer object in order to create the timer's endowments, source code, and
5+
a timer object in order to create the timer's endowments, source code, and
66
`poll()` function.
77

88
## Kernel Configuration
99

1010
The timer service consists of a device (`device-timer`) and a helper vat (`vat-timer`). The host application must configure the device as it builds the swingset kernel, and then the bootstrap vat must finish the job by wiring the device and vat together.
1111

12-
```
12+
```js
1313
import { buildTimer } from `@agoric/swingset-vat`;
1414
const timer = buildTimer();
1515
```
@@ -67,42 +67,86 @@ A single application might have multiple sources of time, which would require th
6767
The `timerService` object can be distributed to other vats as necessary.
6868

6969
```js
70-
// for this example, assume poll() provides seconds-since-epoch as a BigInt
70+
// for this example, assume poll() provides seconds-since-epoch
7171

7272
const now = await E(timerService).getCurrentTimestamp();
73-
74-
// simple non-cancellable Promise-based delay
75-
const p = E(timerService).delay(30); // fires 30 seconds from now
76-
await p;
7773

78-
// to cancel wakeups, first build a handler
74+
// simple one-shot Promise-based relative delay
75+
const p1 = E(timerService).delay(30n); // fires 30 seconds from now
76+
await p1;
77+
78+
// same, but cancellable
79+
const cancel2 = Far('cancel', {}); // any pass-by-reference object
80+
// the cancelToken is always optional
81+
const p2 = E(timerService).delay(30n, cancel2);
82+
// E(timerService).cancel(cancel2) will cancel that
83+
84+
// same, but absolute instead of relative-to-now
85+
const monday = 1_660_000_000;
86+
const p3 = E(timerService).wakeAt(monday, cancel2);
87+
await p3; // fires Mon Aug 8 16:06:40 2022 PDT
7988

89+
// non-Promise API functions needs a handler callback
8090
const handler = Far('handler', {
81-
wake(t) { console.log(`woken up at ${t}`); },
91+
wake(t) { console.log(`woken up, scheduled for ${t}`); },
8292
});
83-
84-
// then for one-shot wakeups:
85-
await E(timerService).setWakeup(startTime, handler);
86-
// handler.wake(t) will be called shortly after 'startTime'
93+
94+
// then for one-shot absolute wakeups:
95+
await E(timerService).setWakeup(monday, handler, cancel2);
96+
// handler.wake(t) will be called shortly after monday
8797

8898
// cancel early:
89-
await E(timerService).removeWakeup(handler);
99+
await E(timerService).cancel(cancel2);
90100

91101
// wake up at least 60 seconds from now:
92-
await E(timerService).setWakeup(now + 60n, handler);
93-
102+
await E(timerService).setWakeup(now + 60n, handler, cancel2);
94103

95-
// makeRepeater() creates a repeating wakeup service: the handler will
104+
// repeatAfter() creates a repeating wakeup service: the handler will
96105
// fire somewhat after 80 seconds from now (delay+interval), and again
97-
// every 60 seconds thereafter. Individual wakeups might be delayed,
98-
// but the repeater will not accumulate drift.
106+
// every 60 seconds thereafter. The next wakeup will not be scheduled
107+
// until the handler message is acknowledged (when its return promise is
108+
// fulfilled), so wakeups might be skipped, but they will always be
109+
// scheduled for the next 'now + delay + k * repeat', so they will not
110+
// accumulate drift. If the handler rejects, the repeater will be
111+
// cancelled.
99112

100113
const delay = 20n;
101114
const interval = 60n;
115+
E(timerService).repeatAfter(delay, interval, handler, cancel2);
116+
117+
// repeating wakeup service, Notifier-style . This supports both the
118+
// native 'E(notifierP).getUpdateSince()' Notifier protocol, and an
119+
// asyncIterator. To use it in a for/await loop (which does not know how
120+
// to make `E()`-style eventual sends to the remote notifier), you must
121+
// wrap it in a local "front-end" Notifier by calling `makeNotifier()`.
122+
123+
const notifierP = E(timerService).makeNotifier(delay, interval, cancel2);
124+
// import { makeNotifier } from '@agoric/notifier';
125+
const notifier = makeNotifier(notifierP);
126+
127+
for await (const scheduled of notifier) {
128+
console.log(`woken up, scheduled for ${scheduled}`);
129+
// note: runs forever, once per 'interval'
130+
break; // unless you escape early
131+
}
132+
133+
// `makeRepeater` creates a "repeater object" with .schedule
134+
// and .disable methods to turn it on and off
135+
102136
const r = E(timerService).makeRepeater(delay, interval);
103137
E(r).schedule(handler);
104138
E(r).disable(); // cancel and delete entire repeater
105-
106-
// repeating wakeup service, Notifier-style
107-
const notifier = E(timerService).makeNotifier(delay, interval);
139+
140+
// the 'clock' facet offers `getCurrentTimestamp` and nothing else
141+
const clock = await E(timerService).getClock();
142+
const now2 = await E(clock).getCurrentTimestamp();
143+
144+
// a "Timer Brand" is an object that identifies the source of time
145+
// used by any given TimerService, without exposing any authority
146+
// to get the time or schedule wakeups
147+
148+
const brand1 = await E(timerService).getTimerBrand();
149+
const brand2 = await E(clock).getTimerBrand();
150+
assert.equal(brand1, brand2);
151+
assert(await E(brand1).isMyTimerService(timerService));
108152
```

packages/SwingSet/src/vats/timer/types.d.ts

+58-8
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ declare global {
7474
*/
7575
type RelativeTime = RelativeTimeRecord | RelativeTimeValue;
7676

77+
/**
78+
* A CancelToken is an arbitrary marker object, passed in with
79+
* each API call that creates a wakeup or repeater, and passed to
80+
* cancel() to cancel them all.
81+
*/
82+
type CancelToken = object;
83+
7784
/**
7885
* Gives the ability to get the current time,
7986
* schedule a single wake() call, create a repeater that will allow scheduling
@@ -87,13 +94,27 @@ declare global {
8794
/**
8895
* Return value is the time at which the call is scheduled to take place
8996
*/
90-
setWakeup: (baseTime: Timestamp, waker: ERef<TimerWaker>) => Timestamp;
97+
setWakeup: (
98+
baseTime: Timestamp,
99+
waker: ERef<TimerWaker>,
100+
cancelToken?: CancelToken,
101+
) => Timestamp;
91102
/**
92-
* Remove the waker
93-
* from all its scheduled wakeups, whether produced by `timer.setWakeup(h)` or
94-
* `repeater.schedule(h)`.
103+
* Create and return a promise that will resolve after the absolte
104+
* time has passed.
95105
*/
96-
removeWakeup: (waker: ERef<TimerWaker>) => Array<Timestamp>;
106+
wakeAt: (
107+
baseTime: Timestamp,
108+
cancelToken?: CancelToken,
109+
) => Promise<Timestamp>;
110+
/**
111+
* Create and return a promise that will resolve after the relative time has
112+
* passed.
113+
*/
114+
delay: (
115+
delay: RelativeTime,
116+
cancelToken?: CancelToken,
117+
) => Promise<Timestamp>;
97118
/**
98119
* Create and return a repeater that will schedule `wake()` calls
99120
* repeatedly at times that are a multiple of interval following delay.
@@ -106,20 +127,49 @@ declare global {
106127
makeRepeater: (
107128
delay: RelativeTime,
108129
interval: RelativeTime,
130+
cancelToken?: CancelToken,
109131
) => TimerRepeater;
132+
/**
133+
* Create a repeater with a handler directly.
134+
*/
135+
repeatAfter: (
136+
delay: RelativeTime,
137+
interval: RelativeTime,
138+
handler: TimerWaker,
139+
cancelToken?: CancelToken,
140+
) => void;
110141
/**
111142
* Create and return a Notifier that will deliver updates repeatedly at times
112143
* that are a multiple of interval following delay.
113144
*/
114145
makeNotifier: (
115146
delay: RelativeTime,
116147
interval: RelativeTime,
148+
cancelToken?: CancelToken,
117149
) => Notifier<Timestamp>;
118150
/**
119-
* Create and return a promise that will resolve after the relative time has
120-
* passed.
151+
* Cancel a previously-established wakeup or repeater.
152+
*/
153+
cancel: (cancelToken: CancelToken) => void;
154+
/**
155+
* Retrieve the read-only Clock facet.
156+
*/
157+
getClock: () => Clock;
158+
/**
159+
* Retrieve the Brand for this timer service.
160+
*/
161+
getTimerBrand: () => TimerBrand;
162+
};
163+
164+
type Clock = {
165+
/**
166+
* Retrieve the latest timestamp
167+
*/
168+
getCurrentTimestamp: () => Timestamp;
169+
/**
170+
* Retrieve the Brand for this timer service.
121171
*/
122-
delay: (delay: RelativeTime) => Promise<Timestamp>;
172+
getTimerBrand: () => TimerBrand;
123173
};
124174

125175
type TimerWaker = {

0 commit comments

Comments
 (0)