mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 08:19:55 +00:00
test(e2e): event handler can re-enter the library with a new request
Adds a C++ e2e case proving re-entrancy: from inside an `on_echo_fired` handler the consumer issues another request to the library, carrying data taken from the event, and gets a correct response back. The handler runs on the FFI thread with the event-registry lock held, so the test documents and exercises the only safe shape: an *async* request. A synchronous call from the handler would self-deadlock (the FFI thread is busy running the handler), and add/removeEventListener would deadlock on the registry lock. The async request merely queues on the FFI channel and is drained once the handler returns; its future is moved out and resolved on the main thread. A one-shot guard avoids the echo->event->echo storm (echo re-fires the event). Timeouts turn any deadlock regression into a failure rather than a hang. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c43563f82f
commit
851409aca4
@ -455,3 +455,49 @@ TEST(TimerE2E, WildcardListenerReceivesEventIdAndDecodesPayload) {
|
||||
EXPECT_EQ(captured.front().decoded->message, "hello");
|
||||
EXPECT_EQ(captured.front().decoded->echoCount, 1);
|
||||
}
|
||||
|
||||
// Re-entrancy: from *inside* an event handler the consumer issues another
|
||||
// request to the library, carrying information taken from the event.
|
||||
//
|
||||
// The handler runs on the FFI thread with the event-registry lock held, so it
|
||||
// must not:
|
||||
// (a) call add/removeEventListener — self-deadlock on the registry lock;
|
||||
// (b) make a *synchronous* request — the FFI thread is busy running
|
||||
// this handler, so a blocking call would wait on itself forever.
|
||||
// Issuing an *async* request is safe: it only queues work on the FFI request
|
||||
// channel and returns immediately; the FFI thread drains it once the handler
|
||||
// returns. We move the returned future out of the handler and resolve it on the
|
||||
// main thread. A hang here (caught by the timeouts below) would mean the
|
||||
// re-entrant request deadlocked.
|
||||
TEST(TimerE2E, EventHandlerCanIssueAsyncRequest) {
|
||||
auto ctx = makeCtx("reentrant");
|
||||
|
||||
std::atomic<bool> issued{false};
|
||||
// The handler stashes the nested request's future here; resolving it inside
|
||||
// the handler would block the FFI thread, so the main thread does it.
|
||||
auto nested = std::make_shared<std::future<Result<EchoResponse>>>();
|
||||
std::promise<void> ready;
|
||||
auto readyFuture = ready.get_future();
|
||||
|
||||
ctx->addOnEchoFiredListener([&, nested](const EchoEvent& evt) {
|
||||
// echo() re-fires this event, so guard to issue exactly one nested
|
||||
// request (otherwise each nested echo would spawn another — a storm).
|
||||
bool expected = false;
|
||||
if (!issued.compare_exchange_strong(expected, true)) return;
|
||||
// Carry information from the event into the new request.
|
||||
*nested = ctx->echoAsync(EchoRequest{"reentrant:" + evt.message, 0});
|
||||
ready.set_value();
|
||||
});
|
||||
|
||||
const auto outer = mustOk(ctx->echo(EchoRequest{"trigger", 1}));
|
||||
EXPECT_EQ(outer.echoed, "trigger");
|
||||
|
||||
ASSERT_EQ(readyFuture.wait_for(std::chrono::seconds(2)), std::future_status::ready)
|
||||
<< "handler never issued the nested request (possible deadlock)";
|
||||
ASSERT_EQ(nested->wait_for(std::chrono::seconds(2)), std::future_status::ready)
|
||||
<< "nested request never completed (possible deadlock)";
|
||||
|
||||
const auto reentrantResp = mustOk(nested->get());
|
||||
EXPECT_EQ(reentrantResp.echoed, "reentrant:trigger");
|
||||
EXPECT_EQ(reentrantResp.timerName, "reentrant");
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user