Integrate Zenoh 1.8.0 - Part 2/2 - #868
Conversation
b25d3db to
0be5d86
Compare
01c71af to
ef695f6
Compare
Generated-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
…t NodeShared::Shutdown Generated-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
Generated-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
Generated-by: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
The key path 'transport/unicast/interests/timeout' does not exist in Zenoh's config schema (the real key is 'routing/interests/timeout'), so insert_json5 fails and the ignored ZResult hides it. The intended value (10000 ms) is also already Zenoh's default, so the block was a no-op twice over. GZ_TRANSPORT_ZENOH_CONFIG_OVERRIDE remains the way to tune this. Generated-by: Claude Fable 5 Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
NodeShared::Shutdown() already guarantees the session is still open when the querier cache is torn down, so the Querier destructors can undeclare cleanly. This removes the release()-leak and the one-shot shutdown machinery from ZenohQuerierEntry, addressing the review feedback about leaking Zenoh wrappers. Generated-by: Claude Fable 5 Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
The previous implementation blocked inside CreateZenohGet for up to timeout+500ms while the calling thread held NodeShared::mutex, serializing every concurrent request and teardown in the process, and then waited again in WaitUntil (double timeout on failure). It also captured raw 'this' in the reply closure, which relied on the handler never being removed from the requests storage (a per-request leak) to avoid a use-after-free. Fire the Querier get asynchronously instead and let Node::Request wait on the handler's condition variable, exactly like the ZeroMQ flow. IReqHandler now inherits enable_shared_from_this so the reply closure holds a weak_ptr and drops late replies harmlessly, and Node::Request removes the handler from the requests storage after WaitUntil, fixing the storage leak. The SetTimeoutMs plumbing and the heap-allocated wait state become unnecessary and are removed. Generated-by: Claude Fable 5 Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
Every zenoh-cpp API used by gz-transport (Querier, declare_querier, liveliness_get, get_peers_z_id, Session::close, ...) is identical between the 1.7.2 and 1.8.0 tags, and the teardown/cold-start fixes are defensive patterns that do not depend on 1.8-only behavior, so the hard 1.8.0 floor dropped 1.7.2 users unnecessarily. Also unify the duplicated min-version variables and fix the found-version message, which referenced an undefined variable. Note: 1.8.0 is what CI validates; drop this commit if we prefer to keep the stricter floor. Generated-by: Claude Fable 5 Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
ef695f6 to
7176ddf
Compare
Publisher teardown used to release() the Zenoh publisher and
liveliness token wrappers, leaking them and leaving a phantom
publisher visible to every other session until process exit. That
release existed to dodge an at-exit crash, but the explicit
NodeShared::Shutdown() introduced in the previous PR closes the
session deterministically, so dropping the wrappers is now safe at
any point: before close it is a quick undeclare (publishers run no
callbacks, so nothing can block or deadlock), after close it takes
the fast error path.
A cleanup queue with a background worker (as suggested in review)
was also considered. It remains a good fit if we later want to
retire the per-teardown detached threads used by subscribers and
queryables, whose undeclare can block on in-flight callbacks, but
for publishers there is nothing to wait on and the queue would only
add thread lifecycle complexity.
Adds INTEGRATION_zenohPublisherLeak, which fails against the old
code ('Topic is still listed after the remote publisher was
destroyed') and passes with this fix.
Generated-by: Claude Fable 5
Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
| zenoh::ZResult result = Z_OK; | ||
| auto replies = this->Session()->liveliness_get( | ||
| zenoh::KeyExpr("@gz/**"), | ||
| zenoh::channels::FifoChannel(SIZE_MAX - 1), |
There was a problem hiding this comment.
I think this won't work, I tried to run this and traced a segfault to this line.
It seems what it tries to do is allocate a channel with a capacity of size_t-1 which is incredibly large, the allocation fails and the code segfaults.
This is wildly out of my comfort zone so I had Gemini suggest a fix that seems to simplify the code a fair bit using an API with a lambda instead of a channel that could grow indefinitely.
This seems to fix the segfault for me but again take it with a lot of salt since I am not familiar with Zenoh or gz-transport:
diff --git a/src/NodeShared.cc b/src/NodeShared.cc
index 2d174850..2b44b609 100644
--- a/src/NodeShared.cc
+++ b/src/NodeShared.cc
@@ -338,28 +338,20 @@ NodeShared::NodeShared()
opts.timeout_ms = kZenohLivelinessGetTimeoutMs;
zenoh::ZResult result = Z_OK;
- auto replies = this->Session()->liveliness_get(
+ this->Session()->liveliness_get(
zenoh::KeyExpr("@gz/**"),
- zenoh::channels::FifoChannel(SIZE_MAX - 1),
+ [this](zenoh::Reply &reply)
+ {
+ if (reply.is_ok())
+ {
+ const auto &sample = reply.get_ok();
+ this->dataPtr->msgDiscovery->LivelinessMsgDataHandler(sample);
+ this->dataPtr->srvDiscovery->LivelinessSrvDataHandler(sample);
+ }
+ },
+ []() {},
std::move(opts),
&result);
-
- if (result == Z_OK)
- {
- for (auto res = replies.recv();
- std::holds_alternative<zenoh::Reply>(res);
- res = replies.recv())
- {
- const auto &reply = std::get<zenoh::Reply>(res);
- if (!reply.is_ok())
- continue;
- const auto &sample = reply.get_ok();
- // Both handlers filter by entityType internally, so it is
- // safe to dispatch every sample to both.
- this->dataPtr->msgDiscovery->LivelinessMsgDataHandler(sample);
- this->dataPtr->srvDiscovery->LivelinessSrvDataHandler(sample);
- }
- }
}
catch (const zenoh::ZException &e)
{There was a problem hiding this comment.
I think this won't work, I tried to run this and traced a segfault to this line. It seems what it tries to do is allocate a channel with a capacity of
size_t-1which is incredibly large, the allocation fails and the code segfaults. This is wildly out of my comfort zone so I had Gemini suggest a fix that seems to simplify the code a fair bit using an API with a lambda instead of a channel that could grow indefinitely. This seems to fix the segfault for me but again take it with a lot of salt since I am not familiar with Zenoh or gz-transport:
Thanks!
I could not reproduce the segfault here. Did it happen in gz-transport or testing with gz-sim or something else? I thought that the FifoChannel was doing only as a logical bound, not an actual preallocation. And just to confirm, are you testing with Zenoh 1.8.0 or a different version?
That said, I'm testing the callback approach with one latch. Otherwise, your suggestion is fire and forget and reintroduces the cold start race this PR is trying to fix.
There was a problem hiding this comment.
I'm on 1.7.2, I was testing with the CLI (i.e. gz topic -l).
Yea again can't really tell what is happening. I dug a bit deeper and indeed you are right and it doesn't preallocate and it was some sort of hallucination, now it's pointing to some sort of double free? Again not sure but the callback based method seems to work.
How have you been testing this? I have been building zenoh-c / zenoh-cpp / zenoh from source on the 1.7.2 tag in a colcon workspace
There was a problem hiding this comment.
I'm testing running all tests in gz-transport, and then, running gz-sim, and making sure that it doesn't crash when shutting down. I'm on Zenoh 1.8.0 right now but 1.7.2 should be fine as well.
There was a problem hiding this comment.
Actually hem apologies, I tried again to do a full clean build and actually it was working fine? I'll try to stress test this set of PRs a bit more but in general functionally it looks good
…completion latch Assisted-by: Claude Fable 5 Signed-off-by: Carlos Aguero <caguero@honurobotics.com>
|
I’ve tried some functional tests on this branch with a few of the example worlds and things seem to work as expected. This is what I’ve tried.
|
Summary
This patch completes the Zenoh 1.8 integration started in #867:
Cold start race in service requests. With interest driven routing (Zenoh > 1.6), the first
Node::Requestto a freshly spawned responder can time out silently. Requests now go through a per process cachedzenoh::Querierwhose interest declaration stays alive, so an in flight request reaches a queryable that appears late (INTEGRATION_twoProcsSrvCallLateResponder).Racy shutdown at exit. A new explicit
NodeShared::Shutdown()closes the session in a deterministic order, removing the 1.8 close race by construction.No more teardown leaks. With the deterministic shutdown in place, cached Queriers and Publisher entities are undeclared normally instead of leaked, so phantom publishers disappear from discovery immediately (
INTEGRATION_zenohPublisherLeak).Also,
CreateZenohGetno longer blocks while holdingNodeShared::mutex(replies arrive through a weak_ptr, mirroring the ZeroMQ flow), and the minimum Zenoh version is relaxed to 1.7.2 after verifying the API surface is identical (drop 7176ddf if we prefer a 1.8.0 floor).Checklist
codecheckpassed (See contributing)Generated-by: Claude Opus 4.7
Note to maintainers: Remember to use Squash-Merge and edit the commit message to match the pull request summary while retaining
Signed-off-byandGenerated-bymessages.Backports: If this is a backport, please use Rebase and Merge instead.