Skip to content

Commit 4c8b3db

Browse files
committed
test: Add testing for request cancellation
- One test cancels an in-flight `ProxyClient` call from another thread. - Another drops the response promise mid-execution, imitating non-libmultiprocess clients.
1 parent 23d2889 commit 4c8b3db

4 files changed

Lines changed: 107 additions & 0 deletions

File tree

‎test/mp/test/foo-types.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <capnp/common.h>
1313
#include <cstddef>
1414
#include <mp/test/foo.capnp.h>
15+
#include <mp/type-cancel.h>
1516
#include <mp/type-context.h>
1617
#include <mp/type-data.h>
1718
#include <mp/type-decay.h>

‎test/mp/test/foo.capnp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface FooInterface $Proxy.wrap("mp::test::FooImplementation") {
3939
passDataPointers @22 (arg :List(Data)) -> (result :List(Data));
4040
listBars @25 (context :Proxy.Context, n :Int32) -> (result :List(BarInterface));
4141
passExtra @26 (arg :Int32) -> (result :Int32) $Proxy.extraParam("extra");
42+
callCancelFnAsync @27 (context :Proxy.Context) -> () $Proxy.extraParam("cancel") $Cxx.allowCancellation;
4243
}
4344

4445
interface FooInit $Proxy.wrap("mp::test::FooInit") {

‎test/mp/test/foo.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ class SimpleBar : public Bar
8888
int value() override { return m_value; }
8989
int m_value;
9090
};
91+
92+
using CancelFn = std::function<void()>;
93+
using CancelArg = std::function<void(CancelFn)>;
94+
9195
class FooImplementation
9296
{
9397
public:
@@ -125,8 +129,14 @@ class FooImplementation
125129
void callFn() { assert(m_fn); m_fn(); }
126130
void callFnAsync() { assert(m_fn); m_fn(); }
127131
int callIntFnAsync(int arg) { assert(m_int_fn); return m_int_fn(arg); }
132+
void callCancelFnAsync(CancelArg cancel)
133+
{
134+
assert(m_cancel_fn);
135+
m_cancel_fn(std::move(cancel));
136+
}
128137
std::function<void()> m_fn;
129138
std::function<int(int)> m_int_fn;
139+
std::function<void(CancelArg)> m_cancel_fn;
130140
};
131141

132142
} // namespace test

‎test/mp/test/test.cpp‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <kj/test.h>
2626
#include <map>
2727
#include <memory>
28+
#include <mutex>
2829
#include <mp/config.h>
2930
#include <mp/proxy.h>
3031
#include <mp/proxy.capnp.h>
@@ -666,5 +667,99 @@ KJ_TEST("Call async IPC method without thread or pool errors correctly")
666667
KJ_EXPECT(error_thrown);
667668
}
668669

670+
KJ_TEST("Cancel an in-flight IPC call")
671+
{
672+
TestSetup setup;
673+
ProxyClient<messages::FooInterface>* foo = setup.client.get();
674+
foo->initThreadMap();
675+
std::promise<void> waiting;
676+
std::promise<void> done;
677+
678+
// Install a function that blocks until its `CancelArg` fires, so
679+
// cancellation is the only way out.
680+
setup.server->m_impl->m_cancel_fn = [&](CancelArg cancel) {
681+
std::mutex mutex;
682+
std::condition_variable cv;
683+
bool canceled = false;
684+
cancel([&] {
685+
const std::lock_guard<std::mutex> lock{mutex};
686+
canceled = true;
687+
cv.notify_all();
688+
});
689+
std::unique_lock<std::mutex> lock{mutex};
690+
waiting.set_value();
691+
cv.wait(lock, [&] { return canceled; });
692+
done.set_value();
693+
};
694+
695+
std::promise<CancelFn> cancel_fn;
696+
std::thread canceler([&] {
697+
CancelFn fire{cancel_fn.get_future().get()};
698+
waiting.get_future().wait();
699+
fire();
700+
});
701+
bool interrupted = false;
702+
try {
703+
foo->callCancelFnAsync([&](CancelFn fn) { cancel_fn.set_value(std::move(fn)); });
704+
} catch (const InterruptException&) {
705+
interrupted = true;
706+
}
707+
canceler.join();
708+
KJ_EXPECT(interrupted);
709+
KJ_EXPECT(done.get_future().wait_for(std::chrono::minutes{5}) == std::future_status::ready);
710+
711+
// Connection should be unaffected.
712+
KJ_EXPECT(foo->add(1, 2) == 3);
713+
}
714+
715+
KJ_TEST("Dropping the client promise cancels an executing method")
716+
{
717+
TestSetup setup;
718+
constexpr std::chrono::seconds timeout{30};
719+
std::promise<void> waiting;
720+
std::promise<void> done;
721+
722+
// Install a function that blocks until its `CancelArg` fires, so
723+
// cancellation is the only way out.
724+
setup.server->m_impl->m_cancel_fn = [&](CancelArg cancel) {
725+
std::mutex mutex;
726+
std::condition_variable cv;
727+
bool canceled = false;
728+
cancel([&] {
729+
const std::lock_guard<std::mutex> lock{mutex};
730+
canceled = true;
731+
cv.notify_all();
732+
});
733+
std::unique_lock<std::mutex> lock{mutex};
734+
waiting.set_value();
735+
cv.wait(lock, [&] { return canceled; });
736+
done.set_value();
737+
};
738+
ProxyClient<messages::FooInterface>* foo{setup.client.get()};
739+
foo->initThreadMap();
740+
741+
// Build the request by hand, the way a non-C++ client would. A normal
742+
// proxy call cannot be abandoned because `clientInvoke` blocks on it.
743+
std::optional<capnp::RemotePromise<messages::FooInterface::CallCancelFnAsyncResults>> remote;
744+
foo->m_context.loop->sync([&] {
745+
auto request{foo->m_client.callCancelFnAsyncRequest()};
746+
request.initContext().setThread(
747+
foo->m_context.connection->m_thread_map.makeThreadRequest().send().getResult());
748+
remote.emplace(request.send());
749+
});
750+
KJ_REQUIRE(waiting.get_future().wait_for(timeout) == std::future_status::ready);
751+
752+
auto done_future{done.get_future()};
753+
KJ_EXPECT(done_future.wait_for(std::chrono::seconds{0}) == std::future_status::timeout);
754+
755+
// Abandon the call without disconnecting.
756+
foo->m_context.loop->sync([&] { remote.reset(); });
757+
758+
KJ_EXPECT(done_future.wait_for(timeout) == std::future_status::ready);
759+
760+
// Connection should be unaffected.
761+
KJ_EXPECT(foo->add(1, 2) == 3);
762+
}
763+
669764
} // namespace test
670765
} // namespace mp

0 commit comments

Comments
 (0)