mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-31 14:01:14 +00:00
* feat: per-module concurrent dispatch (concurrency:"multi") — zero ABI change
A "multi" module serves calls concurrently behind the ORDINARY callMethod — no
new provider/host vtable method, so LogosProviderObject's ABI is byte-identical
to before and an old host/daemon loads and forwards a multi module unmodified.
Mechanism: a multi module's generated glue returns a pending sentinel
({"__logos_pending_call__": callId}) from callMethod and pushes the real result
back later as a __logos_call_complete__ event keyed by callId, over the existing
event channel. The consumer transport detects the sentinel and awaits the
completion transparently, so generated clients are unchanged.
- logos_async_dispatch.h: shared wire constants + the contract.
- remote_transport.cpp (QtRO) / plain_logos_object.{h,cpp} (plain): consumer
sentinel detection + await keyed by callId. The host is a pure forwarder.
- logos_protocol.h + nix/default.nix: protocol 0.2.0 (additive minor; same MAJOR
stays compatible, so an old host accepts a 0.2 "multi" module).
- rpc_server.cpp: fix a teardown self-deadlock (stop() held m_mu while invoking a
per-connection error handler that re-locks m_mu) that the new in-process
subscription path exposed.
- tests/protocol/test_concurrent_dispatch.cpp: proves a multi provider overlaps
two concurrent calls (peak 2) while single serializes (peak 1), over the plain
transport, with the host unchanged from master.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: coalesce concurrent async requestModule handshakes (+ async fan-out test)
A driver that fans out N async calls to an un-tokened target before any
completes used to fire N separate requestModule handshakes. Each mints a
distinct capability token and informs the target, and the later inform
OVERWRITES the earlier token there (the target stores one token per caller),
so the already-dispatched calls carried a superseded token and the target
rejected them as unauthorized ("auth token not recognized"). The sync path
never hit this — it blocks per call, so handshakes never overlap.
Coalesce in LogosAPIClient::invokeRemoteMethodAsync: the first async call to
an un-tokened target starts ONE handshake; concurrent calls to the same
target queue behind it and all drain with the single minted token when it
resolves. m_pendingHandshakes is touched only on the owner thread, so no lock
(appended last per the class's ABI note). This is what lets a concurrency:
"multi" worker actually run a single-threaded driver's fan-out concurrently —
otherwise the fanned-out calls are rejected before reaching dispatch.
Also add MultiProviderOverlapsAsync / SingleProviderSerializesAsync to the
concurrent-dispatch gtest: they fire N concurrent callMethodAsync() calls (the
fan-out pattern over the async consumer path, which the sync tests don't
exercise) and assert peak overlap 4 for "multi", 1 for "single".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
3.1 KiB
C++
80 lines
3.1 KiB
C++
#ifndef LOGOS_PLAIN_LOGOS_OBJECT_H
|
|
#define LOGOS_PLAIN_LOGOS_OBJECT_H
|
|
|
|
#include "logos_object.h"
|
|
|
|
#include "rpc_connection.h"
|
|
|
|
#include <condition_variable>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace logos::plain {
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// PlainLogosObject — consumer-side LogosObject backed by the plain-C++
|
|
// RPC runtime. Identical public shape to LocalLogosObject / RemoteLogosObject
|
|
// so LogosAPIConsumer doesn't care which backend it's talking to.
|
|
//
|
|
// Owns a shared_ptr<RpcConnectionBase>; the transport layer hands the
|
|
// connection over after opening the socket. release() stops the connection.
|
|
// -----------------------------------------------------------------------------
|
|
class PlainLogosObject : public LogosObject {
|
|
public:
|
|
PlainLogosObject(std::string objectName,
|
|
std::shared_ptr<RpcConnectionBase> conn);
|
|
~PlainLogosObject() override;
|
|
|
|
QVariant callMethod(const QString& authToken,
|
|
const QString& methodName,
|
|
const QVariantList& args,
|
|
int timeoutMs) override;
|
|
|
|
void callMethodAsync(const QString& authToken,
|
|
const QString& methodName,
|
|
const QVariantList& args,
|
|
int timeoutMs,
|
|
AsyncResultCallback callback) override;
|
|
|
|
bool informModuleToken(const QString& authToken,
|
|
const QString& moduleName,
|
|
const QString& token,
|
|
int timeoutMs) override;
|
|
|
|
void onEvent(const QString& eventName, EventCallback callback) override;
|
|
void disconnectEvents() override;
|
|
void emitEvent(const QString& eventName, const QVariantList& data) override;
|
|
QJsonArray getMethods() override;
|
|
void release() override;
|
|
quintptr id() const override;
|
|
|
|
private:
|
|
// Deferred ("multi") completion rendezvous. A multi provider returns a
|
|
// pending sentinel (logos::pendingCallKey) from callMethod and later pushes
|
|
// the real result as a logos::callCompleteEvent event keyed by callId. We
|
|
// subscribe to that event EAGERLY (before any call can defer) so a completion
|
|
// racing ahead of the waiter is buffered, then block the caller until the
|
|
// matching callId lands. The completion arrives on the connection's IO
|
|
// thread; the caller waits on another thread — m_completionMu/Cv bridge them.
|
|
void ensureCompletionSub();
|
|
QVariant awaitCompletion(const QString& callId, int timeoutMs);
|
|
|
|
std::string m_objectName;
|
|
std::shared_ptr<RpcConnectionBase> m_conn;
|
|
std::mutex m_mu;
|
|
std::vector<std::pair<QString, EventCallback>> m_subs;
|
|
|
|
std::mutex m_completionMu;
|
|
std::condition_variable m_completionCv;
|
|
std::map<QString, QVariant> m_completions;
|
|
bool m_completionSubscribed = false;
|
|
};
|
|
|
|
} // namespace logos::plain
|
|
|
|
#endif // LOGOS_PLAIN_LOGOS_OBJECT_H
|