Skip to content

Commit

Permalink
fix!(swingset): overhaul vat-timer, durability, API, and tests
Browse files Browse the repository at this point in the history
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. However Notifiers will always report a scheduled
time (some multiple of the interval). The opaque `updateCount` used in
Notifier updates is now a time value, not a counter, so user tests
should refrain from asserting sequentiality.

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.

`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
  • Loading branch information
warner committed Aug 11, 2022
1 parent 7e4b07b commit 3c4183a
Show file tree
Hide file tree
Showing 10 changed files with 2,492 additions and 108 deletions.
88 changes: 66 additions & 22 deletions packages/SwingSet/docs/timer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

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

## Kernel Configuration

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.

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

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

const now = await E(timerService).getCurrentTimestamp();

// simple non-cancellable Promise-based delay
const p = E(timerService).delay(30); // fires 30 seconds from now
await p;

// to cancel wakeups, first build a handler
// simple one-shot Promise-based relative delay
const p1 = E(timerService).delay(30n); // fires 30 seconds from now
await p1;

// same, but cancellable
const cancel2 = Far('cancel', {}); // any pass-by-reference object
// the cancelToken is always optional
const p2 = E(timerService).delay(30n, cancel2);
// E(timerService).cancel(cancel2) will cancel that

// same, but absolute instead of relative-to-now
const monday = 1_660_000_000;
const p3 = E(timerService).wakeAt(monday, cancel2);
await p3; // fires Mon Aug 8 16:06:40 2022 PDT

// non-Promise API functions needs a handler callback
const handler = Far('handler', {
wake(t) { console.log(`woken up at ${t}`); },
wake(t) { console.log(`woken up, scheduled for ${t}`); },
});
// then for one-shot wakeups:
await E(timerService).setWakeup(startTime, handler);
// handler.wake(t) will be called shortly after 'startTime'

// then for one-shot absolute wakeups:
await E(timerService).setWakeup(monday, handler, cancel2);
// handler.wake(t) will be called shortly after monday

// cancel early:
await E(timerService).removeWakeup(handler);
await E(timerService).cancel(cancel2);

// wake up at least 60 seconds from now:
await E(timerService).setWakeup(now + 60n, handler);

await E(timerService).setWakeup(now + 60n, handler, cancel2);

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

const delay = 20n;
const interval = 60n;
E(timerService).repeatAfter(delay, interval, handler, cancel2);

// repeating wakeup service, Notifier-style . This supports both the
// native 'E(notifierP).getUpdateSince()' Notifier protocol, and an
// asyncIterator. To it in a for/await loop, which does not know how
// to make `E()`-style eventual sends to the remote notifier, you must
// wrap it in a local "front-end" Notifier by calling `makeNotifier()`.

const notifierP = E(timerService).makeNotifier(delay, interval, cancel2);
// import { makeNotifier } from '@agoric/notifier';
const notifier = makeNotifier(notifierP);

for await (const scheduled of notifier) {
console.log(`woken up, scheduled for ${scheduled}`);
// note: runs forever, once per 'interval'
break; // unless you escape early
}

// `makeRepeater` creates a "repeater object" with .schedule
// and .disable methods to turn it on and off

const r = E(timerService).makeRepeater(delay, interval);
E(r).schedule(handler);
E(r).disable(); // cancel and delete entire repeater

// repeating wakeup service, Notifier-style
const notifier = E(timerService).makeNotifier(delay, interval);

// the 'clock' facet offers `getCurrentTimestamp` and nothing else
const clock = await E(timerService).getClock();
const now2 = await E(clock).getCurrentTimestamp();

// a "Timer Brand" is an object that identifies the source of time
// used by any given TimerService, without exposing any authority
// to get the time or schedule wakeups

const brand1 = await E(timerService).getTimerBrand();
const brand2 = await E(clock).getTimerBrand();
assert.equal(brand1, brand2);
assert(await E(brand1).isMyTimerService(timerService));
```
6 changes: 0 additions & 6 deletions packages/SwingSet/src/vats/timer/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,6 @@ declare global {
* Return value is the time at which the call is scheduled to take place
*/
setWakeup: (baseTime: Timestamp, waker: ERef<TimerWaker>) => Timestamp;
/**
* Remove the waker
* from all its scheduled wakeups, whether produced by `timer.setWakeup(h)` or
* `repeater.schedule(h)`.
*/
removeWakeup: (waker: ERef<TimerWaker>) => Array<Timestamp>;
/**
* Create and return a repeater that will schedule `wake()` calls
* repeatedly at times that are a multiple of interval following delay.
Expand Down
Loading

0 comments on commit 3c4183a

Please sign in to comment.