Skip to content

Commit 2dcfb46

Browse files
committed
test: Test disconnects during IPC calls
This change adds two unit tests to make sure shutdown happens cleanly if the the client disconnects while the server is processing an IPC call. The first test checks a synchronous case which has always worked, where the disconnect starts but is not processed by the event loop until after the IPC call completes. The second test checks an asynchronous case which was fixed by the previous commit, where the disconnect is processed before the IPC call generates a response. This test would usually crash or hang prior to the fix in the previous commit. It would crash reliably with address sanitizer.
1 parent 7dc8bef commit 2dcfb46

File tree

3 files changed

+72
-0
lines changed

3 files changed

+72
-0
lines changed

test/mp/test/foo.capnp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ interface FooInterface $Proxy.wrap("mp::test::FooImplementation") {
2929
passMutable @14 (arg :FooMutable) -> (arg :FooMutable);
3030
passEnum @15 (arg :Int32) -> (result :Int32);
3131
passFn @16 (context :Proxy.Context, fn :FooFn) -> (result :Int32);
32+
callFn @17 () -> ();
33+
callFnAsync @18 (context :Proxy.Context) -> ();
3234
}
3335

3436
interface FooCallback $Proxy.wrap("mp::test::FooCallback") {

test/mp/test/foo.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#ifndef MP_TEST_FOO_H
66
#define MP_TEST_FOO_H
77

8+
#include <cassert>
89
#include <functional>
910
#include <map>
1011
#include <memory>
@@ -78,6 +79,9 @@ class FooImplementation
7879
FooEnum passEnum(FooEnum foo) { return foo; }
7980
int passFn(std::function<int()> fn) { return fn(); }
8081
std::shared_ptr<FooCallback> m_callback;
82+
void callFn() { assert(m_fn); m_fn(); }
83+
void callFnAsync() { assert(m_fn); m_fn(); }
84+
std::function<void()> m_fn;
8185
};
8286

8387
} // namespace test

test/mp/test/test.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class TestSetup
4545
std::function<void()> client_disconnect;
4646
std::promise<std::unique_ptr<ProxyClient<messages::FooInterface>>> client_promise;
4747
std::unique_ptr<ProxyClient<messages::FooInterface>> client;
48+
ProxyServer<messages::FooInterface>* server{nullptr};
4849

4950
TestSetup(bool client_owns_connection = true)
5051
: thread{[&] {
@@ -58,6 +59,7 @@ class TestSetup
5859
std::make_unique<Connection>(loop, kj::mv(pipe.ends[0]), [&](Connection& connection) {
5960
auto server_proxy = kj::heap<ProxyServer<messages::FooInterface>>(
6061
std::make_shared<FooImplementation>(), connection);
62+
server = server_proxy;
6163
return capnp::Capability::Client(kj::mv(server_proxy));
6264
});
6365
server_disconnect = [&] { loop.sync([&] { server_connection.reset(); }); };
@@ -215,5 +217,69 @@ KJ_TEST("Calling IPC method after server connection is closed")
215217
KJ_EXPECT(disconnected);
216218
}
217219

220+
KJ_TEST("Calling IPC method and disconnecting during the call")
221+
{
222+
TestSetup setup{/*client_owns_connection=*/false};
223+
ProxyClient<messages::FooInterface>* foo = setup.client.get();
224+
KJ_EXPECT(foo->add(1, 2) == 3);
225+
226+
// Set m_fn to initiate client disconnect when server is in the middle of
227+
// handling the callFn call to make sure this case is handled cleanly.
228+
setup.server->m_impl->m_fn = setup.client_disconnect;
229+
230+
bool disconnected{false};
231+
try {
232+
foo->callFn();
233+
} catch (const std::runtime_error& e) {
234+
KJ_EXPECT(std::string_view{e.what()} == "IPC client method call interrupted by disconnect.");
235+
disconnected = true;
236+
}
237+
KJ_EXPECT(disconnected);
238+
}
239+
240+
KJ_TEST("Calling IPC method, disconnecting and blocking during the call")
241+
{
242+
// This test is similar to last test, except that instead of letting the IPC
243+
// call return immediately after triggering a disconnect, make it disconnect
244+
// & wait so server is forced to deal with having a disconnection and call
245+
// in flight at the same time.
246+
//
247+
// Test uses callFnAsync() instead of callFn() to implement this. Both of
248+
// these methods have the same implementation, but the callFnAsync() capnp
249+
// method declaration takes an mp.Context argument so the method executes on
250+
// an asynchronous thread instead of executing in the event loop thread, so
251+
// it is able to block without deadlocking the event lock thread.
252+
//
253+
// This test adds important coverage because it causes the server Connection
254+
// object to be destroyed before ProxyServer object, which is not a
255+
// condition that usually happens because the m_rpc_system.reset() call in
256+
// the ~Connection destructor usually would immediately free all remaing
257+
// ProxyServer objects associated with the connection. Having an in-progress
258+
// RPC call requires keeping the ProxyServer longer.
259+
260+
TestSetup setup{/*client_owns_connection=*/false};
261+
ProxyClient<messages::FooInterface>* foo = setup.client.get();
262+
KJ_EXPECT(foo->add(1, 2) == 3);
263+
264+
foo->initThreadMap();
265+
std::promise<void> signal;
266+
setup.server->m_impl->m_fn = [&] {
267+
EventLoopRef loop{*setup.server->m_context.loop};
268+
setup.client_disconnect();
269+
signal.get_future().get();
270+
};
271+
272+
bool disconnected{false};
273+
try {
274+
foo->callFnAsync();
275+
} catch (const std::runtime_error& e) {
276+
KJ_EXPECT(std::string_view{e.what()} == "IPC client method call interrupted by disconnect.");
277+
disconnected = true;
278+
}
279+
KJ_EXPECT(disconnected);
280+
281+
signal.set_value();
282+
}
283+
218284
} // namespace test
219285
} // namespace mp

0 commit comments

Comments
 (0)