Skip to content

Commit 78d25aa

Browse files
feat: name the subscriber (node + topic) in the UDS send-drop warning and clarify the byte count
The drop warning printed only the peer's socket path and worded the message size as if it were the receive-buffer fill level ("recv buffer full (N bytes)"). Both made a slow-subscriber drop hard to diagnose: the path only encodes a PID, and the byte count looked like it contradicted the configured buffer size. Now the ENOBUFS/EAGAIN branch names the offending subscriber by its fully-qualified node name and topic, states plainly that the number is the dropped message's size, and shows the configured recv-buffer size for reference. The peer label is built once per graph change (in refresh_subscribers, off the hot path) and stored in the copy-on-write subscriber cache alongside the socket path, so the publish hot path still copies just a refcount and never formats a string. send_to takes an optional peer_label; the three publisher fan-out paths supply it, request/response callers pass none and are unaffected. Rewritten against current devel: the wake-side TRANSIENT_LOCAL replay this originally also touched in rmw_wait.cpp no longer exists (#60's pull-based replay), and the three cache-refresh sites it edited are now the single refresh_subscribers helper.
1 parent e819eb5 commit 78d25aa

5 files changed

Lines changed: 101 additions & 34 deletions

File tree

‎rmw_unix_socket_cpp/src/rmw_publisher.cpp‎

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,14 @@ rmw_ret_t rmw_destroy_publisher(rmw_node_t * node, rmw_publisher_t * publisher)
201201
return RMW_RET_OK;
202202
}
203203

204-
// Refresh the generation-keyed subscriber-path cache if `current_gen` moved,
205-
// and return the (possibly shared) path list. The memoization invariant that
206-
// keeps the pull-based replay proof sound: cached_generation only ever
204+
// Refresh the generation-keyed subscriber cache if `current_gen` moved, and
205+
// return the (possibly shared) subscriber list. The memoization invariant
206+
// that keeps the pull-based replay proof sound: cached_generation only ever
207207
// advances to a value loaded BEFORE the query ran, under sub_cache_mutex —
208208
// so current_gen == cached_generation implies the cache already saw every
209209
// subscriber whose registration that generation covers.
210-
static std::shared_ptr<const std::vector<std::string>> refresh_sub_paths(
210+
static std::shared_ptr<const std::vector<rmw_uds::CachedSubscriber>>
211+
refresh_subscribers(
211212
rmw_uds::UdsPublisher * pub_data,
212213
rmw_uds::RegistryHeader * header,
213214
uint64_t current_gen)
@@ -222,17 +223,21 @@ static std::shared_ptr<const std::vector<std::string>> refresh_sub_paths(
222223
auto subs = rmw_uds::registry_query(
223224
header, rmw_uds::ENTRY_SUBSCRIPTION, pub_data->topic_name.c_str(),
224225
nullptr, nullptr);
225-
auto fresh = std::make_shared<std::vector<std::string>>();
226+
auto fresh = std::make_shared<std::vector<rmw_uds::CachedSubscriber>>();
226227
fresh->reserve(subs.size());
227228
for (const auto & s : subs) {
228229
if (!s.socket_path.empty()) {
229-
fresh->push_back(s.socket_path);
230+
fresh->push_back(
231+
{s.socket_path,
232+
rmw_uds::make_peer_label(
233+
s.node_namespace.c_str(), s.node_name.c_str(),
234+
pub_data->topic_name.c_str())});
230235
}
231236
}
232237
pub_data->cached_generation = current_gen;
233-
pub_data->cached_subscriber_paths = std::move(fresh);
238+
pub_data->cached_subscribers = std::move(fresh);
234239
}
235-
return pub_data->cached_subscriber_paths;
240+
return pub_data->cached_subscribers;
236241
}
237242

