CARLA
Loading...
Searching...
No Matches
router.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
8
11
12#include <algorithm>
13#include <utility>
14
15namespace carla {
16namespace multigpu {
17
19 _next(0) { }
20
22 Stop();
23}
24
27 // Joins worker threads before the acceptor closes, so no in-flight accept
28 // can race with teardown.
29 _pool.Stop();
30 // ~Router() never runs in production (PrimaryCommands holds a shared_ptr
31 // back to this Router, see set_router()), so Stop() must release the
32 // listening socket itself. Listener::Stop() is idempotent, so this is
33 // safe even if ~Listener() also runs later.
34 if (_listener) {
35 _listener->Stop();
36 }
37 _listener.reset();
38}
39
40Router::Router(uint16_t port) :
41 _next(0) {
42
43 _endpoint = boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("0.0.0.0"), port);
44 _listener = std::make_shared<carla::multigpu::Listener>(_pool.io_context(), _endpoint);
45}
46
48 // prepare server
49 std::weak_ptr<Router> weak = shared_from_this();
50
51 carla::multigpu::Listener::callback_function_type on_open = [weak](std::shared_ptr<carla::multigpu::Primary> session) {
52 auto self = weak.lock();
53 if (!self) return;
54 self->ConnectSession(std::move(session));
55 };
56
57 carla::multigpu::Listener::callback_function_type on_close = [weak](std::shared_ptr<carla::multigpu::Primary> session) {
58 auto self = weak.lock();
59 if (!self) return;
60 self->DisconnectSession(std::move(session));
61 };
62
64 [weak](std::shared_ptr<carla::multigpu::Primary> session, carla::Buffer buffer) {
65 auto self = weak.lock();
66 if (!self) return;
67 self->HandleResponse(std::move(session), std::move(buffer));
68 };
69
70 _commander.set_router(shared_from_this());
71
72 _listener->Listen(on_open, on_close, on_response);
73 log_info("Listening at ", _endpoint);
74}
75
76void Router::SetNewConnectionCallback(std::function<void(void)> func)
77{
78 _callback = func;
79}
80
81void Router::AsyncRun(size_t worker_threads) {
82 _pool.AsyncRun(worker_threads);
83}
84
85boost::asio::ip::tcp::endpoint Router::GetLocalEndpoint() const {
86 return _endpoint;
87}
88
89void Router::ConnectSession(std::shared_ptr<Primary> session) {
90 DEBUG_ASSERT(session != nullptr);
91 std::scoped_lock<std::mutex> lock(_mutex);
92 _new_sessions.emplace_back(session);
93 _sessions.emplace_back(std::move(session));
95 log_info("Connected secondary servers:", _sessions.size());
96 if (_last_map) {
97 // The resync callback runs from HandleResponse once the map is loaded.
98 const load_map_id_type load_id = ++_last_load_id;
100 _loading[_sessions.back().get()] = load_id;
101 return;
102 }
103 if (_callback)
104 _callback();
105}
106
107void Router::DisconnectSession(std::shared_ptr<Primary> session) {
108 DEBUG_ASSERT(session != nullptr);
109 std::scoped_lock<std::mutex> lock(_mutex);
110 if (_sessions.size() > 0) {
111 _sessions.erase(
112 std::remove(_sessions.begin(), _sessions.end(), session),
113 _sessions.end());
114 _session_count = _sessions.size();
115 log_info("Connected secondary servers:", _sessions.size());
116 }
117 _loading.erase(session.get());
118
119 // A request may still be waiting on this session's response; without this,
120 // the caller blocked in std::future::get() would hang forever.
121 auto pending = _promises.find(session.get());
122 if (pending != _promises.end()) {
123 RejectPromise(pending->second, "secondary server disconnected before responding");
124 _promises.erase(pending);
125 }
126}
127
128void Router::RejectAllPending(std::string_view reason) {
129 for (auto &entry : _promises) {
130 RejectPromise(entry.second, reason);
131 }
132 _promises.clear();
133}
134
136 std::shared_ptr<std::promise<SessionInfo>> promise,
137 std::string_view reason) {
138 log_error("multigpu router: rejecting pending request: ", reason);
139 promise->set_value(SessionInfo{});
140}
141
142void Router::HandleResponse(std::shared_ptr<Primary> session, Buffer buffer) {
143 std::scoped_lock<std::mutex> lock(_mutex);
144
145 // Checked before the promise lookup: no command reply can equal this
146 // exact marker (see commands.h), so it can never be misdelivered as data
147 // for whatever happens to be pending on this session.
148 const std::string_view payload(
149 reinterpret_cast<const char *>(buffer.data()), buffer.size());
150 if (const auto ready_load_id = ParseEpisodeReadyMessage(payload)) {
151 log_info("Secondary episode ready, re-arming full resync");
152 auto loading = _loading.find(session.get());
153 if ((loading != _loading.end()) && (loading->second == *ready_load_id)) {
154 _loading.erase(loading);
155 } else if (loading != _loading.end()) {
156 log_info("multigpu router: ignoring episode ready of load ", *ready_load_id,
157 ", waiting for load ", loading->second);
158 }
159 if (_callback) {
160 _callback();
161 }
162 return;
163 }
164
165 auto prom = _promises.find(session.get());
166 if (prom != _promises.end()) {
167 log_info("Got data from secondary (with promise): ", buffer.size());
168 prom->second->set_value({std::move(session), std::move(buffer)});
169 _promises.erase(prom);
170 } else {
171 log_warning("multigpu router: ignoring unsolicited data with no pending request: ", buffer.size(), " bytes");
172 }
173}
174
176 std::scoped_lock<std::mutex> lock(_mutex);
177 _sessions.clear();
178 _session_count = 0u;
179 _new_sessions.clear();
180 _loading.clear();
181 // Symmetric with DisconnectSession: any request still waiting on a
182 // response from a session that is about to disappear must not be left to
183 // hang forever.
184 RejectAllPending("router is shutting down");
185 log_info("Disconnecting all secondary servers");
186}
187
189 // define the command header
190 CommandHeader header;
191 header.id = id;
192 header.size = buffer.size();
193 Buffer buf_header(reinterpret_cast<uint8_t *>(&header), sizeof(header));
194
195 auto view_header = carla::BufferView::CreateFrom(std::move(buf_header));
196 auto view_data = carla::BufferView::CreateFrom(std::move(buffer));
197 auto message = Primary::MakeMessage(view_header, view_data);
198
199 // write to multiple servers
200 std::scoped_lock<std::mutex> lock(_mutex);
201 for (auto &s : _sessions) {
202 if (s != nullptr) {
203 s->Write(message);
204 }
205 }
206}
207
209 const std::shared_ptr<Primary> &session,
210 std::string_view map,
211 load_map_id_type load_id,
212 const LoadMapExtras &extras) {
213 const std::string payload = MakeLoadMapPayload(map, load_id, extras);
214 Buffer buffer(reinterpret_cast<const unsigned char *>(payload.data()), payload.size());
215
216 CommandHeader header;
218 header.size = static_cast<uint32_t>(buffer.size());
219 Buffer buf_header(reinterpret_cast<uint8_t *>(&header), sizeof(header));
220
221 auto view_header = carla::BufferView::CreateFrom(std::move(buf_header));
222 auto view_data = carla::BufferView::CreateFrom(std::move(buffer));
223 if (_request_observer) {
224 _request_observer(session.get(), MultiGPUCommand::LOAD_MAP, *view_data);
225 }
226 session->Write(Primary::MakeMessage(view_header, view_data));
227}
228
229load_map_id_type Router::WriteLoadMap(std::string_view map, const LoadMapExtras &extras) {
230 std::scoped_lock<std::mutex> lock(_mutex);
231 const load_map_id_type load_id = ++_last_load_id;
232 _last_map = std::string(map);
233 _last_map_extras = extras;
234 for (auto &s : _sessions) {
235 if (s != nullptr) {
236 SendLoadMap(s, map, load_id, extras);
237 _loading[s.get()] = load_id;
238 }
239 }
240 return load_id;
241}
242
244 std::scoped_lock<std::mutex> lock(_mutex);
245 return !_loading.empty();
246}
247
249 std::scoped_lock<std::mutex> lock(_mutex);
250 _loading.clear();
251}
252
253std::future<SessionInfo> Router::WriteToNext(MultiGPUCommand id, Buffer &&buffer) {
254 // define the command header
255 CommandHeader header;
256 header.id = id;
257 header.size = buffer.size();
258 Buffer buf_header(reinterpret_cast<uint8_t *>(&header), sizeof(header));
259
260 auto view_header = carla::BufferView::CreateFrom(std::move(buf_header));
261 auto view_data = carla::BufferView::CreateFrom(std::move(buffer));
262 auto message = Primary::MakeMessage(view_header, view_data);
263
264 // create the promise for the posible answer
265 auto response = std::make_shared<std::promise<SessionInfo>>();
266
267 // write to the next server only
268 std::scoped_lock<std::mutex> lock(_mutex);
269 if (_next >= _sessions.size()) {
270 _next = 0;
271 }
272 if (_next < _sessions.size()) {
273 auto s = _sessions[_next];
274 if (s == nullptr) {
275 RejectPromise(response, "no secondary session available to route the command");
276 } else if (_promises.contains(s.get())) {
277 // One slot per session (no per-request correlation id in the wire
278 // protocol): overwriting it would orphan the earlier promise forever,
279 // so refuse the newer request instead.
280 RejectPromise(response, "a request is already pending on this secondary session");
281 } else {
282 _promises[s.get()] = response;
283 if (_request_observer) {
284 _request_observer(s.get(), id, *view_data);
285 }
286 s->Write(message);
287 }
288 } else {
289 RejectPromise(response, "no secondary server connected");
290 }
291 ++_next;
292 return response->get_future();
293}
294
295bool Router::IsConnected(const std::weak_ptr<Primary> &server) {
296 std::scoped_lock<std::mutex> lock(_mutex);
297 auto s = server.lock();
298 return s && (std::find(_sessions.begin(), _sessions.end(), s) != _sessions.end());
299}
300
301std::vector<std::weak_ptr<Primary>> Router::TakeNewSessions() {
302 std::scoped_lock<std::mutex> lock(_mutex);
303 return std::exchange(_new_sessions, {});
304}
305
306std::future<SessionInfo> Router::WriteToOne(std::weak_ptr<Primary> server, MultiGPUCommand id, Buffer &&buffer) {
307 // define the command header
308 CommandHeader header;
309 header.id = id;
310 header.size = buffer.size();
311 Buffer buf_header(reinterpret_cast<uint8_t *>(&header), sizeof(header));
312
313 auto view_header = carla::BufferView::CreateFrom(std::move(buf_header));
314 auto view_data = carla::BufferView::CreateFrom(std::move(buffer));
315 auto message = Primary::MakeMessage(view_header, view_data);
316
317 // create the promise for the posible answer
318 auto response = std::make_shared<std::promise<SessionInfo>>();
319
320 // write to the specific server only
321 std::scoped_lock<std::mutex> lock(_mutex);
322 auto s = server.lock();
323 // A session's socket can already be closed while the Primary object is
324 // kept alive a little longer elsewhere, so also check _sessions
325 // membership; otherwise Write() would silently no-op and this promise
326 // would never resolve.
327 const bool still_connected =
328 s && (std::find(_sessions.begin(), _sessions.end(), s) != _sessions.end());
329 if (!still_connected) {
330 RejectPromise(response, "secondary session is no longer connected");
331 } else if (_promises.contains(s.get())) {
332 // Same reasoning as WriteToNext: one slot per session.
333 RejectPromise(response, "a request is already pending on this secondary session");
334 } else {
335 _promises[s.get()] = response;
336 if (_request_observer) {
337 _request_observer(s.get(), id, *view_data);
338 }
339 s->Write(message);
340 }
341 return response->get_future();
342}
343
344} // namespace multigpu
345} // namespace carla
#define DEBUG_ASSERT(predicate)
Definition Debug.h:66
static std::shared_ptr< BufferView > CreateFrom(Buffer &&buffer)
Definition BufferView.h:56
A piece of raw data.
const value_type * data() const noexcept
Direct access to the allocated memory or nullptr if no memory is allocated.
size_type size() const noexcept
void Stop()
Stop the ThreadPool and join all its threads.
Definition ThreadPool.h:79
void AsyncRun(size_t worker_threads)
Launch threads to run tasks asynchronously.
Definition ThreadPool.h:54
auto & io_context()
Return the underlying io_context.
Definition ThreadPool.h:36
std::function< void(std::shared_ptr< Primary >)> callback_function_type
Definition listener.h:32
std::function< void(std::shared_ptr< Primary >, carla::Buffer)> callback_function_type_response
Definition listener.h:33
void set_router(std::shared_ptr< Router > router)
static auto MakeMessage(Buffers... buffers)
Definition primary.h:55
boost::asio::ip::tcp::endpoint GetLocalEndpoint() const
Definition router.cpp:85
PrimaryCommands _commander
Definition router.h:173
void Write(MultiGPUCommand id, Buffer &&buffer)
Definition router.cpp:188
load_map_id_type WriteLoadMap(std::string_view map, const LoadMapExtras &extras={})
Broadcasts LOAD_MAP tagged with a new load id; each connected secondary counts as loading until it re...
Definition router.cpp:229
std::vector< std::shared_ptr< Primary > > _sessions
Definition router.h:163
static void RejectPromise(std::shared_ptr< std::promise< SessionInfo > > promise, std::string_view reason)
Fails a pending request instead of leaving its future unresolved.
Definition router.cpp:135
std::future< SessionInfo > WriteToNext(MultiGPUCommand id, Buffer &&buffer)
Definition router.cpp:253
std::optional< std::string > _last_map
Definition router.h:171
std::function< void(void)> _callback
Definition router.h:174
void RejectAllPending(std::string_view reason)
Rejects and clears every currently-pending promise.
Definition router.cpp:128
std::unordered_map< Primary *, std::shared_ptr< std::promise< SessionInfo > > > _promises
Definition router.h:168
std::shared_ptr< Listener > _listener
Definition router.h:166
LoadMapExtras _last_map_extras
Definition router.h:172
std::vector< std::weak_ptr< Primary > > _new_sessions
Definition router.h:165
void ConnectSession(std::shared_ptr< Primary > session)
Definition router.cpp:89
void StopWaitingForSecondaryLoads()
Definition router.cpp:248
std::future< SessionInfo > WriteToOne(std::weak_ptr< Primary > server, MultiGPUCommand id, Buffer &&buffer)
Definition router.cpp:306
boost::asio::ip::tcp::endpoint _endpoint
Definition router.h:162
request_observer_type _request_observer
Definition router.h:175
bool IsConnected(const std::weak_ptr< Primary > &server)
Whether server is still one of the connected secondary sessions.
Definition router.cpp:295
void SendLoadMap(const std::shared_ptr< Primary > &session, std::string_view map, load_map_id_type load_id, const LoadMapExtras &extras)
Sends LOAD_MAP to one session. Caller must already hold _mutex.
Definition router.cpp:208
bool IsAnySecondaryLoading()
Whether a connected secondary sent LOAD_MAP has not yet reported the episode of its latest LOAD_MAP r...
Definition router.cpp:243
void DisconnectSession(std::shared_ptr< Primary > session)
Definition router.cpp:107
void AsyncRun(size_t worker_threads)
Definition router.cpp:81
void SetNewConnectionCallback(std::function< void(void)>)
Definition router.cpp:76
std::vector< std::weak_ptr< Primary > > TakeNewSessions()
Sessions connected since the previous call, oldest first.
Definition router.cpp:301
void HandleResponse(std::shared_ptr< Primary > session, Buffer buffer)
Re-arms the new-connection resync if buffer is the secondary's episode-ready marker (checked first,...
Definition router.cpp:142
load_map_id_type _last_load_id
Definition router.h:170
std::atomic_size_t _session_count
Definition router.h:164
std::unordered_map< const Primary *, load_map_id_type > _loading
Definition router.h:169
uint32_t load_map_id_type
Identifies one LOAD_MAP broadcast.
Definition commands.h:46
std::string MakeLoadMapPayload(std::string_view map, load_map_id_type load_id, const LoadMapExtras &extras={})
LOAD_MAP payload: the NUL-terminated map path, the load id, then the extras as [u8 kind][u32 size][by...
Definition commands.h:63
std::optional< load_map_id_type > ParseEpisodeReadyMessage(std::string_view payload)
Load id of an episode-ready message (0 for a bare marker), or nullopt if payload is not one.
Definition commands.h:132
This file contains definitions of common data structures used in traffic manager.
static void log_error(Args &&... args)
Definition Logging.h:110
static void log_info(Args &&... args)
Definition Logging.h:82
static void log_warning(Args &&... args)
Definition Logging.h:96
What a LOAD_MAP carries besides the map: for a world the primary generated from OpenDRIVE (generate_o...
Definition commands.h:51
Outcome of a routed request.
Definition router.h:37