Promise Pipelining: Is it a Good Thing?

Elsewhere, @crock wrote:

…does Promise Pipelining provide a real benefit? Does it encourage messing up the division of labor? Does it encourage chatty protocols having way too many messages with tight coupling? Is there any evidence that we need it?

Please, discuss!

At the Palm-House Spritely Kickoff Meetup, I recall hearing from Dean that Midori saw significantly improved throughput, but that was a short and compressed conversation.

Elsewhere @danfinlay replied:

I also have been wondering this a bit. Two reasons that it’s come up for me are:

  1. In practice it is rare that all the operations I want to perform are sequentially on the result of a function with no additional arguments.
  2. A function like target.there(codeToRunOnRecipient) can provide all the same benefits, but with the ability to also synchronously interact with other objects on the host, and compose that interaction in complex ways. It requires some kind of metering (or unbroken trust in the caller) to be safe against resource exhaustion, but given the Spritely Goblins time-travel demo, I would suspect you could achieve a form of metering.

Hi! I’ll try replying to each in turn, from my own perspective. But the promise pipelining part of the whitepaper wasn’t finished (though I had removed a TODO comment noting that before publishing it, maybe that was a mistake), but it’s something most reviewers commented on, so it’s clearly worth addressing. It’s probably best that I finish writing that section first, then I can reply here linking to it… that’ll make this a bit easier to reply to @crock and @danfinlay. This is good feedback though, important to address, and it’ll help me write the section!

1 Like

There’s now a promise pipelining section (though I’m a little bit uncertain if that fragment in the link will be stable, I have to see about adding more permanent fragments for the sections). I think this answers some of the comments from @crock and @danfinlay, but especially:

I don’t know for sure what @danfinlay meant by “it is rare that all the operations I want to perform are sequentially on the result of a function with no additional arguments” but hopefully the example has been updated to make it clear that no such restriction exists here?

A function like target.there(codeToRunOnRecipient) can provide all the same benefits, but with the ability to also synchronously interact with other objects on the host, and compose that interaction in complex ways. It requires some kind of metering (or unbroken trust in the caller) to be safe against resource exhaustion, but given the Spritely Goblins time-travel demo, I would suspect you could achieve a form of metering.

That’s true in that target.there(...) is a kind of generalization of promise pipelining on the protocol side (allowing other things also). But target.there(...) requires metering, as you say, which is unlikely to be commonly available, and a shared DSL, whereas promise pipelining can probably work across all CapTP implementing languages just fine and is reasonably safe without metering.

There are two things which are called “promise pipelining” but which are indicated, as the new section indicates. One is a programming style and api, which provides some of the conveniences that are usually otherwise reached for with coroutines, but without the round trips of coroutines or re-entrancy attack risks. The other side is how it integrates with CapTP. Technically these are two separate things, but that they are combined in a way that… I hate to use the word, but “synergizes”, is where a lot of the value lies, I think?

E.where(target, behaviour) (or remotePromise.there(behaviour)) indeed can subsume promise pipelineing.

The argument behaviour above then must be expressed in some format. Such as ecmascript source string, quasiliteral string, or even quasiliteral AST or code sequence trees. Then there is the question if such behaviour is turing complete or limited to primitive recursive arithmetic or semi straight line code without loops (basically Bitcoin script but with function calls added in)

However what promise pipelining allows for is the unforeseen emergent generation of mobile near-code that must be explictly supported by additional tooling that generates the code that captures that behaviour.

Plus promise pipelining is a bit easier to use when the holder of few far refs does not need to know iff they point into the same vat or not.

Another caveat with promise pipelining is that to translate say:

const anotherPromise = E.when(promise, value => {
  if (value > 42) {
    return (value / 3);
  } else {
    return (someValue - (value * 2));
  }
});

into pipelined form:

const anotherPromise = (() => {
  const t1 = E(promise).divideBy(3);
  const t2 = E(promise).multiplyBy(2);
  const t3 = E.when(t2, t2v => E(someValue).subtractBy(t2v));
  const t4 = E(promise).isGreaterThan(42);
  const t5 = E(t4).pick(t1, t3);
  return t5;
})();

one needs support of method versions of math and logic operations and knowledge of if an invoked method has side effects and take that into account.

One idea is to provide a source to source transformer that takes a function and returns a hyper promise pipelined version of it.

1 Like

Well said! I think there’s a certain amount of “when we can do remote code execution safely, a lot of things become absorbed by that” which is true, but your last two paragraphs highlight why I think the combination of protocol with programming API means that promise pipelining is still an important feature to advertise.

Even if we eventually built the pieces, protocol-wise, out of remote code execution, we’d still want the same abstractions for programmers to use, I think.

1 Like

The question should be: Is it a necessary thing?

Everything has utility, and utility seems to be a good thing. But from the perspective, everything is a Good Thing. An all inclusive standard is not a standard.

A tougher standard asks: Is it essential? Can we build good systems without it?

Even if you can build the same functionality with remote code, is it worth keeping around for the sake of interoperability with other systems that may not be capable of doing remote code safely? If I’m communicating with another system that implements CapTP but that isn’t sufficiently advanced to safely execute a function I pass to it (or even one that is but doesn’t have the same semantics as my local implementation), it seems like it may still be valuable to be able to have this functionality.

Is that how promise pipelining works, @jfred? I thought it’s an optimization that means you don’t have to follow the full path through all the promises. Say that a promise that will ultimately resolve to Alice on machine A goes to Bob on machine B, then to Carol on machine C, and finally to Arthur on machine A where it is resolved. With pipelining the resolution can happen on machine A without going through C and B.

