RpcServerTcp::stop() and RpcServerSsl::stop() closed m_acceptor on whatever
thread called them — in practice the host thread, via ~PlainTransportHost —
while doAccept() re-armed async_accept from inside its own completion handler,
on the io worker. Nothing serialized the two.
This is the acceptor half of the race PR #38 fixed for RpcConnection, and it
fails identically: asio acceptors are "Shared objects: Unsafe", and close()
runs cleanup_descriptor_data(), which nulls the reactor's per-descriptor state
while reactive_socket_service_base::start_op() holds it by reference. It was
left out of #38 because every backtrace captured in the wild was a write
initiation, never an accept — but it reproduces on demand:
EXC_BAD_ACCESS KERN_INVALID_ADDRESS at 0x98
logos::plain::RpcServerTcp::doAccept()
...reactive_socket_move_accept_op<...>::do_complete(...)
logos::plain::IoContextPool::IoContextPool()::$_0 <- io worker thread
Both servers now own a strand. doAccept()'s completion handler is
bind_executor'd onto it (so the re-arm runs there) and stop() hands the close
to it with dispatch() — inline when already on the strand, queued and
non-blocking from anywhere else, exactly as RpcConnection::closeStreamOnStrand
does.
start() still runs open/bind/listen inline: callers read boundPort() the moment
it returns. That is safe because no async op on the acceptor exists yet, and
PlainTransportHost serializes start()/stop() under its own mutex. Only the
accept loop moves onto the strand, which is invisible to clients — listen() has
already run, so an early connect waits in the backlog.
Deferring the close leaves the listener open for the microseconds between
stop() returning and the strand running it, so a connection can still be
accepted in that gap. The accept path therefore tests m_stopped and publishes
the connection under one lock, and drops a late socket instead of wrapping it
in a connection and stop()ing it — conn->stop() would call
onConnectionClosed() on the IncomingCallHandler whose destructor started this
teardown. The TLS server gets the same guard, where it was already latent: an
async_handshake in flight was never aborted by closing the acceptor.
Adds RpcServerTeardownTest: a start/connect/stop stress loop shaped like
test_rpc_connection_teardown.cpp, plus a round-trip check that a client
connecting the instant start() returns is still served.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* 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>
* Extract the Logos protocol layer from logos-cpp-sdk
Transports (plain TCP/TLS, qt_local, qt_remote/QRO, mock), token manager,
consumer core (LogosAPIClient/LogosAPIConsumer incl. the capability
auto-requestModule flow), ModuleProxy, the abstract LogosProviderObject
interface, and the canonical QVariant<->JSON conversion — now behind the
language-neutral lp_* C ABI (logos_protocol.h) carrying the protocol
semver (LOGOS_PROTOCOL_VERSION_*, lp_protocol_version()).
Bytes crossing the ABI use the lossless {"_bytes": base64url} tagging
(NUL-safe), matching the plain wire encoding.
Provider lp_* surface is compiled groundwork; serving lands with module
authoring.
* consumer: typed requestModule for the capability flow
Port of logos-cpp-sdk master f5a127dd ('use updated capability module',
cpp-sdk#85, Iuri Matias) — the touched files (logos_api_client.cpp,
logos_api_consumer.{h,cpp}) moved into this repo in the P1 extraction.
The capability auto-requestModule path now calls a typed std::string
helper on the consumer (which acquires the capability object directly)
instead of a stringly invokeRemoteMethod round-trip. 111/111 tests.