Skip to content

Commit 5817d5a

Browse files
committed
[#25513] Fixed flaky RPC reply loss by gating reply write on reader match
Signed-off-by: danipiza <dpizarrogallego@gmail.com>
1 parent ab3d5ab commit 5817d5a

4 files changed

Lines changed: 104 additions & 0 deletions

File tree

ddspipe_core/include/ddspipe_core/interface/IWriter.hpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <cpp_utils/ReturnCode.hpp>
2020

2121
#include <ddspipe_core/interface/IRoutingData.hpp>
22+
#include <ddspipe_core/types/dds/GuidPrefix.hpp>
2223

2324
namespace eprosima {
2425
namespace ddspipe {
@@ -94,6 +95,33 @@ class IWriter
9495
DDSPIPE_CORE_DllAPI
9596
virtual void update_topic_partitions(
9697
const std::map<std::string, std::string>& partition_name) = 0;
98+
99+
/**
100+
* @brief Wait until this Writer has matched a Reader belonging to the participant identified by
101+
* \c reader_participant_guid_prefix, or until \c timeout_ms milliseconds have elapsed.
102+
*
103+
* Needed for RPC reply routing: reply topics use VOLATILE durability, so a reply written before
104+
* the destination client's reply reader is matched on the writer side is silently dropped. As
105+
* ROS 2 service clients never retry, that hangs the client forever. Gating the reply write on
106+
* this method closes the discovery race (a client's \c wait_for_service only proves the match
107+
* from the reader side).
108+
*
109+
* @param reader_participant_guid_prefix GuidPrefix of the participant whose reader must match.
110+
* @param timeout_ms Maximum time to wait, in milliseconds.
111+
*
112+
* @return \c true if a matching reader is present within the timeout, \c false otherwise.
113+
*
114+
* @note Default implementation returns \c true: writers that do not track matching are assumed
115+
* ready and never block the caller.
116+
*/
117+
virtual bool wait_reader_matched(
118+
const types::GuidPrefix& reader_participant_guid_prefix,
119+
const unsigned int timeout_ms) const noexcept
120+
{
121+
static_cast<void>(reader_participant_guid_prefix);
122+
static_cast<void>(timeout_ms);
123+
return true;
124+
}
97125
};
98126

99127
} /* namespace core */

ddspipe_core/src/cpp/communication/rpc/RpcBridge.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ namespace core {
3333

3434
using namespace eprosima::ddspipe::core::types;
3535

36+
//! Maximum time (ms) to wait for the destination reply reader to be matched before writing a reply.
37+
//! Only reached during the discovery race window; the wait returns immediately once matched.
38+
static constexpr unsigned int REPLY_MATCH_MAX_WAIT_MS = 1000;
39+
3640
RpcBridge::RpcBridge(
3741
const RpcTopic& topic,
3842
const std::shared_ptr<ParticipantsDatabase>& participants_database,
@@ -405,6 +409,21 @@ void RpcBridge::transmit_(
405409
rpc_data.write_params.set_level();
406410
rpc_data.write_params.get_reference().related_sample_identity(registry_entry.second);
407411

412+
// Reply topics use VOLATILE durability: a reply written before the destination
413+
// client's reply reader has been matched on this writer's side is silently
414+
// dropped. As ROS 2 service clients never retry a request, that would hang the
415+
// client forever. The client's own \c wait_for_service only guarantees the match
416+
// from the reader side, so wait (bounded) for the writer side to match as well
417+
// before writing, closing the discovery race.
418+
if (!reply_writers_[registry_entry.first]->wait_reader_matched(
419+
registry_entry.second.writer_guid().guidPrefix, REPLY_MATCH_MAX_WAIT_MS))
420+
{
421+
EPROSIMA_LOG_WARNING(DDSPIPE_RPCBRIDGE,
422+
"RpcBridge for service " << rpc_topic_ << " writing reply towards participant "
423+
<< registry_entry.second.writer_guid().guidPrefix <<
424+
" whose reply reader is not matched yet; reply may be lost.");
425+
}
426+
408427
ret = reply_writers_[registry_entry.first]->write(*data);
409428

410429
if (ret != utils::ReturnCode::RETCODE_OK)

ddspipe_participants/include/ddspipe_participants/writer/rtps/CommonWriter.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
#pragma once
1616

1717
#include <atomic>
18+
#include <condition_variable>
19+
#include <mutex>
20+
#include <set>
1821

1922
#include <cpp_utils/pool/IPool.hpp>
2023
#include <cpp_utils/ReturnCode.hpp>
@@ -140,6 +143,16 @@ class CommonWriter : public BaseWriter, public fastdds::rtps::WriterListener
140143
fastdds::rtps::RTPSWriter*,
141144
eprosima::fastdds::dds::PolicyMask qos) noexcept override;
142145

146+
/**
147+
* @brief Wait (bounded) until a Reader belonging to \c reader_participant_guid_prefix is matched.
148+
*
149+
* @note Used by RPC bridges to avoid dropping VOLATILE replies on the writer-side discovery race.
150+
*/
151+
DDSPIPE_PARTICIPANTS_DllAPI
152+
bool wait_reader_matched(
153+
const core::types::GuidPrefix& reader_participant_guid_prefix,
154+
const unsigned int timeout_ms) const noexcept override;
155+
143156
/////////////////////
144157
// STATIC ATTRIBUTES
145158
/////////////////////
@@ -313,6 +326,15 @@ class CommonWriter : public BaseWriter, public fastdds::rtps::WriterListener
313326

314327
//! Pool Configuration to create the internal History.
315328
utils::PoolConfiguration pool_configuration_;
329+
330+
//! GUIDs of the Readers (external to this Participant) currently matched with this Writer
331+
std::set<fastdds::rtps::GUID_t> matched_readers_;
332+
333+
//! Protects \c matched_readers_ and coordinates \c wait_reader_matched
334+
mutable std::mutex matched_readers_mutex_;
335+
336+
//! Notified whenever \c matched_readers_ changes so \c wait_reader_matched can wake up
337+
mutable std::condition_variable matched_readers_cv_;
316338
};
317339

318340
} /* namespace rtps */

ddspipe_participants/src/cpp/writer/rtps/CommonWriter.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,19 @@ void CommonWriter::on_writer_matched(
116116
{
117117
if (!come_from_this_participant_(info.remoteEndpointGuid))
118118
{
119+
{
120+
std::lock_guard<std::mutex> lock(matched_readers_mutex_);
121+
if (info.status == fastdds::rtps::MatchingStatus::MATCHED_MATCHING)
122+
{
123+
matched_readers_.insert(info.remoteEndpointGuid);
124+
}
125+
else
126+
{
127+
matched_readers_.erase(info.remoteEndpointGuid);
128+
}
129+
}
130+
matched_readers_cv_.notify_all();
131+
119132
if (info.status == fastdds::rtps::MatchingStatus::MATCHED_MATCHING)
120133
{
121134
EPROSIMA_LOG_INFO(DDSPIPE_RTPS_COMMONWRITER_LISTENER,
@@ -131,6 +144,28 @@ void CommonWriter::on_writer_matched(
131144
}
132145
}
133146

147+
bool CommonWriter::wait_reader_matched(
148+
const core::types::GuidPrefix& reader_participant_guid_prefix,
149+
const unsigned int timeout_ms) const noexcept
150+
{
151+
std::unique_lock<std::mutex> lock(matched_readers_mutex_);
152+
153+
auto reader_from_participant_matched = [this, &reader_participant_guid_prefix]() -> bool
154+
{
155+
for (const auto& reader_guid : matched_readers_)
156+
{
157+
if (reader_guid.guidPrefix == reader_participant_guid_prefix)
158+
{
159+
return true;
160+
}
161+
}
162+
return false;
163+
};
164+
165+
return matched_readers_cv_.wait_for(
166+
lock, std::chrono::milliseconds(timeout_ms), reader_from_participant_matched);
167+
}
168+
134169
void CommonWriter::on_writer_change_received_by_all(
135170
fastdds::rtps::RTPSWriter* /*writer*/,
136171
fastdds::rtps::CacheChange_t* change)

0 commit comments

Comments
 (0)