Skip to content

Commit 86251f3

Browse files
committed
When a new agent requests the graph, now it does not emit signals when building it
1 parent 1b32a1d commit 86251f3

17 files changed

Lines changed: 327 additions & 55 deletions

CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ include(cmake/profiling.cmake)
2727

2828
add_definitions(-I/usr/include/x86_64-linux-gnu/qt6/QtOpenGLWidgets/)
2929

30-
include_directories(/home/robocomp/robocomp/classes)
3130
add_subdirectory(api)
3231
add_subdirectory(core)
3332
add_subdirectory(gui)

api/CMakeLists.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,6 @@ target_include_directories(dsr_api
6767
# Headers of DSR Core
6868
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../core/include/>
6969
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../../qmat/include/>
70-
# TODO: Don't like, try to fix
71-
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../../../classes/>
7270
# Own include
7371
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/>
7472
$<INSTALL_INTERFACE:>

api/dsr_agent_info_api.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,21 @@
88
#include <unistd.h>
99
#include <sys/stat.h>
1010
#include <sys/times.h>
11+
#include <QMetaObject>
1112

1213
namespace DSR {
1314

15+
// Heartbeat runs on the raw Timer std::thread; bounce the agent-node update onto the DSRGraph's
16+
// thread (the agent's main/GUI thread) so the graph write + Qt-signal emit happen there, never on a
17+
// worker thread. QueuedConnection just posts; the timer thread does not block. If the graph thread's
18+
// event loop isn't running yet the post simply waits — correct and harmless.
19+
void AgentInfoAPI::heartbeat_tick()
20+
{
21+
if (G == nullptr)
22+
return;
23+
QMetaObject::invokeMethod(G, [this]() { create_or_update_agent(); }, Qt::QueuedConnection);
24+
}
25+
1426
namespace {
1527
struct PipeCloser
1628
{

api/dsr_api.cpp

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,45 @@ inline size_t full_graph_payload_size(const DSR::LWWGraphSnapshot& sample)
121121
{
122122
return sample.nodes.size() + sample.edges.size();
123123
}
124+
125+
// ── Deferred signal emission (fix for the bottle-join heap-corruption data race) ─────────────────
126+
// Remote deltas are applied on the MAIN thread (the subscription functors marshal via
127+
// QMetaObject::invokeMethod) while holding unique_lock(_mutex). Emitting DSR Qt signals there is
128+
// hazardous twice over: (1) a queued std::string payload emitted from a worker thread races the
129+
// receiver's QMetaType marshaling (TSan-confirmed; corrupts the heap, fatal in heavy-heap consumers
130+
// like voxelizer); (2) emitting WHILE holding _mutex lets a same-thread re-entrant slot deadlock
131+
// (EDEADLK). So during a remote apply we DEFER every emit into this thread-local buffer and flush it
132+
// AFTER releasing the lock, on the main thread — no cross-thread payload, no re-entrant deadlock.
133+
// Outside a deferred apply (defer depth 0, e.g. local writes) emits fire immediately as before.
134+
thread_local int tl_defer_depth = 0;
135+
thread_local std::vector<std::function<void()>> tl_deferred_signals;
136+
137+
void defer_or_emit(std::function<void()> emit_fn)
138+
{
139+
if (tl_defer_depth > 0)
140+
tl_deferred_signals.push_back(std::move(emit_fn));
141+
else
142+
emit_fn();
143+
}
144+
145+
// RAII for a remote-delta apply: turn on deferral for the duration, then on destruction (which the
146+
// caller arranges to happen AFTER the unique_lock block has ended) flush the buffered emits. So the
147+
// signal emits fire on the main thread, after the graph lock is released.
148+
struct RemoteApplyScope
149+
{
150+
RemoteApplyScope() { ++tl_defer_depth; }
151+
~RemoteApplyScope()
152+
{
153+
if (--tl_defer_depth == 0)
154+
{
155+
auto pending = std::move(tl_deferred_signals);
156+
tl_deferred_signals.clear();
157+
for (auto& f : pending) f();
158+
}
159+
}
160+
RemoteApplyScope(const RemoteApplyScope&) = delete;
161+
RemoteApplyScope& operator=(const RemoteApplyScope&) = delete;
162+
};
124163
}
125164

126165
void print_sample_info(DSR::GraphSettings::LOGLEVEL log_level, const DSR::Transport::ReceivedSampleInfo& info);
@@ -340,10 +379,17 @@ DSRGraph::SampleCallback<Sample> DSRGraph::make_node_subscription_functor_impl(c
340379
qDebug() << name << " Received:" << std::to_string(sample.id).c_str() << " node from: "
341380
<< m_info.source_entity_id;
342381
}
343-
tp.spawn_task([this, sample = std::move(sample)]() mutable {
344-
std::unique_lock<std::shared_mutex> lock(_mutex);
345-
engine_->apply_remote_node_delta(NodeDeltaMessage{std::move(sample)});
346-
});
382+
// Apply on the graph's (main) thread with deferred emit: marshal to main so the apply +
383+
// (deferred) signal emits run on the main/GUI thread — no std::string Qt payload crosses
384+
// a thread boundary (the bottle-join heap-corruption race) — and RemoteApplyScope flushes
385+
// the emits AFTER the unique_lock block ends (no re-entrant EDEADLK). Sample is MOVED in.
386+
QMetaObject::invokeMethod(this, [this, sample = std::move(sample)]() mutable {
387+
RemoteApplyScope defer;
388+
{
389+
std::unique_lock<std::shared_mutex> lock(_mutex);
390+
engine_->apply_remote_node_delta(NodeDeltaMessage{std::move(sample)});
391+
}
392+
}, Qt::QueuedConnection);
347393
}
348394
catch (const std::exception &ex) { std::cerr << ex.what() << std::endl; }
349395
};
@@ -366,10 +412,14 @@ DSRGraph::SampleCallback<Sample> DSRGraph::make_edge_subscription_functor_impl(c
366412
if (!network_compatibility_or_fatal(channel, sample.protocol_version, sample.sync_mode, sync_mode)) {
367413
return;
368414
}
369-
tp.spawn_task([this, sample = std::move(sample)]() mutable {
370-
std::unique_lock<std::shared_mutex> lock(_mutex);
371-
engine_->apply_remote_edge_delta(EdgeDeltaMessage{std::move(sample)});
372-
});
415+
// Apply on the graph's (main) thread with deferred emit — see apply_remote_node_delta note.
416+
QMetaObject::invokeMethod(this, [this, sample = std::move(sample)]() mutable {
417+
RemoteApplyScope defer;
418+
{
419+
std::unique_lock<std::shared_mutex> lock(_mutex);
420+
engine_->apply_remote_edge_delta(EdgeDeltaMessage{std::move(sample)});
421+
}
422+
}, Qt::QueuedConnection);
373423
}
374424
catch (const std::exception &ex) { std::cerr << ex.what() << std::endl; }
375425
};
@@ -396,11 +446,15 @@ DSRGraph::SampleCallback<Batch> DSRGraph::make_edge_attrs_subscription_functor_i
396446
qDebug() << name << " Received:" << samples.vec.size() << " edge attr from: "
397447
<< m_info.source_entity_id;
398448
}
399-
tp_delta_attr.spawn_task([this, samples = std::move(samples)]() mutable {
449+
// Apply on the graph's (main) thread with deferred emit — see apply_remote_node_delta note.
450+
QMetaObject::invokeMethod(this, [this, samples = std::move(samples)]() mutable {
400451
CORTEX_PROFILE_ZONE_N("DSRGraph::edge_attrs_subscription_thread apply batch");
401-
std::unique_lock<std::shared_mutex> lock(_mutex);
402-
engine_->apply_remote_edge_attr_batch(EdgeAttrDeltaBatchMessage{std::move(samples)});
403-
});
452+
RemoteApplyScope defer;
453+
{
454+
std::unique_lock<std::shared_mutex> lock(_mutex);
455+
engine_->apply_remote_edge_attr_batch(EdgeAttrDeltaBatchMessage{std::move(samples)});
456+
}
457+
}, Qt::QueuedConnection);
404458
}
405459
catch (const std::exception &ex) { std::cerr << ex.what() << std::endl; }
406460
};
@@ -427,11 +481,15 @@ DSRGraph::SampleCallback<Batch> DSRGraph::make_node_attrs_subscription_functor_i
427481
qDebug() << name << " Received:" << samples.vec.size() << " node attrs from: "
428482
<< m_info.source_entity_id;
429483
}
430-
tp_delta_attr.spawn_task([this, samples = std::move(samples)]() mutable {
484+
// Apply on the graph's (main) thread with deferred emit — see apply_remote_node_delta note.
485+
QMetaObject::invokeMethod(this, [this, samples = std::move(samples)]() mutable {
431486
CORTEX_PROFILE_ZONE_N("DSRGraph::node_attrs_subscription_thread apply batch");
432-
std::unique_lock<std::shared_mutex> lock(_mutex);
433-
engine_->apply_remote_node_attr_batch(NodeAttrDeltaBatchMessage{std::move(samples)});
434-
});
487+
RemoteApplyScope defer;
488+
{
489+
std::unique_lock<std::shared_mutex> lock(_mutex);
490+
engine_->apply_remote_node_attr_batch(NodeAttrDeltaBatchMessage{std::move(samples)});
491+
}
492+
}, Qt::QueuedConnection);
435493
}
436494
catch (const std::exception &ex) { std::cerr << ex.what() << std::endl; }
437495
};
@@ -1052,46 +1110,57 @@ void DSRGraph::for_each_edge_of_type_cache(const std::string& type, std::functio
10521110
}
10531111
}
10541112

