Currently, libmultiprocess requests are blocking, with no way to cancel them from either the client or server side. Downstream code works around this by pairing each blocking method with a dedicated interruptX() method whose only purpose is to wake it up. For example, bitcoin/bitcoin#33676 introduced BlockTemplate::interruptWait() specifically to wake an in-progress BlockTemplate::waitNext() call.
Cap'n Proto provides a useful cancellation mechanism that libmultiprocess can use to support cancellation from both non-libmultiprocess and libmultiprocess clients. When a promise is dropped, it sends a cancellation request. The server may either cancel immediately or ignore the signal.
This PR implements approach 4 from bitcoin/bitcoin#33575, adding cancellation support on both sides:
- Allow
ProxyClientmethod calls to be canceled from another thread through a cancel function. This is backed by akj::Cancelerthat wraps the request promise and is attached to it. A canceled call throwsInterruptException. - Allow wrapped server methods to register a callback that runs when the request is canceled, whether because the promise was dropped or the connection was interrupted.
The PR also adds a new capnp annotation, $Proxy.extraParam, which declares C++-only parameters that are not sent through RPC. These parameters are handled by CustomBuildExtraParam on the client side and CustomReadExtraParam on the server side. The first user of this mechanism is the cancellation parameter type std::function<void(std::function<void()>)>.
For example, this capnp schema method:
waitNext [@0](/bitcoin-core-multiprocess/contributor/0/) (context :Proxy.Context) -> (result :Template) $Proxy.extraParam("cancel") $Cxx.allowCancellation;
maps to a C++ method with a trailing cancellation argument, without requiring any libmultiprocess type in the interface header:
using CancelFn = std::function<void()>;
using CancelArg = std::function<void(CancelFn)>;
virtual std::unique_ptr<Template> waitNext(CancelArg cancel) = 0;
On the client, the argument receives a function that can cancel the call:
CancelFn cancel_fn;
// ... blocks until the result arrives or another thread runs cancel_fn()
auto tmpl = client->waitNext([&](CancelFn fn) {
cancel_fn = std::move(fn);
});
On the server, the implementation registers a callback that interrupts its wait:
std::unique_ptr<Template> waitNext(CancelArg cancel) override
{
if (cancel) cancel([this] { m_cv.notify_all(); });
// ... wait on m_cv, checking for cancellation
}
Detecting a dropped promise on the server requires the $Cxx.allowCancellation annotation on the method, file, or interface. Without it, Cap'n Proto runs the abandoned call to completion. The annotation requires Cap'n Proto 1.0 (see the "Breaking change" section in https://capnproto.org/news/2023-07-28-capnproto-1.0.html), which this PR also sets as the minimum supported version.