CARLA
Loading...
Searching...
No Matches
test_multigpu_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
7#include "test.h"
8
9#include <carla/Buffer.h>
17
18#include <boost/asio/io_context.hpp>
19#include <boost/asio/executor_work_guard.hpp>
20#include <boost/asio/ip/tcp.hpp>
21#include <boost/asio/read.hpp>
22#include <boost/asio/write.hpp>
23
24#include <chrono>
25#include <cstring>
26#include <future>
27#include <memory>
28#include <mutex>
29#include <optional>
30#include <set>
31#include <string>
32#include <string_view>
33#include <thread>
34#include <utility>
35#include <vector>
36
37// Exercises Router's session-selection and promise-fulfillment logic without
38// a live secondary process: a "session" here is a carla::multigpu::Primary
39// built against an unconnected socket, so only Router's own bookkeeping is
40// under test, never Primary's Write()/ReadData().
41
42namespace {
43
44 class MultiGpuRouterTest : public ::testing::Test {
45 protected:
46 boost::asio::io_context _io_context;
47 boost::asio::executor_work_guard<boost::asio::io_context::executor_type> _work_guard{
48 boost::asio::make_work_guard(_io_context)};
49 std::thread _io_thread{[this]() { _io_context.run(); }};
50 boost::asio::ip::tcp::endpoint _dummy_endpoint{
51 boost::asio::ip::make_address("127.0.0.1"), 0};
52 carla::multigpu::Listener _listener{_io_context, _dummy_endpoint};
53
54 std::shared_ptr<carla::multigpu::Primary> MakeFakeSession() {
55 return std::make_shared<carla::multigpu::Primary>(
56 _io_context, carla::time_duration::seconds(1u), _listener);
57 }
58
59 void TearDown() override {
60 _work_guard.reset();
61 _io_context.stop();
62 _io_thread.join();
63 }
64 };
65
66 bool IsRejected(std::future<carla::multigpu::SessionInfo> &future) {
67 return future.get().session == nullptr;
68 }
69
70 bool WaitUntilPending(
72 const std::shared_ptr<carla::multigpu::Primary> &session) {
73 const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
74 while (std::chrono::steady_clock::now() < deadline) {
75 if (router.TestHasPendingRequest(session)) {
76 return true;
77 }
78 std::this_thread::sleep_for(std::chrono::milliseconds(1));
79 }
80 return false;
81 }
82
83 carla::Buffer MakeBuffer(std::string_view text) {
84 return carla::Buffer(reinterpret_cast<const unsigned char *>(text.data()), text.size());
85 }
86
87 std::string ToString(const carla::Buffer &buffer) {
88 return std::string(reinterpret_cast<const char *>(buffer.data()), buffer.size());
89 }
90
91 carla::Buffer MakeTokenReply(carla::streaming::detail::stream_id_type stream_id, uint16_t port) {
93 data.stream_id = stream_id;
94 data.port = port;
97 data.address.v4 = boost::asio::ip::make_address_v4("127.0.0.1").to_bytes();
98 return carla::Buffer(reinterpret_cast<const unsigned char *>(&data), sizeof(data));
99 }
100
101 carla::Buffer MakeBoolReply(bool value) {
102 return carla::Buffer(reinterpret_cast<const unsigned char *>(&value), sizeof(value));
103 }
104
105 /// Routes @a sensor_id (actor @a actor) through GetToken(); round-robin
106 /// must pick @a session.
107 bool RouteSensorTo(
110 const std::shared_ptr<carla::multigpu::Primary> &session,
113 uint16_t port) {
114 auto routed = std::async(std::launch::async, [&commander, sensor_id, actor]() {
115 return commander.GetToken(sensor_id, actor);
116 });
117 if (!WaitUntilPending(router, session)) {
118 return false;
119 }
120 router.TestHandleResponse(session, MakeTokenReply(sensor_id, port));
121 return (routed.wait_for(std::chrono::seconds(2)) == std::future_status::ready) && routed.get().has_value();
122 }
123
124 /// Port of the cached token of @a sensor_id; fails the test on a round trip.
125 std::optional<uint16_t> CachedPort(
128 carla::multigpu::stream_id sensor_id) {
129 auto cached = std::async(std::launch::async, [&commander, sensor_id]() {
130 return commander.GetToken(sensor_id, 0u);
131 });
132 if (cached.wait_for(std::chrono::milliseconds(200)) != std::future_status::ready) {
133 router.Stop();
134 static_cast<void>(cached.get());
135 return std::nullopt;
136 }
137 const auto token = cached.get();
138 return token ? std::optional<uint16_t>(token->get_port()) : std::nullopt;
139 }
140
141 struct SentRequest {
142 const carla::multigpu::Primary *session;
144 std::vector<unsigned char> payload;
145 };
146
147 /// Records every request the router sends to one secondary.
148 class RequestLog {
149 public:
150 explicit RequestLog(carla::multigpu::Router &router) {
151 router.TestSetRequestObserver([state = _state](
152 const carla::multigpu::Primary *session,
154 const carla::BufferView &payload) {
155 std::scoped_lock<std::mutex> lock(state->mutex);
156 state->sent.push_back(SentRequest{
157 session, command, std::vector<unsigned char>(payload.data(), payload.data() + payload.size())});
158 });
159 }
160
161 std::vector<SentRequest> Take() {
162 std::scoped_lock<std::mutex> lock(_state->mutex);
163 return std::exchange(_state->sent, {});
164 }
165
166 private:
167 struct State {
168 std::mutex mutex;
169 std::vector<SentRequest> sent;
170 };
171 std::shared_ptr<State> _state = std::make_shared<State>();
172 };
173
174 ::testing::AssertionResult IsGetToken(
175 const SentRequest &sent,
176 const std::shared_ptr<carla::multigpu::Primary> &session,
179 if (sent.session != session.get()) {
180 return ::testing::AssertionFailure() << "sent to another session";
181 }
183 return ::testing::AssertionFailure() << "command " << static_cast<uint32_t>(sent.command);
184 }
186 if (sent.payload.size() != sizeof(request)) {
187 return ::testing::AssertionFailure() << "payload of " << sent.payload.size() << " bytes";
188 }
189 std::memcpy(&request, sent.payload.data(), sizeof(request));
190 if ((request.stream_id != sensor_id) || (request.actor_id != actor)) {
191 return ::testing::AssertionFailure() << "stream " << request.stream_id << ", actor " << request.actor_id;
192 }
193 return ::testing::AssertionSuccess();
194 }
195
196 ::testing::AssertionResult IsEnableRos(
197 const SentRequest &sent,
198 const std::shared_ptr<carla::multigpu::Primary> &session,
199 carla::multigpu::stream_id sensor_id) {
200 if (sent.session != session.get()) {
201 return ::testing::AssertionFailure() << "sent to another session";
202 }
204 return ::testing::AssertionFailure() << "command " << static_cast<uint32_t>(sent.command);
205 }
206 carla::multigpu::stream_id payload_id = 0u;
207 if (sent.payload.size() != sizeof(payload_id)) {
208 return ::testing::AssertionFailure() << "payload of " << sent.payload.size() << " bytes";
209 }
210 std::memcpy(&payload_id, sent.payload.data(), sizeof(payload_id));
211 if (payload_id != sensor_id) {
212 return ::testing::AssertionFailure() << "stream " << payload_id;
213 }
214 return ::testing::AssertionSuccess();
215 }
216
217 /// Answers the next request pending on @a session with @a reply.
218 bool Answer(
220 const std::shared_ptr<carla::multigpu::Primary> &session,
221 carla::Buffer reply) {
222 if (!WaitUntilPending(router, session)) {
223 return false;
224 }
225 router.TestHandleResponse(session, std::move(reply));
226 return true;
227 }
228
229 /// Routes sensor 42 (actor 9) to @a session through GetToken().
230 bool RouteSensor(
233 const std::shared_ptr<carla::multigpu::Primary> &session,
234 uint16_t port) {
235 auto routed = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
236 if (!WaitUntilPending(router, session)) {
237 return false;
238 }
239 router.TestHandleResponse(session, MakeTokenReply(42u, port));
240 return (routed.wait_for(std::chrono::seconds(2)) == std::future_status::ready) && routed.get().has_value();
241 }
242
243} // namespace
244
245TEST_F(MultiGpuRouterTest, write_to_next_with_no_sessions_rejects_instead_of_hanging) {
247 router.AsyncRun(1u);
248
250
251 ASSERT_EQ(future.wait_for(std::chrono::seconds(2)), std::future_status::ready);
252 // Specifically a rejection value (Router::RejectPromise), not
253 // std::future_error(broken_promise): the promise here is never stored
254 // anywhere, so if this rejection path were removed the local promise would
255 // be destroyed unset and get() would throw instead.
256 EXPECT_TRUE(IsRejected(future));
257}
258
259TEST_F(MultiGpuRouterTest, write_to_one_with_expired_session_rejects_instead_of_hanging) {
261 router.AsyncRun(1u);
262
263 std::weak_ptr<carla::multigpu::Primary> expired_session;
264 {
265 auto session = MakeFakeSession();
266 expired_session = session;
267 }
268 ASSERT_TRUE(expired_session.expired());
269
270 auto future = router.WriteToOne(
272
273 ASSERT_EQ(future.wait_for(std::chrono::seconds(2)), std::future_status::ready);
274 EXPECT_TRUE(IsRejected(future));
275}
276
277TEST_F(MultiGpuRouterTest, write_to_one_with_disconnected_but_still_alive_session_rejects) {
279 router.AsyncRun(1u);
280 auto session = MakeFakeSession();
281 router.TestConnectSession(session);
282 router.TestDisconnectSession(session);
283 // `session` is still a live shared_ptr here (held by this test), even
284 // though the Router no longer considers it connected. This reproduces the
285 // window production can hit when a Primary's socket has already closed
286 // (DisconnectSession ran) but the object itself is kept alive a little
287 // longer by another in-flight reference: locking the weak_ptr alone would
288 // still succeed, so WriteToOne must also check _sessions membership.
289 auto future = router.WriteToOne(
291
292 ASSERT_EQ(future.wait_for(std::chrono::seconds(2)), std::future_status::ready);
293 EXPECT_TRUE(IsRejected(future));
294}
295
296TEST_F(MultiGpuRouterTest, write_to_next_with_connected_session_stays_pending) {
298 router.AsyncRun(1u);
299 auto session = MakeFakeSession();
300 router.TestConnectSession(session);
301
303
304 EXPECT_TRUE(router.TestHasPendingRequest(session));
305 // A real, still-connected session was picked: the request is dispatched
306 // and the promise is left pending (no response will ever arrive in this
307 // test, since nothing is listening on the other end). This must NOT be
308 // rejected immediately by the same code path that handles a missing or
309 // expired session.
310 EXPECT_EQ(future.wait_for(std::chrono::milliseconds(50)), std::future_status::timeout);
311}
312
313TEST_F(MultiGpuRouterTest, disconnect_rejects_pending_promise_for_that_session) {
315 router.AsyncRun(1u);
316 auto session = MakeFakeSession();
317 router.TestConnectSession(session);
318
320 ASSERT_EQ(future.wait_for(std::chrono::milliseconds(50)), std::future_status::timeout);
321
322 router.TestDisconnectSession(session);
323
324 ASSERT_EQ(future.wait_for(std::chrono::seconds(2)), std::future_status::ready);
325 EXPECT_TRUE(IsRejected(future));
326 EXPECT_FALSE(router.TestHasPendingRequest(session));
327}
328
329TEST_F(MultiGpuRouterTest, response_after_rejection_is_dropped) {
331 router.AsyncRun(1u);
332 auto session = MakeFakeSession();
333 router.TestConnectSession(session);
334
336 router.TestDisconnectSession(session);
337 ASSERT_EQ(future.wait_for(std::chrono::seconds(2)), std::future_status::ready);
338
339 // A reply that was already on the wire when the session was dropped must
340 // not touch the promise that was just rejected.
341 EXPECT_NO_THROW(router.TestHandleResponse(session, MakeBuffer("late reply")));
342 EXPECT_TRUE(IsRejected(future));
343 EXPECT_FALSE(router.TestHasPendingRequest(session));
344}
345
346TEST_F(MultiGpuRouterTest, duplicate_response_does_not_resolve_a_promise_twice) {
348 router.AsyncRun(1u);
349 auto session = MakeFakeSession();
350 router.TestConnectSession(session);
351
353 router.TestHandleResponse(session, MakeBuffer("first"));
354 EXPECT_FALSE(router.TestHasPendingRequest(session));
355
356 // std::promise::set_value() throws if called twice, so this only passes if
357 // the first response released the slot.
358 EXPECT_NO_THROW(router.TestHandleResponse(session, MakeBuffer("second")));
359 EXPECT_NO_THROW(router.TestDisconnectSession(session));
360
361 ASSERT_EQ(future.wait_for(std::chrono::seconds(2)), std::future_status::ready);
362 auto result = future.get();
363 EXPECT_EQ(result.session, session);
364 EXPECT_EQ(ToString(result.buffer), "first");
365}
366
367TEST_F(MultiGpuRouterTest, stop_rejects_every_pending_request) {
369 router.AsyncRun(1u);
370 auto session_a = MakeFakeSession();
371 auto session_b = MakeFakeSession();
372 router.TestConnectSession(session_a);
373 router.TestConnectSession(session_b);
374
377 ASSERT_TRUE(router.TestHasPendingRequest(session_a));
378 ASSERT_TRUE(router.TestHasPendingRequest(session_b));
379
380 router.Stop();
381
382 ASSERT_EQ(future_a.wait_for(std::chrono::seconds(2)), std::future_status::ready);
383 ASSERT_EQ(future_b.wait_for(std::chrono::seconds(2)), std::future_status::ready);
384 EXPECT_TRUE(IsRejected(future_a));
385 EXPECT_TRUE(IsRejected(future_b));
386 EXPECT_FALSE(router.HasClientsConnected());
387 // ~Router() calls Stop() again; a second call must be harmless.
388 EXPECT_NO_THROW(router.Stop());
389}
390
391TEST_F(MultiGpuRouterTest, episode_ready_marker_with_no_pending_promise_triggers_resync_callback) {
393 router.AsyncRun(1u);
394
395 int callback_count = 0;
396 router.SetNewConnectionCallback([&callback_count]() { ++callback_count; });
397
398 auto session = MakeFakeSession();
399 router.TestConnectSession(session);
400 callback_count = 0; // TestConnectSession() itself already fired the callback once.
401
402 carla::Buffer marker(
403 reinterpret_cast<const unsigned char *>(carla::multigpu::kEpisodeReadyMarker.data()),
405 router.TestHandleResponse(session, std::move(marker));
406
407 EXPECT_EQ(callback_count, 1);
408}
409
410TEST_F(MultiGpuRouterTest, session_connecting_after_load_map_loads_the_remembered_map_before_resync) {
412 RequestLog log(router);
413 int callback_count = 0;
414 router.SetNewConnectionCallback([&callback_count]() { ++callback_count; });
415
416 const auto first_load = router.WriteLoadMap("Town03");
417 auto session = MakeFakeSession();
418 router.TestConnectSession(session);
419
420 const auto sent = log.Take();
421 ASSERT_EQ(sent.size(), 1u);
422 EXPECT_EQ(sent[0].session, session.get());
423 EXPECT_EQ(sent[0].command, carla::multigpu::MultiGPUCommand::LOAD_MAP);
424 const auto request = carla::multigpu::ParseLoadMapPayload(
425 std::string_view(reinterpret_cast<const char *>(sent[0].payload.data()), sent[0].payload.size()));
426 EXPECT_EQ(request.map, "Town03");
427 EXPECT_GT(request.load_id, first_load);
428 EXPECT_TRUE(router.IsAnySecondaryLoading());
429 EXPECT_EQ(callback_count, 0);
430
431 const std::string ready = carla::multigpu::MakeEpisodeReadyMessage(request.load_id);
432 router.TestHandleResponse(session, MakeBuffer(ready));
433 EXPECT_FALSE(router.IsAnySecondaryLoading());
434 EXPECT_EQ(callback_count, 1);
435}
436
437TEST_F(MultiGpuRouterTest, session_connecting_without_a_remembered_map_gets_no_load_map) {
439 RequestLog log(router);
440 int callback_count = 0;
441 router.SetNewConnectionCallback([&callback_count]() { ++callback_count; });
442
443 router.TestConnectSession(MakeFakeSession());
444
445 EXPECT_TRUE(log.Take().empty());
446 EXPECT_FALSE(router.IsAnySecondaryLoading());
447 EXPECT_EQ(callback_count, 1);
448}
449
450TEST_F(MultiGpuRouterTest, session_connected_before_load_map_keeps_the_broadcast_behaviour) {
452 RequestLog log(router);
453 int callback_count = 0;
454 router.SetNewConnectionCallback([&callback_count]() { ++callback_count; });
455
456 auto session = MakeFakeSession();
457 router.TestConnectSession(session);
458 EXPECT_EQ(callback_count, 1);
459
460 const auto load_id = router.WriteLoadMap("Town05");
461 const auto sent = log.Take();
462 ASSERT_EQ(sent.size(), 1u);
463 EXPECT_EQ(sent[0].session, session.get());
464 EXPECT_EQ(sent[0].command, carla::multigpu::MultiGPUCommand::LOAD_MAP);
465 EXPECT_TRUE(router.IsAnySecondaryLoading());
466 EXPECT_EQ(callback_count, 1);
467
468 router.TestHandleResponse(session, MakeBuffer(carla::multigpu::MakeEpisodeReadyMessage(load_id)));
469 EXPECT_FALSE(router.IsAnySecondaryLoading());
470 EXPECT_EQ(callback_count, 2);
471}
472
473TEST_F(MultiGpuRouterTest, stale_ready_of_an_older_load_does_not_finish_a_late_secondary_load) {
475 const auto stale_load = router.WriteLoadMap("Town01");
476 router.WriteLoadMap("Town03");
477 auto session = MakeFakeSession();
478 router.TestConnectSession(session);
479 ASSERT_TRUE(router.IsAnySecondaryLoading());
480
481 router.TestHandleResponse(session, MakeBuffer(carla::multigpu::MakeEpisodeReadyMessage(stale_load)));
482
483 EXPECT_TRUE(router.IsAnySecondaryLoading());
484}
485
486TEST_F(MultiGpuRouterTest, unsolicited_non_marker_data_does_not_trigger_resync_callback) {
488 router.AsyncRun(1u);
489
490 int callback_count = 0;
491 router.SetNewConnectionCallback([&callback_count]() { ++callback_count; });
492
493 auto session = MakeFakeSession();
494 router.TestConnectSession(session);
495 callback_count = 0;
496
497 // Anything that is not the exact episode-ready marker must not be
498 // misread as that signal, even though it also arrives with no pending
499 // promise (e.g. a protocol desync or a stray write from a misbehaving
500 // secondary).
501 std::string garbage("not the marker");
502 carla::Buffer buffer(
503 reinterpret_cast<const unsigned char *>(garbage.data()), garbage.size());
504 router.TestHandleResponse(session, std::move(buffer));
505
506 EXPECT_EQ(callback_count, 0);
507}
508
509TEST_F(MultiGpuRouterTest, episode_ready_marker_is_not_misdelivered_to_a_pending_promise) {
511 router.AsyncRun(1u);
512
513 int callback_count = 0;
514 router.SetNewConnectionCallback([&callback_count]() { ++callback_count; });
515
516 auto session = MakeFakeSession();
517 router.TestConnectSession(session);
518 callback_count = 0;
519
520 // A request is already pending on this exact session when the marker
521 // arrives -- e.g. a GetToken round trip in flight at the moment this
522 // secondary's own level reload completes. The marker must still be
523 // recognized as the resync signal, and must NOT be handed to this
524 // request as if it were its (garbage) response data.
526 ASSERT_EQ(future.wait_for(std::chrono::milliseconds(50)), std::future_status::timeout);
527
528 carla::Buffer marker(
529 reinterpret_cast<const unsigned char *>(carla::multigpu::kEpisodeReadyMarker.data()),
531 router.TestHandleResponse(session, std::move(marker));
532
533 EXPECT_EQ(callback_count, 1);
534 EXPECT_EQ(future.wait_for(std::chrono::milliseconds(50)), std::future_status::timeout);
535
536 // The request itself is still healthy: its real reply, once it arrives,
537 // resolves normally.
538 router.TestHandleResponse(session, MakeBuffer("token reply"));
539 ASSERT_EQ(future.wait_for(std::chrono::seconds(2)), std::future_status::ready);
540 auto result = future.get();
541 EXPECT_EQ(result.session, session);
542 EXPECT_EQ(ToString(result.buffer), "token reply");
543}
544
545TEST_F(MultiGpuRouterTest, response_with_pending_promise_does_not_trigger_resync_callback) {
547 router.AsyncRun(1u);
548
549 int callback_count = 0;
550 router.SetNewConnectionCallback([&callback_count]() { ++callback_count; });
551
552 auto session = MakeFakeSession();
553 router.TestConnectSession(session);
554 callback_count = 0;
555
557 ASSERT_EQ(future.wait_for(std::chrono::milliseconds(50)), std::future_status::timeout);
558
559 // Real response data, distinct from the marker (see the priority test
560 // above for the marker-with-pending-promise case).
561 router.TestHandleResponse(session, MakeBuffer("token reply"));
562
563 ASSERT_EQ(future.wait_for(std::chrono::seconds(2)), std::future_status::ready);
564 auto result = future.get();
565 EXPECT_EQ(result.session, session);
566 EXPECT_EQ(ToString(result.buffer), "token reply");
567 EXPECT_EQ(callback_count, 0);
568}
569
570TEST_F(MultiGpuRouterTest, overlapping_request_to_same_session_is_rejected_not_orphaned) {
572 router.AsyncRun(1u);
573 auto session = MakeFakeSession();
574 router.TestConnectSession(session);
575
576 // Only one session is connected, so both requests round-robin to it.
577 // There is no per-request correlation id in the wire protocol (a session
578 // gets exactly one _promises slot, not one per request), so the second
579 // request must be refused explicitly rather than silently overwriting --
580 // and losing -- the promise the first one is still waiting on.
583
584 ASSERT_EQ(second.wait_for(std::chrono::seconds(2)), std::future_status::ready);
585 EXPECT_TRUE(IsRejected(second));
586
587 // The first request must be exactly as pending as before the second one
588 // arrived: not silently resolved, not silently lost.
589 EXPECT_EQ(first.wait_for(std::chrono::milliseconds(50)), std::future_status::timeout);
590
591 router.TestHandleResponse(session, MakeBuffer("first reply"));
592 ASSERT_EQ(first.wait_for(std::chrono::seconds(2)), std::future_status::ready);
593 auto result = first.get();
594 EXPECT_EQ(result.session, session);
595 EXPECT_EQ(ToString(result.buffer), "first reply");
596}
597
598TEST_F(MultiGpuRouterTest, write_to_next_round_robins_across_connected_sessions) {
600 router.AsyncRun(1u);
601 auto session_a = MakeFakeSession();
602 auto session_b = MakeFakeSession();
603 auto session_c = MakeFakeSession();
604 router.TestConnectSession(session_a);
605 router.TestConnectSession(session_b);
606 router.TestConnectSession(session_c);
607
611
612 // None of the three responses will ever arrive (nothing is listening on
613 // the other end of any of these sessions), so all three requests are
614 // still pending right now -- but round-robin must have routed each of
615 // them to a *different* session (connection order A, B, C). Disconnecting
616 // one session at a time and checking which future flips from pending to
617 // rejected is the oracle for "which request went where": if two requests
618 // had collided on the same session, disconnecting that session would flip
619 // two futures instead of one, and TestConnectSession/TestDisconnectSession
620 // are the only way to observe routing without a live secondary.
621 ASSERT_EQ(future_a.wait_for(std::chrono::milliseconds(50)), std::future_status::timeout);
622 ASSERT_EQ(future_b.wait_for(std::chrono::milliseconds(50)), std::future_status::timeout);
623 ASSERT_EQ(future_c.wait_for(std::chrono::milliseconds(50)), std::future_status::timeout);
624
625 router.TestDisconnectSession(session_b);
626 ASSERT_EQ(future_b.wait_for(std::chrono::seconds(2)), std::future_status::ready);
627 EXPECT_TRUE(IsRejected(future_b));
628 EXPECT_EQ(future_a.wait_for(std::chrono::milliseconds(20)), std::future_status::timeout);
629 EXPECT_EQ(future_c.wait_for(std::chrono::milliseconds(20)), std::future_status::timeout);
630
631 router.TestDisconnectSession(session_a);
632 ASSERT_EQ(future_a.wait_for(std::chrono::seconds(2)), std::future_status::ready);
633 EXPECT_TRUE(IsRejected(future_a));
634 EXPECT_EQ(future_c.wait_for(std::chrono::milliseconds(20)), std::future_status::timeout);
635
636 router.TestDisconnectSession(session_c);
637 ASSERT_EQ(future_c.wait_for(std::chrono::seconds(2)), std::future_status::ready);
638 EXPECT_TRUE(IsRejected(future_c));
639}
640
641TEST_F(MultiGpuRouterTest, get_token_with_no_secondary_returns_nullopt_and_caches_nothing) {
642 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
643 router->AsyncRun(1u);
644 carla::multigpu::PrimaryCommands commander(router);
645
646 std::optional<carla::multigpu::token_type> token;
647 ASSERT_NO_THROW(token = commander.GetToken(42u, 42u));
648 EXPECT_FALSE(token.has_value());
649
650 // Had the failure been cached, this would return without a round trip.
651 auto session = MakeFakeSession();
652 router->TestConnectSession(session);
653 auto retried = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 42u); });
654 ASSERT_TRUE(WaitUntilPending(*router, session));
655 router->TestHandleResponse(session, MakeTokenReply(42u, 2001u));
656 ASSERT_EQ(retried.wait_for(std::chrono::seconds(2)), std::future_status::ready);
657 const auto routed = retried.get();
658 ASSERT_TRUE(routed.has_value());
659 EXPECT_EQ(routed->get_port(), 2001u);
660}
661
662TEST_F(MultiGpuRouterTest, get_token_with_malformed_reply_returns_nullopt_and_caches_nothing) {
663 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
664 router->AsyncRun(1u);
665 auto session = MakeFakeSession();
666 router->TestConnectSession(session);
667 carla::multigpu::PrimaryCommands commander(router);
668
669 auto malformed = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 42u); });
670 ASSERT_TRUE(WaitUntilPending(*router, session));
671 // Shorter than token_data: reading it as a token would overrun the buffer.
672 router->TestHandleResponse(session, MakeBuffer("short"));
673 ASSERT_EQ(malformed.wait_for(std::chrono::seconds(2)), std::future_status::ready);
674 EXPECT_FALSE(malformed.get().has_value());
675
676 auto retried = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 42u); });
677 ASSERT_TRUE(WaitUntilPending(*router, session));
678 router->TestHandleResponse(session, MakeTokenReply(42u, 2003u));
679 ASSERT_EQ(retried.wait_for(std::chrono::seconds(2)), std::future_status::ready);
680 const auto token = retried.get();
681 ASSERT_TRUE(token.has_value());
682 EXPECT_EQ(token->get_port(), 2003u);
683}
684
685TEST_F(MultiGpuRouterTest, get_token_not_ready_reply_returns_nullopt_and_caches_nothing) {
686 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
687 router->AsyncRun(1u);
688 auto session = MakeFakeSession();
689 router->TestConnectSession(session);
690 carla::multigpu::PrimaryCommands commander(router);
691
692 auto not_ready = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
693 ASSERT_TRUE(WaitUntilPending(*router, session));
694 router->TestHandleResponse(session, MakeBuffer(carla::multigpu::kTokenNotReadyMarker));
695 ASSERT_EQ(not_ready.wait_for(std::chrono::seconds(2)), std::future_status::ready);
696 EXPECT_FALSE(not_ready.get().has_value());
697
698 auto retried = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
699 ASSERT_TRUE(WaitUntilPending(*router, session));
700 router->TestHandleResponse(session, MakeTokenReply(42u, 2005u));
701 ASSERT_EQ(retried.wait_for(std::chrono::seconds(2)), std::future_status::ready);
702 const auto token = retried.get();
703 ASSERT_TRUE(token.has_value());
704 EXPECT_EQ(token->get_stream_id(), 42u);
705}
706
707TEST_F(MultiGpuRouterTest, get_token_always_carries_the_primary_stream_id) {
708 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
709 router->AsyncRun(1u);
710 auto session = MakeFakeSession();
711 router->TestConnectSession(session);
712 carla::multigpu::PrimaryCommands commander(router);
713
714 auto pending = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
715 ASSERT_TRUE(WaitUntilPending(*router, session));
716 // A secondary-local stream id must never reach the client.
717 router->TestHandleResponse(session, MakeTokenReply(5u, 2006u));
718 ASSERT_EQ(pending.wait_for(std::chrono::seconds(2)), std::future_status::ready);
719 const auto token = pending.get();
720 ASSERT_TRUE(token.has_value());
721 EXPECT_EQ(token->get_stream_id(), 42u);
722 EXPECT_EQ(token->get_port(), 2006u);
723}
724
725TEST_F(MultiGpuRouterTest, cached_token_of_a_disconnected_secondary_is_routed_again) {
726 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
727 router->AsyncRun(1u);
728 auto session_a = MakeFakeSession();
729 auto session_b = MakeFakeSession();
730 router->TestConnectSession(session_a);
731 router->TestConnectSession(session_b);
732 carla::multigpu::PrimaryCommands commander(router);
733
734 auto first = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
735 ASSERT_TRUE(WaitUntilPending(*router, session_a));
736 router->TestHandleResponse(session_a, MakeTokenReply(42u, 2007u));
737 ASSERT_EQ(first.wait_for(std::chrono::seconds(2)), std::future_status::ready);
738 ASSERT_TRUE(first.get().has_value());
739
740 router->TestDisconnectSession(session_a);
741 auto rerouted = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
742 ASSERT_TRUE(WaitUntilPending(*router, session_b));
743 router->TestHandleResponse(session_b, MakeTokenReply(42u, 2008u));
744 ASSERT_EQ(rerouted.wait_for(std::chrono::seconds(2)), std::future_status::ready);
745 const auto token = rerouted.get();
746 ASSERT_TRUE(token.has_value());
747 EXPECT_EQ(token->get_port(), 2008u);
748}
749
750TEST(MultiGpuCommands, token_not_ready_marker_is_distinct_from_other_replies) {
753}
754
755TEST(MultiGpuCommands, get_token_request_starts_with_the_stream_id) {
756 const carla::multigpu::GetTokenRequest request{42u, 9u};
757 carla::streaming::detail::stream_id_type leading_stream_id = 0u;
758 std::memcpy(&leading_stream_id, &request, sizeof(leading_stream_id));
759 EXPECT_EQ(leading_stream_id, 42u);
760}
761
762TEST_F(MultiGpuRouterTest, concurrent_get_token_calls_are_serialized_not_rejected) {
763 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
764 router->AsyncRun(1u);
765 auto session = MakeFakeSession();
766 router->TestConnectSession(session);
767 carla::multigpu::PrimaryCommands commander(router);
768
769 // Both requests target the single secondary. The router refuses a second
770 // in-flight request on one session, so PrimaryCommands must queue the
771 // second caller behind the first rather than let it be rejected.
772 auto first = std::async(std::launch::async, [&commander]() { return commander.GetToken(1u, 1u); });
773 auto second = std::async(std::launch::async, [&commander]() { return commander.GetToken(2u, 2u); });
774
775 ASSERT_TRUE(WaitUntilPending(*router, session));
776 // Gives the other caller time to reach the router, so an unserialized
777 // implementation would have its request refused right here.
778 std::this_thread::sleep_for(std::chrono::milliseconds(50));
779 router->TestHandleResponse(session, MakeTokenReply(100u, 3001u));
780
781 ASSERT_TRUE(WaitUntilPending(*router, session));
782 router->TestHandleResponse(session, MakeTokenReply(101u, 3002u));
783
784 ASSERT_EQ(first.wait_for(std::chrono::seconds(2)), std::future_status::ready);
785 ASSERT_EQ(second.wait_for(std::chrono::seconds(2)), std::future_status::ready);
786 const auto first_token = first.get();
787 const auto second_token = second.get();
788 ASSERT_TRUE(first_token.has_value());
789 ASSERT_TRUE(second_token.has_value());
790 const std::set<uint16_t> ports{first_token->get_port(), second_token->get_port()};
791 EXPECT_EQ(ports, (std::set<uint16_t>{3001u, 3002u}));
792}
793
794TEST_F(MultiGpuRouterTest, get_token_fails_cleanly_when_secondary_disconnects_mid_request) {
795 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
796 router->AsyncRun(1u);
797 auto session = MakeFakeSession();
798 router->TestConnectSession(session);
799 carla::multigpu::PrimaryCommands commander(router);
800
801 auto pending = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 42u); });
802 ASSERT_TRUE(WaitUntilPending(*router, session));
803
804 // The secondary dies with the GET_TOKEN in flight. In CarlaUnreal an
805 // exception escaping GetToken() here crashes the primary (see
806 // Router::RejectPromise), so the failure must come back as a value.
807 router->TestDisconnectSession(session);
808
809 ASSERT_EQ(pending.wait_for(std::chrono::seconds(2)), std::future_status::ready);
810 std::optional<carla::multigpu::token_type> token;
811 ASSERT_NO_THROW(token = pending.get());
812 EXPECT_FALSE(token.has_value());
813
814 // The primary side is still usable: a secondary that connects afterwards
815 // serves the next request.
816 auto replacement = MakeFakeSession();
817 router->TestConnectSession(replacement);
818 auto next = std::async(std::launch::async, [&commander]() { return commander.GetToken(43u, 43u); });
819 ASSERT_TRUE(WaitUntilPending(*router, replacement));
820 router->TestHandleResponse(replacement, MakeTokenReply(43u, 2004u));
821 ASSERT_EQ(next.wait_for(std::chrono::seconds(2)), std::future_status::ready);
822 const auto next_token = next.get();
823 ASSERT_TRUE(next_token.has_value());
824 EXPECT_EQ(next_token->get_stream_id(), 43u);
825}
826
827TEST_F(MultiGpuRouterTest, get_token_after_failed_request_routes_to_remaining_secondary) {
828 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
829 router->AsyncRun(1u);
830 auto session_a = MakeFakeSession();
831 auto session_b = MakeFakeSession();
832 router->TestConnectSession(session_a);
833 router->TestConnectSession(session_b);
834 carla::multigpu::PrimaryCommands commander(router);
835
836 auto failed = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 42u); });
837 ASSERT_TRUE(WaitUntilPending(*router, session_a));
838 router->TestDisconnectSession(session_a);
839 ASSERT_EQ(failed.wait_for(std::chrono::seconds(2)), std::future_status::ready);
840 ASSERT_FALSE(failed.get().has_value());
841
842 // The failure must not have been cached: retrying the same sensor routes
843 // to the surviving secondary and resolves with its reply.
844 auto retried = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 42u); });
845 ASSERT_TRUE(WaitUntilPending(*router, session_b));
846 router->TestHandleResponse(session_b, MakeTokenReply(42u, 2002u));
847 ASSERT_EQ(retried.wait_for(std::chrono::seconds(2)), std::future_status::ready);
848 const auto token = retried.get();
849 ASSERT_TRUE(token.has_value());
850 EXPECT_EQ(token->get_stream_id(), 42u);
851 EXPECT_EQ(token->get_port(), 2002u);
852
853 // Once routed, the token is served from the cache without another round
854 // trip (nothing would answer one here).
855 const auto cached = commander.GetToken(42u, 42u);
856 ASSERT_TRUE(cached.has_value());
857 EXPECT_EQ(cached->get_port(), 2002u);
858 EXPECT_FALSE(router->TestHasPendingRequest(session_b));
859}
860
861TEST_F(MultiGpuRouterTest, enable_for_ros_without_any_secondary_returns_instead_of_recursing) {
862 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
863 router->AsyncRun(1u);
864 carla::multigpu::PrimaryCommands commander(router);
865
866 // GetToken() fails immediately here, so EnableForROS() must give up rather
867 // than retry its routing recursively without bound, and must report the
868 // failure back to the caller instead of silently succeeding.
869 auto done = std::async(std::launch::async, [&commander]() { return commander.EnableForROS(7u, 7u); });
870 ASSERT_EQ(done.wait_for(std::chrono::seconds(2)), std::future_status::ready);
871 bool enabled = true;
872 ASSERT_NO_THROW(enabled = done.get());
873 EXPECT_FALSE(enabled);
874}
875
876TEST_F(MultiGpuRouterTest, enable_for_ros_refused_by_the_secondary_returns_false) {
877 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
878 router->AsyncRun(1u);
879 auto session = MakeFakeSession();
880 router->TestConnectSession(session);
881 carla::multigpu::PrimaryCommands commander(router);
882 ASSERT_TRUE(RouteSensor(*router, commander, session, 2010u));
883
884 auto refused = std::async(std::launch::async, [&commander]() { return commander.EnableForROS(42u, 9u); });
885 ASSERT_TRUE(WaitUntilPending(*router, session));
886 router->TestHandleResponse(session, MakeBoolReply(false));
887 ASSERT_EQ(refused.wait_for(std::chrono::seconds(2)), std::future_status::ready);
888 EXPECT_FALSE(refused.get());
889
890 auto accepted = std::async(std::launch::async, [&commander]() { return commander.EnableForROS(42u, 9u); });
891 ASSERT_TRUE(WaitUntilPending(*router, session));
892 router->TestHandleResponse(session, MakeBoolReply(true));
893 ASSERT_EQ(accepted.wait_for(std::chrono::seconds(2)), std::future_status::ready);
894 EXPECT_TRUE(accepted.get());
895}
896
897TEST_F(MultiGpuRouterTest, enable_for_ros_on_a_disconnected_secondary_routes_the_sensor_again) {
898 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
899 router->AsyncRun(1u);
900 auto session_a = MakeFakeSession();
901 auto session_b = MakeFakeSession();
902 router->TestConnectSession(session_a);
903 router->TestConnectSession(session_b);
904 carla::multigpu::PrimaryCommands commander(router);
905 ASSERT_TRUE(RouteSensor(*router, commander, session_a, 2011u));
906 router->TestDisconnectSession(session_a);
907
908 auto enabled = std::async(std::launch::async, [&commander]() { return commander.EnableForROS(42u, 9u); });
909 ASSERT_TRUE(WaitUntilPending(*router, session_b));
910 router->TestHandleResponse(session_b, MakeTokenReply(42u, 2012u));
911 ASSERT_TRUE(WaitUntilPending(*router, session_b));
912 router->TestHandleResponse(session_b, MakeBoolReply(true));
913 ASSERT_EQ(enabled.wait_for(std::chrono::seconds(2)), std::future_status::ready);
914 EXPECT_TRUE(enabled.get());
915
916 const auto token = commander.GetToken(42u, 9u);
917 ASSERT_TRUE(token.has_value());
918 EXPECT_EQ(token->get_port(), 2012u);
919}
920
921TEST_F(MultiGpuRouterTest, ros_queries_on_a_disconnected_secondary_return_without_a_round_trip) {
922 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
923 router->AsyncRun(1u);
924 auto session_a = MakeFakeSession();
925 auto session_b = MakeFakeSession();
926 router->TestConnectSession(session_a);
927 router->TestConnectSession(session_b);
928 carla::multigpu::PrimaryCommands commander(router);
929 ASSERT_TRUE(RouteSensor(*router, commander, session_a, 2013u));
930 router->TestDisconnectSession(session_a);
931
932 EXPECT_FALSE(commander.IsEnabledForROS(42u));
933 commander.DisableForROS(42u);
934 EXPECT_FALSE(router->TestHasPendingRequest(session_b));
935
936 // The next request routes the sensor again.
937 auto rerouted = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
938 ASSERT_TRUE(WaitUntilPending(*router, session_b));
939 router->TestHandleResponse(session_b, MakeTokenReply(42u, 2014u));
940 ASSERT_EQ(rerouted.wait_for(std::chrono::seconds(2)), std::future_status::ready);
941 const auto token = rerouted.get();
942 ASSERT_TRUE(token.has_value());
943 EXPECT_EQ(token->get_port(), 2014u);
944}
945
946TEST_F(MultiGpuRouterTest, load_map_forgets_cached_tokens) {
947 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
948 router->AsyncRun(1u);
949 auto session = MakeFakeSession();
950 router->TestConnectSession(session);
951 carla::multigpu::PrimaryCommands commander(router);
952 ASSERT_TRUE(RouteSensor(*router, commander, session, 2015u));
953
954 commander.SendLoadMap("/Game/Carla/Maps/Town10HD_Opt");
955 auto routed = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
956 ASSERT_TRUE(WaitUntilPending(*router, session));
957 router->TestHandleResponse(session, MakeTokenReply(42u, 2016u));
958 ASSERT_EQ(routed.wait_for(std::chrono::seconds(2)), std::future_status::ready);
959 const auto token = routed.get();
960 ASSERT_TRUE(token.has_value());
961 EXPECT_EQ(token->get_port(), 2016u);
962}
963
964TEST_F(MultiGpuRouterTest, new_secondary_takes_over_the_sensors_of_a_lost_one) {
965 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
966 router->AsyncRun(1u);
967 RequestLog log(*router);
968 auto lost = MakeFakeSession();
969 router->TestConnectSession(lost);
970 carla::multigpu::PrimaryCommands commander(router);
971 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2020u));
972 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 43u, 10u, 2020u));
973 router->TestDisconnectSession(lost);
974 auto replacement = MakeFakeSession();
975 router->TestConnectSession(replacement);
976 static_cast<void>(log.Take());
977
978 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
979 // Targeted at the new session and issued one sensor at a time.
980 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(7u, 2020u)));
981 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(8u, 2021u)));
982 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
983 EXPECT_EQ(rerouting.get(), 2u);
984 const auto sent = log.Take();
985 ASSERT_EQ(sent.size(), 2u);
986 EXPECT_TRUE(IsGetToken(sent[0], replacement, 42u, 9u));
987 EXPECT_TRUE(IsGetToken(sent[1], replacement, 43u, 10u));
988
989 EXPECT_EQ(CachedPort(*router, commander, 42u), std::optional<uint16_t>(2020u));
990 EXPECT_EQ(CachedPort(*router, commander, 43u), std::optional<uint16_t>(2021u));
991
992 auto enabled = std::async(std::launch::async, [&commander]() { return commander.IsEnabledForROS(42u); });
993 ASSERT_TRUE(Answer(*router, replacement, MakeBoolReply(true)));
994 ASSERT_EQ(enabled.wait_for(std::chrono::seconds(2)), std::future_status::ready);
995 EXPECT_TRUE(enabled.get());
996
997 // The episode-ready path fires the same callback but brings no new session.
998 router->TestHandleResponse(replacement, MakeBuffer(carla::multigpu::kEpisodeReadyMarker));
999 EXPECT_EQ(commander.RerouteLostSensors(), 0u);
1000 EXPECT_FALSE(router->TestHasPendingRequest(replacement));
1001}
1002
1003TEST_F(MultiGpuRouterTest, reroute_without_lost_sensors_is_a_no_op) {
1004 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1005 router->AsyncRun(1u);
1006 auto session = MakeFakeSession();
1007 router->TestConnectSession(session);
1008 carla::multigpu::PrimaryCommands commander(router);
1009 ASSERT_TRUE(RouteSensorTo(*router, commander, session, 42u, 9u, 2022u));
1010 auto added = MakeFakeSession();
1011 router->TestConnectSession(added);
1012
1013 EXPECT_EQ(commander.RerouteLostSensors(), 0u);
1014 EXPECT_FALSE(router->TestHasPendingRequest(session));
1015 EXPECT_FALSE(router->TestHasPendingRequest(added));
1016 EXPECT_EQ(CachedPort(*router, commander, 42u), std::optional<uint16_t>(2022u));
1017}
1018
1019TEST_F(MultiGpuRouterTest, reroute_after_load_map_is_a_no_op) {
1020 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1021 router->AsyncRun(1u);
1022 auto lost = MakeFakeSession();
1023 router->TestConnectSession(lost);
1024 carla::multigpu::PrimaryCommands commander(router);
1025 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2023u));
1026 router->TestDisconnectSession(lost);
1027 auto replacement = MakeFakeSession();
1028 router->TestConnectSession(replacement);
1029
1030 commander.SendLoadMap("/Game/Carla/Maps/Town10HD_Opt");
1031
1032 EXPECT_EQ(commander.RerouteLostSensors(), 0u);
1033 EXPECT_FALSE(router->TestHasPendingRequest(replacement));
1034}
1035
1036TEST_F(MultiGpuRouterTest, sensor_refused_by_the_new_secondary_is_routed_lazily_later) {
1037 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1038 router->AsyncRun(1u);
1039 auto lost = MakeFakeSession();
1040 router->TestConnectSession(lost);
1041 carla::multigpu::PrimaryCommands commander(router);
1042 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2024u));
1043 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 43u, 10u, 2024u));
1044 router->TestDisconnectSession(lost);
1045 auto replacement = MakeFakeSession();
1046 router->TestConnectSession(replacement);
1047
1048 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1049 // Sent when the secondary cannot resolve the sensor actor to a stream.
1050 ASSERT_TRUE(Answer(*router, replacement, MakeBuffer(carla::multigpu::kTokenNotReadyMarker)));
1051 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(43u, 2024u)));
1052 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1053 EXPECT_EQ(rerouting.get(), 1u);
1054 EXPECT_EQ(CachedPort(*router, commander, 43u), std::optional<uint16_t>(2024u));
1055
1056 auto retried = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
1057 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(42u, 2025u)));
1058 ASSERT_EQ(retried.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1059 const auto token = retried.get();
1060 ASSERT_TRUE(token.has_value());
1061 EXPECT_EQ(token->get_port(), 2025u);
1062}
1063
1064TEST_F(MultiGpuRouterTest, reroute_leaves_sensors_of_live_secondaries_alone) {
1065 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1066 router->AsyncRun(1u);
1067 auto lost = MakeFakeSession();
1068 auto alive = MakeFakeSession();
1069 router->TestConnectSession(lost);
1070 router->TestConnectSession(alive);
1071 carla::multigpu::PrimaryCommands commander(router);
1072 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2026u));
1073 ASSERT_TRUE(RouteSensorTo(*router, commander, alive, 43u, 10u, 2027u));
1074 // As the engine does on the tick after these sessions connected.
1075 ASSERT_EQ(commander.RerouteLostSensors(), 0u);
1076 router->TestDisconnectSession(lost);
1077 auto replacement = MakeFakeSession();
1078 router->TestConnectSession(replacement);
1079
1080 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1081 ASSERT_TRUE(WaitUntilPending(*router, replacement));
1082 EXPECT_FALSE(router->TestHasPendingRequest(alive));
1083 router->TestHandleResponse(replacement, MakeTokenReply(42u, 2026u));
1084 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1085 EXPECT_EQ(rerouting.get(), 1u);
1086
1087 EXPECT_EQ(CachedPort(*router, commander, 42u), std::optional<uint16_t>(2026u));
1088 EXPECT_EQ(CachedPort(*router, commander, 43u), std::optional<uint16_t>(2027u));
1089 auto enabled = std::async(std::launch::async, [&commander]() { return commander.IsEnabledForROS(43u); });
1090 ASSERT_TRUE(Answer(*router, alive, MakeBoolReply(true)));
1091 ASSERT_EQ(enabled.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1092 EXPECT_TRUE(enabled.get());
1093}
1094
1095TEST_F(MultiGpuRouterTest, reroute_stops_when_the_new_secondary_disconnects_mid_way) {
1096 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1097 router->AsyncRun(1u);
1098 RequestLog log(*router);
1099 auto lost = MakeFakeSession();
1100 router->TestConnectSession(lost);
1101 carla::multigpu::PrimaryCommands commander(router);
1102 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2028u));
1103 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 43u, 10u, 2028u));
1104 router->TestDisconnectSession(lost);
1105 auto short_lived = MakeFakeSession();
1106 router->TestConnectSession(short_lived);
1107 static_cast<void>(log.Take());
1108
1109 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1110 ASSERT_TRUE(WaitUntilPending(*router, short_lived));
1111 router->TestDisconnectSession(short_lived);
1112 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1113 EXPECT_EQ(rerouting.get(), 0u);
1114 auto sent = log.Take();
1115 ASSERT_EQ(sent.size(), 1u);
1116 EXPECT_TRUE(IsGetToken(sent[0], short_lived, 42u, 9u));
1117
1118 // Both sensors stay lost, including the one in flight, so the next
1119 // secondary recovers them under their stream ids.
1120 auto replacement = MakeFakeSession();
1121 router->TestConnectSession(replacement);
1122 auto second_try = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1123 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(42u, 2029u)));
1124 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(43u, 2029u)));
1125 ASSERT_EQ(second_try.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1126 EXPECT_EQ(second_try.get(), 2u);
1127 sent = log.Take();
1128 ASSERT_EQ(sent.size(), 2u);
1129 EXPECT_TRUE(IsGetToken(sent[0], replacement, 42u, 9u));
1130 EXPECT_TRUE(IsGetToken(sent[1], replacement, 43u, 10u));
1131 EXPECT_EQ(CachedPort(*router, commander, 42u), std::optional<uint16_t>(2029u));
1132 EXPECT_EQ(CachedPort(*router, commander, 43u), std::optional<uint16_t>(2029u));
1133}
1134
1135TEST_F(MultiGpuRouterTest, first_new_secondary_takes_every_lost_sensor) {
1136 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1137 router->AsyncRun(1u);
1138 RequestLog log(*router);
1139 auto lost = MakeFakeSession();
1140 router->TestConnectSession(lost);
1141 carla::multigpu::PrimaryCommands commander(router);
1142 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2030u));
1143 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 43u, 10u, 2030u));
1144 router->TestDisconnectSession(lost);
1145 auto first = MakeFakeSession();
1146 auto second = MakeFakeSession();
1147 router->TestConnectSession(first);
1148 router->TestConnectSession(second);
1149 static_cast<void>(log.Take());
1150
1151 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1152 ASSERT_TRUE(Answer(*router, first, MakeTokenReply(42u, 2030u)));
1153 ASSERT_TRUE(Answer(*router, first, MakeTokenReply(43u, 2030u)));
1154 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1155 EXPECT_EQ(rerouting.get(), 2u);
1156 const auto sent = log.Take();
1157 ASSERT_EQ(sent.size(), 2u);
1158 EXPECT_TRUE(IsGetToken(sent[0], first, 42u, 9u));
1159 EXPECT_TRUE(IsGetToken(sent[1], first, 43u, 10u));
1160}
1161
1162TEST_F(MultiGpuRouterTest, next_new_secondary_takes_over_when_the_first_drops_mid_way) {
1163 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1164 router->AsyncRun(1u);
1165 RequestLog log(*router);
1166 auto lost = MakeFakeSession();
1167 router->TestConnectSession(lost);
1168 carla::multigpu::PrimaryCommands commander(router);
1169 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2031u));
1170 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 43u, 10u, 2031u));
1171 router->TestDisconnectSession(lost);
1172 auto first = MakeFakeSession();
1173 auto second = MakeFakeSession();
1174 router->TestConnectSession(first);
1175 router->TestConnectSession(second);
1176 static_cast<void>(log.Take());
1177
1178 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1179 ASSERT_TRUE(WaitUntilPending(*router, first));
1180 router->TestDisconnectSession(first);
1181 ASSERT_TRUE(Answer(*router, second, MakeTokenReply(42u, 2032u)));
1182 ASSERT_TRUE(Answer(*router, second, MakeTokenReply(43u, 2032u)));
1183 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1184 EXPECT_EQ(rerouting.get(), 2u);
1185 const auto sent = log.Take();
1186 ASSERT_EQ(sent.size(), 3u);
1187 EXPECT_TRUE(IsGetToken(sent[0], first, 42u, 9u));
1188 EXPECT_TRUE(IsGetToken(sent[1], second, 42u, 9u));
1189 EXPECT_TRUE(IsGetToken(sent[2], second, 43u, 10u));
1190 EXPECT_EQ(CachedPort(*router, commander, 42u), std::optional<uint16_t>(2032u));
1191}
1192
1193TEST_F(MultiGpuRouterTest, session_that_left_before_the_call_is_skipped) {
1194 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1195 router->AsyncRun(1u);
1196 RequestLog log(*router);
1197 auto lost = MakeFakeSession();
1198 router->TestConnectSession(lost);
1199 carla::multigpu::PrimaryCommands commander(router);
1200 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2033u));
1201 router->TestDisconnectSession(lost);
1202 auto gone = MakeFakeSession();
1203 router->TestConnectSession(gone);
1204 router->TestDisconnectSession(gone);
1205 static_cast<void>(log.Take());
1206
1207 EXPECT_EQ(commander.RerouteLostSensors(), 0u);
1208 EXPECT_TRUE(log.Take().empty());
1209
1210 auto gone_again = MakeFakeSession();
1211 router->TestConnectSession(gone_again);
1212 router->TestDisconnectSession(gone_again);
1213 auto replacement = MakeFakeSession();
1214 router->TestConnectSession(replacement);
1215 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1216 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(42u, 2034u)));
1217 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1218 EXPECT_EQ(rerouting.get(), 1u);
1219 const auto sent = log.Take();
1220 ASSERT_EQ(sent.size(), 1u);
1221 EXPECT_TRUE(IsGetToken(sent[0], replacement, 42u, 9u));
1222}
1223
1224TEST_F(MultiGpuRouterTest, lost_route_survives_ros_queries_and_failed_get_token) {
1225 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1226 router->AsyncRun(1u);
1227 RequestLog log(*router);
1228 auto lost = MakeFakeSession();
1229 router->TestConnectSession(lost);
1230 carla::multigpu::PrimaryCommands commander(router);
1231 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2035u));
1232 router->TestDisconnectSession(lost);
1233 static_cast<void>(log.Take());
1234
1235 EXPECT_FALSE(commander.IsEnabledForROS(42u));
1236 commander.DisableForROS(42u);
1237 // No secondary is left to answer, so this fails without a request.
1238 EXPECT_FALSE(commander.GetToken(42u, 9u).has_value());
1239 EXPECT_TRUE(log.Take().empty());
1240
1241 auto replacement = MakeFakeSession();
1242 router->TestConnectSession(replacement);
1243 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1244 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(42u, 2035u)));
1245 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1246 EXPECT_EQ(rerouting.get(), 1u);
1247 const auto sent = log.Take();
1248 ASSERT_EQ(sent.size(), 1u);
1249 EXPECT_TRUE(IsGetToken(sent[0], replacement, 42u, 9u));
1250}
1251
1252TEST_F(MultiGpuRouterTest, get_token_reroutes_a_lost_route_before_reroute_runs) {
1253 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1254 router->AsyncRun(1u);
1255 RequestLog log(*router);
1256 auto lost = MakeFakeSession();
1257 router->TestConnectSession(lost);
1258 carla::multigpu::PrimaryCommands commander(router);
1259 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2036u));
1260 router->TestDisconnectSession(lost);
1261 auto replacement = MakeFakeSession();
1262 router->TestConnectSession(replacement);
1263 static_cast<void>(log.Take());
1264
1265 auto lazy = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
1266 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(42u, 2037u)));
1267 ASSERT_EQ(lazy.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1268 const auto token = lazy.get();
1269 ASSERT_TRUE(token.has_value());
1270 EXPECT_EQ(token->get_port(), 2037u);
1271 const auto sent = log.Take();
1272 ASSERT_EQ(sent.size(), 1u);
1273 EXPECT_TRUE(IsGetToken(sent[0], replacement, 42u, 9u));
1274
1275 EXPECT_EQ(commander.RerouteLostSensors(), 0u);
1276 EXPECT_FALSE(router->TestHasPendingRequest(replacement));
1277}
1278
1279TEST_F(MultiGpuRouterTest, forgotten_sensor_is_not_rerouted) {
1280 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1281 router->AsyncRun(1u);
1282 RequestLog log(*router);
1283 auto lost = MakeFakeSession();
1284 router->TestConnectSession(lost);
1285 carla::multigpu::PrimaryCommands commander(router);
1286 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2038u));
1287 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 43u, 10u, 2038u));
1288 commander.ForgetSensor(42u);
1289 router->TestDisconnectSession(lost);
1290 auto replacement = MakeFakeSession();
1291 router->TestConnectSession(replacement);
1292 static_cast<void>(log.Take());
1293
1294 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1295 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(43u, 2039u)));
1296 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1297 EXPECT_EQ(rerouting.get(), 1u);
1298 const auto sent = log.Take();
1299 ASSERT_EQ(sent.size(), 1u);
1300 EXPECT_TRUE(IsGetToken(sent[0], replacement, 43u, 10u));
1301
1302 // Forgetting a live route drops its cached token too.
1303 commander.ForgetSensor(43u);
1304 auto routed = std::async(std::launch::async, [&commander]() { return commander.GetToken(43u, 10u); });
1305 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(43u, 2040u)));
1306 ASSERT_EQ(routed.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1307 const auto token = routed.get();
1308 ASSERT_TRUE(token.has_value());
1309 EXPECT_EQ(token->get_port(), 2040u);
1310}
1311
1312TEST_F(MultiGpuRouterTest, reroute_enables_ros_again_on_the_new_secondary) {
1313 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1314 router->AsyncRun(1u);
1315 RequestLog log(*router);
1316 auto lost = MakeFakeSession();
1317 router->TestConnectSession(lost);
1318 carla::multigpu::PrimaryCommands commander(router);
1319 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2041u));
1320 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 43u, 10u, 2041u));
1321 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 44u, 11u, 2041u));
1322 auto enabled = std::async(std::launch::async, [&commander]() { return commander.EnableForROS(42u, 9u); });
1323 ASSERT_TRUE(Answer(*router, lost, MakeBoolReply(true)));
1324 ASSERT_EQ(enabled.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1325 ASSERT_TRUE(enabled.get());
1326 auto toggled = std::async(std::launch::async, [&commander]() { return commander.EnableForROS(44u, 11u); });
1327 ASSERT_TRUE(Answer(*router, lost, MakeBoolReply(true)));
1328 ASSERT_EQ(toggled.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1329 ASSERT_TRUE(toggled.get());
1330 auto disabled = std::async(std::launch::async, [&commander]() { commander.DisableForROS(44u); });
1331 ASSERT_TRUE(Answer(*router, lost, MakeBoolReply(true)));
1332 ASSERT_EQ(disabled.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1333 router->TestDisconnectSession(lost);
1334 auto replacement = MakeFakeSession();
1335 router->TestConnectSession(replacement);
1336 static_cast<void>(log.Take());
1337
1338 auto rerouting = std::async(std::launch::async, [&commander]() { return commander.RerouteLostSensors(); });
1339 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(42u, 2041u)));
1340 // Best effort: a refused ENABLE_ROS still counts the sensor as routed.
1341 ASSERT_TRUE(Answer(*router, replacement, MakeBoolReply(false)));
1342 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(43u, 2041u)));
1343 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(44u, 2041u)));
1344 ASSERT_EQ(rerouting.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1345 EXPECT_EQ(rerouting.get(), 3u);
1346 const auto sent = log.Take();
1347 ASSERT_EQ(sent.size(), 4u);
1348 EXPECT_TRUE(IsGetToken(sent[0], replacement, 42u, 9u));
1349 EXPECT_TRUE(IsEnableRos(sent[1], replacement, 42u));
1350 EXPECT_TRUE(IsGetToken(sent[2], replacement, 43u, 10u));
1351 EXPECT_TRUE(IsGetToken(sent[3], replacement, 44u, 11u));
1352}
1353
1354TEST_F(MultiGpuRouterTest, get_token_on_a_lost_route_enables_ros_again) {
1355 auto router = std::make_shared<carla::multigpu::Router>(TESTING_PORT);
1356 router->AsyncRun(1u);
1357 RequestLog log(*router);
1358 auto lost = MakeFakeSession();
1359 router->TestConnectSession(lost);
1360 carla::multigpu::PrimaryCommands commander(router);
1361 ASSERT_TRUE(RouteSensorTo(*router, commander, lost, 42u, 9u, 2042u));
1362 auto enabled = std::async(std::launch::async, [&commander]() { return commander.EnableForROS(42u, 9u); });
1363 ASSERT_TRUE(Answer(*router, lost, MakeBoolReply(true)));
1364 ASSERT_EQ(enabled.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1365 ASSERT_TRUE(enabled.get());
1366 router->TestDisconnectSession(lost);
1367 auto replacement = MakeFakeSession();
1368 router->TestConnectSession(replacement);
1369 static_cast<void>(log.Take());
1370
1371 auto lazy = std::async(std::launch::async, [&commander]() { return commander.GetToken(42u, 9u); });
1372 ASSERT_TRUE(Answer(*router, replacement, MakeTokenReply(42u, 2043u)));
1373 ASSERT_TRUE(Answer(*router, replacement, MakeBoolReply(true)));
1374 ASSERT_EQ(lazy.wait_for(std::chrono::seconds(2)), std::future_status::ready);
1375 EXPECT_TRUE(lazy.get().has_value());
1376 const auto sent = log.Take();
1377 ASSERT_EQ(sent.size(), 2u);
1378 EXPECT_TRUE(IsGetToken(sent[0], replacement, 42u, 9u));
1379 EXPECT_TRUE(IsEnableRos(sent[1], replacement, 42u));
1380}
1381
1382TEST_F(MultiGpuRouterTest, load_map_waits_until_every_secondary_is_ready_or_gone) {
1384 router.AsyncRun(1u);
1385 auto session_a = MakeFakeSession();
1386 auto session_b = MakeFakeSession();
1387 router.TestConnectSession(session_a);
1388 router.TestConnectSession(session_b);
1389 EXPECT_FALSE(router.IsAnySecondaryLoading());
1390
1391 router.Write(carla::multigpu::MultiGPUCommand::SEND_FRAME, MakeBuffer("frame"));
1392 EXPECT_FALSE(router.IsAnySecondaryLoading());
1393
1394 const auto load_id = router.WriteLoadMap("map");
1395 EXPECT_TRUE(router.IsAnySecondaryLoading());
1396 router.TestHandleResponse(session_a, MakeBuffer(carla::multigpu::MakeEpisodeReadyMessage(load_id)));
1397 EXPECT_TRUE(router.IsAnySecondaryLoading());
1398 router.TestDisconnectSession(session_b);
1399 EXPECT_FALSE(router.IsAnySecondaryLoading());
1400}
1401
1402TEST_F(MultiGpuRouterTest, stale_episode_ready_for_map_a_does_not_clear_map_b_wait) {
1404 router.AsyncRun(1u);
1405 auto session = MakeFakeSession();
1406 router.TestConnectSession(session);
1407
1408 const auto map_a_load = router.WriteLoadMap("map_a");
1409 const auto map_b_load = router.WriteLoadMap("map_b");
1410 ASSERT_NE(map_a_load, map_b_load);
1411 ASSERT_TRUE(router.IsAnySecondaryLoading());
1412
1413 router.TestHandleResponse(session, MakeBuffer(carla::multigpu::MakeEpisodeReadyMessage(map_a_load)));
1414 EXPECT_TRUE(router.IsAnySecondaryLoading());
1415 router.TestHandleResponse(session, MakeBuffer(carla::multigpu::MakeEpisodeReadyMessage(map_b_load)));
1416 EXPECT_FALSE(router.IsAnySecondaryLoading());
1417}
1418
1419TEST_F(MultiGpuRouterTest, untagged_episode_ready_does_not_clear_a_tagged_load_wait) {
1421 router.AsyncRun(1u);
1422 auto session = MakeFakeSession();
1423 router.TestConnectSession(session);
1424
1425 static_cast<void>(router.WriteLoadMap("map"));
1426 router.TestHandleResponse(session, MakeBuffer(carla::multigpu::kEpisodeReadyMarker));
1427 EXPECT_TRUE(router.IsAnySecondaryLoading());
1428}
1429
1430TEST_F(MultiGpuRouterTest, stale_episode_ready_still_rearms_resync) {
1432 router.AsyncRun(1u);
1433 int callback_count = 0;
1434 router.SetNewConnectionCallback([&callback_count]() { ++callback_count; });
1435 auto session = MakeFakeSession();
1436 router.TestConnectSession(session);
1437 callback_count = 0;
1438
1439 const auto first_load = router.WriteLoadMap("map_a");
1440 static_cast<void>(router.WriteLoadMap("map_b"));
1441 router.TestHandleResponse(session, MakeBuffer(carla::multigpu::MakeEpisodeReadyMessage(first_load)));
1442
1443 EXPECT_EQ(callback_count, 1);
1444 EXPECT_FALSE(router.TestHasPendingRequest(session));
1445}
1446
1447TEST(MultiGpuCommands, load_map_payload_round_trips_map_and_load_id) {
1448 const auto payload = carla::multigpu::MakeLoadMapPayload("/Game/Carla/Maps/Town15", 7u);
1449 const auto request = carla::multigpu::ParseLoadMapPayload(payload);
1450 EXPECT_EQ(request.map, "/Game/Carla/Maps/Town15");
1451 EXPECT_EQ(request.load_id, 7u);
1452}
1453
1454TEST(MultiGpuCommands, load_map_payload_without_load_id_parses_as_zero) {
1455 const std::string legacy("/Game/Carla/Maps/Town15\0", 24u);
1456 const auto request = carla::multigpu::ParseLoadMapPayload(legacy);
1457 EXPECT_EQ(request.map, "/Game/Carla/Maps/Town15");
1458 EXPECT_EQ(request.load_id, 0u);
1459}
1460
1461TEST(MultiGpuCommands, episode_ready_message_round_trips_load_id) {
1462 const auto message = carla::multigpu::MakeEpisodeReadyMessage(42u);
1463 const auto load_id = carla::multigpu::ParseEpisodeReadyMessage(message);
1464 ASSERT_TRUE(load_id.has_value());
1465 EXPECT_EQ(*load_id, 42u);
1467 EXPECT_FALSE(carla::multigpu::ParseEpisodeReadyMessage("Yes, I'm alive").has_value());
1468 EXPECT_FALSE(carla::multigpu::ParseEpisodeReadyMessage(message.substr(0u, message.size() - 1u)).has_value());
1469}
1470
1471TEST_F(MultiGpuRouterTest, load_map_wait_can_be_abandoned) {
1473 router.AsyncRun(1u);
1474 auto session = MakeFakeSession();
1475 router.TestConnectSession(session);
1476
1477 static_cast<void>(router.WriteLoadMap("map"));
1478 ASSERT_TRUE(router.IsAnySecondaryLoading());
1480 EXPECT_FALSE(router.IsAnySecondaryLoading());
1481}
1482
1483TEST_F(MultiGpuRouterTest, load_map_without_secondaries_waits_for_nothing) {
1485 router.AsyncRun(1u);
1486 static_cast<void>(router.WriteLoadMap("map"));
1487 EXPECT_FALSE(router.IsAnySecondaryLoading());
1488}
1489
1490TEST(MultiGpuRouterProductionShutdown, stop_releases_listening_port_despite_reference_cycle) {
1491 // Reproduces the production call shape (make_shared<Router>, SetCallbacks(),
1492 // AsyncRun()): SetCallbacks() gives PrimaryCommands a shared_ptr back to
1493 // this Router, so ~Router() never runs -- Stop() itself, not destruction,
1494 // must release the listening socket.
1495 //
1496 // A bound, ephemeral port obtained up front, not TESTING_PORT (0) passed
1497 // straight to Router: GetLocalEndpoint() returns the endpoint the Router
1498 // was constructed with, not the acceptor's actual bound local_endpoint(),
1499 // so constructing it with port 0 would make the probe below re-check a
1500 // different, freshly-assigned port instead of the one actually in use.
1501 uint16_t fixed_test_port;
1502 {
1503 boost::asio::io_context probe_io_context;
1504 carla::multigpu::Listener port_probe(
1505 probe_io_context, boost::asio::ip::tcp::endpoint(boost::asio::ip::make_address("0.0.0.0"), 0));
1506 fixed_test_port = port_probe.GetLocalEndpoint().port();
1507 }
1508
1509 auto router = std::make_shared<carla::multigpu::Router>(fixed_test_port);
1510 router->SetCallbacks();
1511 router->AsyncRun(1u);
1512 const auto endpoint = router->GetLocalEndpoint();
1513
1514 // Give the background thread a bounded window to run the initial
1515 // async_accept SetCallbacks()/Listen() posts, arming the handler whose
1516 // capture of shared_from_this() this test exists to guard against leaking.
1517 std::this_thread::sleep_for(std::chrono::milliseconds(100));
1518
1519 router->Stop();
1520
1521 // `router` is deliberately still alive with its reference cycle intact
1522 // here, exactly as in production: the check below must not depend on
1523 // ~Router()/~Listener() ever running.
1524 {
1525 boost::asio::io_context probe_io_context;
1526 EXPECT_NO_THROW({
1527 carla::multigpu::Listener probe(probe_io_context, endpoint);
1528 }) << "Router::Stop() did not release its listening port";
1529 }
1530
1531 // Break the self-reference cycle so this test's Router doesn't leak for
1532 // the rest of the process; production leaks it on purpose.
1533 router->GetCommander().set_router(nullptr);
1534}
1535
1536// -- Transport: real sockets, every wait bounded -----------------------------
1537
1538namespace {
1539
1540 using tcp = boost::asio::ip::tcp;
1541
1542 /// Polls @a condition for up to @a timeout.
1543 template <typename F>
1544 bool WaitFor(F condition, std::chrono::milliseconds timeout = std::chrono::milliseconds(5000)) {
1545 const auto deadline = std::chrono::steady_clock::now() + timeout;
1546 while (!condition()) {
1547 if (std::chrono::steady_clock::now() >= deadline) {
1548 return false;
1549 }
1550 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1551 }
1552 return true;
1553 }
1554
1555 uint16_t FreePort() {
1556 boost::asio::io_context io_context;
1557 carla::multigpu::Listener probe(io_context, tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), 0));
1558 return probe.GetLocalEndpoint().port();
1559 }
1560
1561 /// A Router as the engine starts it, torn down without leaking its thread.
1562 struct RunningRouter {
1563 std::shared_ptr<carla::multigpu::Router> router;
1564 tcp::endpoint endpoint;
1565
1566 RunningRouter() {
1567 const uint16_t port = FreePort();
1568 router = std::make_shared<carla::multigpu::Router>(port);
1569 router->SetCallbacks();
1570 router->AsyncRun(1u);
1571 endpoint = tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), port);
1572 }
1573
1574 ~RunningRouter() {
1575 router->Stop();
1576 router->GetCommander().set_router(nullptr);
1577 }
1578 };
1579
1580 struct ReadCommand {
1581 uint32_t id = 0u;
1582 std::string payload;
1583 };
1584
1585 /// Reads one [u32 size][CommandHeader][payload] message from @a socket.
1586 ReadCommand ReadOneCommand(tcp::socket &socket) {
1587 uint32_t total = 0u;
1588 boost::asio::read(socket, boost::asio::buffer(&total, sizeof(total)));
1589 std::string bytes(total, '\0');
1590 boost::asio::read(socket, boost::asio::buffer(bytes.data(), bytes.size()));
1592 std::memcpy(&header, bytes.data(), sizeof(header));
1593 ReadCommand command;
1594 command.id = static_cast<uint32_t>(header.id);
1595 command.payload = bytes.substr(sizeof(header), header.size);
1596 return command;
1597 }
1598
1599} // namespace
1600
1601TEST(MultiGpuTransport, write_queue_preserves_order_under_backpressure) {
1602 RunningRouter running;
1603 boost::asio::io_context io_context;
1604 tcp::socket client(io_context);
1605 client.open(tcp::v4());
1606 client.set_option(boost::asio::socket_base::receive_buffer_size(4096));
1607 client.connect(running.endpoint);
1608 ASSERT_TRUE(WaitFor([&]() { return running.router->HasClientsConnected(); }));
1609
1610 const std::string first(1u << 20u, 'a');
1611 const std::string second(1u << 20u, 'b');
1612 const std::string third("8 bytes!");
1613 for (const std::string *payload : {&first, &second, &third}) {
1614 running.router->Write(carla::multigpu::SEND_FRAME, carla::Buffer(
1615 reinterpret_cast<const unsigned char *>(payload->data()), payload->size()));
1616 }
1617 // Let the server's socket fill up, so each message needs many partial writes.
1618 std::this_thread::sleep_for(std::chrono::milliseconds(200));
1619
1620 auto received = std::async(std::launch::async, [&]() {
1621 std::vector<ReadCommand> commands;
1622 for (int i = 0; i < 3; ++i) {
1623 commands.push_back(ReadOneCommand(client));
1624 }
1625 return commands;
1626 });
1627 if (received.wait_for(std::chrono::seconds(10)) != std::future_status::ready) {
1628 boost::system::error_code ignored;
1629 client.close(ignored);
1630 FAIL() << "the three messages did not arrive";
1631 }
1632 const std::vector<ReadCommand> commands = received.get();
1633 ASSERT_EQ(commands.size(), 3u);
1634 EXPECT_EQ(commands[0].payload, first);
1635 EXPECT_EQ(commands[1].payload, second);
1636 EXPECT_EQ(commands[2].payload, third);
1637 for (const ReadCommand &command : commands) {
1638 EXPECT_EQ(command.id, static_cast<uint32_t>(carla::multigpu::SEND_FRAME));
1639 }
1640}
1641
1642TEST(MultiGpuTransport, oversize_header_closes_the_session) {
1643 RunningRouter running;
1644 boost::asio::io_context io_context;
1645 tcp::socket client(io_context);
1646 client.connect(running.endpoint);
1647 ASSERT_TRUE(WaitFor([&]() { return running.router->HasClientsConnected(); }));
1648
1649 const uint32_t announced = static_cast<uint32_t>(carla::multigpu::kMaxMultiGpuMessageBytes + 1u);
1650 boost::asio::write(client, boost::asio::buffer(&announced, sizeof(announced)));
1651 EXPECT_TRUE(WaitFor([&]() { return !running.router->HasClientsConnected(); }))
1652 << "the primary kept a session that announced an impossible message";
1653}
1654
1655namespace {
1656
1657 /// A bare primary end: accepts one secondary on an ephemeral port.
1658 struct FakePrimary {
1659 boost::asio::io_context io_context;
1660 tcp::acceptor acceptor{io_context, tcp::endpoint(boost::asio::ip::make_address("127.0.0.1"), 0)};
1661 tcp::socket peer{io_context};
1662
1663 uint16_t port() const {
1664 return acceptor.local_endpoint().port();
1665 }
1666
1667 /// Accepts the secondary's connection, waiting for at most 5 s.
1668 bool Accept() {
1669 auto accepted = std::async(std::launch::async, [this]() {
1670 boost::system::error_code ec;
1671 acceptor.accept(peer, ec);
1672 return !ec;
1673 });
1674 if (accepted.wait_for(std::chrono::seconds(5)) != std::future_status::ready) {
1675 boost::system::error_code ignored;
1676 acceptor.close(ignored);
1677 accepted.wait();
1678 return false;
1679 }
1680 return accepted.get();
1681 }
1682 };
1683
1684 std::shared_ptr<carla::multigpu::Secondary> MakeSecondary(uint16_t port) {
1685 return std::make_shared<carla::multigpu::Secondary>(
1686 "127.0.0.1", port, [](carla::multigpu::MultiGPUCommand, carla::Buffer) {});
1687 }
1688
1689} // namespace
1690
1691TEST(MultiGpuSecondary, reports_connected_until_the_primary_closes) {
1692 FakePrimary primary;
1693 auto secondary = MakeSecondary(primary.port());
1694 secondary->Connect();
1695 ASSERT_TRUE(primary.Accept());
1696 EXPECT_TRUE(WaitFor([&]() { return secondary->IsConnected(); }));
1697
1698 primary.peer.close();
1699 EXPECT_TRUE(WaitFor([&]() { return !secondary->IsConnected(); }))
1700 << "the secondary still reports a connection the primary closed";
1701 secondary->Stop();
1702}
1703
1704TEST(MultiGpuSecondary, is_not_connected_before_connect_or_after_stop) {
1705 FakePrimary primary;
1706 auto secondary = MakeSecondary(primary.port());
1707 EXPECT_FALSE(secondary->IsConnected());
1708 secondary->Connect();
1709 ASSERT_TRUE(primary.Accept());
1710 ASSERT_TRUE(WaitFor([&]() { return secondary->IsConnected(); }));
1711 secondary->Stop();
1712 EXPECT_FALSE(secondary->IsConnected());
1713}
1714
1715TEST(MultiGpuSecondary, oversize_message_from_the_primary_disconnects) {
1716 FakePrimary primary;
1717 auto secondary = MakeSecondary(primary.port());
1718 secondary->Connect();
1719 ASSERT_TRUE(primary.Accept());
1720 ASSERT_TRUE(WaitFor([&]() { return secondary->IsConnected(); }));
1721
1722 const uint32_t announced = static_cast<uint32_t>(carla::multigpu::kMaxMultiGpuMessageBytes + 1u);
1723 boost::asio::write(primary.peer, boost::asio::buffer(&announced, sizeof(announced)));
1724 EXPECT_TRUE(WaitFor([&]() { return !secondary->IsConnected(); }));
1725 secondary->Stop();
1726}
1727
1728TEST(MultiGpuSecondary, replies_reach_the_primary_in_order) {
1729 FakePrimary primary;
1730 auto secondary = MakeSecondary(primary.port());
1731 secondary->Connect();
1732 ASSERT_TRUE(primary.Accept());
1733 ASSERT_TRUE(WaitFor([&]() { return secondary->IsConnected(); }));
1734
1735 const std::string first(256u * 1024u, 'x');
1736 secondary->Write(carla::Buffer(reinterpret_cast<const unsigned char *>(first.data()), first.size()));
1737 secondary->Write(std::string("second"));
1738
1739 auto received = std::async(std::launch::async, [&]() {
1740 std::vector<std::string> messages;
1741 for (int i = 0; i < 2; ++i) {
1742 uint32_t size = 0u;
1743 boost::asio::read(primary.peer, boost::asio::buffer(&size, sizeof(size)));
1744 std::string bytes(size, '\0');
1745 boost::asio::read(primary.peer, boost::asio::buffer(bytes.data(), bytes.size()));
1746 messages.push_back(std::move(bytes));
1747 }
1748 return messages;
1749 });
1750 if (received.wait_for(std::chrono::seconds(10)) != std::future_status::ready) {
1751 boost::system::error_code ignored;
1752 primary.peer.close(ignored);
1753 FAIL() << "the replies did not arrive";
1754 }
1755 const std::vector<std::string> messages = received.get();
1756 ASSERT_EQ(messages.size(), 2u);
1757 EXPECT_EQ(messages[0], first);
1758 EXPECT_EQ(messages[1], "second");
1759 secondary->Stop();
1760}
1761
1762TEST(MultiGpuLoadMap, payload_carries_the_generated_world) {
1764 extras.opendrive = std::string("<OpenDRIVE>\0binary-safe</OpenDRIVE>", 35u);
1765 extras.generation_parameters = std::string("\x97\xcb\x40\x00", 4u);
1766 const std::string payload = carla::multigpu::MakeLoadMapPayload("/Game/Carla/Maps/OpenDriveMap", 7u, extras);
1767 const auto request = carla::multigpu::ParseLoadMapPayload(payload);
1768 EXPECT_EQ(request.map, "/Game/Carla/Maps/OpenDriveMap");
1769 EXPECT_EQ(request.load_id, 7u);
1770 EXPECT_EQ(request.extras, extras);
1771}
1772
1773TEST(MultiGpuLoadMap, payload_without_extras_has_none) {
1775 EXPECT_EQ(request.load_id, 2u);
1776 EXPECT_FALSE(request.extras.opendrive.has_value());
1777 EXPECT_FALSE(request.extras.generation_parameters.has_value());
1778}
1779
1780TEST(MultiGpuLoadMap, unknown_extras_are_skipped_and_cut_ones_dropped) {
1782 extras.opendrive = std::string("xodr");
1783 std::string payload = carla::multigpu::MakeLoadMapPayload("map", 1u);
1784 // A kind of a newer build, then the OpenDRIVE.
1785 const uint32_t unknown_size = 3u;
1786 payload.push_back(static_cast<char>(9));
1787 payload.append(reinterpret_cast<const char *>(&unknown_size), sizeof(unknown_size));
1788 payload.append("abc");
1789 const std::string with_xodr = payload + carla::multigpu::MakeLoadMapPayload("map", 1u, extras).substr(payload.size() - 8u);
1790 const auto request = carla::multigpu::ParseLoadMapPayload(with_xodr);
1791 EXPECT_EQ(request.extras.opendrive, std::optional<std::string>("xodr"));
1792
1793 const std::string full = carla::multigpu::MakeLoadMapPayload("map", 1u, extras);
1794 const auto cut = carla::multigpu::ParseLoadMapPayload(std::string_view(full).substr(0u, full.size() - 1u));
1795 EXPECT_EQ(cut.load_id, 1u);
1796 EXPECT_FALSE(cut.extras.opendrive.has_value());
1797}
1798
1799TEST_F(MultiGpuRouterTest, session_connecting_later_gets_the_remembered_generated_world) {
1801 RequestLog log(router);
1803 extras.opendrive = std::string("<OpenDRIVE/>");
1804 extras.generation_parameters = std::string("params");
1805 router.WriteLoadMap("/Game/Carla/Maps/OpenDriveMap", extras);
1806 auto session = MakeFakeSession();
1807 router.TestConnectSession(session);
1808
1809 const auto sent = log.Take();
1810 ASSERT_EQ(sent.size(), 1u);
1811 const auto request = carla::multigpu::ParseLoadMapPayload(
1812 std::string_view(reinterpret_cast<const char *>(sent[0].payload.data()), sent[0].payload.size()));
1813 EXPECT_EQ(request.map, "/Game/Carla/Maps/OpenDriveMap");
1814 EXPECT_EQ(request.extras, extras);
1815
1816 // A later plain map load forgets it.
1817 router.WriteLoadMap("Town01");
1818 auto late = MakeFakeSession();
1819 log.Take();
1820 router.TestConnectSession(late);
1821 const auto again = log.Take();
1822 ASSERT_EQ(again.size(), 1u);
1823 const auto plain = carla::multigpu::ParseLoadMapPayload(
1824 std::string_view(reinterpret_cast<const char *>(again[0].payload.data()), again[0].payload.size()));
1825 EXPECT_EQ(plain.map, "Town01");
1826 EXPECT_FALSE(plain.extras.opendrive.has_value());
1827}
long long first
Definition LaneGraph.cpp:34
Creating a constant view from an existing buffer.
Definition BufferView.h:32
size_type size() const noexcept
Definition BufferView.h:110
const value_type * data() const noexcept
Direct access to the allocated memory or nullptr if no memory is allocated.
Definition BufferView.h:80
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
endpoint GetLocalEndpoint() const
Definition listener.h:38
void set_router(std::shared_ptr< Router > router)
std::optional< token_type > GetToken(stream_id sensor_id, actor_id sensor_actor_id)
Returns std::nullopt if no secondary could provide the token (e.g.
std::size_t RerouteLostSensors()
Routes every sensor whose secondary disconnected to the secondaries connected since the previous call...
void DisableForROS(stream_id sensor_id)
bool EnableForROS(stream_id sensor_id, actor_id sensor_actor_id)
Returns false if no secondary accepted this sensor or the secondary holding it did not reply; ROS ena...
void ForgetSensor(stream_id sensor_id)
Drops the routing of a destroyed sensor so it is never routed again.
bool IsEnabledForROS(stream_id sensor_id)
void SendLoadMap(std::string map, const LoadMapExtras &extras={})
A TCP server session.
Definition primary.h:35
boost::asio::ip::tcp::endpoint GetLocalEndpoint() const
Definition router.cpp:85
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::future< SessionInfo > WriteToNext(MultiGPUCommand id, Buffer &&buffer)
Definition router.cpp:253
void StopWaitingForSecondaryLoads()
Definition router.cpp:248
std::future< SessionInfo > WriteToOne(std::weak_ptr< Primary > server, MultiGPUCommand id, Buffer &&buffer)
Definition router.cpp:306
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 AsyncRun(size_t worker_threads)
Definition router.cpp:81
void SetNewConnectionCallback(std::function< void(void)>)
Definition router.cpp:76
bool HasClientsConnected() const
Any thread: reads a count kept next to the session list, which is itself only touched under the route...
Definition router.h:81
PrimaryCommands & GetCommander()
Definition router.h:85
static time_duration seconds(size_t timeout)
Definition Time.h:23
constexpr std::size_t kMaxMultiGpuMessageBytes
Largest message either end accepts: a frame (capped at 1 GiB by the primary) plus room for its header...
Definition commands.h:37
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
LoadMapRequest ParseLoadMapPayload(std::string_view payload)
Definition commands.h:90
constexpr std::string_view kTokenNotReadyMarker
GET_TOKEN reply when the secondary cannot resolve the sensor to a stream (or the request is malformed...
Definition commands.h:159
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
carla::streaming::detail::stream_id_type stream_id
std::string MakeEpisodeReadyMessage(load_map_id_type load_id)
Episode-ready message: the marker followed by the load id of the LOAD_MAP that started the episode,...
Definition commands.h:124
constexpr std::string_view kEpisodeReadyMarker
Sent unprompted by a secondary, on the same channel as command responses, to signal that its episode ...
Definition commands.h:42
const char * ToString(LaneType type)
GET_TOKEN request payload.
Definition commands.h:151
What a LOAD_MAP carries besides the map: for a world the primary generated from OpenDRIVE (generate_o...
Definition commands.h:51
std::optional< std::string > generation_parameters
Definition commands.h:53
std::optional< std::string > opendrive
Definition commands.h:52
enum carla::streaming::detail::token_data::address address_type
constexpr uint16_t TESTING_PORT
Definition test.h:24
TEST(MultiGpuCommands, token_not_ready_marker_is_distinct_from_other_replies)
TEST_F(MultiGpuRouterTest, write_to_next_with_no_sessions_rejects_instead_of_hanging)