question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

eventual-send could wrap errors when target resolution throws

See original GitHub issue

I propose that eventual-send should wrap errors when the target resolution results in a throw. It would raise a new TypeError with the appropriate message (e.g. Cannot send to rejected promise), and use the thrown value as cause for this new error.

Conversation from https://github.com/Agoric/agoric-sdk/issues/5185:

I actually hadn’t noticed this contagion through send/then. Any reason for the contagion to exist in the first place?

_Originally posted by @mhofman in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111565227_

OMG contagion is critical and pervasive across our kind of promises. It is one of the main things that make them usable. In any case, JS’s .then, await, and Promise.all already implement literal contagion, which is why I withdrew my original suggestion. If we introduced a special value representing disconnection, .then, await, and Promise.all would all implement literal contagion, so we must as well. I forgot this when I wrote the “If so, we should still ask”. Our hand is forced. In this case, I don’t mind having this decision be forced in this direction. I think net, it is more right than any alternative.

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111568390_

Sorry I shouldn’t have included then, but I am confused about send contagion.

The same way I do not expect undefined.foo() to result in undefined, I wouldn’t expect Promise.reject('err')~.foo() to result in an 'err' rejection, but in a TypeError: cannot send 'foo' to rejected promise with a property cause: 'err' on that new error.

_Originally posted by @mhofman in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111572170_

The analogy of reject is not return, it is throw. The analogy of contagion is unhindered propagation of a thrown value through non-interfering intermediaries.

The semantics of await depend on this analogy.

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111573563_

> async function foo() { "use strict"; (await Promise.reject('err')).foo(); }
undefined
> foo()
Promise {
  <pending>,
}
> Uncaught 'err'

It does result in an 'err' rejection.

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111575369_

I really don’t understand. When the user does E(resultP) they make the assumption that the eventual resolution of resultP will be a remotable. If they perform E(resultP).foo() and their assumption breaks down, they should be told that their assumption broke (new error), and why it broke (the initial rejection of resultP as cause).

We have an opportunity with eventual send to not propagate the await contagion blindly.

In the case of remotable result, awaiting the result promise will in most cases not yield you a local object, but only a remote presence, on which you need to use eventual send anyway.

_Originally posted by @mhofman in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111577702_

Today if a user does const t = Promise.resolve(harden({someData: 42})); E(t).foo(), an error is generated, right? Why not similarly generate an error in the case where t = Promise.reject(harden({someData: 42})) ?

_Originally posted by @mhofman in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111579433_

@michaelfig and I have discussed raising the abstraction level of E to be more marshal-aware. This question is undecided. We’ll likely leave it at the eventual-send level of abstraction, where it is now. It does

return harden((...args) => HandledPromise.applyMethod(x, p, args));

which uses only concepts from SES (harden) and from the eventual-send proposal (currently spelled HandledPromise.applyMethod).

You can thus E any JS value.

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111581358_

E(t).foo()

does not generate an error because harden({someData: 42}) is not remotable. It generates an error only later, when it tries to call its foo method.

@michaelfig please correct me if I’m not properly representing the semantics or plans for E.

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111582394_

SwingSet does blindly propagate the rejection, but I maintain this a decision in the design of eventual-send, and not one which I’m convinced about right now.

https://github.com/Agoric/agoric-sdk/blob/4d464ac8cbf6c2f10db6c21b4b6ed5259ad00e55/packages/SwingSet/src/kernel/kernel.js#L774-L781

_Originally posted by @mhofman in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111583186_

Because harden({someData: 42}) is local, the semantics of E(t).foo() is identical to

Promise.resolve(t).then(r => r.foo());

is identical to

(await t).foo()

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111584066_

Yes, it is a SwingSet choice whether to implement a semantics consistent with the local JS semantics, or to implement distributed semantics which differs from local semantics in hard to explain ways. I just don’t like the second choice 😉 .

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111585009_

But it is the proposed eventual-send semantics to propagate a rejection of x in the case of HandledPromise.applyMethod(x, p, args) instead of raising a new TypeError with cause which I’m suggesting. I still don’t see why pure JS semantics are allegedly involved here.

_Originally posted by @mhofman in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111586447_

The proposed eventual-send semantics were designed to produce the above equivalences when t is anything other than a handled promise (or a promise forwarded to a handled promise). If I saw such a proposal that did not preserve this semantics for the local case, I would likely reject it. I don’t believe any other choice for the eventual-send proposal is reasonable.

It is then a choice, only of the provider of the handler of a handled promise. In this case, liveSlots, whether to implement a similar semantics. I’ve already acked that this is a SwingSet choice. I just think that SwingSet should preserve the semantics. But we can argue about that.

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111589700_

If I saw such a proposal that did not preserve this semantics for the local case, I would likely reject it.

Given that the language now has a native concept of error cause I believe we should consider when an operation is complex enough and has a clear boundary that it does not blindly bubble underlying errors but instead creates its own errors and chains them appropriately (aka catch and throw with cause). I believe eventual-send is such a case where chaining is appropriate instead of pass-through for the target.

