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.

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
  • Loading branch information
warner committed Aug 19, 2022
1 parent 406c7c0 commit 9719a67
Show file tree
Hide file tree
Showing 11 changed files with 2,746 additions and 119 deletions.
92 changes: 69 additions & 23 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,88 @@ 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
// 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.
// repeatAfter() creates a repeating wakeup service: the handler will
// fire somewhat after 20 seconds from now (now+delay), and again
// 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 'now + delay + k * interval', 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 use 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 the `makeNotifier()`
// you get from the '@agoric/notifier' package.

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));
assert(await E(brand1).isMyClock(clock));
```
77 changes: 63 additions & 14 deletions packages/SwingSet/src/vats/timer/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,17 @@ import type { RankComparison } from '@agoric/store';
// meant to be globally accessible as a side-effect of importing this module.
declare global {
/**
* TODO As of PR #5821 there is no `TimerBrand` yet. The purpose of #5821
* is to prepare the ground for time objects labeled by `TimerBrand` in
* much the same way that `Amounts` are asset/money values labeled by
* `Brands`.
* As of #5821 (the time of this writing), a `TimerService` is actually
* used everywhere a `TimerBrand` is called for.
* TODO Timestamps are not yet labeled with the TimerBrand (in much
* the same way that `Amounts` are asset/money values labeled by
* `Brands`), and a `TimerService` is still used everywhere a
* `TimerBrand` is called for.
*
* See https://github.com/Agoric/agoric-sdk/issues/5798
* and https://github.com/Agoric/agoric-sdk/pull/5821
*/
type TimerBrand = {
isMyTimer: (timer: TimerService) => ERef<boolean>;
isMyClock: (clock: Clock) => ERef<boolean>;
};

/**
Expand Down Expand Up @@ -74,6 +73,13 @@ declare global {
*/
type RelativeTime = RelativeTimeRecord | RelativeTimeValue;

/**
* A CancelToken is an arbitrary marker object, passed in with
* each API call that creates a wakeup or repeater, and passed to
* cancel() to cancel them all.
*/
type CancelToken = object;

/**
* Gives the ability to get the current time,
* schedule a single wake() call, create a repeater that will allow scheduling
Expand All @@ -87,13 +93,27 @@ declare global {
/**
* Return value is the time at which the call is scheduled to take place
*/
setWakeup: (baseTime: Timestamp, waker: ERef<TimerWaker>) => Timestamp;
setWakeup: (
baseTime: Timestamp,
waker: ERef<TimerWaker>,
cancelToken?: CancelToken,
) => Timestamp;
/**
* Remove the waker
* from all its scheduled wakeups, whether produced by `timer.setWakeup(h)` or
* `repeater.schedule(h)`.
* Create and return a promise that will resolve after the absolte
* time has passed.
*/
removeWakeup: (waker: ERef<TimerWaker>) => Array<Timestamp>;
wakeAt: (
baseTime: Timestamp,
cancelToken?: CancelToken,
) => Promise<Timestamp>;
/**
* Create and return a promise that will resolve after the relative time has
* passed.
*/
delay: (
delay: RelativeTime,
cancelToken?: CancelToken,
) => Promise<Timestamp>;
/**
* Create and return a repeater that will schedule `wake()` calls
* repeatedly at times that are a multiple of interval following delay.
Expand All @@ -106,20 +126,49 @@ declare global {
makeRepeater: (
delay: RelativeTime,
interval: RelativeTime,
cancelToken?: CancelToken,
) => TimerRepeater;
/**
* Create a repeater with a handler directly.
*/
repeatAfter: (
delay: RelativeTime,
interval: RelativeTime,
handler: TimerWaker,
cancelToken?: CancelToken,
) => void;
/**
* Create and return a Notifier that will deliver updates repeatedly at times
* that are a multiple of interval following delay.
*/
makeNotifier: (
delay: RelativeTime,
interval: RelativeTime,
cancelToken?: CancelToken,
) => Notifier<Timestamp>;
/**
* Create and return a promise that will resolve after the relative time has
* passed.
* Cancel a previously-established wakeup or repeater.
*/
cancel: (cancelToken: CancelToken) => void;
/**
* Retrieve the read-only Clock facet.
*/
getClock: () => Clock;
/**
* Retrieve the Brand for this timer service.
*/
getTimerBrand: () => TimerBrand;
};

type Clock = {
/**
* Retrieve the latest timestamp
*/
getCurrentTimestamp: () => Timestamp;
/**
* Retrieve the Brand for this timer service.
*/
delay: (delay: RelativeTime) => Promise<Timestamp>;
getTimerBrand: () => TimerBrand;
};

type TimerWaker = {
Expand Down
Loading

0 comments on commit 9719a67

Please sign in to comment.