Allow request cancellation for wrapped C++ methods - #342
Conversation
|
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ReviewsSee the guideline and AI policy for information on the review process.
If your review is incorrectly listed, please copy-paste ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
3300995 to
0986a13
Compare
|
CI failures seem unrelated |
|
Nice work! I've had a number of ideas about this feature over the years so it's really interesting to see it implemented. I will just give some quick thoughts for now so I don't get nerdsniped and wind up spending all day or more on this. Thoughts:
EDIT: Added |
|
Thanks for your feedback @ryanofsky. Sorry for the late response. I took some time to fill some C++ gaps (mostly to understand whether client-side detection of the
This makes complete sense. I believe I now grasp the original idea. This is cleaner for consumers and non-libmultiprocess clients, which don't need a
Yes, you're correct here as well.
This is interesting. I think we could combine this with
I prefer this over the dedicated hooks. However, I don't fully understand how it would handle the client side. For So I think the hook can tell
Yes, definitely we can cover both sides in this PR. I need to think about this more though, together with the mpgen issue above.
Ok perfect. I'll drop it.
Thanks, I really thought it wasn't caused by this PR, but I guess I had it wrong. I have some WIP rework locally, so these errors may be different the next time I push. |
Hmm yeah. Actually when I wrote this I wasn't thinking about the need to unregister, and the
You're right, I wasn't thinking about the client side of this very clearly. I was thinking as the client code was processing parameters it could use So I think a capnproto method annotation of some kind might be necessary. I was thinking of adding annotations anyway to support more flexible mapping of c++ parameters to capnproto parameters like: in context of #282. But something less general could also work well here. |
The vector of callbacks exists because another callback (pre-existing) is registered in
So during a cancellable request, there are two registrations: one internal, and another the wrapped method itself registers. But I do agree it looks a bit awkward. The raw pointers are there so
Good, this settles the client side then. I'd like to start with something minimal that we can scale up later into something more general. I was thinking of a method annotation (your waitValue @7 (context :Proxy.Context, timeoutMs :Int32) -> (result :Int32) $Proxy.extraParam("cancel"); |
4c8b3db to
4cfc251
Compare
4821d99 to
4731f5d
Compare
4731f5d to
f6944b2
Compare
|
@ryanofsky Just addressed your comments, rebased with master, and pushed. Added 9422b97, which supports the Updated the description too. Thanks for your feedback :) |
|
Thanks for the updates! And approach ACK f6944b2. I think it's good to add client and server cancellation support together in the same PR. Quickly skimming changes, the It would also be great to see this put to use in bitcoin core by dropping the |
d2014d9 to
22000b4
Compare
Done at bitcoin/bitcoin#36097 |
| template <typename LocalType, typename Value> | ||
| void MaybeBuildExtraParam(TypeList<LocalType> param, ClientInvokeContext& invoke_context, Value&& value) | ||
| { | ||
| if constexpr (requires { CustomBuildExtraParam(param, invoke_context, std::forward<Value>(value)); }) { |
There was a problem hiding this comment.
In commit "Edited init.cpp to include a check that -datadir exists" (ed2c01405f5a5b913c4fd64397394559051e041a)
Can we change this to a static_assert like MaybeReadExtraParam below instead of discarding the argument passed by the client?
I think this would be safer because it's easy to define a CustomBuildExtraParam function with a slightly incorrect signature that could get silently skipped. And I think in practice for real use-cases, it should make sense to define CustomBuildExtraParam and CustomReadExtraParam functions in pairs, and easy to leave implementations empty if one of them is not needed.
| }; | ||
|
|
||
| template <typename LocalType, typename Value> | ||
| void MaybeBuildExtraParam(TypeList<LocalType> param, ClientInvokeContext& invoke_context, Value&& value) |
There was a problem hiding this comment.
In commit "Allow wrapped C++ methods to take parameters that are not sent over RPC" (88ea9fa)
These functions (unlike MaybeReadField and MaybeBuildField) are only called one place, and it would seem simpler and less confusing to just inline them where they are used.
| for (const auto annotation : method.getProto().getAnnotations()) { | ||
| if (annotation.getId() == EXTRA_PARAM_ANNOTATION_ID) ++annotations_count; | ||
| } | ||
| if (annotations_count > 1) { |
There was a problem hiding this comment.
In commit "Allow wrapped C++ methods to take parameters that are not sent over RPC" (88ea9fa)
Is there a specific reason this limitation was added? It seems like multiple extraparams could work without any extra support, so unclear why they are not allowed. Similar for the other checks, it doesn't really seem like it should be a problem for these parameters not to have names for the names not to be unique. The names seem like they can only exist for descriptive purposes, and there's not really anything meaningful we could do with them, so there doesn't seem to be a reason to require them or restrict them.
There was a problem hiding this comment.
The only reason was to ensure that only 1 extra parameter was allowed, and yes you're right. This works without extra support, so this restriction no longer makes sense. Thanks!
Done at 1eaa501
| // The `CustomReadExtraParam` overload in `foo-types.h` builds the | ||
| // server-side value, hardcoded to 1. As a result this always returns | ||
| // arg + 1 regardless of the value passed for extra. | ||
| KJ_EXPECT(foo->passExtra(1, 999) == 2); |
0d7a2ff to
6ee0307
Compare
|
Thanks for the review @ryanofsky. I addressed your feedback on the |
3bad2fd to
d51e391
Compare
enirox001
left a comment
There was a problem hiding this comment.
Concept ACK
IIUC, this allows one thread to cancel an IPC call that another thread is blocked waiting for, without closing the connection. The server method can also register a callback to respond to cancellation.
This seems complementary to #335 as thesse both provide mechanisms in libmultiprocess that reduce the need for custom interruption and shutdown handling in Bitcoin Core. That seems like a useful direction.
There was a problem hiding this comment.
Concept ACK. Commented 3 nits.
I also used claude to review and it found that ServerCall::invoke cancellation is currently racy:
"The server unlocks request_lock for the whole method body and only reacquires it afterwards to clear cancel_fn. In the gap between the method returning (its frame, and anything the callback captured from it, destroyed) and that reacquire, the event loop can take the mutex, see cancel_fn still set, and invoke a callback whose captures are gone."
The race window seems pretty narrow but possible.
Diff widening this race window with sleep and a test demonstrating the race
diff --git a/include/mp/proxy-types.h b/include/mp/proxy-types.h
index 2172326..25edf50 100644
--- a/include/mp/proxy-types.h
+++ b/include/mp/proxy-types.h
@@ -7,8 +7,10 @@
#include <mp/proxy-io.h>
+#include <chrono>
#include <exception>
#include <optional>
+#include <thread>
#include <set>
#include <typeindex>
#include <vector>
@@ -608,6 +610,10 @@ struct ServerCall
extra);
},
[&] {
+ // TEMPORARY REPRO HACK: widen the existing window between the
+ // wrapped method returning (its locals already destroyed) and
+ // the worker thread reacquiring request_lock.
+ if (server_context.request_lock) std::this_thread::sleep_for(std::chrono::milliseconds{500});
if (server_context.request_lock) server_context.request_lock->m_lock.lock();
// The method returned, so destroy the callback it registered
// through its cancellation argument, if any.
diff --git a/test/mp/test/test.cpp b/test/mp/test/test.cpp
index 619930e..fefd141 100644
--- a/test/mp/test/test.cpp
+++ b/test/mp/test/test.cpp
@@ -816,6 +816,55 @@ KJ_TEST("Cancel an in-flight IPC call")
KJ_EXPECT(foo->add(1, 2) == 3);
}
+KJ_TEST("Cancel racing method return is not a use-after-free")
+{
+ TestSetup setup;
+ ProxyClient<messages::FooInterface>* foo = setup.client.get();
+ foo->initThreadMap();
+ std::promise<void> registered;
+ std::atomic<bool> callback_ran{false};
+ std::atomic<bool> ran_after_return{false};
+
+ // Register a cancel callback, then return immediately instead of waiting
+ // for cancellation. This targets the window in proxy-types.h between the
+ // wrapped method returning (its frame, and anything the callback captured
+ // from it, destroyed) and request_lock being reacquired to clear
+ // cancel_fn. The callback holds a weak_ptr to a frame-owned canary
+ // instead of a dangling reference, so it can observe that it was invoked
+ // after the frame died without actually corrupting memory.
+ setup.server->m_impl->m_cancel_fn = [&](CancelArg cancel) {
+ auto canary = std::make_shared<int>(42);
+ cancel([weak = std::weak_ptr<int>{canary}, &callback_ran, &ran_after_return] {
+ callback_ran = true;
+ if (weak.expired()) ran_after_return = true;
+ });
+ registered.set_value();
+ };
+
+ std::promise<CancelFn> cancel_fn;
+ std::thread canceler([&] {
+ CancelFn fire{cancel_fn.get_future().get()};
+ registered.get_future().wait();
+ // Let the method return before canceling, to land in the window.
+ std::this_thread::sleep_for(std::chrono::milliseconds{100});
+ fire();
+ });
+ bool interrupted = false;
+ try {
+ foo->callCancelFnAsync([&](CancelFn fn) { cancel_fn.set_value(std::move(fn)); });
+ } catch (const InterruptException&) {
+ interrupted = true;
+ }
+ canceler.join();
+ KJ_EXPECT(interrupted);
+ KJ_EXPECT(callback_ran);
+ // Fails today: the callback is invoked after the method's frame is gone.
+ KJ_EXPECT(!ran_after_return);
+
+ // Connection should be unaffected.
+ KJ_EXPECT(foo->add(1, 2) == 3);
+}
+
KJ_TEST("Dropping the client promise cancels an executing method")
{
TestSetup setup;
A possible fix is having registration return an RAII handle that clears cancel_fn under the mutex.
| add_accessor(field_name); | ||
| const bool extra{field.extra_name.size() > 0}; | ||
| const auto field_name = | ||
| extra ? field.extra_name : (field.param_is_set ? field.param : field.result).getProto().getName(); |
There was a problem hiding this comment.
In "Allow wrapped C++ methods to take parameters that are not sent over RPC" 1eaa501
nit: nested ternaries here are hard to read. Maybe:
kj::StringPtr field_name;
if (extra) {
field_name = field.extra_name;
} else {
field_name = (field.param_is_set ? field.param : field.result).getProto().getName();
add_accessor(field_name);
}which also folds in the if (!extra) add_accessor(...) on the next line.
| if (has_extra) { | ||
| client << " static_assert(M" << method_ordinal << "::Params::size == " << argc | ||
| << ", \"C++ method " << proxied_class_type << "::" << proxied_method_name | ||
| << " should have " << argc << " parameters to match capnp method " << method_prefix | ||
| << ".\");\n"; | ||
| } |
There was a problem hiding this comment.
In "Allow wrapped C++ methods to take parameters that are not sent over RPC" 1eaa501
nit: not sure if this is helpful. Arity mismatches are already compile errors without it, and since static_assert doesn't stop further diagnostics it only prepends a nicer first line rather than replacing the noise. It also fires only for methods with $Proxy.extraParam, while an ordinary method with the same mismatch gets nothing.
There was a problem hiding this comment.
Nice catch! I agree this is kind of redundant. Dropped this check in c14d0d5. Thanks!
| }).attach(kj::mv(request_canceler))); | ||
| }); | ||
|
|
||
| if (invoke_context && invoke_context->cancel_receiver) { |
There was a problem hiding this comment.
In "Allow request cancellation for wrapped C++ methods" cfdcb45
nit: I found "cancel_receiver" a confusing name because it makes you think it receives cancellation but it is only a registration hook. A better name could be "register_cancel"
|
Thanks for the review @ViniciusCestarii, and nice catch on the
So the fix must either clear
This implies that @ryanofsky, since the RAII handle requires changing |
d51e391 to
2a20412
Compare
| [&](RemoveCvRef<Extra>&... extra_args) -> decltype(auto) { | ||
| return ProxyServerMethodTraits< | ||
| typename decltype(server_context.call_context.getParams())::Reads | ||
| >::invoke(server_context, std::forward<Args>(args)..., extra_args...); |
There was a problem hiding this comment.
In 1eaa501: Allow wrapped C++ methods to take parameters that are not sent over RPC
nit:
Here, the extra argument passed by value here gets copied; I assume the problems with this have been considered since it was made to use an std::move in cfdcb45
But I think the move should be introduced in this commit instead
| const auto& f = field.param_is_set ? field.param : field.result; | ||
| auto field_name = f.getProto().getName(); | ||
| add_accessor(field_name); | ||
| const bool extra{field.extra_name.size() > 0}; |
There was a problem hiding this comment.
In 1eaa501: Allow wrapped C++ methods to take parameters that are not sent over RPC
We can improve how the generator identifies an extra parameter; currently it uses a non-empty name as a marker. Still, if there is an extra entry that is empty, $Proxy.extraParam("") that entry has no real capnp field, but the generator treats it as an ordinary field, because its name is empty; it then attempts to obtain a name from the nonexistent field.
The generator also exits successfully, but produces invalid c++
MakeClientParam<Accessor<empty_fields::, FIELD_OUT>>(M0::Fwd<1>())
I think we should be rejecting empty names with a clear error
There was a problem hiding this comment.
Thanks for spotting it. A check for this existed at 88ea9fa and threw an error if no name was provided. I removed it after Ryan's feedback (comment) since there's no real reason to require a name. But as you noted, mpgen was still relying on the name's length being > 0, and an empty extraParam fell through the normal capnp path.
Fixed in 780120c. It adds a new extra flag to the Field struct, sets it when it detects the annotation, and replaces the name-length check to initialize the local extra variable. The empty name turns into unnamed_extra_paramN.
| //! reading params (`call_context.getParams()`) or writing results | ||
| //! (`call_context.getResults()`). | ||
| Lock* cancel_lock{nullptr}; | ||
| Lock* request_lock{nullptr}; |
There was a problem hiding this comment.
In commit 7f31996: proxy: rename cancel_lock and cancel_mutex to request_lock and request_mutex
This is a good change to use the naming request_lock, the previous name would have been confusing, as it could suggest that it controls whether cancellation happens. The new change better describes what it protects, which is the request data.
nit: there is a comment that I think you missed;
index 63b3fdb..c1ca04f 100644
--- a/include/mp/type-context.h
+++ b/include/mp/type-context.h
@@ -154,7 +154,7 @@ auto PassField(Priority<1>, TypeList<>, ServerContext& server_context, const Fn&
// makes another IPC call), so avoid modifying the map.
const bool erase_thread{inserted};
KJ_DEFER(
- // Release the cancel lock before calling loop->sync and
+ // Release the request lock before calling loop->sync and
// waiting for the event loop thread, because if a
// cancellation happened, it needs to run the on_cancel
// callback above. It's safe to release request_lock at| if (server_context.request_lock) server_context.request_lock->m_lock.lock(); | ||
| // The method returned, so destroy the callback it registered | ||
| // through its cancellation argument, if any. | ||
| server_context.cancel_fn = nullptr; |
There was a problem hiding this comment.
In cfdcb45: proxy: support cancellation extra parameters
Could cancellation run the server callback after the local variables it references have been destroyed?
The callback is cleared only after the wrapped method returns, but the method’s local variables are destroyed before that. If cancellation arrives in between, the callback could access destroyed state—for example, a local mutex or condition variable.
I reproduced this with a test that pauses during method cleanup and triggers cancellation after the local state is destroyed. The callback still runs.
index 619930e..5ad02a0 100644
--- a/test/mp/test/test.cpp
+++ b/test/mp/test/test.cpp
@@ -816,6 +816,85 @@ KJ_TEST("Cancel an in-flight IPC call")
KJ_EXPECT(foo->add(1, 2) == 3);
}
+KJ_TEST("Cancellation callback does not outlive server method locals")
+{
+ TestSetup setup;
+ auto* foo = setup.client.get();
+ foo->initThreadMap();
+ constexpr std::chrono::seconds timeout{30};
+ std::promise<void> locals_destroyed;
+ auto locals_destroyed_future = locals_destroyed.get_future();
+ std::promise<void> cancellation_attempted;
+ auto cancellation_attempted_future = cancellation_attempted.get_future();
+ std::atomic<bool> callback_after_destruction{false};
+
+ setup.server->m_impl->m_cancel_fn = [&](CancelArg cancel) {
+ // This guard runs after local_state is destroyed, but before the
+ // method returns to ServerCall's callback cleanup. Hold that window
+ // open so cancellation reliably arrives during normal scope exit.
+ KJ_DEFER(
+ locals_destroyed.set_value();
+ KJ_EXPECT(cancellation_attempted_future.wait_for(timeout) == std::future_status::ready);
+ );
+ );
+ KJ_EXPECT(cancellation_attempted_future.wait_for(timeout) == std::future_status::ready);
+ );
+ auto local_state = std::make_shared<int>(42);
+ cancel([state = std::weak_ptr<int>{local_state}, &callback_after_destruction] {
+ // Observe the lifetime safely, instead of dereferencing a dangling
+ // reference to a local mutex/condition variable as real code might.
+ callback_after_destruction = state.expired();
+ });
+ // Return normally; do not wait for cancellation in the method body.
+ };
+
+ std::promise<CancelFn> cancel_fn;
+ auto cancel_future = cancel_fn.get_future();
+ std::thread canceler([&] {
+ CancelFn fire = cancel_future.get();
+ KJ_EXPECT(locals_destroyed_future.wait_for(timeout) == std::future_status::ready);
+ fire();
+ cancellation_attempted.set_value();
+ });
+ bool interrupted{false};
+ try {
+ foo->callCancelFnAsync([&](CancelFn fn) { cancel_fn.set_value(std::move(fn)); });
+ } catch (const InterruptException&) {
+ interrupted = true;
+ }
+ canceler.join();
+ KJ_EXPECT(interrupted);
+ KJ_EXPECT(!callback_after_destruction.load());
+ KJ_EXPECT(foo->add(1, 2) == 3);
+}
+Should we provide a way to unregister the callback before the captured local variables go out of scope?
There was a problem hiding this comment.
@ViniciusCestarii reported the same issue and suggested an RAII guard that unregisters the cancel callback before the wrapped method's locals go out of scope.
Following @ryanofsky's suggestion in #342 (comment), it's a bit of a redesign. In the latest pushes, type-cancel.h was moved to test/, and now the source code changes are smaller than in your last review.
| }).attach(kj::mv(request_canceler))); | ||
| }); | ||
|
|
||
| if (invoke_context && invoke_context->cancel_receiver) { |
There was a problem hiding this comment.
In cfdcb45: proxy: support cancellation extra parameters
From the previous commit bf8a8b1, every ipc call now creates a ClientCancelState allocates a RequestCanceler and wraps the response promise even when the caller does not want cancellation. I think with this commit that connects cancel_receiver, we could have a way where this would happen only when a cancellation receiver is present. This would reduce the allocations for ordinary calls
There was a problem hiding this comment.
Thanks! Dropped this, and now clientInvoke checks whether the new field invoke_context.set_canceler is set before constructing a kj::Canceler object and attaching it to the request promise.
|
|
||
| find_package(Threads REQUIRED) | ||
| find_package(CapnProto 0.9 NO_MODULE) | ||
| find_package(CapnProto 1.0 NO_MODULE) |
There was a problem hiding this comment.
In 527a099: build: require Cap'n Proto 1.0
nit: There is a text here that needs to be updated i think
index f2199cb..a6a8a0f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,7 +18,7 @@ find_package(CapnProto 1.0 NO_MODULE)
message(FATAL_ERROR
"Cap'n Proto is required but was not found.\n"
"To resolve, choose one of the following:\n"
- " - Install Cap'n Proto (version 1.0+ recommended)\n"
+ " - Install Cap'n Proto (version 1.0+ required)\n"
" - For Bitcoin Core compilation build with -DENABLE_IPC=OFF to disable multiprocess support\n"
)
endif()There was a problem hiding this comment.
This makes more sense, thanks. Done in 45514af.
Yes I'm a little behind on this PR (as of 2a20412), but my thinking is that this PR should not add a I am also a little suspicious of complexity I see in I'd expect the
I think this approach would reduce overhead and complexity and assign responsibilities better. |
Actually, this won't work because clientInvoke skips reading fields if the request is cancelled. But this could be an opportunity to add support for custom exceptions on other types of events too (like disconnections) not just cancellations. Specifically could add some code to the client error handler to translate the try {
IterateFields().handleChain(
*invoke_context, e, FieldList(), typename FieldObjs::HandleError{&fields}...);
} catch (...) {
exception = std::current_exception();
}Alternately maybe this is too complicated and it simpler to throw |
ae075cb to
88e531b
Compare
|
Thanks for the reviews and feedback, everyone! Addressed feedback and force-pushed: 2a20412 -> 88e531b (compare). re: #342 (comment)
There is no invoke_context.handle_error = [state](const kj::Exception&) {
if (state->canceled()) throw InterruptException{"canceled"};
};re: #342 (comment) I followed your suggestion, and I can confirm that the source code changes are smaller and more efficient, most notable changes are:
Thanks for the suggestion. |
88e531b to
ab05fdc
Compare
Add a `$Proxy.extraParam` method annotation that declares an extra C++-only parameter in the generated method signature. The parameter has no corresponding capnp parameter and is not serialized or sent over RPC. The annotation value names the parameter in generated C++ code. Client behavior: - A matching `CustomBuildExtraParam(TypeList<T>, ClientInvokeContext&, T&&)` overload MUST be implemented. The parameter is passed to it before the RPC message is dispatched. Server behavior: - A matching `CustomReadExtraParam(TypeList<T>, ServerContext&)` overload MUST be implemented. No data arrives for this parameter so this overload reconstructs the parameter value on the server side. The test checks the values the client passes reach the client overload and the ones the server reconstructs arrive instead.
…`request_mutex` The mutex guards the request's params and results structs, not the cancellation itself. The old names would be confusing next to the CancelState class added in the following commits. Pure rename, no behavior change.
Add two optional hooks to `ClientInvokeContext` that client overloads can set: - `set_canceler` receives the `kj::Canceler` object wrapping the request promise once it is sent, and null when the request finishes and the canceler is destroyed. `clientInvoke` only creates the canceler (heap-allocated and attached to the promise) if the hook is set. - `handle_error` receives the `kj::Exception` of a failed request and lets clients throw a custom exception, which `clientInvoke` rethrows to the caller. For example, `kj::Canceler::cancel` raises `DISCONNECTED`, the same error a disconnect produces, so only the parameter that canceled can tell them apart.
Add two members to `ServerInvokeContext` that server overloads can use: - `request_mutex` is the mutex `request_lock` refers to, set together with it by the `Proxy.Context` `PassField` overload, and null for methods running on the event loop thread. Overloads lock it to touch `cancel_fn`. - `cancel_fn` holds a callback that the cancel monitor runs on the event loop thread, when the request is canceled while the method executes. `ServerCall` clears it after the method returns.
The `$Cxx.allowCancellation` annotation used by the cancellation tests does not exist in older versions. Remove the configure-time checks that only covered them, and move the olddeps CI config to 1.0.0.
- One test cancels an in-flight `ProxyClient` call from another thread. - Another drops the response promise mid-execution, imitating non-libmultiprocess clients. Additionally, this commit adds a reference implementation of a cancel parameter and its corresponding `CustomBuildExtraParam`/`CustomReadExtraParam` overloads that applications could replicate to fully support cancellations.
ab05fdc to
89bc79c
Compare
|
FWIW: I just pushed changes to bitcoin/bitcoin#36097 to sync with this PR's current state. I also updated the description here to reference it. |
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 introducedBlockTemplate::interruptWait()specifically to wake an in-progressBlockTemplate::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:
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.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 byCustomBuildExtraParamon the client side andCustomReadExtraParamon the server side. Paired with the cancellation mechanism, this feature lets applications define their own cancel parameter types. A reference implementation lives undertest/mp/test/foo-types.handtest/mp/test/foo.h, used in tests.For example, given the following capnp schema method:
and this C++ method with an application-defined cancel parameter:
The caller (client) passes a callback that receives the function that cancels the call:
The callee (server) registers a callback that interrupts its wait and retains the returned guard until the wait ends:
Detecting a dropped promise on the server requires the
$Cxx.allowCancellationannotation 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 theinterrupt()/interruptWait()workarounds.