1113+
// NOTE: these run during a remote-delta apply (on the main thread, under unique_lock(_mutex)).
1114+
// defer_or_emit buffers each emit (args captured by value, produced here on main) so it fires AFTER
1115+
// the lock is released — see the deferred-emission note near the top of this file.
10551116
void DSRGraph::on_remote_node_updated(uint64_t id, const std::string& type, uint32_t agent_id)
10561117
{
1057-
emitter.update_node_signal(id, type, SignalInfo{agent_id});
1118+
defer_or_emit([this, id, type, agent_id]{ emitter.update_node_signal(id, type, SignalInfo{agent_id}); });
10581119
}
10591120

10601121
void DSRGraph::on_remote_node_deleted(uint64_t id, const std::optional<Node>& node, const std::vector<Edge>& removed_edges, uint32_t agent_id)
10611122
{
1062-
emitter.del_node_signal(id, SignalInfo{agent_id});
1063-
if (node.has_value()) {
1064-
emitter.deleted_node_signal(*node, SignalInfo{agent_id});
1065-
}
1066-
for (const auto& edge : removed_edges) {
1067-
emitter.del_edge_signal(edge.from(), edge.to(), edge.type(), SignalInfo{agent_id});
1068-
emitter.deleted_edge_signal(edge, SignalInfo{agent_id});
1069-
}
1123+
defer_or_emit([this, id, node, removed_edges, agent_id]{
1124+
emitter.del_node_signal(id, SignalInfo{agent_id});
1125+
if (node.has_value()) {
1126+
emitter.deleted_node_signal(*node, SignalInfo{agent_id});
1127+
}
1128+
for (const auto& edge : removed_edges) {
1129+
emitter.del_edge_signal(edge.from(), edge.to(), edge.type(), SignalInfo{agent_id});
1130+
emitter.deleted_edge_signal(edge, SignalInfo{agent_id});
1131+
}
1132+
});
10701133
}
10711134