238243
// Latch a TRANSIENT_LOCAL sample into the pull cache and fan it out live.
@@ -296,7 +301,7 @@ static rmw_ret_t transient_local_publish(
296301

297302
auto * header = rmw_uds::registry_header(pub_data->context->registry_ptr);
298303
const uint64_t current_gen = rmw_uds::registry_generation(header);
299-
auto sub_paths = refresh_sub_paths(pub_data, header, current_gen);
304+
auto targets = refresh_subscribers(pub_data, header, current_gen);
300305

301306
// Live fan-out: payloads at or above the datagram threshold reuse the
302307
// durable segment the latch just committed (same bytes, same
@@ -311,11 +316,12 @@ static rmw_ret_t transient_local_publish(
311316
wire_size = sizeof(staged_desc);
312317
}
313318

314-
if (sub_paths) {
315-
for (const auto & path : *sub_paths) {
319+
if (targets) {
320+
for (const auto & t : *targets) {
316321
if (rmw_uds::send_to(
317322
pub_data->context->send_socket_fd,
318-
path, hdr, wire_data, wire_size) == rmw_uds::SendResult::ConfigError)
323+
t.socket_path, hdr, wire_data, wire_size,
324+
t.label.c_str()) == rmw_uds::SendResult::ConfigError)
319325
{
320326
config_error = true;
321327
}
@@ -380,7 +386,7 @@ rmw_ret_t rmw_publish(
380386
// since we last cached the subscriber list. The hot path is purely local.
381387
auto * header = rmw_uds::registry_header(pub_data->context->registry_ptr);
382388
uint64_t current_gen = rmw_uds::registry_generation(header);
383-
auto sub_paths = refresh_sub_paths(pub_data, header, current_gen);
389+
auto targets = refresh_subscribers(pub_data, header, current_gen);
384390

385391
// Non-latched path: serialize once, choosing the destination by size — a
386392
// large payload is written directly into the reserved ring record (no heap
@@ -404,11 +410,12 @@ rmw_ret_t rmw_publish(
404410

405411
// Surface EMSGSIZE; soft drops (EAGAIN/ENOENT) are logged in send_to.
406412
bool config_error = false;
407-
if (sub_paths) {
408-
for (const auto & path : *sub_paths) {
413+
if (targets) {
414+
for (const auto & t : *targets) {
409415
if (rmw_uds::send_to(
410416
pub_data->context->send_socket_fd,
411-
path, hdr, wire.data, wire.size) == rmw_uds::SendResult::ConfigError)
417+
t.socket_path, hdr, wire.data, wire.size,
418+
t.label.c_str()) == rmw_uds::SendResult::ConfigError)
412419
{
413420
config_error = true;
414421
}
@@ -463,7 +470,7 @@ rmw_ret_t rmw_publish_serialized_message(
463470
// Reuse the cached subscriber list (refresh only on graph change)
464471
auto * header = rmw_uds::registry_header(pub_data->context->registry_ptr);
465472
uint64_t current_gen = rmw_uds::registry_generation(header);
466-
auto sub_paths = refresh_sub_paths(pub_data, header, current_gen);
473+
auto targets = refresh_subscribers(pub_data, header, current_gen);
467474

468475
// Large non-TL payloads: stage once into the cycling ring, fan out descriptors.
469476
rmw_uds::ShmPayloadDescriptor desc;
@@ -472,11 +479,12 @@ rmw_ret_t rmw_publish_serialized_message(
472479
serialized_message->buffer, serialized_message->buffer_length, hdr, desc);
473480

474481
bool config_error = false;
475-
if (sub_paths) {
476-
for (const auto & path : *sub_paths) {
482+
if (targets) {
483+
for (const auto & t : *targets) {
477484
if (rmw_uds::send_to(
478485
pub_data->context->send_socket_fd,
479-
path, hdr, wire.data, wire.size) == rmw_uds::SendResult::ConfigError)
486+
t.socket_path, hdr, wire.data, wire.size,
487+
t.label.c_str()) == rmw_uds::SendResult::ConfigError)
480488
{
481489
config_error = true;
482490
}

‎rmw_unix_socket_cpp/src/transport.cpp‎

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,33 @@ int create_send_socket()
108108
return fd;
109109
}
110110

111+
std::string make_peer_label(
112+
const char * node_namespace, const char * node_name, const char * topic)
113+
{
114+
std::string out = "node '";
115+
// ROS fully-qualified node name: namespace + '/' + name, with a "/" (root)
116+
// namespace collapsing so we emit "/name" rather than "//name".
117+
if (node_namespace && node_namespace[0] && std::strcmp(node_namespace, "/") != 0) {
118+
out += node_namespace;
119+
}
120+
out += '/';
121+
out += (node_name && node_name[0]) ? node_name : "<unknown>";
122+
out += '\'';
123+
if (topic && topic[0]) {
124+
out += " on topic '";
125+
out += topic;
126+
out += '\'';
127+
}
128+
return out;
129+
}
130+
111131
SendResult send_to(
112132
int send_fd,
113133
const std::string & dest_path,
114134
const WireHeader & header,
115135
const uint8_t * payload,
116-
size_t payload_size)
136+
size_t payload_size,
137+
const char * peer_label)
117138
{
118139
struct sockaddr_un addr;
119140
std::memset(&addr, 0, sizeof(addr));
@@ -139,9 +160,9 @@ SendResult send_to(
139160
}
140161

141162
// Classify the failure. Hot path — every category is throttled so a
142-
// persistently-failing peer can't drown the log. The destination path
143-
// encodes the peer's prefix+pid (see make_socket_path) which is enough
144-
// to identify the offending subscriber from the message alone.
163+
// persistently-failing peer can't drown the log. `peer_label` (when the
164+
// caller supplies one) names the offending subscriber; the destination
165+
// path also encodes its prefix+pid (see make_socket_path).
145166
const int err = errno;
146167
const size_t total = sizeof(WireHeader) + payload_size;
147168
SendResult result = SendResult::SoftDrop;
@@ -164,12 +185,15 @@ SendResult send_to(
164185
case ENOBUFS:
165186
// Peer's receive queue is full (or our send queue, briefly). This
166187
// is the classic "slow subscriber" symptom — the subscriber isn't
167-
// calling rmw_take fast enough to drain its socket.
188+
// calling rmw_take fast enough to drain its socket. The byte count is
189+
// this dropped message's size, not the buffer's fill level.
168190
RMW_UDS_LOG_WARN_THROTTLE(
169191
1000,
170-
"UDS send to '%s' dropped: subscriber recv buffer full (%zu bytes, errno=%s). "
192+
"UDS send to %s (socket '%s') dropped: subscriber recv queue full — "
193+
"dropped a %zu-byte message (recv buf configured %d MB, errno=%s). "
171194
"Slow subscriber or undersized SO_RCVBUF.",
172-
dest_path.c_str(), total, std::strerror(err));
195+
peer_label ? peer_label : "peer", dest_path.c_str(), total,
196+
RECV_BUF_SIZE / (1024 * 1024), std::strerror(err));
173197
break;
174198
case ENOENT:
175199
case ECONNREFUSED:

‎rmw_unix_socket_cpp/src/transport.hpp‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,23 @@ int create_send_socket();
4040
// ConfigError = EMSGSIZE (hard, surface to caller); SoftDrop = transient.
4141
enum class SendResult { Ok, SoftDrop, ConfigError };
4242

43-
// Send a datagram (header + payload) to the given socket path.
43+
// Send a datagram (header + payload) to the given socket path. `peer_label`,
44+
// if non-null, is a human-readable peer identity (see make_peer_label) shown
45+
// in the drop/failure diagnostics; pass null when no identity is available.
4446
SendResult send_to(
4547
int send_fd,
4648
const std::string & dest_path,
4749
const WireHeader & header,
4850
const uint8_t * payload,
49-
size_t payload_size);
51+
size_t payload_size,
52+
const char * peer_label = nullptr);
53+
54+
// Build a human-readable peer identity for diagnostics, e.g.
55+
// "node '/ns/name' on topic '/topic'". Null/empty fields are omitted; a "/"
56+
// (root) namespace collapses to a single leading slash (ROS fully-qualified
57+
// name), so ("/", "name", ...) yields "/name" rather than "//name".
58+
std::string make_peer_label(
59+
const char * node_namespace, const char * node_name, const char * topic);
5060

5161
// Receive a single datagram from a non-blocking socket.
5262
// Returns true if a message was received, false if EAGAIN/EWOULDBLOCK or error.

‎rmw_unix_socket_cpp/src/types.hpp‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,15 @@ struct UdsNode
207207
int32_t registry_index = -1;
208208
};
209209

210+
// A matched subscriber cached on the publisher. `label` is a pre-formatted
211+
// human-readable identity (node + topic, see make_peer_label) used only in
212+
// diagnostic logs; built once per graph change so publish never formats it.
213+
struct CachedSubscriber
214+
{
215+
std::string socket_path;
216+
std::string label;
217+
};
218+
210219
// Publisher data
211220
struct UdsPublisher
212221
{
@@ -221,14 +230,15 @@ struct UdsPublisher
221230
UdsContext * context = nullptr;
222231
UdsNode * node = nullptr;
223232

224-
// PERFORMANCE: cache the matching subscriber socket paths to avoid locking
225-
// the registry on every publish. We re-query only when the registry's
226-
// generation counter changes (graph topology actually changed).
233+
// PERFORMANCE: cache the matching subscribers to avoid locking the registry
234+
// on every publish. We re-query only when the registry's generation counter
235+
// changes (graph topology actually changed).
227236
std::mutex sub_cache_mutex;
228237
uint64_t cached_generation = 0;
229238
// Copy-on-write: swapped wholesale on graph-generation change so the publish/
230-
// wait hot path copies one refcount instead of N strings. Null until first refresh.
231-
std::shared_ptr<const std::vector<std::string>> cached_subscriber_paths;
239+
// wait hot path copies one refcount instead of N entries. Each entry carries
240+
// its diagnostic label, built here (off the hot path). Null until first refresh.
241+
std::shared_ptr<const std::vector<CachedSubscriber>> cached_subscribers;
232242

233243
// TRANSIENT_LOCAL latched cache, pulled by late joiners themselves at
234244
// rmw_create_subscription (see shm_transport.hpp). cache_mutex serializes

‎rmw_unix_socket_cpp/test/test_transport.cpp‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ class TransportTest : public ::testing::Test
3737
}
3838
};
3939

40+
TEST_F(TransportTest, MakePeerLabelFormatsNodeAndTopic)
41+
{
42+
EXPECT_EQ(
43+
"node '/ns/talker' on topic '/chatter'",
44+
rmw_uds::make_peer_label("/ns", "talker", "/chatter"));
45+
// Root namespace "/" collapses to a single leading slash (not "//talker").
46+
EXPECT_EQ(
47+
"node '/talker' on topic '/chatter'",
48+
rmw_uds::make_peer_label("/", "talker", "/chatter"));
49+
// Empty namespace behaves like root; a null topic drops the topic clause.
50+
EXPECT_EQ("node '/talker'", rmw_uds::make_peer_label("", "talker", nullptr));
51+
// All-null is tolerated (name falls back to a placeholder).
52+
EXPECT_EQ("node '/<unknown>'", rmw_uds::make_peer_label(nullptr, nullptr, nullptr));
53+
}
54+
4055
TEST_F(TransportTest, CreateBoundSocket)
4156
{
4257
auto path = rmw_uds::make_socket_path(domain_id, "test");

0 commit comments

Comments
 (0)