├── README.md
└── evsend-miniplayer.png
/README.md:
--------------------------------------------------------------------------------
1 | ## `Promise.delegated`
2 |
3 | *API support for distributed promise pipelining.*
4 |
5 | * Mark S. Miller @erights, Agoric
6 | * Michael FIG @michaelfig, Agoric
7 | * Chip Morningstar @FUDCo, Evernote
8 |
9 | ## Status
10 |
11 | Presented to TC39 (Javascript standards committee), achieving stage 1. (Note that the actual
12 | API has been changed since this talk, to using `Promise.delegated` and other `Promise` static methods
13 | instead of a new `HandledPromise` global.)
14 |
15 | [
](https://www.youtube.com/watch?v=UXR0O-CufTk&list=PLzDw4TTug5O0ywHrOz4VevVTYr6Kj_KtW)
16 |
17 | [Slides](https://github.com/tc39/agendas/blob/master/2019/10.eventual-send-as-recorded.pdf)
18 |
19 |
20 | ## Background
21 |
22 | Promises were invented in the late 1980s, originally as a technique for
23 | compensating for roundtrip latency in operations invoked remotely over a
24 | network, though promises have since proven valuable for dealing with all manner
25 | of asynchronous delays in computational systems.
26 |
27 | The fundamental insight behind promises is this: in the classic presentation of
28 | object-oriented programming, an object is something that you can send messages
29 | to in order to invoke operations on it. If the result of such an operation is
30 | another object, that result in turn is something that you can send messages to.
31 | If the operation initiated by a message entails an asynchronous delay to get
32 | the result, rather than forcing the sender to wait (possibly a long time) for
33 | the result to eventually become available, the system can instead immediately
34 | return another object - a promise - that can stand in for the result in the
35 | meantime. Since, as was just said, an object is something you send messages
36 | to, a promise is, in that respect, potentially as good as the object it is a
37 | promise for -- you simply send it messages as if it was the actual result.
38 |
39 | The
40 | promise can't perform the invoked operation directly, since what that means is
41 | not yet known, but it *can* enqueue the request for later processing or relay
42 | it to the other end of a network connection where the result will eventually be
43 | known. This deferral of operations through enqueuing or relaying can be
44 | pipelined an arbitrary number of operations deep; it is only at the point where
45 | there is a semantic requirement to actually see the result (such as the need to
46 | display it to a human) that the pipeline must stall to await the final outcome.
47 | Furthermore, experience with this paradigm has shown that the point at which
48 | such waiting is truly required can often be much later in a chain of
49 | computational activity than many people's intuitions lead them to expect.
50 |
51 | Since network latency is often the largest component of delay in a remotely
52 | invoked operation, the overlapping of network transmissions that promise
53 | pipelining makes possible can result an enormous overall improvement in
54 | throughput in distributed systems. For example, implementations of promise
55 | pipelining for remote method invocation in the [Xanadu hypertext
56 | system](http://udanax.xanadu.com/gold/) and in Microsoft's [Midori operating
57 | system](http://joeduffyblog.com/2015/11/03/blogging-about-midori/) measured
58 | speedups of 10 to 1,000 over traditional synchronous RPC, depending on use
59 | case.
60 |
61 | Promises in JavaScript were proposed in the 2011 [ECMAScript strawman
62 | concurrency
63 | proposal](https://web.archive.org/web/20161026162206/http://wiki.ecmascript.org/doku.php?id=strawman:concurrency).
64 | These promises descend from the [E language](http://erights.org/) via the
65 | [Waterken Q library](http://waterken.sourceforge.net/web_send/) and [Kris
66 | Kowal's Q library](https://github.com/kriskowal/q). A good early presentation
67 | is Tom Van Cutsem's [Communicating Event Loops: An exploration in
68 | JavaScript](http://soft.vub.ac.be/~tvcutsem/talks/presentations/WGLD_CommEventLoops.pdf).
69 | All of these efforts introduced promises as a first step towards distributed
70 | computing, with the goal of using promises as asynchronous references to remote
71 | objects. However, since the JavaScript language itself does not contain any
72 | intrinsic I/O machinery, relying entirely on the host environment for this,
73 | Promises as JavaScript currently defines them are not by themselves sufficient
74 | to realize the distributed computation vision that originally motivated them.
75 |
76 | Kris Kowal's [Q-connection library](https://github.com/kriskowal/q-connection)
77 | extended Q's promises for distributed computing with [promise
78 | pipelining](https://capnproto.org/rpc.html), essentially in the way we have in
79 | mind. However, in the absence of platform support for [Weak
80 | References](https://github.com/tc39/proposal-weakrefs), this approach was not
81 | practical. Given weak references, the [Midori
82 | project](http://joeduffyblog.com/2015/11/19/asynchronous-everything/) and
83 | [Cap'n Proto](https://capnproto.org/rpc.html), among others, demonstrate that
84 | this approach to distributed computing works well at scale.
85 |
86 | ## Summary
87 |
88 | This proposal adds *eventual-send* operations to JavaScript Promises, to express
89 | invocation of operations on potentially remote objects. We introduce the notion
90 | of a *delegated Promise*, which may have a handler to provide alternate
91 | eventual-send behavior. We also introduce the concept of *Presences*, which may
92 | also have handlers, but are not promises. These mechanisms, together with weak
93 | references, enable the creation of remote object communications systems, but
94 | without committing to any specific implementation. In particular, this proposal
95 | specifies a general mechanism for hooking in whatever host-provided remote
96 | communications facilities are at hand, without constraining the nature of those
97 | facilities.
98 |
99 | This proposal does not mandate any specific usage of the mechanisms it
100 | describes. Such usages as are mentioned here are provided as explanatory and
101 | motivating examples and as ways testing the adequacy of the design, rather than
102 | proposing a particular implementation of remote messaging.
103 |
104 |
105 | ## Design Principles
106 |
107 | 1. Support *promise pipelining* to reduce the cost of network latency.
108 | 1. Prevent reentrancy attacks (a form of plan interference).
109 |
110 | ## Details
111 |
112 | To specify eventual-send operations and delegated promises, we follow the pattern
113 | used to incorporate proxies into JavaScript: That pattern specified...
114 |
115 | * ***internal methods*** that all objects must support.
116 | * ***static methods*** on `Reflect` for invoking these internal methods.
117 | * ***invariants*** that these methods must uphold.
118 | * ***default behaviors*** of these methods for normal (non-exotic) objects.
119 | * ***handler traps***. Proxies implement these methods by delegating most of their behaviors to corresponding traps on their handlers.
120 | * ***proxy invariant enforcement***. The remaining behavior in the proxy methods to guarantee that these invariants are upheld despite arbitrary behavior by the handler.
121 | * ***fallback behaviors*** for absent traps, implemented in terms of the remaining traps.
122 |
123 | Following this analogy, this proposal adds internal eventual-send methods to all
124 | promises, provides default behaviors for undelegated promises, and introduces
125 | delegated promises whose handlers provide traps for these methods.
126 |
127 | A new static method, `Promise.delegated`, enables the creation of delegated
128 | promises. The static methods below are static methods of this maker, that is,
129 | `Promise.delegated.eventualGet`, etc:
130 |
131 | | Internal Method | Static Method |
132 | | --- | --- |
133 | | `p.[[EventualGet]](prop, opts)` | `eventualGet(p, prop, opts = {})` |
134 | | `p.[[EventualApply]](args, opts)` | `eventualApply(p, args, opts = {})` |
135 | | `p.[[EventualSend]](prop, args, opts)`| `eventualSend(p, prop, args, opts = {})` |
136 |
137 | The static methods first do a `Promise.resolve` on their first
138 | argument, to coerce it to a promise with these internal methods. Thus, for
139 | example,
140 |
141 | ```js
142 | Promise.delegated.eventualGet(p, prop)
143 | ```
144 | actually does the equivalent of
145 | ```js
146 | Promise.resolve(p).[[EventualGet]](prop, {})
147 | ```
148 |
149 | Via the internal methods, the static methods cause either the default behavior,
150 | or, for delegated promises, the behavior that calls the associated handler trap.
151 |
152 | | Static Method | Default Behavior | Handler trap |
153 | | --- | --- | --- |
154 | | `eventualGet(p, prop, opts)` | `p.then(t => t[prop])` | `h.eventualGet(t, prop, { opts })` |
155 | | `eventualApply(p, args, opts)` | `p.then(t => t(...args))` | `h.eventualApply(t, args, { opts })` |
156 | | `eventualSend(p, prop, args, opts)` | `p.then(t => t[prop](...args))` | `h.eventualSend(t, prop, args, { opts })` |
157 |
158 | To protect against reentrancy, the proxy internal method postpones the
159 | execution of the handler trap to a later turn, and immediately returns a
160 | promise for what the trap will return. For example, the [[EventualGet]] internal
161 | method of a delegated promise is effectively
162 |
163 | ```js
164 | p.then(t => h.eventualGet(t, prop, { opts }))
165 | ```
166 |
167 | ### `E` convenience proxy maker
168 |
169 | Probably the most common distributed programming case, invocation of remote
170 | methods with or without requiring return results, can be implemented by
171 | powerless proxies. All authority needed to enable communication between the
172 | peers can be implemented in the delegated promise infrastructure.
173 |
174 | The `E(target)` proxy maker wraps a target (which may or may not be remote) and
175 | allows for a single remote method call returning a promise for the result.
176 |
177 | ```js
178 | E(target).method(arg1, arg2...) // Promise
179 | ```
180 |
181 | Example usage:
182 |
183 | ```js
184 | import { E } from '@agoric/far';
185 |
186 | // Invoke pipelined RPCs.
187 | const fileP = E(
188 | E(target).openDirectory(dirName)
189 | ).openFile(fileName);
190 | // Process the read results after a round trip.
191 | E(fileP).read().then(contents => {
192 | console.log('file contents', contents);
193 | // We don't use the result of this send.
194 | E(fileP, { _oneway: true }).append('fire-and-forget');
195 | });
196 | ```
197 |
198 | ### `Promise.delegated` function
199 |
200 | In a manner analogous to *Proxy* handlers, a **delegated promise** is associated
201 | with a handler object.
202 |
203 | ```js
204 | new Proxy(target, handler) -> fresh proxy
205 |
206 | new Promise((resolve, reject) => {
207 | ...
208 | resolve(resolution) -> void
209 | reject(reason) -> void
210 | ...
211 | }) -> fresh undelegated promise
212 |
213 |
214 | Promise.delegated((resolve, reject, resolveWithPresence) => {
215 | ...
216 | resolve(resolution) -> void
217 | reject(reason) -> void
218 | resolveWithPresence(presenceHandler) -> fresh presence
219 | ...
220 | }, unfulfilledHandler) -> fresh delegated promise
221 | ```
222 |
223 | For example,
224 |
225 | ```js
226 | const delegatedExecutor = async (resolve, reject, resolveWithPresence) => {
227 | // Do something that may need a delay to complete.
228 | const { err, presenceHandler, other } = await determineResolution();
229 | if (presenceHandler) {
230 | // presence is a freshly-created Object.create(null) whose handler
231 | // is presenceHandler. The targetP below will be resolved to this
232 | // presence.
233 | const presence = resolveWithPresence(presenceHandler);
234 | presence.toString = () => 'My Special Presence';
235 | } else if (err) {
236 | // Reject targetP with err.
237 | reject(err);
238 | } else {
239 | // Resolve targetP to other, using other's handler if there is one.
240 | resolve(other);
241 | }
242 | };
243 |
244 | // Create a delegated promise with an initial handler.
245 | // A pendingHandler could speculatively send traffic to remote hosts.
246 | const targetP = new Promise.delegated(delegatedExecutor, pendingHandler);
247 | E(E(targetP).remoteMethod(someArg, someArg2)).callOnResult(...otherArgs);
248 | ```
249 |
250 | The handlers are not exposed to the user of the delegated promise, so it
251 | provides a secure separation between the unprivileged client (which uses the `E`
252 | proxy maker or static `Promise.delegated` methods) and the privileged system
253 | which implements the communication mechanism.
254 |
255 | ### `Promise.prototype`
256 |
257 | Since delegated promises are still Promises, they can be used anyplace a
258 | Promise can. However, with the additional semantics of `Promise.resolve`,
259 | it is possible to detect if an object is a presence.
260 |
261 |
262 | ### Handler traps
263 |
264 | A handler object can provide handler traps (`eventualGet`, `eventualApply`,
265 | `eventualSend`).
266 |
267 | ```ts
268 | ({
269 | eventualGet (target, prop, modifiers): Promise,
270 | eventualApply (target, args, modifiers): Promise,
271 | eventualSend (target, prop, args, modifiers): Promise,
272 | })
273 | ```
274 |
275 | If the handler omits a trap, invoking the associated operation returns a promise
276 | rejection. The only exception to that behaviour is if the handler does not
277 | provide the `eventualSend` optimization trap. Then, its default implementation
278 | is
279 | ```js
280 | Promise.delegated.eventualApply(Promise.delegated.eventualGet(p, prop), args, opts)
281 | ```
282 |
283 | This expansion requires that the promise for the remote method be unnecessarily
284 | reified.
285 |
286 | For a pending handler, the trap's `target` argument is the unsettled delegated
287 | promise, so that the handler can gain control before the promise is resolved.
288 | For a presence handler, the trap's `target` argument is the presence that was
289 | created by `resolveWithPresence`.
290 |
291 | ### `Promise.delegated` static methods
292 |
293 | The methods in this section are used to implement higher-level communication
294 | primitives, such as the `E` proxy maker.
295 |
296 | These methods are analogous to the `Reflect` API, but asynchronously invoke a
297 | delegated promise's handler regardless of whether the target has resolved. This
298 | is necessary in order to allow pipelining of messages before the exact
299 | destination is known (i.e. before the delegated promise is resolved).
300 |
301 | ```js
302 | Promise.delegated.eventualGet(target, prop, opts = {}); // Promise
303 | Promise.delegated.eventualApply(target, [...args], opts = {}); // Promise
304 | ```
305 |
306 | The `eventualSend` call combines property lookup with function application in
307 | order to distinguish them from an `eventualGet` whose value is separately
308 | inspected, and for the handler to be able to bundle the two operations as a
309 | single message.
310 |
311 | ```js
312 | Promise.delegated.eventualSend(target, prop, args, opts = {}); // Promise
313 | ```
314 |
315 | ### Opt-In/Opt-Out Modifiers
316 |
317 | The last argument of the handler trap is called `modifiers`, and it is
318 | constructed as follows:
319 |
320 | - Modifier properties that can be safely ignored (opt-in modifiers) must begin
321 | with an underscore (`_`).
322 | - All other (required, non-underscore, opt-out) modifier properties must be
323 | examined and if they are unrecognized by the promise's handler, must result in
324 | a rejected promise.
325 | - Any caller-supplied `opts` (defaulting to an empty object `{}`), is made
326 | available as `modifiers.opts`. The same convention applies; `opts` that are
327 | safe to ignore must begin with an underscore.
328 | - Other immediate properties of the `modifiers` are implementation-defined.
329 | - The `Promise.delegated` implementation should enforce that the required
330 | modifiers are examined by the handler trap. The implementation should reject
331 | the result promise if any required `modifiers` or `modifiers.opts` property is
332 | not read.
333 |
334 | These conventions help distinguish between modifiers that are optional
335 | optimization hints versus required changes to behaviour. For example,
336 | `modifiers.opts._oneway` can be safely ignored, since it is not strictly
337 | necessary for correctness, but `modifiers.opts.after` cannot be ignored.
338 |
339 | ## Platform Support
340 |
341 | All the above behavior, as described so far, will be implemented in the [Eventual
342 | Send Shim](https://github.com/Agoric/agoric-sdk/tree/master/packages/eventual-send).
343 | However, there is one critical behavior that we specify, that can easily be
344 | provided by a conforming platform, but is infeasible to emulate on top of
345 | current platform promises. Without it, many cases that should pipeline do not,
346 | disrupting desired ordering guarantees. Consider:
347 |
348 | ```js
349 | let pResolve;
350 | const p = new Promise(r => pResolve = r);
351 | E(p).foo();
352 | let qResolve;
353 | const q = new Promise.delegated(r => qResolve = r, qPendingHandler);
354 | pResolve(q);
355 | ```
356 |
357 | After `p` is resolved to `q`, the delayed `foo` invocation should be forwarded
358 | to `q` and trap to `q`'s `qPendingHandler`. Although a shim could monkey patch
359 | the `Promise` constructor to provide an altered `resolve` function which does
360 | that, there are plenty of internal resolution steps that would bypass it. There
361 | is no way for a shim to detect that unsettled undelegated promise `p` has been
362 | resolved to unsettled delegated `q` by one of these. Instead, the `foo`
363 | invocation will languish until a round trip fulfills `q`, thus
364 | * losing the benefits of promise pipelining, and
365 | * potentially arriving after other messages when it really should have
366 | arrived before them.
367 |
368 | ## Syntactic Support
369 |
370 | A separate [Wavy Dot Proposal](https://github.com/Agoric/proposal-wavy-dot)
371 | proposes a more convenient syntax for calling the new internal methods proposed
372 | here. However, the eventual-send API described here is valuable even without
373 | the wavy dot syntax.
374 |
375 | ## Completing the Proxy Analogy
376 |
377 | * ***internal methods*** that all promises must support
378 | * [[EventualGet]],
379 | * [[EventualApply]],
380 | * [[EventualSend]]
381 | * ***static methods*** on `Promise.delegated` for invoking these internal methods.
382 | * `Promise.delegated.eventualGet`,
383 | * `Promise.delegated.eventualApply`,
384 | * `Promise.delegated.eventualSend`,
385 | * ***invariants*** that these methods must uphold.
386 | * Safety from reentrancy.
387 | * `p === Promise.resolve(t)` vs `p.then(t => ...)`
388 | * ***default behaviors*** of these methods for undelegated promises to normal objects.
389 | * `p~.foo` ==> `p.then(t => t.foo)`
390 | * `p~.(x)` ==> `p.then(t => t(x))`
391 | * `p~.foo(x)` ==> `p.then(t => t.foo(x))`
392 | * ***handler traps***. Proxies implement these methods by delegating most of their behaviors to corresponding traps on their handlers.
393 | * `p~.foo` ==> `p.then(t => h.eventualGet(t, 'foo'))`
394 | * `p~.(x)` ==> `p.then(t => h.eventualApply(t, [x])`
395 | * `p~.foo(x)` ==> `p.then(t => h.eventualSend(t, 'foo', [x])`
396 | * ***promise invariant enforcement***.
397 | * The `p.then` pattern above
398 | * ***fallback behaviors*** for absent traps, implemented in terms of the remaining traps.
399 | * `h.eventualSend(t, 'foo', [x])` defaults to
400 | `h.eventualApply(t, h.eventualGet(t, 'foo'), [x])`
401 |
--------------------------------------------------------------------------------
/evsend-miniplayer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tc39/proposal-eventual-send/faaad301139c7489454aaf707d38cc171ac57482/evsend-miniplayer.png
--------------------------------------------------------------------------------