├── 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 | [Presentation to TC39](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 --------------------------------------------------------------------------------