Skip to content

Fix startup race in example - #315

Merged
ryanofsky merged 2 commits into
bitcoin-core:masterfrom
xyzconstant:fix-race-in-mpexample
Jul 30, 2026
Merged

Fix startup race in example#315
ryanofsky merged 2 commits into
bitcoin-core:masterfrom
xyzconstant:fix-race-in-mpexample

Conversation

@xyzconstant

@xyzconstant xyzconstant commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Since #274, connection setup has been split into two calls: MakeStream followed by ConnectStream. Between these calls, the event loop's reference count can temporarily drop to zero, allowing EventLoop::loop() to exit (and the EventLoop to be destroyed) before ConnectStream posts its work. This crashes mpexample on startup.

Fix this by keeping an EventLoopRef alive in main() so the loop stays running while it is in use. This same pattern is already used in Bitcoin Core (m_loop_ref member of CapnpProtocol).

A second commit documents the reference-counted EventLoop lifetime in the EventLoopRef class comment and in doc/usage.md.

NOTE: On macOS, mpexample was crashing intermittently on startup (sometimes failing the m_post_fn == nullptr assert in the EventLoop destructor, sometimes with a mutex lock failure), depending on timing. With this change, the crashes are gone.

@DrahtBot

DrahtBot commented Jul 22, 2026

Copy link
Copy Markdown

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Reviews

See the guideline and AI policy for information on the review process.

Type Reviewers
ACK ryanofsky
Stale ACK ViniciusCestarii

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

@xyzconstant
xyzconstant force-pushed the fix-race-in-mpexample branch from b031c1f to ff9f84c Compare July 22, 2026 03:59
@xyzconstant

Copy link
Copy Markdown
Contributor Author

failed CI job seems unrelated

@xyzconstant
xyzconstant force-pushed the fix-race-in-mpexample branch from ff9f84c to f2fabf6 Compare July 22, 2026 04:19

@ViniciusCestarii ViniciusCestarii left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK f2fabf6 nice catch! I was able to reproduce on linux and it also intermittently fails without this fix. I liked the doc explanation too.

nit: a more surgical fix for against against master could look like:

+#include "mp/proxy.h"
 #include <init.capnp.h>
 #include <init.capnp.proxy.h>
 
int main(int argc, char** argv)
         loop.loop();
     });
     mp::EventLoop* loop = promise.get_future().get();
+    mp::EventLoopRef loop_ref{*loop};
 
     auto [printer_init, printer_pid] = Spawn(*loop, argv[0], "mpprinter");
     auto [calc_init, calc_pid] = Spawn(*loop, argv[0], "mpcalculator");
int main(int argc, char** argv)
     mp::WaitProcess(calc_pid);
     printer_init.reset();
     mp::WaitProcess(printer_pid);
+    loop_ref.reset();
     loop_thread.join();
     std::cout << "Bye!" << std::endl;
     return 0;

@xyzconstant
xyzconstant force-pushed the fix-race-in-mpexample branch from f2fabf6 to 265302b Compare July 23, 2026 23:26

@ryanofsky ryanofsky left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK 265302b. Nice catch and this fix looks correct, but it seems a little more complicated than it needs to be and I suggested a simpler version below.

I was initially confused how #274 causes this bug, but it happens specifically because of the loop.sync call in MakeStream. Each time loop.sync is called it causes the event loop to spin and check the reference count and potentially exit. The sync call in MakeStream is actually not necessary in practice, but was added as precaution in case the the wrapFd call was changed to update shared state. But because creating the stream object and connecting used to happen in one sync call and now happens in two calls, adding an extra reference to the event loop is necessary.

Comment thread example/example.cpp Outdated
namespace fs = std::filesystem;

static auto Spawn(mp::EventLoop& loop, const std::string& process_argv0, const std::string& new_exe_name)
static auto Spawn(const mp::EventLoopRef& loop_ref, const std::string& process_argv0, const std::string& new_exe_name)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "Fix startup race in mpexample" (f433a3f)

I don't think it actually makes sense to change this function and pass in a loop ref object when this isn't going to actually change the reference count. Would be better to revert changes to this function.

Similarly I think most of the changes below are not needed. Would suggest a simpler fix:

--- a/example/example.cpp
+++ b/example/example.cpp
@@ -55,6 +55,7 @@ int main(int argc, char** argv)
         loop.loop();
     });
     mp::EventLoop* loop = promise.get_future().get();
+    mp::EventLoopRef loop_ref{*loop};
 
     auto [printer_init, printer_pid] = Spawn(*loop, argv[0], "mpprinter");
     auto [calc_init, calc_pid] = Spawn(*loop, argv[0], "mpcalculator");
@@ -71,6 +72,7 @@ int main(int argc, char** argv)
     mp::WaitProcess(calc_pid);
     printer_init.reset();
     mp::WaitProcess(printer_pid);
+    loop_ref.reset();
     loop_thread.join();
     std::cout << "Bye!" << std::endl;
     return 0;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done at 3a997e1

Thanks!

Comment thread example/example.cpp Outdated
std::thread loop_thread([&] {
mp::EventLoop loop("mpexample", LogPrint);
promise.set_value(&loop);
promise.set_value(mp::EventLoopRef(loop));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In commit "Fix startup race in mpexample" (f433a3f)

Suggested an alternative in diff above, and I think creating an EventLoopRef here and then moving it into another EventLoopRef variable is too complicated and not necessary. It should only be necessary to increment the event loop usage count before using the event loop, and doesn't have to be done when creating it.

Since bitcoin-core#274, connection setup has been split into two calls: `MakeStream`
followed by `ConnectStream`. Between these calls, the event loop's reference
count can temporarily drop to zero, allowing `EventLoop::loop()` to exit
before `ConnectStream` posts its work, crashing the example on startup.

Keep an `EventLoopRef` alive in main() so the loop stays running while it
is in use.
`EventLoop::loop()` exits when the last `EventLoopRef` is released, so
code making multiple calls against the loop needs to hold its own
reference. State this in the `EventLoopRef` comment and usage.md.
@xyzconstant
xyzconstant force-pushed the fix-race-in-mpexample branch from 265302b to 36c6c63 Compare July 30, 2026 14:18
@xyzconstant

xyzconstant commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews!

The same diff has been suggested twice, so I just applied it in the latest push (36c6c63).

With #323 now merged, CI jobs should be green.

@ryanofsky ryanofsky left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review ACK 36c6c63. Thanks for the fix, documentation, and simplification!

@ryanofsky
ryanofsky merged commit 8d6d464 into bitcoin-core:master Jul 30, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants