Goby3 3.7.0
2026.11.12
Loading...
Searching...
No Matches
interprocess.h
Go to the documentation of this file.
1// Copyright 2026:
2// GobySoft, LLC (2013-)
3// Community contributors (see AUTHORS file)
4// File authors:
5// Toby Schneider <toby@gobysoft.org>
6//
7//
8// This file is part of the Goby Underwater Autonomy Project Libraries
9// ("The Goby Libraries").
10//
11// The Goby Libraries are free software: you can redistribute them and/or modify
12// them under the terms of the GNU Lesser General Public License as published by
13// the Free Software Foundation, either version 2.1 of the License, or
14// (at your option) any later version.
15//
16// The Goby Libraries are distributed in the hope that they will be useful,
17// but WITHOUT ANY WARRANTY; without even the implied warranty of
18// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19// GNU Lesser General Public License for more details.
20//
21// You should have received a copy of the GNU Lesser General Public License
22// along with Goby. If not, see <http://www.gnu.org/licenses/>.
23
24#ifndef GOBY_ZENOH_TRANSPORT_INTERPROCESS_H
25#define GOBY_ZENOH_TRANSPORT_INTERPROCESS_H
26
27#include <atomic>
28#include <chrono>
29#include <deque>
30#include <memory>
31#include <mutex>
32#include <set>
33#include <string>
34#include <thread>
35#include <unordered_map>
36
37#include <zenoh.hxx>
38
45
49
50namespace goby
51{
52namespace middleware
53{
54template <typename Data> class Publisher;
55} // namespace middleware
56
57namespace zenoh
58{
61
63std::string make_key_root(const protobuf::InterProcessPortalConfig& cfg, const std::string& layer);
64
69std::string make_hold_root(const protobuf::InterProcessPortalConfig& cfg, const std::string& layer);
70
71template <typename InnerTransporter,
72 template <typename Derived, typename InnerTransporterType, typename ImplementationTag_>
73 class PortalBase,
74 typename ImplementationTag>
76 : public PortalBase<
77 InterProcessPortalImplementation<InnerTransporter, PortalBase, ImplementationTag>,
78 InnerTransporter, ImplementationTag>
79{
80 public:
83 InnerTransporter, ImplementationTag>;
84
86 {
87 _init();
88 }
89
90 InterProcessPortalImplementation(InnerTransporter& inner,
92 : Base(inner), cfg_(cfg)
93 {
94 _init();
95 }
96
98 {
99 // undeclare every subscriber before the state its callback touches goes away
100 subscribers_.clear();
101 wildcard_subscriber_.reset();
102 liveliness_subscriber_.reset();
103 ready_token_.reset();
104 session_.reset();
105 }
106
111 void ready()
112 {
113 if (cfg_.client_name().empty())
114 {
115 goby::glog.is_warn() && goby::glog << "Zenoh: no client_name, so this process cannot "
116 "announce readiness to any peer holding for it"
117 << std::endl;
118 return;
119 }
120
121 LocalDeliveryScope scope(local_delivery_thread_);
122 ready_token_ =
123 std::make_unique<::zenoh::LivelinessToken>(session_->liveliness_declare_token(
124 ::zenoh::KeyExpr(hold_root_ + "/" + detail::escape_chunk(cfg_.client_name()))));
125 }
126
128 bool hold_state() { return hold_; }
129
130 friend Base;
131 friend typename Base::Base;
132 friend typename Base::Common;
133
134 private:
135 void _init()
136 {
138 key_root_ = make_key_root(cfg_, ImplementationTag::layer);
139 session_ = std::make_unique<::zenoh::Session>(open_session(cfg_));
140
141 goby::glog.is_debug1() && goby::glog << "Zenoh: session open, key root: " << key_root_
142 << std::endl;
143
144 hold_root_ = make_hold_root(cfg_, ImplementationTag::layer);
145 hold_ = cfg_.hold().required_client_size() > 0;
146 if (hold_)
147 {
148 ::zenoh::Session::LivelinessSubscriberOptions options;
149 // deliver the tokens of clients that were ready before this subscription existed
150 options.history = true;
151
152 liveliness_subscriber_ =
153 std::make_unique<::zenoh::Subscriber<void>>(session_->liveliness_declare_subscriber(
154 ::zenoh::KeyExpr(hold_root_ + "/**"), [this](const ::zenoh::Sample& sample)
155 { _on_liveliness(sample); }, ::zenoh::closures::none, std::move(options)));
156 }
157 }
158
159 // runs on a Zenoh thread
160 void _on_liveliness(const ::zenoh::Sample& sample)
161 {
162 const std::string key(sample.get_keyexpr().as_string_view());
163 const auto last = key.find_last_of('/');
164 if (last == std::string::npos)
165 return;
166 const std::string client = detail::unescape_chunk(key.substr(last + 1));
167
168 {
169 std::lock_guard<std::mutex> lock(rx_mutex_);
170 if (sample.get_kind() == ::zenoh::SampleKind::Z_SAMPLE_KIND_PUT)
171 ready_clients_.insert(client);
172 else
173 ready_clients_.erase(client);
174 }
175 _wake_poller();
176 }
177
178 // once released the hold is not reapplied: a client that later dies must not silently stop
179 // the publications its peers are making
180 void _update_hold()
181 {
182 std::set<std::string> ready;
183 {
184 std::lock_guard<std::mutex> lock(rx_mutex_);
185 ready = ready_clients_;
186 }
187
188 for (const auto& client : cfg_.hold().required_client())
189 if (!ready.count(client))
190 return;
191
192 // a client's token can reach this session ahead of its subscriber declarations, and a
193 // sample put in that gap is routed to no one
194 std::this_thread::sleep_for(std::chrono::duration<double>(cfg_.hold().settle_seconds()));
195
196 goby::glog.is_debug1() && goby::glog << "Zenoh: hold released" << std::endl;
197 hold_ = false;
198
199 for (auto& queued : publish_queue_) _put(queued.first, queued.second);
200 publish_queue_.clear();
201 }
202
203 void _do_publish(const std::string& identifier, const std::vector<char>& bytes)
204 {
205 if (hold_)
206 {
207 publish_queue_.emplace_back(identifier, bytes);
208 return;
209 }
210 _put(identifier, bytes);
211 }
212
213 void _put(const std::string& identifier, const std::vector<char>& bytes)
214 {
215 const std::string key = detail::identifier_to_key(key_root_, identifier, false);
216
217 goby::glog.is_debug3() && goby::glog << "Zenoh: publishing " << bytes.size() << "B to "
218 << key << std::endl;
219
220 ::zenoh::Session::PutOptions options;
221 options.congestion_control =
223 ? ::zenoh::CongestionControl::Z_CONGESTION_CONTROL_DROP
224 : ::zenoh::CongestionControl::Z_CONGESTION_CONTROL_BLOCK;
225 options.is_express = cfg_.express();
226
227 LocalDeliveryScope scope(local_delivery_thread_);
228 session_->put(::zenoh::KeyExpr(key),
229 ::zenoh::Bytes(std::string(bytes.begin(), bytes.end())), std::move(options));
230 }
231
232 void _do_portal_subscribe(const std::string& identifier)
233 {
234 portal_identifiers_.insert(identifier);
235 // the wildcard subscriber already covers this key, and a second matching subscriber would
236 // deliver every sample twice
237 if (!wildcard_subscriber_)
238 _declare(identifier);
239 }
240
241 void _do_portal_unsubscribe(const std::string& identifier)
242 {
243 portal_identifiers_.erase(identifier);
244 subscribers_.erase(identifier);
245 }
246
247 void _do_portal_wildcard_subscribe()
248 {
249 subscribers_.clear();
250 wildcard_subscriber_ =
251 std::make_unique<::zenoh::Subscriber<void>>(_make_subscriber(key_root_ + "/**"));
252 }
253
254 void _do_portal_wildcard_unsubscribe()
255 {
256 wildcard_subscriber_.reset();
257 for (const auto& identifier : portal_identifiers_) _declare(identifier);
258 }
259
260 void _declare(const std::string& identifier)
261 {
262 const std::string key = detail::identifier_to_key(key_root_, identifier, true);
263 subscribers_.erase(identifier);
264 subscribers_.emplace(identifier, _make_subscriber(key));
265 }
266
267 ::zenoh::Subscriber<void> _make_subscriber(const std::string& key)
268 {
269 goby::glog.is_debug2() && goby::glog << "Zenoh: subscribing to " << key << std::endl;
270
271 return session_->declare_subscriber(
272 ::zenoh::KeyExpr(key), [this](const ::zenoh::Sample& sample) { _on_sample(sample); },
273 ::zenoh::closures::none);
274 }
275
276 // runs on a Zenoh thread
277 void _on_sample(const ::zenoh::Sample& sample)
278 {
279 std::string identifier = detail::key_to_identifier(
280 key_root_, std::string(sample.get_keyexpr().as_string_view()));
281 if (identifier.empty())
282 {
283 goby::glog.is_warn() && goby::glog << "Zenoh: received sample outside the key root: "
284 << sample.get_keyexpr().as_string_view()
285 << std::endl;
286 return;
287 }
288
289 // the layer above expects the identifier, its end delimiter and the payload as one string
291 identifier += sample.get_payload().as_string();
292
293 {
294 std::lock_guard<std::mutex> lock(rx_mutex_);
295 rx_.push_back(std::move(identifier));
296 }
297 _wake_poller();
298 }
299
300 // Zenoh runs a matching local subscriber's callback on the thread that put the sample or
301 // declared the token, before that call returns
302 class LocalDeliveryScope
303 {
304 public:
305 LocalDeliveryScope(std::atomic<std::thread::id>& thread) : thread_(thread)
306 {
307 thread_ = std::this_thread::get_id();
308 }
309 ~LocalDeliveryScope() { thread_ = std::thread::id(); }
310
311 private:
312 std::atomic<std::thread::id>& thread_;
313 };
314
315 void _wake_poller()
316 {
317 // a local delivery lands on the polling thread, which drains the queues before it waits
318 // and may already hold the poll mutex
319 if (std::this_thread::get_id() == local_delivery_thread_.load())
320 return;
321 middleware::detail::notify_poller(*this->poll_mutex(), *this->cv());
322 }
323
324 int _poll(std::unique_ptr<std::unique_lock<std::mutex>>& lock)
325 {
326 if (hold_)
327 _update_hold();
328
329 std::deque<std::string> received;
330 {
331 std::lock_guard<std::mutex> rx_lock(rx_mutex_);
332 received.swap(rx_);
333 }
334
335 int items = 0;
336 for (const auto& data : received)
337 {
338 ++items;
339 this->_handle_received_data(lock, data);
340 }
341 return items;
342 }
343
344 private:
345 const protobuf::InterProcessPortalConfig cfg_;
346 std::string key_root_;
347 std::unique_ptr<::zenoh::Session> session_;
348
349 // main thread only
350 std::set<std::string> portal_identifiers_;
351 std::unordered_map<std::string, ::zenoh::Subscriber<void>> subscribers_;
352 std::unique_ptr<::zenoh::Subscriber<void>> wildcard_subscriber_;
353
354 std::string hold_root_;
355 bool hold_{false};
356 std::unique_ptr<::zenoh::LivelinessToken> ready_token_;
357 std::unique_ptr<::zenoh::Subscriber<void>> liveliness_subscriber_;
358 std::deque<std::pair<std::string, std::vector<char>>> publish_queue_;
359 std::atomic<std::thread::id> local_delivery_thread_{std::thread::id()};
360
361 // written by Zenoh threads, drained by the main thread in _poll()
362 std::mutex rx_mutex_;
363 std::deque<std::string> rx_;
364 std::set<std::string> ready_clients_;
365};
366
367template <typename InnerTransporter = middleware::NullTransporter>
371
372template <typename InnerTransporter = middleware::NullTransporter>
375
376} // namespace zenoh
377} // namespace goby
378
379namespace goby
380{
381namespace middleware
382{
383namespace detail
384{
385template <> struct implementation_traits<goby::zenoh::detail::InterProcessTag>
386{
387 template <typename InnerTransporter>
390 static constexpr const char* name = "zenoh";
391};
392} // namespace detail
393} // namespace middleware
394} // namespace goby
395
396#endif
Implements the forwarder concept for the interprocess layer.
void set_lock_action(logger_lock::LockAction lock_action)
void ready()
When using hold functionality, call when the process is ready to receive publications (typically done...
InterProcessPortalImplementation(const protobuf::InterProcessPortalConfig &cfg)
InterProcessPortalImplementation(InnerTransporter &inner, const protobuf::InterProcessPortalConfig &cfg)
bool hold_state()
When using hold functionality, returns whether the system is holding (true) and thus waiting for all ...
::goby::zenoh::protobuf::InterProcessPortalConfig_CongestionControl congestion_control() const
const ::goby::zenoh::protobuf::InterProcessPortalConfig_Hold & hold() const
detail namespace with internal helper functions
Definition json.hpp:263
void notify_poller(std::mutex &poll_mutex, std::condition_variable &cv)
Wake a Poller's poll() loop after data has been made available to one of its _poll() implementations.
std::string identifier_to_key(const std::string &root, const std::string &identifier, bool wildcard_suffix)
Map a Goby identifier onto a key expression below root.
std::string escape_chunk(const std::string &chunk)
Percent-encode the characters a key expression chunk cannot carry.
std::string key_to_identifier(const std::string &root, const std::string &key)
Invert identifier_to_key() for a received sample, whose key is always fully qualified.
std::string unescape_chunk(const std::string &chunk)
Invert escape_chunk(). A trailing or malformed escape is passed through unchanged.
std::string make_hold_root(const protobuf::InterProcessPortalConfig &cfg, const std::string &layer)
Where clients announce readiness: "<prefix>/<platform>/hold/<layer>".
::zenoh::Session open_session(const protobuf::InterProcessPortalConfig &cfg)
Open a Zenoh session from the Goby configuration.
std::string make_key_root(const protobuf::InterProcessPortalConfig &cfg, const std::string &layer)
The key expression chunks shared by every message on one layer: "<prefix>/<platform>/<layer>".
The global namespace for the Goby project.
util::FlexOstream glog
Access the Goby logger through this object.
ImplementationTag for zenoh interprocess transporters.
Definition tags.h:36