-
Notifications
You must be signed in to change notification settings - Fork 55
Fix error handling when creating clients (mp::ConnectStream)
#298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
060c1a5
231361a
44d1914
bb47369
fae9a63
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| // Copyright (c) The Bitcoin Core developers | ||
| // Distributed under the MIT software license, see the accompanying | ||
| // file COPYING or http://www.opensource.org/licenses/mit-license.php. | ||
| #include "unixlistener.h" | ||
| #include <kj/async.h> | ||
| #include <kj/common.h> | ||
| #include <kj/debug.h> | ||
| #include <kj/memory.h> | ||
| #include <kj/test.h> | ||
| #include <mp/proxy.h> | ||
| #include <mp/proxy-io.h> | ||
| #include <mp/test/foo.capnp.h> | ||
| #include <mp/test/foo.capnp.proxy.h> | ||
| #include <sys/socket.h> | ||
| #include <sys/types.h> | ||
| #include <unistd.h> | ||
|
|
||
| #include <chrono> | ||
| #include <condition_variable> | ||
| #include <cstring> // IWYU pragma: keep | ||
| #include <functional> | ||
| #include <future> | ||
| #include <memory> | ||
| #include <mutex> | ||
| #include <optional> | ||
| #include <stdexcept> | ||
| #include <string> | ||
| #include <string_view> | ||
| #include <thread> | ||
|
|
||
| namespace mp { | ||
| namespace test { | ||
| namespace { | ||
|
|
||
| constexpr auto FAILURE_TIMEOUT = std::chrono::seconds{30}; | ||
|
|
||
| //! Default event loop log handler used by tests, throws so the calling code | ||
| //! can assert on errors. | ||
| void DefaultLogHandler(mp::LogMessage log) | ||
| { | ||
| if (log.level == mp::Log::Raise) | ||
| throw std::runtime_error(log.message); | ||
| } | ||
|
|
||
| class TestSetup | ||
| { | ||
| public: | ||
| int client_fd; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Add test coverage for ConnectStream" (44d1914) All these class members should have
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addressed this in 70467c5 (follow-up PR). |
||
| int server_fd; | ||
|
|
||
| mp::EventLoop* loop; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Add test coverage for ConnectStream" (44d1914) Would probably drop
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, addressed at 5790bec (follow-up PR). |
||
| std::optional<mp::EventLoopRef> loop_ref; | ||
| //! Thread variable should be after other struct members so the thread does | ||
| //! not start until the other members are initialized. | ||
| std::thread loop_thread; | ||
|
|
||
| TestSetup(mp::LogFn log_handler = DefaultLogHandler) | ||
| : TestSetup( | ||
| [](int fds[2]) { | ||
| KJ_REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != -1); | ||
| }, | ||
| log_handler) {} | ||
|
|
||
| TestSetup(const std::function<void(int[2])>& init_sockets, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Add test coverage for ConnectStream" (44d1914) Having the init_sockets callback and the client_fd and server_fd members seems unnecessarily complicated given that nothing else in the test setup class uses them. Would seem simpler to drop these diff
--- a/test/mp/test/connect_tests.cpp
+++ b/test/mp/test/connect_tests.cpp
@@ -9,16 +9,15 @@
#include <kj/test.h>
#include <mp/proxy.h>
#include <mp/proxy-io.h>
+#include <mp/util.h>
#include <mp/test/foo.capnp.h>
#include <mp/test/foo.capnp.proxy.h>
#include <sys/socket.h>
-#include <sys/types.h>
#include <unistd.h>
#include <chrono>
#include <condition_variable>
#include <cstring> // IWYU pragma: keep
-#include <functional>
#include <future>
#include <memory>
#include <mutex>
@@ -45,9 +44,6 @@ void DefaultLogHandler(mp::LogMessage log)
class TestSetup
{
public:
- int client_fd;
- int server_fd;
-
mp::EventLoop* loop;
std::optional<mp::EventLoopRef> loop_ref;
//! Thread variable should be after other struct members so the thread does
@@ -55,14 +51,6 @@ public:
std::thread loop_thread;
TestSetup(mp::LogFn log_handler = DefaultLogHandler)
- : TestSetup(
- [](int fds[2]) {
- KJ_REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != -1);
- },
- log_handler) {}
-
- TestSetup(const std::function<void(int[2])>& init_sockets,
- mp::LogFn log_handler = DefaultLogHandler)
{
std::promise<mp::EventLoop*> loop_promise;
loop_thread = std::thread([&, log_handler] {
@@ -72,13 +60,6 @@ public:
});
loop = loop_promise.get_future().get();
loop_ref.emplace(*loop);
-
- // Initialize and store sockets
- int fds[2] = {-1, -1};
- init_sockets(fds);
-
- client_fd = fds[0];
- server_fd = fds[1];
}
~TestSetup()
@@ -91,15 +72,16 @@ public:
KJ_TEST("ConnectStream connects to a socket serving a valid init interface")
{
TestSetup setup;
+ auto [client_fd, server_fd] = SocketPair();
- std::thread server_thread([&setup]() {
+ std::thread server_thread([&]() {
mp::EventLoop server_loop("mptest-valid-server", DefaultLogHandler);
std::unique_ptr<FooInit> init = std::make_unique<FooInit>();
- ServeStream<messages::FooInit>(server_loop, MakeStream(server_loop, setup.server_fd), *init);
+ ServeStream<messages::FooInit>(server_loop, MakeStream(server_loop, server_fd), *init);
server_loop.loop();
});
- auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
+ auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, client_fd));
init.reset();
server_thread.join();
@@ -109,11 +91,12 @@ KJ_TEST("ConnectStream connects to a socket serving a valid init interface")
KJ_TEST("ConnectStream throws when the socket is already disconnected")
{
TestSetup setup;
+ auto [client_fd, server_fd] = SocketPair();
- close(setup.server_fd);
+ close(server_fd);
try {
- auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
+ auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, client_fd));
KJ_EXPECT(false);
} catch (const std::runtime_error& e) {
@@ -126,12 +109,13 @@ KJ_TEST("ConnectStream throws when the socket is already disconnected")
KJ_TEST("ConnectStream defers disconnect failure to the first IPC request for interfaces without construct()")
{
TestSetup setup;
+ auto [client_fd, server_fd] = SocketPair();
- close(setup.server_fd);
+ close(server_fd);
// Without a construct() method no IPC call is made during client
// creation, so ConnectStream succeeds even though the peer is gone.
- auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
+ auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, client_fd));
try {
foo->add(1, 2);
@@ -157,10 +141,11 @@ KJ_TEST("ConnectStream handles a disconnect when no client calls are made")
}
DefaultLogHandler(log);
});
+ auto [client_fd, server_fd] = SocketPair();
- close(setup.server_fd);
+ close(server_fd);
- auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
+ auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, client_fd));
// The disconnect handler registered by ProxyClientBase should run and
// delete the connection even when no calls are ever made.
@@ -171,20 +156,21 @@ KJ_TEST("ConnectStream handles a disconnect when no client calls are made")
KJ_TEST("ConnectStream throws when the socket disconnects after receiving data")
{
TestSetup setup;
+ auto [client_fd, server_fd] = SocketPair();
- std::thread server_thread([&setup]() {
+ std::thread server_thread([&]() {
char buf[128];
ssize_t bytes_received =
- recv(setup.server_fd, buf, sizeof(buf), 0);
+ recv(server_fd, buf, sizeof(buf), 0);
if (bytes_received > 0) {
- close(setup.server_fd);
+ close(server_fd);
}
});
try {
- auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
+ auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, client_fd));
if (server_thread.joinable()) server_thread.join();
KJ_EXPECT(false);
@@ -199,16 +185,14 @@ KJ_TEST("ConnectStream throws when the socket disconnects after receiving data")
KJ_TEST("ConnectStream throws when a connection accepted from a listener disconnects after receiving data")
{
UnixListener listener;
+ TestSetup setup;
+ int client_fd = listener.MakeConnectedSocket();
+ int server_fd = listener.release();
- TestSetup setup([&listener](int fds[2]) {
- fds[0] = listener.MakeConnectedSocket(); // client_fd
- fds[1] = listener.release(); // server_fd
- });
-
- std::thread server_thread([&setup]() {
+ std::thread server_thread([&]() {
char buf[128];
- int connection_fd = accept(setup.server_fd, nullptr, nullptr);
+ int connection_fd = accept(server_fd, nullptr, nullptr);
if (connection_fd >= 0) {
ssize_t bytes_received =
@@ -218,11 +202,11 @@ KJ_TEST("ConnectStream throws when a connection accepted from a listener disconn
close(connection_fd);
}
}
- close(setup.server_fd);
+ close(server_fd);
});
try {
- auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd));
+ auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, client_fd));
if (server_thread.joinable()) server_thread.join();
KJ_EXPECT(false);
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, you're right, having them was indeed unnecessary. Addressed this at 466afd0 (follow-up PR). Thanks for the diff! |
||
| mp::LogFn log_handler = DefaultLogHandler) | ||
| { | ||
| std::promise<mp::EventLoop*> loop_promise; | ||
| loop_thread = std::thread([&, log_handler] { | ||
| mp::EventLoop loop("mptest-connect", log_handler); | ||
| loop_promise.set_value(&loop); | ||
| loop.loop(); | ||
| }); | ||
| loop = loop_promise.get_future().get(); | ||
| loop_ref.emplace(*loop); | ||
|
|
||
| // Initialize and store sockets | ||
| int fds[2] = {-1, -1}; | ||
| init_sockets(fds); | ||
|
|
||
| client_fd = fds[0]; | ||
| server_fd = fds[1]; | ||
| } | ||
|
|
||
| ~TestSetup() | ||
| { | ||
| loop_ref.reset(); | ||
| loop_thread.join(); | ||
| } | ||
| }; | ||
|
|
||
| KJ_TEST("ConnectStream connects to a socket serving a valid init interface") | ||
| { | ||
| TestSetup setup; | ||
|
|
||
| std::thread server_thread([&setup]() { | ||
| mp::EventLoop server_loop("mptest-valid-server", DefaultLogHandler); | ||
| std::unique_ptr<FooInit> init = std::make_unique<FooInit>(); | ||
| ServeStream<messages::FooInit>(server_loop, MakeStream(server_loop, setup.server_fd), *init); | ||
| server_loop.loop(); | ||
| }); | ||
|
|
||
| auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Add test coverage for ConnectStream" (44d1914) Might be good to note in a comment that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, done at 76ab72f (follow-up PR). |
||
|
|
||
| init.reset(); | ||
| server_thread.join(); | ||
| KJ_EXPECT(true); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Add test coverage for ConnectStream" (44d1914) Expecting true here seems unnecessary.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dropped at 8d30ea8 (follow-up PR). |
||
| } | ||
|
|
||
| KJ_TEST("ConnectStream throws when the socket is already disconnected") | ||
| { | ||
| TestSetup setup; | ||
|
|
||
| close(setup.server_fd); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Fix error handling when creating clients" (bb47369) Would be good to use KJ_SYSCALL here and below to check close return value
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done at 2977072 (follow-up PR). |
||
|
|
||
| try { | ||
| auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd)); | ||
|
|
||
| KJ_EXPECT(false); | ||
| } catch (const std::runtime_error& e) { | ||
| std::string_view reason = e.what(); | ||
|
|
||
| KJ_EXPECT(reason == "IPC client method call interrupted by disconnect."); | ||
| } | ||
| } | ||
|
|
||
| KJ_TEST("ConnectStream defers disconnect failure to the first IPC request for interfaces without construct()") | ||
| { | ||
| TestSetup setup; | ||
|
|
||
| close(setup.server_fd); | ||
|
|
||
| // Without a construct() method no IPC call is made during client | ||
| // creation, so ConnectStream succeeds even though the peer is gone. | ||
| auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, setup.client_fd)); | ||
|
|
||
| try { | ||
| foo->add(1, 2); | ||
| KJ_EXPECT(false); | ||
| } catch (const std::runtime_error& e) { | ||
| std::string_view reason = e.what(); | ||
|
|
||
| KJ_EXPECT(reason == "IPC client method called after disconnect."); | ||
| } | ||
| } | ||
|
|
||
| KJ_TEST("ConnectStream handles a disconnect when no client calls are made") | ||
| { | ||
| std::mutex mutex; | ||
| std::condition_variable cv; | ||
| bool warned = false; | ||
|
|
||
| TestSetup setup([&](mp::LogMessage log) { | ||
| if (log.level == mp::Log::Warning && log.message.find("unexpected network disconnect") != std::string::npos) { | ||
| const std::lock_guard<std::mutex> lock(mutex); | ||
| warned = true; | ||
| cv.notify_all(); | ||
| } | ||
| DefaultLogHandler(log); | ||
| }); | ||
|
|
||
| close(setup.server_fd); | ||
|
|
||
| auto foo = ConnectStream<messages::FooInterface>(*setup.loop, MakeStream(*setup.loop, setup.client_fd)); | ||
|
|
||
| // The disconnect handler registered by ProxyClientBase should run and | ||
| // delete the connection even when no calls are ever made. | ||
| std::unique_lock<std::mutex> lock(mutex); | ||
| KJ_EXPECT(cv.wait_for(lock, FAILURE_TIMEOUT, [&] { return warned; })); | ||
|
xyzconstant marked this conversation as resolved.
|
||
| } | ||
|
|
||
| KJ_TEST("ConnectStream throws when the socket disconnects after receiving data") | ||
| { | ||
| TestSetup setup; | ||
|
|
||
| std::thread server_thread([&setup]() { | ||
| char buf[128]; | ||
|
|
||
| ssize_t bytes_received = | ||
| recv(setup.server_fd, buf, sizeof(buf), 0); | ||
|
|
||
| if (bytes_received > 0) { | ||
| close(setup.server_fd); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Add test coverage for ConnectStream" (44d1914) I don't understand the reason for only closing the descriptor if bytes or received (and leaking otherwise)? Would make more sense to close it unconditionally. Same applies to test below. If there is a reason for this conditional it would be good to explain in a comment
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestion. Honestly, when I wrote this I thought |
||
| } | ||
| }); | ||
|
|
||
| try { | ||
| auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd)); | ||
|
|
||
| if (server_thread.joinable()) server_thread.join(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Add test coverage for ConnectStream" (44d1914) I don't think it makes sense to call Same comment also applies to test below
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch. Addressed at 24f17bd (follow-up PR). |
||
| KJ_EXPECT(false); | ||
| } catch (const std::runtime_error& e) { | ||
| if (server_thread.joinable()) server_thread.join(); | ||
|
|
||
| std::string_view reason = e.what(); | ||
| KJ_EXPECT(reason == "IPC client method call interrupted by disconnect."); | ||
| } | ||
| } | ||
|
|
||
| KJ_TEST("ConnectStream throws when a connection accepted from a listener disconnects after receiving data") | ||
| { | ||
| UnixListener listener; | ||
|
|
||
| TestSetup setup([&listener](int fds[2]) { | ||
| fds[0] = listener.MakeConnectedSocket(); // client_fd | ||
| fds[1] = listener.release(); // server_fd | ||
| }); | ||
|
|
||
| std::thread server_thread([&setup]() { | ||
| char buf[128]; | ||
|
|
||
| int connection_fd = accept(setup.server_fd, nullptr, nullptr); | ||
|
|
||
| if (connection_fd >= 0) { | ||
| ssize_t bytes_received = | ||
| recv(connection_fd, buf, sizeof(buf), 0); | ||
|
|
||
| if (bytes_received > 0) { | ||
| close(connection_fd); | ||
| } | ||
| } | ||
| close(setup.server_fd); | ||
| }); | ||
|
|
||
| try { | ||
| auto init = ConnectStream<messages::FooInit>(*setup.loop, MakeStream(*setup.loop, setup.client_fd)); | ||
|
|
||
| if (server_thread.joinable()) server_thread.join(); | ||
| KJ_EXPECT(false); | ||
| } catch (const std::runtime_error& e) { | ||
| if (server_thread.joinable()) server_thread.join(); | ||
|
|
||
| std::string_view reason = e.what(); | ||
| KJ_EXPECT(reason == "IPC client method call interrupted by disconnect."); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Fix error handling when creating clients" (bb47369) It would be helpful if commit message noted the reason existing tests in this commit are changing, that because the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, couldn't reword the commit in time. Thanks for the feedback, though. |
||
| } | ||
| } | ||
|
|
||
| } // namespace | ||
| } // namespace test | ||
| } // namespace mp | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,6 +39,10 @@ interface FooInterface $Proxy.wrap("mp::test::FooImplementation") { | |
| passDataPointers @22 (arg :List(Data)) -> (result :List(Data)); | ||
| } | ||
|
|
||
| interface FooInit $Proxy.wrap("mp::test::FooInit") { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In commit "Add test coverage for ConnectStream" (31c1ac2) Am curious why this new FooInit interface is needed and existing Foo interface isn't used. It also seems like a potentially complicating factor that could make the tests harder to debug & understand for this to have a construct method. I wonder if it could be dropped or at least the Thread map parameters could be dropped since it doesn't look like anything in these tests requires threadmaps EDIT: Oh, I see in next commit it looks like there are new tests that rely on the construct call failing. I think it would be to only use the FooInit type for the tests which actually need the construct method, and use FooInterface for other tests. Also would be good to drop ThreadMap parameters as I believe they should not be needed.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice suggestion! I addressed it in 44d1914 by removing the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding the FooInit/FooInterface split, the tests already follow this. Only the 4 tests that require the Also, I've replaced the |
||
| construct @0 () -> (); | ||
| } | ||
|
|
||
| interface FooCallback $Proxy.wrap("mp::test::FooCallback") { | ||
| destroy @0 (context :Proxy.Context) -> (); | ||
| call @1 (context :Proxy.Context, arg :Int32) -> (result :Int32); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In commit "Add test coverage for ConnectStream" (44d1914)
This seems to be slightly different than the DefaultLogHandler defined in listen_tests.cpp. Would be nice to define a shared on, maybe in a test.h file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice suggestion! I moved this logger to a
common.hfile instead at ae5c6bc (follow-up PR).