10721135
void DSRGraph::on_remote_edge_updated(uint64_t from, uint64_t to, const std::string& type, uint32_t agent_id)
10731136
{
1074-
emitter.update_edge_signal(from, to, type, SignalInfo{agent_id});
1137+
defer_or_emit([this, from, to, type, agent_id]{ emitter.update_edge_signal(from, to, type, SignalInfo{agent_id}); });
10751138
}
10761139

10771140
void DSRGraph::on_remote_edge_deleted(uint64_t from, uint64_t to, const std::string& type, const std::optional<Edge>& edge, uint32_t agent_id)
10781141
{
1079-
emitter.del_edge_signal(from, to, type, SignalInfo{agent_id});
1080-
if (edge.has_value()) {
1081-
emitter.deleted_edge_signal(*edge, SignalInfo{agent_id});
1082-
}
1142+
defer_or_emit([this, from, to, type, edge, agent_id]{
1143+
emitter.del_edge_signal(from, to, type, SignalInfo{agent_id});
1144+
if (edge.has_value()) {
1145+
emitter.deleted_edge_signal(*edge, SignalInfo{agent_id});
1146+
}
1147+
});
10831148
}
10841149

10851150
void DSRGraph::on_remote_node_attrs_updated(uint64_t id, const std::string& type, const std::vector<std::string>& attrs, uint32_t agent_id)
10861151
{
1087-
emitter.update_node_attr_signal(id, attrs, SignalInfo{agent_id});
1088-
emitter.update_node_signal(id, type, SignalInfo{agent_id});
1152+
defer_or_emit([this, id, type, attrs, agent_id]{
1153+
emitter.update_node_attr_signal(id, attrs, SignalInfo{agent_id});
1154+
emitter.update_node_signal(id, type, SignalInfo{agent_id});
1155+
});
10891156
}
10901157

