Skip to content

Commit 263b434

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 #4282 refs #4286 closes #4296 closes #5616 closes #5668 closes #5709 refs #5798
1 parent 497d157 commit 263b434

11 files changed

+2741
-119
lines changed

packages/SwingSet/docs/timer.md

+69-23
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,88 @@ 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
96-
// 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.
104+
// repeatAfter() creates a repeating wakeup service: the handler will
105+
// fire somewhat after 20 seconds from now (now+delay), and again
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 * interval', 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 the `makeNotifier()`
122+
// you get from the '@agoric/notifier' package.
123+
124+
const notifierP = E(timerService).makeNotifier(delay, interval, cancel2);
125+
// import { makeNotifier } from '@agoric/notifier';
126+
const notifier = makeNotifier(notifierP);
127+
128+
for await (const scheduled of notifier) {
129+
console.log(`woken up, scheduled for ${scheduled}`);
130+
// note: runs forever, once per 'interval'
131+
break; // unless you escape early
132+
}
133+
134+
// `makeRepeater` creates a "repeater object" with .schedule
135+
// and .disable methods to turn it on and off
136+
102137
const r = E(timerService).makeRepeater(delay, interval);
103138
E(r).schedule(handler);
104139
E(r).disable(); // cancel and delete entire repeater
105-
106-
// repeating wakeup service, Notifier-style
107-
const notifier = E(timerService).makeNotifier(delay, interval);
140+
141+
// the 'clock' facet offers `getCurrentTimestamp` and nothing else
142+
const clock = await E(timerService).getClock();
143+
const now2 = await E(clock).getCurrentTimestamp();
144+
145+
// a "Timer Brand" is an object that identifies the source of time
146+
// used by any given TimerService, without exposing any authority
147+
// to get the time or schedule wakeups
148+
149+
const brand1 = await E(timerService).getTimerBrand();
150+
const brand2 = await E(clock).getTimerBrand();
151+
assert.equal(brand1, brand2);
152+
assert(await E(brand1).isMyTimerService(timerService));
153+
assert(await E(brand1).isMyClock(clock));
108154
```

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

+63-14
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,17 @@ import type { RankComparison } from '@agoric/store';
99
// meant to be globally accessible as a side-effect of importing this module.
1010
declare global {
1111
/**
12-
* TODO As of PR #5821 there is no `TimerBrand` yet. The purpose of #5821
13-
* is to prepare the ground for time objects labeled by `TimerBrand` in
14-
* much the same way that `Amounts` are asset/money values labeled by
15-
* `Brands`.
16-
* As of #5821 (the time of this writing), a `TimerService` is actually
17-
* used everywhere a `TimerBrand` is called for.
12+
* TODO Timestamps are not yet labeled with the TimerBrand (in much
13+
* the same way that `Amounts` are asset/money values labeled by
14+
* `Brands`), and a `TimerService` is still used everywhere a
15+
* `TimerBrand` is called for.
1816
*
1917
* See https://github.com/Agoric/agoric-sdk/issues/5798
2018
* and https://github.com/Agoric/agoric-sdk/pull/5821
2119
*/
2220
type TimerBrand = {
2321
isMyTimer: (timer: TimerService) => ERef<boolean>;
22+
isMyClock: (clock: Clock) => ERef<boolean>;
2423
};
2524

2625
/**
@@ -74,6 +73,13 @@ declare global {
7473
*/
7574
type RelativeTime = RelativeTimeRecord | RelativeTimeValue;
7675

76+
/**
77+
* A CancelToken is an arbitrary marker object, passed in with
78+
* each API call that creates a wakeup or repeater, and passed to
79+
* cancel() to cancel them all.
80+
*/
81+
type CancelToken = object;
82+
7783
/**
7884
* Gives the ability to get the current time,
7985
* schedule a single wake() call, create a repeater that will allow scheduling
@@ -87,13 +93,27 @@ declare global {
8793
/**
8894
* Return value is the time at which the call is scheduled to take place
8995
*/
90-
setWakeup: (baseTime: Timestamp, waker: ERef<TimerWaker>) => Timestamp;
96+
setWakeup: (
97+
baseTime: Timestamp,
98+
waker: ERef<TimerWaker>,
99+
cancelToken?: CancelToken,
100+
) => Timestamp;
91101
/**
92-
* Remove the waker
93-
* from all its scheduled wakeups, whether produced by `timer.setWakeup(h)` or
94-
* `repeater.schedule(h)`.
102+
* Create and return a promise that will resolve after the absolte
103+
* time has passed.
95104
*/
96-
removeWakeup: (waker: ERef<TimerWaker>) => Array<Timestamp>;
105+
wakeAt: (
106+
baseTime: Timestamp,
107+
cancelToken?: CancelToken,
108+
) => Promise<Timestamp>;
109+
/**
110+
* Create and return a promise that will resolve after the relative time has
111+
* passed.
112+
*/
113+
delay: (
114+
delay: RelativeTime,
115+
cancelToken?: CancelToken,
116+
) => Promise<Timestamp>;
97117
/**
98118
* Create and return a repeater that will schedule `wake()` calls
99119
* repeatedly at times that are a multiple of interval following delay.
@@ -106,20 +126,49 @@ declare global {
106126
makeRepeater: (
107127
delay: RelativeTime,
108128
interval: RelativeTime,
129+
cancelToken?: CancelToken,
109130
) => TimerRepeater;
131+
/**
132+
* Create a repeater with a handler directly.
133+
*/
134+
repeatAfter: (
135+
delay: RelativeTime,
136+
interval: RelativeTime,
137+
handler: TimerWaker,
138+
cancelToken?: CancelToken,
139+
) => void;
110140
/**
111141
* Create and return a Notifier that will deliver updates repeatedly at times
112142
* that are a multiple of interval following delay.
113143
*/
114144
makeNotifier: (
115145
delay: RelativeTime,
116146
interval: RelativeTime,
147+
cancelToken?: CancelToken,
117148
) => Notifier<Timestamp>;
118149
/**
119-
* Create and return a promise that will resolve after the relative time has
120-
* passed.
150+
* Cancel a previously-established wakeup or repeater.
151+
*/
152+
cancel: (cancelToken: CancelToken) => void;
153+
/**
154+
* Retrieve the read-only Clock facet.
155+
*/
156+
getClock: () => Clock;
157+
/**
158+
* Retrieve the Brand for this timer service.
159+
*/
160+
getTimerBrand: () => TimerBrand;
161+
};
162+
163+
type Clock = {
164+
/**
165+
* Retrieve the latest timestamp
166+
*/
167+
getCurrentTimestamp: () => Timestamp;
168+
/**
169+
* Retrieve the Brand for this timer service.
121170
*/
122-
delay: (delay: RelativeTime) => Promise<Timestamp>;
171+
getTimerBrand: () => TimerBrand;
123172
};
124173

125174
type TimerWaker = {

0 commit comments

Comments
 (0)