_Originally posted by @mhofman in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111595149_

One constraint on the eventual-send proposal that I should emphasize: It motivates handled promises from the use-case of distributed objects, enabling a great variety of distributed object systems such as ours to be built. If we made the proposal too specific to our choices of distributed object semantics, I would likely object. For the proposal to be accepted into the language, it should have broad applicability, enabling the creation of many different systems.

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111595190_

That’s a good point! I can imagine that handled promise operations add a new outer layer to the cause chain even if .then, await, and Promise.all do not. You got me on the fence with this observation.

What stack would you associate with this auto-generated outer error? (The answer I’d love is, sadly, probably too expensive to get most engine makers to agree to.)

@michaelfig , eventual-send is our proposal. What do you think of adding such automatic error wrapping?

_Originally posted by @erights in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111601776_

What stack would you associate with this auto-generated outer error?

I would expect the stack at the point where applyMethod is called, but I feel this may be a trick question 😉

_Originally posted by @mhofman in https://github.com/Agoric/agoric-sdk/issues/5185#issuecomment-1111617806_

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:10 (8 by maintainers)

github_iconTop GitHub Comments

1reaction
erightscommented, Apr 30, 2022

Still waiting for @dtribble to clarify what async tail recursion problem he’s worried about. But until then, I’m going to argue that none of this is a problem. I’ll start by dividing into two cases.

Non-recursive deep send-chains by composition.

When debugging conventional sequential programs, I know I’m often surprised by the stack depth we get in the absence of recursion, just through composition across multiple layers of abstraction. These are often deep enough to make understanding hard, and hence a burden in debugging. But these are almost never deep enough to have a performance penalty, even in the absence of any tail optimization. I’m pretty sure this same observation applies to non-recursive async composition of send-chains. Indeed, Causeway was all about bringing the same kind of debugger’s view to send-chains that conventional debuggers bring to call stacks. I think the analogy holds.

The distributed async case has a further reason to consider tail optimization, which is shortening unresolved promise chains to cut out unnecessary intermediary vats. This is nice to reduce future message latency along those paths, but not crucial. It also makes the availability of those paths stop depending on the continued reachability of those intermediary machines. In any case, while we should and will need to pay attention to this, we would not currently be able to do this shortening anyway, so let’s set this aside.

Knowingly recursive tail send-chains

By “knowingly”, I mean that the entire tail recursion is co-designed, and therefore subject to a refactoring that changes how the recursion is done. For example, the refactoring at https://github.com/Agoric/agoric-sdk/pull/5214 changes async send-chain tail recursion into async iteration using a for/await/of loop. However, this is an invasive refactoring that takes some work, and can reduce the quality of the code. (More often it probably improves the quality of the code, but nevertheless…)

Even if eventual-sends are properly tail in their direct storage obligations, there is still a storage leak that would motivate this refactoring: they would leave behind promise forwarding chains that an implementation might not collapse before the end of the chain is settled (fulfilled or rejected). An implementation could, and could even guarantee it if its GC did the collapsing. But this isn’t the path of least resistance, so we should not expect such guarantees in general.

Fortunately, there’s a universally applicable small refactoring. Split the outer function into a non-recursive wrapper which allocates a promise/resolver pair and an inner recursive function with an extra argument which is the resolver. The outer function returns the promise and then calls the inner function with the resolver argument. Where the original function would have returned a promise, it instead returns void put passes the same resolver forward through the recursion. No new promises created. No promise forwarding chains. When we reach the recursion base case, it simply fulfills that one resolver to the tail answer.

Example coming up when I have time to come back to it.

Thus, if all async tail sends of interest fall into one of the above two cases, I don’t believe there is any remaining async tail recursion optimization needed. Then, this would no longer stand in the way of @mhofman 's suggested rejection wrapping.

0reactions
Tartuffocommented, May 23, 2022

@mhofman @kriskowal and I moved this into the Product Backlog. Please let us know if you disagree with that prioritization.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Working with Errors in Go 1.13
The result of unwrapping an error may itself have an Unwrap method; we call the sequence of errors produced by repeated unwrapping the...
Read more >
vat-container options: XS, Worker, WASM, etc #1127 - GitHub
This might happen because of simple bugs in otherwise-trusted code, higher usage ... it could transfer it to the target vat later, again...
Read more >
The Limits of Network Transparency in a Distributed ...
This dissertation presents a study on the extent and limits of network trans- parency in distributed programming languages. This property states that the....
Read more >
Actor-based Concurrency in Newspeak 4 - SJSU ScholarWorks
The actor model stands at the same level of abstraction and can be ... is wrapping the code that follows the eventual-send in...
Read more >
The "ITL-0002: 'Target Resolution Date' should be same or ...
Error is thrown ITL-0002: 'Target Resolution Date' should be same or ... So if you upgraded from a pre-15.7 release, you might still...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found