10911158
void DSRGraph::on_remote_edge_attrs_updated(uint64_t from, uint64_t to, const std::string& type, const std::vector<std::string>& attrs, uint32_t agent_id)
10921159
{
1093-
emitter.update_edge_attr_signal(from, to, type, attrs, SignalInfo{agent_id});
1094-
emitter.update_edge_signal(from, to, type, SignalInfo{agent_id});
1160+
defer_or_emit([this, from, to, type, attrs, agent_id]{
1161+
emitter.update_edge_attr_signal(from, to, type, attrs, SignalInfo{agent_id});
1162+
emitter.update_edge_signal(from, to, type, SignalInfo{agent_id});
1163+
});
10951164
}
10961165

10971166

api/dsr_crdt_sync_engine.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ CRDTSyncEngine::CRDTSyncEngine(SyncEngineHost& host, const CRDTSyncEngine& other
8484
unprocessed_delta_node_att_(other.unprocessed_delta_node_att_),
8585
unprocessed_delta_edge_from_(other.unprocessed_delta_edge_from_),
8686
unprocessed_delta_edge_to_(other.unprocessed_delta_edge_to_),
87-
unprocessed_delta_edge_att_(other.unprocessed_delta_edge_att_)
87+
unprocessed_delta_edge_att_(other.unprocessed_delta_edge_att_),
88+
first_full_graph_(other.first_full_graph_)
8889
{
8990
}
9091

@@ -986,6 +987,12 @@ void CRDTSyncEngine::join_full_graph(OrMap&& full_graph)
986987
return;
987988
}
988989

990+
// The FIRST full-graph import builds the local copy from scratch (join). There is no change to
991+
// notify, so emit NO signals — the whole-graph signal cascade from this (non-Qt) sync thread is
992+
// the churn heap-corruption crash. Later imports (re-sync) still emit their deltas.
993+
const bool emit_signals = !first_full_graph_;
994+
first_full_graph_ = false;
995+
989996
std::vector<std::tuple<bool, uint64_t, std::string, std::optional<CRDT::Node>, std::optional<CRDT::Node>>> updates;
990997

991998
uint64_t id{0}, timestamp{0};
@@ -1099,6 +1106,7 @@ void CRDTSyncEngine::join_full_graph(OrMap&& full_graph)
10991106
}
11001107
{
11011108
CORTEX_PROFILE_DETAIL_N("CRDTSyncEngine::join_full_graph emit phase");
1109+
if (emit_signals)
11021110
for (auto &[signal, node_id, type, nd, current_nd] : updates) {
11031111
if (signal) {
11041112
if (!nd.has_value() || nd->attrs != current_nd->attrs) {

0 commit comments

Comments
 (0)