fix: marshal provider events onto the source thread (#68)

* fix: marshal provider events onto the source thread

ModuleProxy's event listener emitted eventResponse directly on whatever thread
the module fired the event from (its worker/FFI thread). QtRemoteObjects then
serialized and sent the event from that foreign thread, racing the source
socket against a method reply being sent from the source thread, which silently
dropped the reply.

This is why a method that emits an event mid-call never returns to the caller
(e.g. delivery_module start(), which emits connectionStateChanged as the node
connects) while a method that emits nothing (createNode) returns fine.

Marshal the emission onto the ModuleProxy's own thread via a queued invocation
so events and method replies are serialized on the single thread
QtRemoteObjects expects to own the source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: generalize the threading comment

* fix: use AutoConnection so same-thread emits stay synchronous

QueuedConnection deferred every emission, breaking same-thread callers that
emit-then-assert and crashing when a queued lambda outlived the object.
AutoConnection invokes synchronously when already on the source thread and only
queues cross-thread emissions (the actual fix); passing 'this' as context
cancels a queued call if the object is destroyed first.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Igor Sirotin
2026-05-25 09:35:25 -04:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 4e20614cda
commit d77c3dd616
+14 -1
View File
@@ -9,7 +9,20 @@ ModuleProxy::ModuleProxy(LogosProviderObject* provider, QObject* parent)
if (m_provider) {
m_provider->setEventListener([this](const QString& eventName, const QVariantList& data) {
qDebug() << "[LogosProviderObject] ModuleProxy: forwarding event" << eventName << "as Qt signal";
emit eventResponse(eventName, data);
// Events may be fired from any thread (e.g. a module's worker/FFI
// thread), but this object is the QtRemoteObjects source and must be
// driven from its own thread. Emitting directly from a foreign
// thread runs QtRO's source serialization there, racing the source
// socket against a reply being sent from the source thread, which
// can silently drop the reply. AutoConnection keeps same-thread
// callers synchronous (the common case) and only queues the
// emission when it arrives from another thread, so events and
// replies stay serialized on the thread QtRO expects to own the
// source. Passing `this` as the context also cancels a queued
// emission if this object is destroyed first.
QMetaObject::invokeMethod(this, [this, eventName, data]() {
emit eventResponse(eventName, data);
}, Qt::AutoConnection);
});
qDebug() << "[LogosProviderObject] ModuleProxy: created, wrapping LogosProviderObject"
<< m_provider->providerName();