Did I get that right?

1 Like

Even just supporting the syntax of eventual sends with promise pipeling is worth it in the case where it is implemented as code to be sent to the remote vats and evaluated there.

But as you ( @jfred ) pointed out, not all remote vats could implement safe evaluation of such code sent in to be evaluated. Either due to constraints of runtime such as cpu/memory speed/size limits or complexity of limiting how resource consumption heavy such sent in code could be.

To answer your question, yes I think it is essential for two big reasons.
The first is the latency issue of waiting one round trip time per reference dependent method invocation. This is what made nearly all RPC systems pretty unusable from users perspective.
The second reason springs forth from API design complexity that arise when the designer tries to cut down on round trips by combining together methods she anticipates would be heavily used. With promise pipelining one gains the freedom to design API with modularity and unanticipated composability in mind without having to worry about the extra latency cost.

1 Like

Is promise pipelining really more complicated, at the protocol level, than RPC? I had the impression it was like tail recursion, which is actually simpler than the alternative of procedure calls that always build continuations, many of them useless. Shouldn’t we be asking whether the protocol should support RPC given that it already has to support something equivalent to promise pipelining, rather than the other way around?

Or are we talking about programming language design? The PL seems much less important than the protocol since after deployment it will be much easier to replace it.

1 Like

Wow. Never tthought of it that way, but yes, the parallels to tail call elimination is profound.

Not by much but it depends on the RPC protocol.

If we have a simple RPC protocol that has an request id field in its invocation messages then to add promise pipelining we only need to extend what the target of an invocation can be and what the arguments in such can be. (Basically the Descs in CapTP and ocapn).

If you are wondering now about the difference between deliverOp and deliverOnlyOp then imagine a simple udp based protocol to invoke methods on remotely exposed objects. (The udp payload is encrypted with the public key of that vat). Lets say you can only send the equiv of deliverOnlyOp, one per udp packet. I think there would quickly evolve a connvention of passing along in such a invocation a cap to ‘return’ either the result or the error of the invocation. This is what the ReDirectoR in CapTP deliverOp is, basically.

As the Agoric folks have shown with the E() helper you do not nescisarrily need ProgrammingLanguage support for eventual sends.

Another corollary of tail-recusion-elimination is what I usually call “explicit customers” for actors. With explicit customers, call/return stacks are often converted to pipelines, and flow-control is expressed with multiple and/or conditional customers.

1 Like

I was thinking about this, and I thought:

In some ways, pipelining would also expose itself to resource exhaustion attacks in some ways (even just a long series of eventual sends, but one could imagine causing a loop in the eventual send chain).

You could draw one of two lessons from this:

  1. Having no loops keeps us safe, therefore a there() method would be safe if it simply disallowed looping (for example, giving it an environment with no lambda or otherwise loop-capable methods). This solution resembles the solution Bitcoin’s scripting language came up with.
  2. Pipeline APIs also might want to consider having a form of metering, since they can themselves potentially be chained very long. This reminds me of a vuln that was once in PGP where long endorsement chains could crash people’s clients.

I’m inclined to suspect metering would not be hard to build on top of an evaluator, so it should be easy for users to meter any capability they pass out.

For example, if you wanted to meter the math-env from the spritely primer, you could do something like this:

(define (create-meter limit)
  (begin
    (define remaining limit)

    ; Per method attenuator:
    (lambda (target cost)
      (lambda (first . rest)
        (begin
          (set! remaining (- remaining cost))
          (display (string-append "remaining gas " (number->string (+ remaining 1)) "\n"))
          (if (> remaining -1)
            (apply target (cons first rest))
            (error "Exceeded gas limit")))))))


(define meter (create-meter 8))
                     
(define math-env
   ; Most methods cost 1:
  `((+ . ,(meter + 1))
    (- . ,(meter - 1))
    (* . ,(meter * 1))

    ; Division costs 4:
    (/ . ,(meter / 4))))


(evaluate '(+ 1 2)
                math-env)
(evaluate '(- 2 3)
                math-env)
(evaluate '(* 2 3)
                math-env)
(evaluate '(/ 2 3)
                math-env)
(evaluate '(/ 2 3)
                math-env)
; Error: Exceeded gas limit

Metering more open-ended computation is naturally more complicated: Especially in an ocap context where a given object can return an additional object that needs to be metered, this will probably require writing a full disruption membrane, but that would seem like an essential tool for adding attenuation of all sorts, so I’d rate it as high priority (and am definitely thinking about how to implement one in scheme…).

1 Like

Ok this was a fun little puzzle but yes metering can be added to the demo evaluator extremely easily. I assume it would get more complicated when you’re dealing with presences, but at least as a basic proof of concept I’m optimistic: scheme-metering/metered-evaluator.rkt at 23f572b0f59d058da45309e80366903595f35ae2 · danfinlay/scheme-metering · GitHub

2 Likes

There is also the option to not allow immediate invocations and only allow eventual sends inside a closure like object.

That is, basically a ?template? of a sequence of eventual sends that look when the template holes are filled in as if they were recieved via capTP connection in one packet-tcp segment.

This is basically the idea behind seqBlock whose most current public implementation is embedded in a handwritten bundle. (Btw in the same bundle there is a cost checker and a collection of handy async utilities)

But that assumes that the atomicity of appending a chunk of eventual sends transitivitzes to the atomicity of invoking such seqBlocks. That is, nothing can shoehorn in new eventual send into the queue betwixt eventual sends already in it.

Plus I see no conflict if both ways, metered immediate calls and seqBlocks, are supported.

1 Like