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.ClientInvokeContextprovides two hooks an application overload can set:set_canceler, which receives thekj::CancelerthatclientInvokewraps the request promise in, andhandle_error, which receives thekj::Exceptionfrom a failed request and decides what the caller sees. The second hook is how a cancel parameter turns theDISCONNECTEDthat akj::Cancelerraises intoInterruptException. - 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. Paired with the cancellation mechanism, this feature lets applications define their own cancel parameter types. A reference implementation lives under test/mp/test/foo-types.h and test/mp/test/foo.h, used in tests.
For example, given the following capnp schema method:
waitNext [@0](/bitcoin-core-multiprocess/contributor/0/) (context :Proxy.Context) -> (result :Template) $Proxy.extraParam("cancel") $Cxx.allowCancellation;
and this C++ method with an application-defined cancel parameter:
using CancelFn = std::function<void()>;
class CancelGuard; // unregisters the callback when destroyed
using CancelArg = std::function<CancelGuard(CancelFn)>;
virtual std::unique_ptr<Template> waitNext(CancelArg cancel) = 0;
The caller (client) passes a callback that receives the function that cancels 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);
return CancelGuard{};
});
The callee (server) registers a callback that interrupts its wait and retains the returned guard until the wait ends:
std::unique_ptr<Template> waitNext(CancelArg cancel) override
{
CancelGuard guard;
if (cancel) guard = 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.
NOTE: bitcoin/bitcoin#36097 builds on this PR by adding cancel arguments to the blocking mining methods (waitTipChanged, createNewBlock, BlockTemplate::waitNext) and deprecating the interrupt()/interruptWait() workarounds.