Promise Pipelining - c80k/capnproto-dotnetcore GitHub Wiki

Promise pipelining is one of Cap'n Proto's unique selling points. Let's say we have a method which eventually returns another capability:

interface SomeService
{
  doSomething @0 (arg: Int32);
}

interface SomeServiceFactory
{
  createSomeService @0 () -> (cap: SomeService);
}

We'll get this method:

public Task<ISomeService> CreateSomeService(CancellationToken cancellationToken_);

Application code might look like this:

async Task DoIt(ISomeServiceFactory factory)
{
    var task = factory.CreateSomeService();
    var service = await task;
    await service.DoSomething(123);
}

Obviously, the snippet has two await expressions. From a protocol point of view, we get two sequential network roundtrips: one for CreateSomeService, the other one for DoSomething. Can we do better than that? The answer is: Yes. On the protocol level, we formulate the call to DoSomething in a way that does not depend on the reply from CreateSomeService. In natural language, we'd request something like "Call DoSomething on the capability which will be returned by CreateSomeService". We can send this request immediately, without having to wait for CreateSomeService to return. Cap'n Proto supports exactly that, and it's called promise pipelining. From a syntactical point of view, it is about immediately transforming Task<ISomeService> in ISomeService. This is what the extension method Impatient.Eager() does:

async Task DoIt(ISomeServiceFactory factory)
{
    using var service = factory.CreateSomeService().Eager();
    await service.DoSomething(123);
}

Promise pipeling improves performance because it saves on a network roundtrip. Of course, that trick works only if the capability returned by CreateSomeService() is indeed a remote capability. If it is locally available, calling on it does not require any network I/O. Pipelining is pointless in such cases. The Eager method comes with an additional parameter which lets you decide how to behave in the "pointless case":

public static TInterface Eager<TInterface>(this Task<TInterface> task, bool allowNoPipeliningFallback = false) where TInterface : class, IDisposable;
  • If allowNoPipeliningFallback is false (the default), you will see a System.ArgumentException when "real" promise pipelining is not possible.
  • If it is true, the method simply emulates the expected result: First, wait for the capability, then call on that capability.

Hands on capability lifecycle management: Eager will take ownership of the capability which will eventually be returned by task, saving you a double-Dispose.

The Eager method is hard-coded into Capnp.Net.Runtime. It covers the majority of schemas where the capability to pipeline is a single method return value. Of course, there are more complex scenarios, when the capability is digged inside some structure.

struct Foo {
  foo @0: UInt8;
  cap @1: MyInterface;
}

interface MyInterface {
  foo@0 () -> (i: Int32, box: Foo);
}

The code generator produces custom extension methods for such cases, looking similar to this snippet:

public static partial class PipeliningSupportExtensions_test
{
    static readonly MemberAccessPath Path__MyInterface_foo_Box_Cap = new MemberAccessPath(0U, 0U);
    public static CapnpGen.IMyInterface Box_Cap(this Task<(int, CapnpGen.Foo)> task)
    {
        async Task<IDisposable> AwaitProxy() => (await task).Item2?.Cap;
        return (CapnpGen.IMyInterface)CapabilityReflection.CreateProxy<CapnpGen.IMyInterface>(Impatient.Access(task, Path__MyInterface_foo_Box_Cap, AwaitProxy()));
    }
}

Application code may use it like this:

IMyInterface i = ...
using var c = i.Foo().Box_Cap();
⚠️ **GitHub.com Fallback** ⚠️