morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
backend.hpp
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <array>
6#include <atomic>
7#include <chrono>
8#include <cstddef>
9#include <cstdint>
10#include <exception>
11#include <functional>
12#include <future>
13#include <memory>
14#include <mutex>
15#include <optional>
16#include <stdexcept>
17#include <string>
18#include <string_view>
19#include <type_traits>
20#include <unordered_map>
21#include <unordered_set>
22#include <utility>
23#include <vector>
24
25#include "../attributes.hpp"
26#include "../session/session.hpp"
27#include "completion.hpp"
28#include "detail/instance_directory.hpp"
30#include "model.hpp"
31#include "observability.hpp"
32#include "profiler.hpp"
33#include "registry.hpp"
34#include "strand.hpp"
35#include "wire.hpp"
36
37namespace morph::backend {
38
39namespace detail {
40
41/// @brief All the information needed to dispatch one action call through a backend.
42///
43/// Backends that execute locally use `localOp` directly. Remote backends
44/// serialize via `serializeAction` and deserialize replies via `deserializeResult`.
45///
46/// @par Why function *pointers*, and why the action travels beside them
47/// The three callables are plain function pointers and the action object they
48/// operate on rides in `action`, rather than each callable capturing its own
49/// handle to it. That is a deliberate allocation decision, not a style
50/// preference. `Bridge::executeVia` builds an `ActionCall` on **every** call,
51/// including one that a `LocalBackend` will serve and that will therefore never
52/// touch `serializeAction` or `deserializeResult`. A `std::function` whose
53/// target is not trivially copyable cannot use libstdc++'s small-object buffer,
54/// so a lambda capturing a `shared_ptr<Action>` heap-allocates — three captured
55/// callables cost up to three allocations per dispatch whichever path the call
56/// takes. Stateless operations parameterised on the action cost none: the
57/// per-`(Model, Action)` behaviour is a compile-time constant, addressed rather
58/// than copied. Measured with `morph_bench_alloc`: this shape and the two
59/// `string_view` ids below together account for 3 of the allocations a local
60/// round trip would otherwise make, out of 17.
61///
62/// @par Lifetime contract for the callables
63/// `serializeAction` and `localOp` take the action as an opaque pointer and do
64/// **not** own it: `action` does. A backend that defers either call past the
65/// `ActionCall`'s own lifetime must carry a copy of the `action` handle with
66/// it — `LocalBackend::execute` does exactly that when it posts to the strand.
67/// `deserializeResult` never reads the action and so may outlive it, which is
68/// what lets the remote backends store it in their pending-reply table.
69struct ActionCall {
70 /// @brief String id of the target model type (from `ModelTraits`).
71 ///
72 /// A view, not a copy, valid for as long as the call is. A typed call's id
73 /// is `ModelTraits<Model>::typeId()`, a view of the string literal
74 /// `BRIDGE_REGISTER_MODEL` was given, with static storage duration. A raw
75 /// call's id points into the call's own `action`, so it lives exactly as
76 /// long as the `ActionCall` and its copies. A backend that keeps an id
77 /// past the call — a reply table, a metrics label — copies it. A
78 /// hand-built `ActionCall` must observe the same rule: the id must outlive
79 /// the dispatch, which a temporary `std::string` does not.
80 std::string_view modelTypeId;
81
82 /// @brief String id of the action type (from `ActionTraits`).
83 ///
84 /// Same storage rule as `modelTypeId`.
85 std::string_view actionTypeId;
86
87 /// @brief Type-erased owner of the action object `serializeAction` and
88 /// `localOp` read.
89 ///
90 /// Null only for a hand-built call whose callables ignore their action
91 /// argument.
92 std::shared_ptr<void> action;
93
94 /// @brief Serialises `action` to JSON. Called only on the remote path.
95 std::string (*serializeAction)(const void* action) = nullptr;
96
97 /// @brief Deserialises a JSON reply into the opaque result `shared_ptr<void>`.
98 std::shared_ptr<void> (*deserializeResult)(std::string_view json) = nullptr;
99
100 /// @brief Executes `action` directly against a model holder. Used on the local path.
101 ///
102 /// Takes the action as a mutable pointer because the local path recomputes
103 /// an action's computed fields in place before the validator or the model
104 /// sees it (`Bridge::executeVia`, `morph::forms::recomputeAll`).
105 std::shared_ptr<void> (*localOp)(::morph::model::detail::IModelHolder& holder, void* action) = nullptr;
106
107 /// @brief Receives a Task handler's outcome on the local path: the opaque
108 /// result, or the exception (with a null result).
109 using LocalDone = std::function<void(std::shared_ptr<void>, std::exception_ptr)>;
110
111 /// @brief Starts a Task handler against a model holder, on the model's
112 /// strand, and reports its outcome through the last argument when the
113 /// Task completes. Set instead of `localOp` for an action whose
114 /// handler returns `core::async::Task`; see
115 /// `docs/spec/core/coroutines.md`.
116 ///
117 /// Takes the action's owner rather than a borrowed pointer: the handler's
118 /// frame outlives the call that starts it.
119 void (*localOpAsync)(::morph::model::detail::IModelHolder& holder, std::shared_ptr<void> action,
120 const std::shared_ptr<::morph::exec::detail::TaskResumer>& executor,
121 ::core::async::StopToken token, LocalDone done) = nullptr;
122
123 /// @brief The stop source a Task handler's token comes from, or null for
124 /// none. `Bridge::executeVia` sets one when an execute deadline is
125 /// armed, and the deadline requests stop on it.
126 std::shared_ptr<::core::async::StopSource> stopSource;
127
128 /// @brief Session context attached to this call.
129 ///
130 /// Local backends thread it through a thread-local before invoking `localOp`;
131 /// remote backends serialise it into the wire envelope.
132 ::morph::session::Context session;
133
134 /// @brief Invokes `serializeAction` on the action this call owns.
135 ///
136 /// The one place the borrow in `serializeAction`'s signature is closed, so
137 /// no remote backend has to remember to pair the pointer with its owner --
138 /// and the one place the null check lives. An empty `std::function` used to
139 /// raise `std::bad_function_call` here; a null function pointer would be
140 /// undefined behaviour instead, so the check is explicit and names the
141 /// field.
142 ///
143 /// @return The JSON body for the wire envelope.
144 /// @throws std::runtime_error if `serializeAction` is null.
145 [[nodiscard]] std::string serializeBody() const {
146 if (serializeAction == nullptr) {
147 throw std::runtime_error{"ActionCall::serializeAction is null: nothing to serialise"};
148 }
149 return serializeAction(action.get());
150 }
151};
152
153/// @brief One *bind* request: everything needed to acquire a model instance.
154///
155/// The request half of `IBackend::bindModel`, the one acquire verb. Its *shape*
156/// selects the behaviour:
157///
158/// | `primary` | `current` | Meaning |
159/// |---|---|---|
160/// | empty | `0` | A private instance, never entered in the shared directory. |
161/// | non-empty | `0` | Register-or-attach the shared instance for `(typeId, primary)`. |
162/// | non-empty | non-zero | Re-point from `current` to the shared instance for `(typeId, primary)`. |
163/// | empty | non-zero | Give `current` up and bind a private instance instead. |
164///
165/// Every string is **owned**: a bind may outlive the frame that issued it, so a
166/// `string_view` into the caller's stack would dangle for a backend that builds
167/// its envelope after the call returns.
168struct BindRequest {
169 /// @brief String type-id of the model to instantiate (from `ModelTraits`).
170 std::string typeId;
171
172 /// @brief Callable that constructs the `IModelHolder`. Local path only; a
173 /// wire backend never calls it, and an attach to a live shared
174 /// instance does not call it either.
175 std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory;
176
177 /// @brief Entity key for the action log; empty if none. See `journal::LogEntry::entityKey`.
178 std::string contextKey;
179
180 /// @brief Canonical string encoding of the primary key. Empty means "no
181 /// identity": the bind produces a private instance that never enters
182 /// the shared directory.
183 std::string primary;
184
185 /// @brief Instance currently held, or `ModelId{0}` if none. Non-zero makes
186 /// this bind a *re-point*: the backend releases it once the
187 /// replacement is acquired.
188 ::morph::exec::detail::ModelId current{};
189};
190
191/// @brief One *promote* request: file an already-live instance under a key.
192///
193/// The request half of `IBackend::promoteModel`, the structural counterpart of
194/// `assignPrimary`. Its strings are owned for the same reason `BindRequest`'s
195/// are.
196struct PromoteRequest {
197 /// @brief Live instance to promote.
198 ::morph::exec::detail::ModelId mid{};
199
200 /// @brief Model type id — the directory's first key component.
201 std::string typeId;
202
203 /// @brief Canonical string encoding of the key to file @p mid under.
204 std::string primary;
205};
206
207/// @brief Abstract interface for execution backends (local, remote, …).
208///
209/// A backend owns model instances and dispatches actions against them.
210/// `Bridge` holds one active backend at a time and can swap it via
211/// `Bridge::switchBackend()`.
212///
213/// A backend installed in a `Bridge` belongs to the bridge's owner: the bridge
214/// calls every verb from there (`setOwner` tells the backend which executor
215/// that is), so a backend keeps the state those verbs touch without a lock.
216/// What arrives from the backend's own threads — a reply, a reconnect — reaches
217/// the bridge through an executor the bridge names: `bindModel`/`promoteModel`
218/// deliver on the one they are given, and `setReconnectHandler` posts to the
219/// one it is given.
220// NOLINTBEGIN(cppcoreguidelines-special-member-functions)
221struct IBackend {
222 virtual ~IBackend() = default;
223
224 /// @brief Registers a new model instance and returns its opaque id.
225 /// @param typeId String type-id of the model to instantiate.
226 /// @param factory Callable that constructs the `IModelHolder` (local path only).
227 /// @return Newly assigned `ModelId`.
228 virtual ::morph::exec::detail::ModelId registerModel(
229 const std::string& typeId, std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory) = 0;
230
231 /// @brief Registers a new model instance, additionally passing @p contextKey —
232 /// the instance's stable identity (e.g. an account id) — through to
233 /// backends that can make use of it.
234 ///
235 /// Default implementation forwards to `registerModel()` and drops @p contextKey,
236 /// which is exactly correct for `LocalBackend`: the caller's own @p factory
237 /// closure already captures whatever identity it needs directly (see
238 /// `IModelHolder::attachActionLog`), so there is nothing for the backend to
239 /// forward. Backends whose model instances live behind a wire protocol
240 /// (`SimulatedRemoteBackend`) override this to carry @p contextKey across —
241 /// see `wire::Envelope::contextKey` and `ServerConfig::logProvider`.
242 /// @param typeId String type-id of the model to instantiate.
243 /// @param factory Callable that constructs the `IModelHolder` (local path only).
244 /// @param contextKey Stable identity of the new instance; empty if none.
245 /// @return Newly assigned `ModelId`.
246 virtual ::morph::exec::detail::ModelId registerModelWithContext(
247 const std::string& typeId, std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory,
248 std::string_view contextKey) {
249 (void)contextKey;
250 return registerModel(typeId, std::move(factory));
251 }
252
253 /// @brief Enters an already-live instance into the directory under @p primary.
254 ///
255 /// The *promotion* half of keyed instances, and what makes a result-sourced
256 /// key work without losing state: an action that creates its own entity runs
257 /// on an instance that does not yet have a key, and the key only exists once
258 /// the result comes back. Re-pointing to a freshly created instance would
259 /// strand everything the create just did, so instead the instance the action
260 /// ran on is given the generated key in place.
261 ///
262 /// A no-op when @p primary is empty, when @p mid is not live, when another
263 /// instance already holds that key, when @p mid itself already holds a
264 /// *different* real key, or when @p mid was *created* for a key — even one
265 /// it has since been evicted from as poisoned. The existing holder of a key
266 /// always wins (a promotion can never silently displace a directory entry
267 /// other handlers are attached to), and an instance that was ever keyed
268 /// never changes key (a promotion can never silently move one out from
269 /// under handlers already attached to it, nor hand later attachers a model
270 /// that still identifies itself as the entity it was built for) — only an
271 /// instance that has never held a key can be promoted.
272 ///
273 /// @param mid Live instance to promote.
274 /// @param typeId Model type id — the directory's first key component.
275 /// @param primary Canonical string encoding of the key to file it under.
276 virtual void assignPrimary(::morph::exec::detail::ModelId mid, const std::string& typeId,
277 std::string_view primary) {
278 (void)mid;
279 (void)typeId;
280 (void)primary;
281 }
282
283 /// @brief Acquires a model instance, selected by @p request's shape (see
284 /// `BindRequest`), and reports it through a `Completion` delivered
285 /// on @p cbExec.
286 ///
287 /// A backend that can settles the `Completion` before returning — the
288 /// default does, and so does every backend whose registration needs no
289 /// round trip — so a `Bridge` handler bound through it can dispatch at once.
290 /// A backend whose registration is a round trip settles it when the reply
291 /// arrives, on whatever thread that is; the continuation still runs on
292 /// @p cbExec, which `Bridge` passes as its owner.
293 ///
294 /// The default implementation has no shared directory: every shape binds a
295 /// private instance through `registerModelWithContext`, and a non-zero
296 /// `current` is released once the replacement is acquired, so a throwing
297 /// acquire never strands the caller with neither instance. An exception
298 /// from either verb rejects the `Completion` rather than propagating, so a
299 /// caller has one failure channel.
300 ///
301 /// @param request Owning bind request; moved from.
302 /// @param cbExec Executor the continuation is delivered on. Borrowed: it
303 /// must outlive the returned `Completion`.
304 /// @return A `Completion` resolved with the bound `ModelId`, or rejected
305 /// with the failure.
306 virtual ::morph::async::Completion<::morph::exec::detail::ModelId> bindModel(BindRequest request,
307 ::morph::exec::IExecutor& cbExec) {
308 auto [completion, promise] =
310 try {
311 auto const next = registerModelWithContext(request.typeId, std::move(request.factory), request.contextKey);
312 if (request.current.v != 0U) {
313 deregisterModel(request.current);
314 }
315 promise.resolve(next);
316 } catch (...) {
317 promise.reject(std::current_exception());
318 }
319 return std::move(completion);
320 }
321
322 /// @brief Files an already-live instance under a key: the structural
323 /// counterpart of `assignPrimary`.
324 ///
325 /// The default implementation calls `assignPrimary` and settles the
326 /// returned `Completion` before returning, echoing @p request's `mid`
327 /// back — including for every no-op case `assignPrimary` documents
328 /// (empty primary, dead `mid`, key already taken, `mid` already keyed
329 /// differently), which are not backend failures and therefore resolve
330 /// rather than reject.
331 ///
332 /// @param request Owning promote request. By value, matching `bindModel`,
333 /// so an overriding backend can move it into its pending
334 /// state; this default only reads it.
335 /// @param cbExec Executor the continuation is delivered on. Borrowed: it
336 /// must outlive the returned `Completion`.
337 /// @return A `Completion` resolved with `request.mid`, or rejected with the
338 /// failure.
339 // NOLINTBEGIN(performance-unnecessary-value-param) — by value to match `bindModel` and because an overriding
340 // backend moves the request into its pending-reply state; this default happens only to read it.
341 virtual ::morph::async::Completion<::morph::exec::detail::ModelId> promoteModel(PromoteRequest request,
342 ::morph::exec::IExecutor& cbExec) {
343 auto [completion, promise] =
345 try {
346 assignPrimary(request.mid, request.typeId, request.primary);
347 promise.resolve(request.mid);
348 } catch (...) {
349 promise.reject(std::current_exception());
350 }
351 return std::move(completion);
352 }
353 // NOLINTEND(performance-unnecessary-value-param)
354
355 /// @brief Tells this backend which owner its caller runs on.
356 ///
357 /// `Bridge` calls it when it installs the backend, with its own affinity,
358 /// and calls every other verb from that owner afterwards. A backend that
359 /// keeps state for those verbs records it to check, in a debug build, that
360 /// each is called there. Default implementation: ignore it.
361 /// @param owner The bridge's owner affinity. Its executor is borrowed: it
362 /// must outlive this backend's use by that bridge.
363 virtual void setOwner(const ::morph::exec::detail::OwnerAffinity& owner) { (void)owner; }
364
365 /// @brief Lists the primary keys of live shared instances of @p typeId.
366 ///
367 /// Only instances bound with a non-empty `primary` appear; a private
368 /// instance is invisible to the directory by construction. The result is a
369 /// snapshot and is stale the moment it is returned.
370 ///
371 /// Synchronous. `instances()` is the same question answered through a
372 /// `Completion`, which is what `Bridge` asks.
373 ///
374 /// @param typeId String type-id to enumerate.
375 /// @return Canonical key strings of the live shared instances; empty by default.
376 virtual std::vector<std::string> listInstances(const std::string& typeId) {
377 (void)typeId;
378 return {};
379 }
380
381 /// @brief Lists the primary keys of live shared instances of @p typeId,
382 /// answered through a `Completion` delivered on @p cbExec.
383 ///
384 /// What `BridgeHandler::instances()` reaches. The default answers from
385 /// `listInstances` and settles before returning, so a backend whose
386 /// directory is local needs nothing more. A backend whose directory is
387 /// across a round trip overrides it and settles when the reply arrives.
388 /// A throw from `listInstances` rejects the `Completion` rather than
389 /// propagating, so a caller has one failure channel.
390 ///
391 /// @param typeId String type-id to enumerate.
392 /// @param cbExec Executor the answer is delivered on. Borrowed: it must
393 /// outlive the returned `Completion`.
394 /// @return A `Completion` resolved with the canonical key strings of the
395 /// live shared instances, or rejected with the failure.
396 virtual ::morph::async::Completion<std::vector<std::string>> instances(const std::string& typeId,
397 ::morph::exec::IExecutor& cbExec) {
398 auto [completion, promise] = ::morph::async::Completion<std::vector<std::string>>::makeSettleable(&cbExec);
399 try {
400 promise.resolve(listInstances(typeId));
401 } catch (...) {
402 promise.reject(std::current_exception());
403 }
404 return std::move(completion);
405 }
406
407 /// @brief Removes the model identified by @p mid from the backend.
408 ///
409 /// For a shared instance this *decrements* its attach count and destroys the
410 /// instance only when the count reaches zero, so one caller releasing an
411 /// instance never tears it out from under another that is still attached.
412 /// @param mid Id of the instance to release.
413 virtual void deregisterModel(::morph::exec::detail::ModelId mid) = 0;
414
415 /// @brief Dispatches @p call against the model identified by @p mid.
416 /// @param mid Target model id.
417 /// @param call Bundled action; moved from.
418 /// @param cbExec Executor the returned `Completion`'s callbacks are posted on.
419 /// @return A `Completion` settled with the opaque result or the failure.
420 virtual ::morph::async::Completion<std::shared_ptr<void>> execute(::morph::exec::detail::ModelId mid,
421 ActionCall call,
422 ::morph::exec::IExecutor* cbExec) = 0;
423
424 /// @brief Dispatches @p call and settles @p sink with its outcome.
425 ///
426 /// The same dispatch as `execute`, with the result delivered to a sink the
427 /// caller already owns instead of to a fresh `Completion` the caller then
428 /// has to forward into its own. `Bridge::executeVia` hands down a sink that
429 /// **is** the typed completion state the caller was given, so there is no
430 /// forwarding block between the two — no erased `CompletionState`, no
431 /// `.then`/`.onError` closures, no second pair of handler vectors, and one
432 /// posted settle task instead of two.
433 ///
434 /// @par Why this has a default rather than being pure
435 /// `IBackend::execute` is implemented by five production backends and
436 /// roughly ten test doubles. A pure virtual here would be a fifteen-site
437 /// change to gain an allocation on one path. The default below is exactly
438 /// the forwarding block it replaces, so a backend that does not override it
439 /// costs precisely what it costs today — no gain, no regression — and a
440 /// backend that does gets the whole saving. `LocalBackend` overrides it;
441 /// nothing else does yet.
442 ///
443 /// @par Contract
444 /// Settles @p sink exactly once, through `settleValue` or `settleException`,
445 /// on the same thread and at the same point in the sequence the `execute`
446 /// overload would have resolved its completion. A backend that also has to
447 /// answer `cancelPending` must track the sink, not a state of its own.
448 /// Throwing out of this call is permitted and means the dispatch never
449 /// started — `Bridge::executeVia` undoes its pending count and its deadline
450 /// on that path, exactly as it does for `execute`.
451 ///
452 /// @param mid Target model id.
453 /// @param call Bundled action; moved from.
454 /// @param cbExec Executor for delivering callbacks attached to the caller's
455 /// own completion. A backend passes it along unchanged.
456 /// @param sink Where to settle the outcome. Never null.
457 virtual void executeInto(::morph::exec::detail::ModelId mid, ActionCall call, ::morph::exec::IExecutor* cbExec,
458 std::shared_ptr<::morph::async::detail::ISettleSink> sink) {
459 // Deliberately built from this class's own `execute`, not duplicated:
460 // one definition of what a dispatch is, whichever entry point a caller
461 // uses.
462 execute(mid, std::move(call), cbExec)
463 .then([sink](const std::shared_ptr<void>& value) { sink->settleValue(value); })
464 .onError([sink = std::move(sink)](const std::exception_ptr& exc) { sink->settleException(exc); });
465 }
466
467 /// @brief Called by `Bridge::switchBackend()` after all handlers are re-registered.
468 virtual void notifyBackendChanged() = 0;
469
470 /// @brief Resolves every still-pending completion this backend produced with @p exc.
471 ///
472 /// Called by `Bridge::switchBackend()` on the outgoing backend after the swap,
473 /// and by `Bridge`'s destructor. After this call, any later `setValue` /
474 /// `setException` on those states is a no-op (the state is already ready), so
475 /// in-flight server replies cannot resurrect a cancelled completion. A
476 /// backend that runs calls it can stop (`LocalBackend`'s Task handlers)
477 /// also requests stop on each one it fails.
478 /// @param exc Exception delivered to every pending completion's error sink.
479 virtual void cancelPending(const std::exception_ptr& exc) = 0;
480
481 /// @brief Installs a callback invoked when the backend reconnects to its peer.
482 ///
483 /// Used by backends that may lose and re-establish their transport (e.g.
484 /// `QtWebSocketBackend`). `Bridge` installs a handler that re-registers every
485 /// live `HandlerBinding` so model ids stay valid after the reconnect.
486 ///
487 /// Deliberately fires only on the *second and later* connects, never the
488 /// first — re-registering handlers only makes sense after a drop; on the
489 /// first connect there is nothing yet to re-register. See
490 /// `setConnectHandler` for a hook that also covers the first connect.
491 ///
492 /// The handler is never run on the backend's own thread: after a
493 /// reconnect the backend posts it to @p exec, so a `Bridge`'s
494 /// re-registration is an ordinary task on the bridge's owner.
495 ///
496 /// Default implementation: store-and-ignore. Backends with no transport (e.g.
497 /// `LocalBackend`) never invoke it.
498 /// @param handler Callable posted to @p exec after each successful
499 /// reconnect. Pass `nullptr` to clear.
500 /// @param exec Executor the handler is posted to. Borrowed: it must
501 /// outlive the installation. May be null only when
502 /// @p handler is.
503 // NOLINTNEXTLINE(performance-unnecessary-value-param): the base discards it; overrides take it by value and move it in.
504 virtual void setReconnectHandler(std::function<void()> handler, ::morph::exec::IExecutor* exec) {
505 (void)handler;
506 (void)exec;
507 }
508
509 /// @brief Installs a callback invoked for each server-initiated event the
510 /// backend receives.
511 ///
512 /// A transport backend decodes an `event` envelope, routes it here before
513 /// reply matching, and posts the handler to @p exec, so the handler is an
514 /// ordinary task on the installer's owner. Events on one connection are
515 /// delivered in the order they arrived.
516 ///
517 /// Default implementation: store-and-ignore. A backend with no server
518 /// channel never invokes it.
519 /// @param handler Callable posted to @p exec once per event. Pass
520 /// `nullptr` to clear.
521 /// @param exec Executor the handler is posted to. Borrowed: it must
522 /// outlive the installation. May be null only when
523 /// @p handler is.
524 // NOLINTNEXTLINE(performance-unnecessary-value-param): the base discards it; overrides take it by value and move it in.
525 virtual void setEventHandler(std::function<void(::morph::wire::EventBody)> handler,
526 ::morph::exec::IExecutor* exec) {
527 (void)handler;
528 (void)exec;
529 }
530
531 /// @brief Installs a callback invoked on every successful connect, including the first.
532 ///
533 /// `setReconnectHandler` deliberately skips the first connect (there is
534 /// nothing to re-register yet); this is the complementary hook for UI that
535 /// needs to know the transport is up at all — a "connecting… / connected /
536 /// offline" status indicator, for instance. `waitForConnected()` (where a
537 /// concrete backend offers one, e.g. `QtWebSocketBackend`) answers the same
538 /// question but blocks the calling thread, which is unusable on a
539 /// browser/WASM main thread and undesirable even on desktop if it means
540 /// blocking startup on a network round-trip; this hook is fired
541 /// asynchronously instead.
542 ///
543 /// Default implementation: store-and-ignore. Backends with no transport (e.g.
544 /// `LocalBackend`) never invoke it.
545 /// @param handler Callable invoked on the backend's transport thread after
546 /// every successful connect (first and subsequent). Pass
547 /// `nullptr` to clear.
548 virtual void setConnectHandler(const std::function<void()>& handler) { (void)handler; }
549
550 /// @brief Installs a callback invoked whenever the transport drops.
551 ///
552 /// Fires before any reconnect is scheduled, so an observer sees the
553 /// disconnected state even when a retry follows immediately — a status
554 /// indicator that skipped straight from "connected" to a fresh "connected"
555 /// (after an instant reconnect) would misreport an outage that did happen.
556 /// Without this hook a client learns the socket dropped only indirectly,
557 /// when a later action fails.
558 ///
559 /// Default implementation: store-and-ignore. Backends with no transport (e.g.
560 /// `LocalBackend`) never invoke it.
561 /// @param handler Callable invoked on the backend's transport thread whenever
562 /// the connection drops. Pass `nullptr` to clear.
563 virtual void setDisconnectHandler(const std::function<void()>& handler) { (void)handler; }
564
565 /// @brief Installs the session `Bridge` stamps onto every control envelope
566 /// this backend builds (`register`, `registerShared`, `attach`,
567 /// `assign`, `deregister`).
568 ///
569 /// `Bridge::executeVia` already stamps `Bridge::defaultSession()` onto the
570 /// `ActionCall` passed to `execute()`, so the session reaches `execute`
571 /// envelopes regardless of this hook. Control messages are different: they
572 /// are built directly by the concrete backend (`bindModel`, `promoteModel`,
573 /// `registerModelWithContext`, `assignPrimary`, `deregisterModel`),
574 /// which has no other way to learn the `Bridge`'s current session except
575 /// this hook. Stamping the stored session onto those envelopes is what lets
576 /// `RemoteServer::authorizeRegister` see a caller's identity and the owner
577 /// principal recorded at `register` time reflect the registering session —
578 /// which `authorizeInstance`'s ownership check relies on for every instance
579 /// a `Bridge` registers.
580 ///
581 /// `Bridge::setDefaultSession()` calls this immediately, and
582 /// `Bridge::switchBackend()` calls it on the new backend before any
583 /// re-registration runs, so every control envelope built afterward carries
584 /// the current session. Default implementation: store-and-ignore, matching
585 /// `setReconnectHandler`'s pattern. `LocalBackend` needs no override — the
586 /// local path never serialises a session onto a wire envelope in the first
587 /// place (see docs/spec/session/session.md).
588 /// @param session Session to stamp onto every subsequently built control envelope.
589 virtual void setSession(::morph::session::Context session) { (void)session; }
590};
591// NOLINTEND(cppcoreguidelines-special-member-functions)
592
593} // namespace detail
594
595/// @brief Thrown to in-flight `Completion`s when `Bridge::switchBackend()` runs.
596///
597/// Surfaces in the `.onError(...)` callback so the GUI can retry on the new backend
598/// or surface a "backend changed" message — there is no public cancel API on
599/// `Completion` itself.
600struct BackendChangedError : std::runtime_error {
601 /// @brief Constructs the error with a canned diagnostic message.
602 BackendChangedError() : std::runtime_error{"backend changed before completion resolved"} {}
603};
604
605/// @brief Thrown to in-flight `Completion`s when `Bridge` is destroyed.
606struct BridgeDestroyedError : std::runtime_error {
607 /// @brief Constructs the error with a canned diagnostic message.
608 BridgeDestroyedError() : std::runtime_error{"bridge destroyed before completion resolved"} {}
609};
610
611/// @brief Thrown to every call a `BridgeHandler` still holds for its bind when
612/// the handler is destroyed before the bind settles.
613struct HandlerDestroyedError : std::runtime_error {
614 /// @brief Constructs the error with a canned diagnostic message.
615 HandlerDestroyedError() : std::runtime_error{"handler destroyed before its bind settled"} {}
616};
617
618/// @brief Thrown to in-flight `Completion`s when a transport drops mid-call (e.g. a
619/// Qt WebSocket disconnect). The framework retries the call on reconnect if
620/// the backend supports it; otherwise the GUI's `.onError(...)` runs.
621struct DisconnectedError : std::runtime_error {
622 /// @brief Constructs the error with a canned diagnostic message.
623 DisconnectedError() : std::runtime_error{"transport disconnected before completion resolved"} {}
624};
625
626/// @brief Thrown to a pending `Completion` when the server-side
627/// `morph::backend::LimitPolicy::executeTimeout` elapses before the
628/// model's action replies.
629///
630/// The action keeps running to completion on its strand — morph never
631/// interrupts an in-flight `Model::execute` — but the caller's wait is bounded.
632/// Distinguishes a timeout from any other `err` reply (a generic
633/// `std::runtime_error` on `QtWebSocketBackend` / `SimulatedRemoteBackend`), so
634/// callers can retry or surface a specific "request timed out" message. See
635/// `docs/spec/core/backend.md` (`LimitPolicy`).
636struct TimeoutError : std::runtime_error {
637 /// @brief Constructs the error with a canned diagnostic message.
638 TimeoutError() : std::runtime_error{"execute timed out on the server"} {}
639};
640
641/// @brief Thrown to a pending `Completion` when `Bridge::setExecuteDeadline`'s
642/// duration elapses before any reply arrives — a frame silently
643/// dropped by `QtWebSocketServerConfig::messagesPerSecond`, or a
644/// genuinely hung server, either way.
645///
646/// Distinct from `TimeoutError`: that type means the *server* explicitly
647/// replied that it hit `LimitPolicy::executeTimeout` while the action was
648/// still running. `ClientTimeoutError` means the client gave up waiting —
649/// no reply of any kind arrived, so whether the server ever received the
650/// request, is still processing it, or replied to a connection that had
651/// already dropped is unknown. See `docs/spec/core/completion.md`.
652struct ClientTimeoutError : std::runtime_error {
653 /// @brief Constructs the error with a canned diagnostic message.
654 ClientTimeoutError() : std::runtime_error{"execute timed out waiting for any reply"} {}
655};
656
657/// @brief Makes a blocking backend reach its caller without blocking it — and
658/// without touching the backend.
659///
660/// A decorator, not a base class: it wraps an existing `IBackend` and runs
661/// every one of its verbs on one control strand over an executor this adapter
662/// names. That strand is the wrapped backend's one owner, so a backend that
663/// keeps its state without a lock (`LocalBackend`) stays correct behind it,
664/// and the caller's thread never pays for a blocking control call.
665///
666/// @par What it does and does not change
667/// The wrapped backend still blocks — nothing here makes a nested event loop
668/// or a socket round trip non-blocking. What changes is **which thread pays**:
669/// `bindModel` returns at once with an unsettled `Completion`, settled from the
670/// strand when the wrapped backend's bind has run there, and `execute` returns
671/// at once too. A single-threaded WASM main thread has no such executor to
672/// offer and is therefore not what this adapter is for; that case needs a
673/// backend with a genuinely non-blocking path.
674///
675/// @par Why the executor is required rather than optional
676/// "Where does the blocking happen" is the only question this class exists to
677/// answer, so it is a constructor parameter with no default.
678///
679/// @par Ordering
680/// Every verb is queued on the one strand, so the wrapped backend sees them
681/// one at a time and in the order they were issued. `~SynchronousBackendAdapter`
682/// waits for every queued and in-flight call, so a reply can never land in a
683/// destroyed adapter; the executor must therefore still be running tasks when
684/// this adapter is destroyed (see docs/spec/concurrency_and_lifetimes.md,
685/// "Destruction ordering"). The single-threaded WebAssembly build has no
686/// thread to wait for: there the calls still queued are dropped.
687///
688/// @par Owner
689/// The adapter itself belongs to its caller's owner (`setOwner`): `bindModel`,
690/// `promoteModel` and `cancelPending` keep the list of pending binds there,
691/// without a lock. The synchronous verbs (`registerModel`,
692/// `registerModelWithContext`, `assignPrimary`, `listInstances`) wait for the
693/// strand, so they must not be called from the executor's only thread.
694class SynchronousBackendAdapter : public detail::IBackend {
695public:
696 /// @brief Wraps @p inner, running every one of its verbs on @p blockingExec.
697 /// @param inner Backend to wrap. Owned (shared): the adapter keeps it
698 /// alive for as long as any call it queued is still in
699 /// flight. Must not be null.
700 /// @param blockingExec Executor the wrapped backend's calls run on.
701 /// Borrowed: it must outlive this adapter and keep
702 /// running tasks until the destructor's wait has
703 /// completed.
704 /// @throws std::invalid_argument if @p inner is null.
705 SynchronousBackendAdapter(std::shared_ptr<detail::IBackend> inner,
706 ::morph::exec::IExecutor& blockingExec MORPH_LIFETIMEBOUND)
707 : _inner{std::move(inner)}, _control{blockingExec} {
708 if (_inner == nullptr) {
709 throw std::invalid_argument{"SynchronousBackendAdapter requires a backend to wrap"};
710 }
711 }
712
713 /// @brief Waits for every queued and in-flight call; see "Ordering" above.
715 _control.teardown([] {});
716 }
717
719 SynchronousBackendAdapter& operator=(const SynchronousBackendAdapter&) = delete;
722
723 /// @brief The producer side of a `bindModel`/`promoteModel` completion.
724 ///
725 /// Named because `cancelPending` has to hold these weakly; see
726 /// `trackPending`.
728
729 /// @brief The wrapped backend.
730 /// @return Reference to the backend passed at construction; never null.
731 [[nodiscard]] detail::IBackend& wrapped() const noexcept { return *_inner; }
732
733 /// @brief Acquires a model instance without blocking the calling thread.
734 ///
735 /// Queues the wrapped backend's `bindModel` on the control strand and
736 /// returns. Exactly one of the returned `Completion`'s `then`/`onError`
737 /// handlers runs, on @p cbExec.
738 /// @param request Owning bind request; moved from.
739 /// @param cbExec Executor the continuation is delivered on. Borrowed: it
740 /// must outlive the returned `Completion`.
741 /// @return A `Completion` resolved with the bound `ModelId`, or rejected
742 /// with the wrapped backend's failure.
744 ::morph::exec::IExecutor& cbExec) override {
745 note("SynchronousBackendAdapter::bindModel");
746 return dispatch(cbExec, [request = std::move(request)](detail::IBackend& inner,
747 ::morph::exec::IExecutor& replyExec) mutable {
748 return inner.bindModel(std::move(request), replyExec);
749 });
750 }
751
752 /// @brief Files an already-live instance under a key without blocking the caller.
753 ///
754 /// The promote counterpart of `bindModel` above, on the same strand and
755 /// with the same settling rules.
756 /// @param request Owning promote request; moved from.
757 /// @param cbExec Executor the continuation is delivered on. Borrowed: it
758 /// must outlive the returned `Completion`.
759 /// @return A `Completion` resolved with `request.mid`, or rejected.
761 detail::PromoteRequest request, ::morph::exec::IExecutor& cbExec) override {
762 note("SynchronousBackendAdapter::promoteModel");
763 return dispatch(cbExec, [request = std::move(request)](detail::IBackend& inner,
764 ::morph::exec::IExecutor& replyExec) mutable {
765 return inner.promoteModel(std::move(request), replyExec);
766 });
767 }
768
769 /// @brief Records the caller's owner. Not forwarded: the wrapped backend's
770 /// owner is this adapter's control strand.
771 /// @param owner The caller's owner.
772 void setOwner(const ::morph::exec::detail::OwnerAffinity& owner) override { _affinity.emplace(owner); }
773
774 /// @brief Runs the wrapped backend's `registerModel` on the strand and waits for it.
775 /// @param typeId String type-id of the model to instantiate.
776 /// @param factory Callable that constructs the `IModelHolder`.
777 /// @return The wrapped backend's `ModelId`.
778 ::morph::exec::detail::ModelId registerModel(
779 const std::string& typeId,
780 std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory) override {
781 return runAndWait([typeId, factory = std::move(factory)](detail::IBackend& inner) mutable {
782 return inner.registerModel(typeId, std::move(factory));
783 });
784 }
785
786 /// @brief Runs the wrapped backend's `registerModelWithContext` on the strand and waits for it.
787 /// @param typeId String type-id of the model to instantiate.
788 /// @param factory Callable that constructs the `IModelHolder`.
789 /// @param contextKey Stable identity of the new instance; empty if none.
790 /// @return The wrapped backend's `ModelId`.
791 ::morph::exec::detail::ModelId registerModelWithContext(
792 const std::string& typeId, std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory,
793 std::string_view contextKey) override {
794 return runAndWait(
795 [typeId, factory = std::move(factory), key = std::string{contextKey}](detail::IBackend& inner) mutable {
796 return inner.registerModelWithContext(typeId, std::move(factory), key);
797 });
798 }
799
800 /// @brief Runs the wrapped backend's `assignPrimary` on the strand and waits for it.
801 /// @param mid Live instance to promote.
802 /// @param typeId Model type id — the directory's first key component.
803 /// @param primary Canonical string encoding of the key to file it under.
804 void assignPrimary(::morph::exec::detail::ModelId mid, const std::string& typeId,
805 std::string_view primary) override {
806 static_cast<void>(runAndWait([mid, typeId, key = std::string{primary}](detail::IBackend& inner) {
807 inner.assignPrimary(mid, typeId, key);
808 return true;
809 }));
810 }
811
812 /// @brief Runs the wrapped backend's `listInstances` on the strand and waits for it.
813 /// @param typeId String type-id to enumerate.
814 /// @return The wrapped backend's snapshot of live shared instance keys.
815 std::vector<std::string> listInstances(const std::string& typeId) override {
816 return runAndWait([typeId](detail::IBackend& inner) { return inner.listInstances(typeId); });
817 }
818
819 /// @brief Queues the wrapped backend's `deregisterModel`.
820 /// @param mid Instance to release.
821 void deregisterModel(::morph::exec::detail::ModelId mid) override {
822 post([mid](detail::IBackend& inner) { inner.deregisterModel(mid); });
823 }
824
825 /// @brief Queues the dispatch on the strand and returns its `Completion` at once.
826 /// @param mid Instance to dispatch against.
827 /// @param call The action call to dispatch.
828 /// @param cbExec Executor the resulting `Completion`'s callbacks are posted on.
829 /// @return A `Completion` settled by the wrapped backend.
831 detail::ActionCall call,
832 ::morph::exec::IExecutor* cbExec) override {
833 auto state = std::make_shared<::morph::async::detail::CompletionState<std::shared_ptr<void>>>();
835 executeInto(mid, std::move(call), cbExec,
836 std::make_shared<::morph::async::detail::CompletionSettleSink>(state));
837 return comp;
838 }
839
840 /// @brief Queues the wrapped backend's `executeInto` on the strand.
841 /// @param mid Instance to dispatch against.
842 /// @param call The action call to dispatch.
843 /// @param cbExec Executor for delivering callbacks attached to the caller's own completion.
844 /// @param sink Where the wrapped backend settles the outcome.
845 void executeInto(::morph::exec::detail::ModelId mid, detail::ActionCall call, ::morph::exec::IExecutor* cbExec,
846 std::shared_ptr<::morph::async::detail::ISettleSink> sink) override {
847 post([mid, call = std::move(call), cbExec, sink = std::move(sink)](detail::IBackend& inner) mutable {
848 try {
849 inner.executeInto(mid, std::move(call), cbExec, sink);
850 } catch (...) {
851 sink->settleException(std::current_exception());
852 }
853 });
854 }
855
856 /// @brief Queues the wrapped backend's `notifyBackendChanged`.
857 void notifyBackendChanged() override {
858 post([](detail::IBackend& inner) { inner.notifyBackendChanged(); });
859 }
860
861 /// @brief Rejects the binds *this adapter* produced, then queues the
862 /// wrapped backend's own `cancelPending`.
863 ///
864 /// A `bindModel` here settles from a task on the strand, holding a promise
865 /// the wrapped backend never sees, so the wrapped backend's `cancelPending`
866 /// reaches nothing of it: without this a bind cancelled by `~Bridge` or by
867 /// `switchBackend` would go on to resolve **successfully** afterwards.
868 ///
869 /// Each queued bind carries a cancellation flag beside its promise, set
870 /// here before the promise is rejected: a bind still queued sees it when it
871 /// reaches the head of the strand and never reaches the wrapped backend, so
872 /// no instance is created that nothing will deregister. A bind already
873 /// running on the strand cannot be recalled; its completion stays rejected.
874 /// The wrapped backend's own pending calls are cancelled on the strand, in
875 /// order with everything queued before this call.
876 /// @param exc Exception delivered to every still-pending completion, this
877 /// adapter's own and then the wrapped backend's.
878 void cancelPending(const std::exception_ptr& exc) override {
879 note("SynchronousBackendAdapter::cancelPending");
880 auto snapshot = std::exchange(_pending, {});
881 _compactAt = kPendingCompactFloor;
882 for (auto& pendingWeak : snapshot) {
883 if (auto pending = pendingWeak.lock()) {
884 pending->cancelled.store(true, std::memory_order_release);
885 pending->promise.reject(exc);
886 }
887 }
888 post([exc](detail::IBackend& inner) { inner.cancelPending(exc); });
889 }
890
891 /// @brief Queues the installation on the wrapped backend.
892 /// @param handler Callable posted to @p exec after a successful reconnect; `nullptr` clears.
893 /// @param exec Executor the handler is posted to.
894 void setReconnectHandler(std::function<void()> handler, ::morph::exec::IExecutor* exec) override {
895 post([handler = std::move(handler), exec](detail::IBackend& inner) mutable {
896 inner.setReconnectHandler(std::move(handler), exec);
897 });
898 }
899
900 /// @brief Queues the installation on the wrapped backend.
901 /// @param handler Callable posted to @p exec once per event; `nullptr` clears.
902 /// @param exec Executor the handler is posted to.
903 void setEventHandler(std::function<void(::morph::wire::EventBody)> handler,
904 ::morph::exec::IExecutor* exec) override {
905 post([handler = std::move(handler), exec](detail::IBackend& inner) mutable {
906 inner.setEventHandler(std::move(handler), exec);
907 });
908 }
909
910 /// @brief Queues the installation on the wrapped backend.
911 /// @param handler Callable invoked after every successful connect; `nullptr` clears.
912 void setConnectHandler(const std::function<void()>& handler) override {
913 post([handler](detail::IBackend& inner) { inner.setConnectHandler(handler); });
914 }
915
916 /// @brief Queues the installation on the wrapped backend.
917 /// @param handler Callable invoked whenever the transport drops; `nullptr` clears.
918 void setDisconnectHandler(const std::function<void()>& handler) override {
919 post([handler](detail::IBackend& inner) { inner.setDisconnectHandler(handler); });
920 }
921
922 /// @brief Queues the session on the wrapped backend.
923 /// @param session Session stamped onto every subsequently built control envelope.
924 void setSession(::morph::session::Context session) override {
925 post(
926 [session = std::move(session)](detail::IBackend& inner) mutable { inner.setSession(std::move(session)); });
927 }
928
929private:
930 /// @brief One queued bind: its promise and its cancellation flag.
931 ///
932 /// The strand task holds the only `shared_ptr` to this record; `_pending`
933 /// holds `weak_ptr`s, so an entry expires by itself when the task is
934 /// destroyed.
935 struct PendingControl {
936 /// @brief Takes ownership of the dispatched call's promise.
937 /// @param dispatched Producer side of the `Completion` handed to the caller.
938 explicit PendingControl(BindPromise dispatched) : promise{std::move(dispatched)} {}
939
940 /// @brief Producer side of the completion this call settles, from the
941 /// strand or from `cancelPending`; `CompletionState` settles once.
942 BindPromise promise;
943
944 /// @brief Set by `cancelPending` before it rejects; read by the task
945 /// before it runs the bind.
946 std::atomic_bool cancelled{false};
947 };
948
949 /// @brief Checks, in a debug build, that the caller is on the owner the
950 /// adapter was given. Nothing to check before `setOwner`.
951 /// @param site Name of the calling body.
952 void note(char const* site) const noexcept {
953 if (_affinity) {
954 _affinity->note(site);
955 }
956 }
957
958 /// @brief Queues @p task, run with the wrapped backend on the strand.
959 /// @tparam F Callable taking `detail::IBackend&`.
960 /// @param task What to run.
961 template <typename F>
962 void post(F task) {
963 _control.post(kControlStrand, [inner = _inner, task = std::move(task)]() mutable { task(*inner); });
964 }
965
966 /// @brief Runs @p task with the wrapped backend on the strand and waits for
967 /// its result: inline when already on the strand.
968 /// @tparam F Callable taking `detail::IBackend&` and returning a value.
969 /// @param task What to run.
970 /// @return What @p task returned; rethrows what it threw.
971 template <typename F>
972 std::invoke_result_t<F&, detail::IBackend&> runAndWait(F task) {
973 using R = std::invoke_result_t<F&, detail::IBackend&>;
974 if (_control.runningHere(kControlStrand)) {
975 return task(*_inner);
976 }
977 auto done = std::make_shared<std::promise<R>>();
978 auto result = done->get_future();
979 post([done, task = std::move(task)](detail::IBackend& inner) mutable {
980 try {
981 done->set_value(task(inner));
982 } catch (...) {
983 done->set_exception(std::current_exception());
984 }
985 });
986 return result.get();
987 }
988
989 /// @brief Queues @p op on the strand and settles a `Completion` with the
990 /// outcome of the `Completion` it returns.
991 ///
992 /// The wrapped backend's answer is delivered on @p cbExec too, and is
993 /// attached to there, by a task posted from the strand: its owner is the
994 /// caller's, which runs one task at a time, while the strand is not an
995 /// executor that answer could be delivered on.
996 /// @tparam Op Callable taking `detail::IBackend&` and the executor its
997 /// `Completion<ModelId>` is to be delivered on, and returning it.
998 /// @param cbExec Executor the continuation is delivered on.
999 /// @param op The call to run on the strand.
1000 /// @return A `Completion` settled by @p op's outcome.
1001 template <typename Op>
1002 ::morph::async::Completion<::morph::exec::detail::ModelId> dispatch(::morph::exec::IExecutor& cbExec, Op op) {
1003 using Settled = ::morph::async::Completion<::morph::exec::detail::ModelId>;
1004 auto [completion, promise] = Settled::makeSettleable(&cbExec);
1005 auto pending = std::make_shared<PendingControl>(std::move(promise));
1006 trackPending(pending);
1007 post([pending, op = std::move(op), replyExec = &cbExec](detail::IBackend& inner) mutable {
1008 if (pending->cancelled.load(std::memory_order_acquire)) {
1009 return;
1010 }
1011 try {
1012 auto answer = std::make_shared<Settled>(op(inner, *replyExec));
1013 replyExec->post([pending, answer] {
1014 answer->then([pending](::morph::exec::detail::ModelId mid) { pending->promise.resolve(mid); })
1015 .onError([pending](const std::exception_ptr& failure) { pending->promise.reject(failure); });
1016 });
1017 } catch (...) {
1018 pending->promise.reject(std::current_exception());
1019 }
1020 });
1021 return std::move(completion);
1022 }
1023
1024 /// @brief Records @p pending as cancellable until its task settles it.
1025 ///
1026 /// Swept on the same amortised schedule as `LocalBackend::trackPending`:
1027 /// dead entries are reclaimed only when the list reaches `_compactAt`,
1028 /// which each sweep re-arms at twice the surviving count.
1029 /// @param pending Record to cancel and reject if `cancelPending` runs before
1030 /// its task settles it.
1031 void trackPending(const std::shared_ptr<PendingControl>& pending) {
1032 if (_pending.size() >= _compactAt) {
1033 std::erase_if(_pending, [](const auto& weak) { return weak.expired(); });
1034 _compactAt = std::max(kPendingCompactFloor, _pending.size() * 2);
1035 }
1036 _pending.emplace_back(pending);
1037 }
1038
1039 /// @brief The single strand key every call shares, so they run one at a
1040 /// time. Not a real model id: these strands are private to the
1041 /// adapter.
1042 static constexpr ::morph::exec::detail::ModelId kControlStrand{1};
1043
1044 /// @brief Smallest size at which `trackPending` sweeps; see `LocalBackend`'s.
1045 static constexpr std::size_t kPendingCompactFloor = 32;
1046
1047 std::shared_ptr<detail::IBackend> _inner;
1048 ::morph::exec::detail::ModelStrands _control;
1049 std::optional<::morph::exec::detail::OwnerAffinity> _affinity;
1050 // Every bind handed to a `_control` task and not yet settled by it. Weak,
1051 // so a settled task's record drops out on its own. Touched only on the
1052 // caller's owner: `bindModel`/`promoteModel` add, `cancelPending` takes.
1053 std::vector<std::weak_ptr<PendingControl>> _pending;
1054 // Size at which `trackPending` next sweeps `_pending`.
1055 std::size_t _compactAt = kPendingCompactFloor;
1056};
1057
1058/// @brief In-process backend that executes model actions on a thread pool strand.
1059///
1060/// Each model instance gets its own strand so actions are serialised per-model
1061/// without a global lock on the pool.
1062///
1063/// Its registry, its pending list and its record of Task runs belong to the
1064/// caller's owner — the `Bridge`'s, given through `setOwner` — and are touched
1065/// only there, without a lock. Only the strand tasks it posts run elsewhere,
1066/// and they reach nothing of the backend but what each carries.
1067class LocalBackend : public detail::IBackend {
1068public:
1069 /// @brief Constructs the backend using @p workerPool to run model actions.
1070 /// @param workerPool Executor (typically a `ThreadPoolExecutor`) for model
1071 /// work. Borrowed, not owned: this backend's strands run
1072 /// on it, so it must outlive the backend *and* keep
1073 /// running tasks until teardown completes — destroying it
1074 /// first deadlocks (see
1075 /// `docs/spec/concurrency_and_lifetimes.md`, "Destruction
1076 /// ordering").
1077 explicit LocalBackend(::morph::exec::IExecutor& workerPool MORPH_LIFETIMEBOUND)
1078 : _strands{std::make_shared<::morph::exec::detail::ModelStrands>(workerPool)} {}
1079
1080 /// @brief Stops the Task handlers still running, lets the strands drain,
1081 /// seals them, drains them again, and only then closes them
1082 /// (`ModelStrands::teardown`). See `docs/spec/core/coroutines.md`,
1083 /// "Teardown".
1084 ///
1085 /// Where threads exist, in that order:
1086 /// 1. Every live Task run's stop is requested. A handler suspended in an
1087 /// awaitable that resumes on the current executor -- morph's own,
1088 /// `core::async::AsyncQueue::pop` -- resumes, cancelled, through its
1089 /// strand, which still admits it. One suspended on a `core::net` socket
1090 /// or timer resumes on that loop instead, and unwinds there; its end is
1091 /// posted to the strand.
1092 /// 2. The strands are drained: the resumptions and ends queued on them,
1093 /// and every queued action -- skipped, if `cancelPending` already failed
1094 /// it. This drain comes before the seal because a handler's end that
1095 /// arrives from a loop thread now is still queued behind its instance's
1096 /// other tasks; sealed, it would run inline on the loop thread while
1097 /// this drain ran one of those tasks on a pool thread, two threads
1098 /// inside the instance's action gate.
1099 /// 3. The strands are sealed: from here on a resumption or a handler's end
1100 /// is refused and runs inline, where it arrives, rather than queued on a
1101 /// strand step 5 would drop.
1102 /// 4. The strands are drained again, for what reached them between steps
1103 /// 2 and 3. It does not wait for a handler still unwinding on another
1104 /// executor.
1105 /// 5. The strands are closed.
1106 ///
1107 /// Must not run on one of this backend's strand threads, whose drain it
1108 /// would wait for; a debug build asserts that. The single-threaded
1109 /// WebAssembly build seals, stops the handlers, then closes: sealed first,
1110 /// each stopped handler unwinds inline in the stop, and the drains wait for
1111 /// nothing, since nothing else could run the strands.
1112 ~LocalBackend() override {
1113 note("LocalBackend::~LocalBackend");
1114 auto runs = std::exchange(_taskRuns, {});
1115 _strands->teardown([&runs] {
1116 for (auto const& weak : runs) {
1117 if (auto const run = weak.lock()) {
1118 run->stopSource->request_stop();
1119 }
1120 }
1121 });
1122 }
1123
1124 /// @brief Installs the event handler `deliverEvent` posts to.
1125 /// @param handler Callable posted to @p exec once per delivered event; `nullptr` clears.
1126 /// @param exec Executor the handler is posted to. Borrowed: it must outlive the installation.
1127 void setEventHandler(std::function<void(::morph::wire::EventBody)> handler,
1128 ::morph::exec::IExecutor* exec) override {
1129 std::scoped_lock const lock{_eventMutex};
1130 _eventHandler =
1131 handler ? std::make_shared<std::function<void(::morph::wire::EventBody)>>(std::move(handler)) : nullptr;
1132 _eventExec = exec;
1133 }
1134
1135 /// @brief Test seam: delivers @p event as if a server had sent it.
1136 ///
1137 /// A local backend has no server and produces no events itself; this lets
1138 /// a test drive the client's event path without one. Thread-safe. The
1139 /// handler is posted to the executor given to `setEventHandler`; with no
1140 /// handler installed the event is dropped.
1141 /// @param event The event to deliver.
1143 std::shared_ptr<std::function<void(::morph::wire::EventBody)>> handler;
1144 ::morph::exec::IExecutor* exec = nullptr;
1145 {
1146 std::scoped_lock const lock{_eventMutex};
1147 handler = _eventHandler;
1148 exec = _eventExec;
1149 }
1150 if (!handler || exec == nullptr) {
1151 return;
1152 }
1153 exec->post(
1154 [handler = std::move(handler), event = std::move(event)]() mutable { (*handler)(std::move(event)); });
1155 }
1156
1157 LocalBackend(const LocalBackend&) = delete;
1158 LocalBackend& operator=(const LocalBackend&) = delete;
1159 LocalBackend(LocalBackend&&) = delete;
1160 LocalBackend& operator=(LocalBackend&&) = delete;
1161
1162 /// @brief Creates a model instance via @p factory and registers it.
1163 ///
1164 /// The `typeId` parameter is accepted for interface compatibility but not
1165 /// used — it is unnamed in the signature below, and the concrete type is
1166 /// captured by the factory closure. If the new holder's
1167 /// `isBackendChangeAware()` returns `true`, the new id (a local in
1168 /// `createAndTrack`, not a parameter here) is also recorded in
1169 /// `_changeAware` so `notifyBackendChanged()` finds it without a
1170 /// `dynamic_cast` sweep.
1171 /// @param factory Callable that constructs the `IModelHolder`.
1172 /// @return Newly assigned `ModelId`.
1173 ::morph::exec::detail::ModelId registerModel(
1174 const std::string& /*typeId*/,
1175 std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory) override {
1176 note("LocalBackend::registerModel");
1177 ::morph::observe::detail::emitMetric(::morph::observe::Metric::registerCount, 1.0);
1178 return createAndTrack(std::move(factory));
1179 }
1180
1181 /// @brief Acquires an instance for @p request and settles the returned
1182 /// `Completion` before returning.
1183 ///
1184 /// Every shape of `BindRequest` is answered here, against this backend's
1185 /// own directory: an empty `primary` creates a private instance; a
1186 /// non-empty one registers-or-attaches the shared instance for
1187 /// `(typeId, primary)`, lazily evicting one whose first action failed
1188 /// (`InstanceDirectory::attach`); a non-zero `current` is released once
1189 /// the replacement is acquired, so a same-key re-attach keeps the instance
1190 /// it already holds and a throwing acquire never strands the caller.
1191 /// @param request Owning bind request; moved from.
1192 /// @param cbExec Executor the continuation is delivered on. Borrowed: it
1193 /// must outlive the returned `Completion`.
1194 /// @return A settled `Completion` carrying the bound id or the failure.
1196 ::morph::exec::IExecutor& cbExec) override {
1197 note("LocalBackend::bindModel");
1198 auto [completion, promise] =
1200 try {
1201 promise.resolve(bindNow(std::move(request)));
1202 } catch (...) {
1203 promise.reject(std::current_exception());
1204 }
1205 return std::move(completion);
1206 }
1207
1208 /// @brief Records the owner every verb below is called from.
1209 /// @param owner The caller's owner.
1210 void setOwner(const ::morph::exec::detail::OwnerAffinity& owner) override { _affinity.emplace(owner); }
1211
1212 /// @brief Enters an already-live, still-anonymous instance into the
1213 /// directory under @p primary.
1214 /// @param mid Live instance to promote.
1215 /// @param typeId Model type id — the directory's first key component.
1216 /// @param primary Canonical string encoding of the key to file it under.
1217 void assignPrimary(::morph::exec::detail::ModelId mid, const std::string& typeId,
1218 std::string_view primary) override {
1219 note("LocalBackend::assignPrimary");
1220 if (primary.empty()) {
1221 return;
1222 }
1223 // A no-op unless `mid` is live, has never held a directory key, and the
1224 // key is free — `InstanceDirectory::promote` holds all the guards and
1225 // the reasons for them.
1226 (void)_instances.promote(mid, detail::DirectoryKey{typeId, std::string{primary}});
1227 }
1228
1229 /// @brief Lists the primary keys of live shared instances of @p typeId.
1230 /// @param typeId String type-id to enumerate.
1231 /// @return Canonical key strings of the live shared instances, in unspecified order.
1232 std::vector<std::string> listInstances(const std::string& typeId) override {
1233 note("LocalBackend::listInstances");
1234 return _instances.keysOfType(typeId);
1235 }
1236
1237 /// @brief Removes the model with @p mid, or releases one attachment to it.
1238 ///
1239 /// A private instance is erased outright. A shared instance has its attach
1240 /// count decremented and is erased — and removed from the directory — only
1241 /// when that count reaches zero, so releasing one handler never destroys an
1242 /// instance another handler still holds.
1243 /// @param mid Id returned by a prior `registerModel()`/`bindModel()` call.
1244 void deregisterModel(::morph::exec::detail::ModelId mid) override {
1245 note("LocalBackend::deregisterModel");
1246 ::morph::observe::detail::emitMetric(::morph::observe::Metric::deregisterCount, 1.0);
1247 // One lookup, not five: the attach count, the directory key and the
1248 // holder are one record, so releasing the last reference unfiles and
1249 // destroys it in a single step that cannot half-happen.
1250 if (_instances.release(mid) == detail::InstanceDirectory::Release::destroyed) {
1251 _changeAware.erase(mid);
1252 }
1253 }
1254
1255 /// @brief Schedules `onBackendChanged()` on each change-aware model's strand.
1256 ///
1257 /// Only models recorded in `_changeAware` — maintained by `createAndTrack`/
1258 /// `deregisterModel` from `IModelHolder::isBackendChangeAware()`, a
1259 /// compile-time answer per model type — are visited; there is no
1260 /// `dynamic_cast` and no scan of models that never opted in. Each such
1261 /// model's `onBackendChanged()` (the `IModelHolder` base virtual) is
1262 /// **posted onto that model's own strand** (the same per-`ModelId` serial
1263 /// queue `execute` uses), rather than invoked inline on the caller's thread.
1264 /// Two properties follow, and they are the whole point:
1265 ///
1266 /// - **Strand-serialised, lock-free model code.** `onBackendChanged()` runs
1267 /// on the pool inside the model's strand, so it never overlaps an
1268 /// `execute()` on the same model. A model reacting to a backend switch
1269 /// (e.g. draining an offline queue and mutating counters) needs no locking
1270 /// of its own state — exactly what `offline.md` promises.
1271 /// - **Off the owner.** The model body runs later on a pool thread, never
1272 /// inside `Bridge::switchBackend`. A model that wants to reach the bridge
1273 /// from `onBackendChanged()` posts to the bridge's owner.
1274 ///
1275 /// A `shared_ptr` copy of each holder is captured into the posted task so a
1276 /// concurrent `deregisterModel` cannot free the model out from under its
1277 /// pending notification (mirrors `execute`'s holder capture).
1278 void notifyBackendChanged() override {
1279 note("LocalBackend::notifyBackendChanged");
1280 for (auto modelId : _changeAware) {
1281 if (const auto* inst = _instances.find(modelId)) {
1282 _strands->post(modelId, [held = inst->holder]() mutable { held->onBackendChanged(); });
1283 }
1284 }
1285 }
1286
1287 /// @brief Schedules `call.localOp` on the model's strand and returns a `Completion`.
1288 ///
1289 /// The completion resolves with the opaque result on the strand thread and
1290 /// the callbacks are delivered via @p cbExec.
1291 ///
1292 /// Expressed in terms of `executeInto` rather than beside it, so there is
1293 /// one definition of what a local dispatch does whichever entry point a
1294 /// caller uses. The extra `CompletionState` and its adapter sink are the
1295 /// price of the `Completion`-returning shape — which is exactly the cost
1296 /// `executeInto` exists to let `Bridge` avoid.
1297 ///
1298 /// @par Why this is `final`
1299 /// `Bridge::executeVia` calls `executeInto`, not `execute`. A subclass that
1300 /// overrode `execute` alone would therefore be bypassed for every bridge
1301 /// dispatch and intercept only the handful of direct `execute` callers —
1302 /// silently, with every test it has still passing on the paths it does
1303 /// reach. `final` turns that into a compile error naming the fix: **derive
1304 /// from `LocalBackend` and override `executeInto`**, which is the primitive
1305 /// both entry points now share. (A backend deriving straight from
1306 /// `IBackend` is unaffected: it overrides `execute` and inherits the
1307 /// default `executeInto`, which forwards to it.)
1308 ///
1309 /// @param mid Target model id.
1310 /// @param call Bundled action; `localOp` is the only field used here.
1311 /// @param cbExec Executor for delivering callbacks.
1312 /// @return Completion that will carry the result or an exception.
1314 detail::ActionCall call,
1315 ::morph::exec::IExecutor* cbExec) final {
1316 auto compState = std::make_shared<::morph::async::detail::CompletionState<std::shared_ptr<void>>>();
1317 ::morph::async::Completion<std::shared_ptr<void>> comp{compState, cbExec};
1318 executeInto(mid, std::move(call), cbExec,
1319 std::make_shared<::morph::async::detail::CompletionSettleSink>(compState));
1320 return comp;
1321 }
1322
1323 /// @brief Schedules `call.localOp` on the model's strand and settles @p sink.
1324 ///
1325 /// @param mid Target model id.
1326 /// @param call Bundled action; `localOp` is the only field used here.
1327 /// @param cbExec Unused here: a sink carries its own delivery. Accepted so
1328 /// the signature matches `IBackend::executeInto`, whose remote
1329 /// implementations do need it.
1330 /// @param sink Settled on the strand thread with the result or the
1331 /// exception, exactly once.
1332 void executeInto(::morph::exec::detail::ModelId mid, detail::ActionCall call, ::morph::exec::IExecutor* cbExec,
1333 std::shared_ptr<::morph::async::detail::ISettleSink> sink) override {
1334 (void)cbExec;
1335 MORPH_ZONE("LocalBackend::executeInto");
1336 MORPH_ZONE_TEXT(call.session.requestId);
1337 note("LocalBackend::executeInto");
1338
1339 std::shared_ptr<::morph::model::detail::IModelHolder> holder;
1340 std::shared_ptr<detail::HydrationState> hydration;
1341 // One lookup for the holder and its hydration state together: they
1342 // are fields of one record, not entries in two maps kept in lockstep
1343 // by convention.
1344 if (const auto* inst = _instances.find(mid)) {
1345 holder = inst->holder;
1346 hydration = inst->hydration;
1347 }
1348 if (!holder) {
1349 sink->settleException(
1350 std::make_exception_ptr(std::runtime_error("model not found: id=" + std::to_string(mid.v))));
1351 return;
1352 }
1353 auto const admittedEpoch = trackPending(sink);
1354 LocalRun run;
1355 run.localOp = call.localOp;
1356 run.localOpAsync = call.localOpAsync;
1357 run.stopSource = std::move(call.stopSource);
1358 // The action handle travels with `localOp` into the strand task, not
1359 // just as far as this function: `ActionCall::localOp` borrows the
1360 // action rather than owning it (see that struct), and `call` is gone
1361 // long before the task runs.
1362 run.action = std::move(call.action);
1363 run.session = std::move(call.session);
1364 run.modelTypeId = call.modelTypeId;
1365 run.actionTypeId = call.actionTypeId;
1366 run.holder = std::move(holder);
1367 run.sink = std::move(sink);
1368 run.hydration = std::move(hydration);
1369 run.strands = _strands;
1370 run.cancels = _cancels;
1371 run.admittedEpoch = admittedEpoch;
1372 run.mid = mid;
1373 // Held by shared_ptr, never by raw `this`. A shared_ptr copy has its
1374 // own lifetime, independent of LocalBackend's, so it stays valid even
1375 // if the backend is torn down while this task is still queued or
1376 // running. `hydration` follows the same rule and may be null (a
1377 // private instance has no entry).
1378 run.inFlightCounter = _inFlight;
1379 auto const inFlightAfterInc = run.inFlightCounter->fetch_add(1, std::memory_order_relaxed) + 1;
1380 ::morph::observe::detail::emitMetric(::morph::observe::Metric::executeInFlight,
1381 static_cast<double>(inFlightAfterInc));
1382 // Through the instance's action gate: an action starts only once the one
1383 // before it has finished, which a Task handler does when its Task
1384 // completes rather than when the strand task that started it returns.
1385 if (run.localOpAsync != nullptr) {
1386 // A Task run is shared: its completion callback outlives the strand
1387 // task, and every Task run can be stopped, deadline or not -- the
1388 // destructor stops the ones still running.
1389 if (!run.stopSource) {
1390 run.stopSource = std::make_shared<::core::async::StopSource>();
1391 }
1392 auto shared = std::make_shared<LocalRun>(std::move(run));
1393 rememberTaskRun(shared);
1394 _strands->post(mid,
1395 [shared] { shared->holder->actionGate().enter([shared] { startTaskLocal(shared); }); });
1396 return;
1397 }
1398 // An ordinary run travels by value in the strand task, so a dispatch
1399 // costs the post's one allocation and nothing more:
1400 // `bench.alloc_budget` holds that line. The task keeps it while the
1401 // handler runs, as it kept its captures before there was a gate; only
1402 // a run that has to wait behind a suspended Task handler is moved out,
1403 // into the gate's queue.
1404 _strands->post(mid, [run = std::move(run)]() mutable {
1405 auto& gate = run.holder->actionGate();
1406 if (gate.tryEnter()) {
1407 startLocal(run);
1408 return;
1409 }
1410 gate.enter([waiting = std::make_shared<LocalRun>(std::move(run))] { startLocal(*waiting); });
1411 });
1412 }
1413
1414 /// @brief Resolves every still-pending completion this backend produced
1415 /// with @p exc, and requests stop on every Task handler still running.
1416 ///
1417 /// A run still waiting for its instance's action gate is failed where it
1418 /// waits (see `admitLocal`); a Task handler already running observes the
1419 /// stop on its token at its next `co_await`.
1420 /// @param exc Exception delivered to every pending completion's error sink.
1421 void cancelPending(const std::exception_ptr& exc) override {
1422 note("LocalBackend::cancelPending");
1423 auto snapshot = std::exchange(_pending, {});
1424 _compactAt = kPendingCompactFloor;
1425 // With the swap: a run admitted before this point is in `snapshot`
1426 // and is failed below, and one admitted after it is not.
1427 _cancels->record(exc);
1428 // Settle before stopping. A stopped Task handler resumes on its strand
1429 // and settles its own sink with `OperationCancelled`; requested first,
1430 // that settle can land before this one, and the caller would be
1431 // answered with the stop instead of @p exc. Settled first, the
1432 // handler's later settle is the ignored one.
1433 for (auto& weak : snapshot) {
1434 if (auto sink = weak.lock()) {
1435 // May race a reply settling the same sink; `ISettleSink`'s
1436 // contract makes the second call a no-op, which is what
1437 // `CompletionState`'s first-result-wins gave this loop before
1438 // the sink existed.
1439 sink->settleException(exc);
1440 }
1441 }
1442 for (auto const& weak : _taskRuns) {
1443 if (auto const run = weak.lock()) {
1444 static_cast<void>(run->stopSource->request_stop());
1445 }
1446 }
1447 }
1448
1449 /// @brief Number of entries currently held in the pending list.
1450 ///
1451 /// **Not** the number of calls in flight: the list is compacted amortised
1452 /// (see `trackPending`), so it also carries entries whose completion has
1453 /// already been destroyed and which `cancelPending` would skip. It is an
1454 /// upper bound on the in-flight count and a direct measure of what the
1455 /// compaction policy costs in memory — which is what it exists to make
1456 /// observable. For a count of in-flight *calls*, use `Bridge::pendingCalls()`.
1457 /// @return Size of the pending list, live and dead entries alike.
1458 [[nodiscard]] std::size_t trackedPendingCount() const {
1459 note("LocalBackend::trackedPendingCount");
1460 return _pending.size();
1461 }
1462
1463private:
1464 /// @brief Checks, in a debug build, that the caller is on the owner the
1465 /// backend was given. Nothing to check before `setOwner`.
1466 /// @param site Name of the calling body.
1467 void note(char const* site) const noexcept {
1468 if (_affinity) {
1469 _affinity->note(site);
1470 }
1471 }
1472
1473 /// @brief `bindModel`'s body: acquires the instance @p request names.
1474 /// @param request Owning bind request; moved from.
1475 /// @return The bound id.
1476 ::morph::exec::detail::ModelId bindNow(detail::BindRequest request) {
1477 ::morph::observe::detail::emitMetric(::morph::observe::Metric::registerCount, 1.0);
1478 ::morph::exec::detail::ModelId next{};
1479 if (request.primary.empty()) {
1480 auto [mid, holder] = createHolder(std::move(request.factory));
1481 journalUnder(*holder, request.contextKey);
1482 _instances.insertPrivate(mid, std::move(holder));
1483 next = mid;
1484 } else {
1485 detail::DirectoryKey dirKey{request.typeId, request.primary};
1486 if (auto const attached = _instances.attach(dirKey)) {
1487 next = *attached;
1488 } else {
1489 auto [mid, holder] = createHolder(std::move(request.factory));
1490 journalUnder(*holder, request.contextKey.empty() ? request.primary : request.contextKey);
1491 _instances.insertShared(mid, std::move(holder), std::move(dirKey));
1492 next = mid;
1493 }
1494 }
1495 // After the acquire: a same-key re-attach took a second reference to
1496 // `current`, and this releases the redundant one.
1497 if (request.current.v != 0U) {
1498 deregisterModel(request.current);
1499 }
1500 return next;
1501 }
1502
1503 /// @brief Attaches the default action log to @p holder under @p contextKey, the key a server's
1504 /// `ServerConfig::logProvider` attaches it under, so a keyed instance journals under its identity whichever
1505 /// backend holds it. Nothing when the key is empty or no default log is installed: the holder keeps what
1506 /// its factory attached.
1507 /// @param holder The new instance.
1508 /// @param contextKey Its stable identity; empty if none.
1509 static void journalUnder(::morph::model::detail::IModelHolder& holder, std::string const& contextKey) {
1510 if (contextKey.empty()) {
1511 return;
1512 }
1513 if (auto log = ::morph::journal::defaultActionLog()) {
1514 holder.attachActionLog(std::move(log), contextKey);
1515 }
1516 }
1517
1518 /// @brief Builds a holder via @p factory, records it under a fresh id, and
1519 /// returns that id.
1520 ///
1521 /// Shared by `registerModel` and `bindModel`'s private path.
1522 /// @param factory Callable that constructs the `IModelHolder`.
1523 /// @return Newly assigned `ModelId`.
1524 ::morph::exec::detail::ModelId createAndTrack(
1525 std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory) {
1526 auto [mid, holder] = createHolder(std::move(factory));
1527 _instances.insertPrivate(mid, std::move(holder));
1528 return mid;
1529 }
1530
1531 /// @brief Allocates an id, builds the holder and notes change-awareness,
1532 /// without filing it anywhere.
1533 ///
1534 /// The half the private and the shared paths have in common; they differ
1535 /// only in how the result is filed, which is `InstanceDirectory`'s job.
1536 /// @param factory Callable that constructs the `IModelHolder`.
1537 /// @return The newly assigned id and the constructed holder.
1538 std::pair<::morph::exec::detail::ModelId, std::shared_ptr<::morph::model::detail::IModelHolder>> createHolder(
1539 std::function<std::unique_ptr<::morph::model::detail::IModelHolder>()> factory) {
1540 ::morph::exec::detail::ModelId const mid{_nextId.fetch_add(1) + 1};
1541 std::shared_ptr<::morph::model::detail::IModelHolder> holder = factory();
1542 if (holder->isBackendChangeAware()) {
1543 _changeAware.insert(mid);
1544 }
1545 return {mid, std::move(holder)};
1546 }
1547
1548 /// @brief Records @p state in the pending list, compacting it amortised-O(1).
1549 ///
1550 /// The list is append-only between compactions; dead entries are swept only
1551 /// when its size reaches `_compactAt`, which each sweep re-arms at twice the
1552 /// number of entries that survived it. Doubling the threshold off the live
1553 /// count is what makes the sweep amortised: a sweep costs O(size), and at
1554 /// least `_compactAt / 2` appends must happen before the next one, so the
1555 /// per-append cost is O(1) however deep the queue gets. The list is
1556 /// correspondingly bounded at twice the live count (plus the floor), which
1557 /// is the whole price of dropping the per-dispatch scan.
1558 ///
1559 /// Sweeping on *every* append instead would cost `n` atomic
1560 /// `weak_ptr::expired()` loads to admit one call with
1561 /// `n` in flight, so a burst of `n` costs O(n²) — measured at 362ms of pure
1562 /// admission time for 32k queued executes against one slow model, against
1563 /// 7.6ms without the sweep.
1564 ///
1565 /// Purely a cost change: `cancelPending` still sees every live state,
1566 /// because a dead entry is one whose `CompletionState` is already gone and
1567 /// which `cancelPending`'s `weak.lock()` has always skipped. Carrying dead
1568 /// entries for longer changes nothing it observes.
1569 /// @param sink Settle sink to track until it expires or is cancelled.
1570 /// @return The cancel epoch @p sink was admitted in: `cancelPending` has
1571 /// failed the dispatch once the epoch has moved on.
1572 std::uint64_t trackPending(const std::shared_ptr<::morph::async::detail::ISettleSink>& sink) {
1573 if (_pending.size() >= _compactAt) {
1574 std::erase_if(_pending, [](const auto& weak) { return weak.expired(); });
1575 _compactAt = std::max(kPendingCompactFloor, _pending.size() * 2);
1576 }
1577 _pending.emplace_back(sink);
1578 return _cancels->epoch();
1579 }
1580
1581 /// What `cancelPending` has done so far, shared with every run: how many
1582 /// times it has run, and with what reason the last time.
1583 ///
1584 /// Written only by `cancelPending`, on the owner; read by runs on their
1585 /// strands. Each reason is an immutable node published with a release
1586 /// store before the epoch moves, so a run that sees the epoch move reads a
1587 /// complete reason. Nodes live as long as the record: one per
1588 /// `cancelPending`, which runs on a switch, a teardown or a disconnect.
1589 class CancelRecord {
1590 public:
1591 /// Publishes @p reason, then bumps the epoch. One writer: the owner.
1592 void record(const std::exception_ptr& reason) {
1593 auto node = std::make_unique<Node>(Node{.reason = reason, .previous = std::move(_chain)});
1594 _latest.store(node.get(), std::memory_order_release);
1595 _chain = std::move(node);
1596 _epoch.fetch_add(1);
1597 }
1598
1599 /// @return How many times `cancelPending` has run.
1600 [[nodiscard]] std::uint64_t epoch() const noexcept { return _epoch.load(); }
1601
1602 /// @return The reason the last `cancelPending` gave; null before the first.
1603 [[nodiscard]] std::exception_ptr lastReason() const {
1604 Node const* const node = _latest.load(std::memory_order_acquire);
1605 return node == nullptr ? std::exception_ptr{} : node->reason;
1606 }
1607
1608 private:
1609 struct Node {
1610 std::exception_ptr reason;
1611 std::unique_ptr<Node> previous;
1612 };
1613 std::atomic<std::uint64_t> _epoch{0};
1614 std::atomic<Node const*> _latest{nullptr};
1615 std::unique_ptr<Node> _chain;
1616 };
1617
1618 /// Everything one local dispatch carries from `executeInto` to the moment
1619 /// it settles. An ordinary handler's run is held by its strand task, or by
1620 /// the gate's queue while it waits there; a Task handler's is shared,
1621 /// because its completion callback holds it too.
1622 struct LocalRun {
1623 std::shared_ptr<void> (*localOp)(::morph::model::detail::IModelHolder&, void*) = nullptr;
1624 decltype(detail::ActionCall::localOpAsync) localOpAsync = nullptr;
1625 std::shared_ptr<::core::async::StopSource> stopSource;
1626 std::shared_ptr<void> action;
1627 ::morph::session::Context session;
1628 std::string_view modelTypeId;
1629 std::string_view actionTypeId;
1630 std::shared_ptr<::morph::model::detail::IModelHolder> holder;
1631 std::shared_ptr<::morph::async::detail::ISettleSink> sink;
1632 std::shared_ptr<detail::HydrationState> hydration;
1633 std::shared_ptr<std::atomic<std::size_t>> inFlightCounter;
1634 std::shared_ptr<::morph::exec::detail::ModelStrands> strands;
1635 std::shared_ptr<CancelRecord> cancels;
1636 std::uint64_t admittedEpoch = 0;
1637 ::morph::exec::detail::ModelId mid{};
1638 std::chrono::steady_clock::time_point start;
1639 ::morph::observe::SpanId spanId{};
1640 };
1641
1642 /// Stamps a dispatch's start once it holds its instance's action gate, and
1643 /// settles it instead if `cancelPending` failed it while it waited there.
1644 /// @return Whether the handler is to run.
1645 static bool admitLocal(LocalRun& run) {
1646 run.start = std::chrono::steady_clock::now();
1647 run.spanId = ::morph::observe::detail::beginSpan(run.session.requestId, run.modelTypeId, run.actionTypeId);
1648 if (run.cancels->epoch() == run.admittedEpoch) {
1649 return true;
1650 }
1651 // `cancelPending` failed this call while it waited for the gate;
1652 // the handler does not run for a caller that has been answered.
1653 // The sink is settled here, with the reason `cancelPending` gave,
1654 // because this may be the only place it can be: `cancelPending`
1655 // reaches sinks through `weak_ptr`s, and when the caller has
1656 // dropped its `Completion` this run holds the last reference, so
1657 // the sink is gone by the time `cancelPending` would reach it.
1658 // Where `cancelPending` got there first, this settle is ignored.
1659 finishLocal(run, nullptr, run.cancels->lastReason());
1660 return false;
1661 }
1662
1663 /// Runs an ordinary handler's dispatch once it holds its instance's action
1664 /// gate, on the strand, and finishes it.
1665 static void startLocal(LocalRun& run) {
1666 MORPH_ZONE("LocalBackend::startLocal");
1667 MORPH_ZONE_TEXT(run.session.requestId);
1668 if (!admitLocal(run)) {
1669 return;
1670 }
1671 std::shared_ptr<void> value;
1672 std::exception_ptr error;
1673 try {
1674 ::morph::session::detail::ScopedContext const scoped{run.session};
1675 // Explicit, because `localOp` is a function pointer now and
1676 // a null one is undefined behaviour rather than the
1677 // `std::bad_function_call` an empty `std::function` used to
1678 // raise. Same outcome for the caller -- the completion
1679 // resolves through its error sink -- with a diagnostic that
1680 // names the field instead of the library.
1681 if (run.localOp == nullptr) {
1682 throw std::runtime_error{"ActionCall::localOp is null: nothing to execute"};
1683 }
1684 value = run.localOp(*run.holder, run.action.get());
1685 } catch (...) {
1686 error = std::current_exception();
1687 }
1688 finishLocal(run, std::move(value), error);
1689 }
1690
1691 /// Starts a Task handler once its dispatch holds the instance's action
1692 /// gate, on the strand. It finishes when its Task completes, through the
1693 /// callback it is handed.
1694 static void startTaskLocal(const std::shared_ptr<LocalRun>& run) {
1695 MORPH_ZONE("LocalBackend::startTaskLocal");
1696 MORPH_ZONE_TEXT(run->session.requestId);
1697 if (!admitLocal(*run)) {
1698 return;
1699 }
1700 std::shared_ptr<::morph::exec::detail::TaskResumer> executor;
1701 try {
1702 ::morph::session::detail::ScopedContext const scoped{run->session};
1703 executor = std::make_shared<::morph::exec::detail::TaskResumer>(run->strands, run->mid, run->session);
1704 run->strands->enroll(run->mid, executor);
1705 auto token = run->stopSource->get_token();
1706 run->localOpAsync(*run->holder, run->action, executor, std::move(token),
1707 [run, resumer = executor.get()](std::shared_ptr<void> value, std::exception_ptr error) {
1708 run->strands->withdraw(run->mid, resumer);
1709 // A handler whose last await resumed on
1710 // another executor -- a `core::net` loop --
1711 // ends there; what follows its end belongs
1712 // on the strand.
1713 run->strands->runOnStrand(run->mid,
1714 [run, value = std::move(value), error = std::move(error)] {
1715 finishLocal(*run, value, error);
1716 });
1717 });
1718 } catch (...) {
1719 run->strands->withdraw(run->mid, executor.get());
1720 finishLocal(*run, nullptr, std::current_exception());
1721 }
1722 }
1723
1724 /// Records a finished dispatch and settles its sink, then leaves the action
1725 /// gate so the next action on the instance can start. On the strand.
1726 static void finishLocal(LocalRun& run, std::shared_ptr<void> value, const std::exception_ptr& error) {
1727 MORPH_ZONE("LocalBackend::finishLocal");
1728 MORPH_ZONE_TEXT(run.session.requestId);
1729 bool const succeeded = error == nullptr;
1730 // Resolve the sink only after every metric and `endSpan` below are
1731 // recorded — nothing synchronizes a `.then()`/`.onError()` callback
1732 // (delivered via `cbExec`, which may run inline/synchronously) with
1733 // anything after `setValue`/`setException` returns, so resolving first
1734 // would let the caller observe completion before these metrics are
1735 // emitted. This is a real race, not just a theoretical one.
1736 //
1737 // Settle hydration the moment the first action's outcome is known —
1738 // before `endSpan`, before any metric, and before the `Completion`
1739 // resolves. Each of those hands control to host code that is free
1740 // to attach to this instance's key, and an attacher reaching the
1741 // directory while the outcome is known but unrecorded is handed an
1742 // instance whose first action has already failed — exactly what
1743 // docs/spec/core/shared_instances.md's Failure modes section says
1744 // must not happen. Only the *first* action settles it; `settle` is
1745 // a single compare-exchange and ignores every later call.
1746 if (run.hydration) {
1747 run.hydration->settle(succeeded);
1748 }
1749 ::morph::observe::detail::endSpan(run.spanId, succeeded);
1750 auto const elapsedMs =
1751 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - run.start).count();
1752 std::array<std::pair<std::string_view, std::string_view>, 2> const tags{
1753 {{"modelType", run.modelTypeId}, {"actionType", run.actionTypeId}}};
1754 ::morph::observe::detail::emitMetric(::morph::observe::Metric::executeLatencyMs, elapsedMs, tags);
1755 if (!succeeded) {
1756 ::morph::observe::detail::emitMetric(::morph::observe::Metric::executeErrors, 1.0, tags);
1757 }
1758 auto const inFlightAfterDec = run.inFlightCounter->fetch_sub(1, std::memory_order_relaxed) - 1;
1759 ::morph::observe::detail::emitMetric(::morph::observe::Metric::executeInFlight,
1760 static_cast<double>(inFlightAfterDec));
1761 // Resolve last: the sink is still settled exactly once, only its
1762 // position relative to the now-recorded instrumentation moved.
1763 if (succeeded) {
1764 run.sink->settleValue(std::move(value));
1765 } else {
1766 run.sink->settleException(error);
1767 }
1768 run.holder->actionGate().leave();
1769 }
1770
1771 /// Records @p run among the Task runs the destructor stops, sweeping the
1772 /// ones that have finished amortised, as `trackPending` does.
1773 void rememberTaskRun(const std::shared_ptr<LocalRun>& run) {
1774 if (_taskRuns.size() >= _taskRunsCompactAt) {
1775 std::erase_if(_taskRuns, [](const auto& weak) { return weak.expired(); });
1776 _taskRunsCompactAt = std::max(kPendingCompactFloor, _taskRuns.size() * 2);
1777 }
1778 _taskRuns.emplace_back(run);
1779 }
1780
1781 // One strand per model instance. Shared with the Task handlers started
1782 // here, whose resumers outlive the backend; closed by the destructor.
1783 std::shared_ptr<::morph::exec::detail::ModelStrands> _strands;
1784 // The owner every verb is called from, once `setOwner` has named it.
1785 std::optional<::morph::exec::detail::OwnerAffinity> _affinity;
1786 // Every live instance, private and shared alike, plus the shared-instance
1787 // directory over them — holder, attach count, directory key and hydration
1788 // state as one record per instance rather than five parallel ModelId-keyed
1789 // maps held in lockstep by convention. Owner-only.
1790 detail::InstanceDirectory _instances;
1791 // Ids of models whose holder answered `isBackendChangeAware() == true` at
1792 // registration time. An index over `_instances`, maintained on the owner
1793 // (inserted in `createHolder`, erased in `deregisterModel` when the instance
1794 // is actually destroyed) so `notifyBackendChanged()` never needs to inspect
1795 // a model it doesn't have to. Always a subset of `_instances`' keys.
1796 //
1797 // Not folded into `InstanceDirectory`: backend-change awareness is a
1798 // LocalBackend-only concern with no `RemoteServer` counterpart, and the
1799 // directory is the state the two backends genuinely share.
1800 std::unordered_set<::morph::exec::detail::ModelId, ::morph::exec::detail::ModelIdHash> _changeAware;
1801 std::atomic<uint64_t> _nextId{0};
1802 // Smallest size at which `trackPending` will sweep. Below it the sweep costs
1803 // more than the handful of dead `weak_ptr`s it could reclaim, and a backend
1804 // that only ever has a few calls in flight never sweeps at all.
1805 static constexpr std::size_t kPendingCompactFloor = 32;
1806 // Sinks, not completion states: a dispatch's settle point is whatever the
1807 // caller handed down, which for `Bridge` is its own typed completion state.
1808 // A `weak_ptr`, so this list cannot keep a finished dispatch alive.
1809 std::vector<std::weak_ptr<::morph::async::detail::ISettleSink>> _pending;
1810 // Size at which `trackPending` next sweeps `_pending` for expired entries;
1811 // re-armed at twice the surviving count after each sweep. See `trackPending`.
1812 std::size_t _compactAt = kPendingCompactFloor;
1813 // Recorded by `cancelPending` as it takes the pending list. Shared with every run, which notes the epoch it was
1814 // admitted under and skips its handler if the epoch has moved on by the time it starts.
1815 std::shared_ptr<CancelRecord> _cancels = std::make_shared<CancelRecord>();
1816 // The Task runs `cancelPending` and the destructor stop. Weak, so a
1817 // finished run is not kept. Owner-only.
1818 std::vector<std::weak_ptr<LocalRun>> _taskRuns;
1819 // The event handler and its executor. `deliverEvent` may be called from any thread, so both sit behind a mutex.
1820 std::mutex _eventMutex;
1821 std::shared_ptr<std::function<void(::morph::wire::EventBody)>> _eventHandler;
1822 ::morph::exec::IExecutor* _eventExec = nullptr;
1823 std::size_t _taskRunsCompactAt = kPendingCompactFloor;
1824 // Concurrent in-flight executes, for the executeInFlight metric. A
1825 // shared_ptr (not a plain atomic member) so strand tasks hold their own
1826 // reference instead of capturing `this` — see execute()'s comment.
1827 std::shared_ptr<std::atomic<std::size_t>> _inFlight = std::make_shared<std::atomic<std::size_t>>(0);
1828};
1829
1830} // namespace morph::backend
Command run(MutationHandle< Action > const &mutation, Body< Action > const &body={}, RunOptions const &options={})
run: runs a mutation.
Definition author.hpp:2755
AnyHandle session(std::string const &name)
A session attribute, {"session": name}.
Definition author.hpp:1214
Producer-side handle paired with a Completion<T> by makeSettleable().
Move-only handle representing the eventual result of an asynchronous operation.
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
In-process backend that executes model actions on a thread pool strand.
Definition backend.hpp:1067
std::vector< std::string > listInstances(const std::string &typeId) override
Lists the primary keys of live shared instances of typeId.
Definition backend.hpp:1232
LocalBackend(::morph::exec::IExecutor &workerPool)
Constructs the backend using workerPool to run model actions.
Definition backend.hpp:1077
::morph::exec::detail::ModelId registerModel(const std::string &, std::function< std::unique_ptr<::morph::model::detail::IModelHolder >()> factory) override
Creates a model instance via factory and registers it.
Definition backend.hpp:1173
std::size_t trackedPendingCount() const
Number of entries currently held in the pending list.
Definition backend.hpp:1458
::morph::async::Completion< std::shared_ptr< void > > execute(::morph::exec::detail::ModelId mid, detail::ActionCall call, ::morph::exec::IExecutor *cbExec) final
Schedules call.localOp on the model's strand and returns a Completion.
Definition backend.hpp:1313
void deliverEvent(::morph::wire::EventBody event)
Test seam: delivers event as if a server had sent it.
Definition backend.hpp:1142
void setEventHandler(std::function< void(::morph::wire::EventBody)> handler, ::morph::exec::IExecutor *exec) override
Installs the event handler deliverEvent posts to.
Definition backend.hpp:1127
void cancelPending(const std::exception_ptr &exc) override
Resolves every still-pending completion this backend produced with exc, and requests stop on every Ta...
Definition backend.hpp:1421
void deregisterModel(::morph::exec::detail::ModelId mid) override
Removes the model with mid, or releases one attachment to it.
Definition backend.hpp:1244
void assignPrimary(::morph::exec::detail::ModelId mid, const std::string &typeId, std::string_view primary) override
Enters an already-live, still-anonymous instance into the directory under primary.
Definition backend.hpp:1217
void executeInto(::morph::exec::detail::ModelId mid, detail::ActionCall call, ::morph::exec::IExecutor *cbExec, std::shared_ptr<::morph::async::detail::ISettleSink > sink) override
Schedules call.localOp on the model's strand and settles sink.
Definition backend.hpp:1332
void notifyBackendChanged() override
Schedules onBackendChanged() on each change-aware model's strand.
Definition backend.hpp:1278
void setOwner(const ::morph::exec::detail::OwnerAffinity &owner) override
Records the owner every verb below is called from.
Definition backend.hpp:1210
~LocalBackend() override
Stops the Task handlers still running, lets the strands drain, seals them, drains them again,...
Definition backend.hpp:1112
::morph::async::Completion<::morph::exec::detail::ModelId > bindModel(detail::BindRequest request, ::morph::exec::IExecutor &cbExec) override
Acquires an instance for request and settles the returned Completion before returning.
Definition backend.hpp:1195
Makes a blocking backend reach its caller without blocking it — and without touching the backend.
Definition backend.hpp:694
void cancelPending(const std::exception_ptr &exc) override
Rejects the binds this adapter produced, then queues the wrapped backend's own cancelPending.
Definition backend.hpp:878
SynchronousBackendAdapter(std::shared_ptr< detail::IBackend > inner, ::morph::exec::IExecutor &blockingExec)
Wraps inner, running every one of its verbs on blockingExec.
Definition backend.hpp:705
void notifyBackendChanged() override
Queues the wrapped backend's notifyBackendChanged.
Definition backend.hpp:857
~SynchronousBackendAdapter() override
Waits for every queued and in-flight call; see "Ordering" above.
Definition backend.hpp:714
void executeInto(::morph::exec::detail::ModelId mid, detail::ActionCall call, ::morph::exec::IExecutor *cbExec, std::shared_ptr<::morph::async::detail::ISettleSink > sink) override
Queues the wrapped backend's executeInto on the strand.
Definition backend.hpp:845
::morph::async::Completion<::morph::exec::detail::ModelId > promoteModel(detail::PromoteRequest request, ::morph::exec::IExecutor &cbExec) override
Files an already-live instance under a key without blocking the caller.
Definition backend.hpp:760
::morph::async::Completion<::morph::exec::detail::ModelId >::Promise BindPromise
The producer side of a bindModel/promoteModel completion.
Definition backend.hpp:727
void setConnectHandler(const std::function< void()> &handler) override
Queues the installation on the wrapped backend.
Definition backend.hpp:912
void setSession(::morph::session::Context session) override
Queues the session on the wrapped backend.
Definition backend.hpp:924
void setOwner(const ::morph::exec::detail::OwnerAffinity &owner) override
Records the caller's owner. Not forwarded: the wrapped backend's owner is this adapter's control stra...
Definition backend.hpp:772
void assignPrimary(::morph::exec::detail::ModelId mid, const std::string &typeId, std::string_view primary) override
Runs the wrapped backend's assignPrimary on the strand and waits for it.
Definition backend.hpp:804
void setEventHandler(std::function< void(::morph::wire::EventBody)> handler, ::morph::exec::IExecutor *exec) override
Queues the installation on the wrapped backend.
Definition backend.hpp:903
::morph::async::Completion< std::shared_ptr< void > > execute(::morph::exec::detail::ModelId mid, detail::ActionCall call, ::morph::exec::IExecutor *cbExec) override
Queues the dispatch on the strand and returns its Completion at once.
Definition backend.hpp:830
std::vector< std::string > listInstances(const std::string &typeId) override
Runs the wrapped backend's listInstances on the strand and waits for it.
Definition backend.hpp:815
::morph::async::Completion<::morph::exec::detail::ModelId > bindModel(detail::BindRequest request, ::morph::exec::IExecutor &cbExec) override
Acquires a model instance without blocking the calling thread.
Definition backend.hpp:743
::morph::exec::detail::ModelId registerModel(const std::string &typeId, std::function< std::unique_ptr<::morph::model::detail::IModelHolder >()> factory) override
Runs the wrapped backend's registerModel on the strand and waits for it.
Definition backend.hpp:778
void setDisconnectHandler(const std::function< void()> &handler) override
Queues the installation on the wrapped backend.
Definition backend.hpp:918
void deregisterModel(::morph::exec::detail::ModelId mid) override
Queues the wrapped backend's deregisterModel.
Definition backend.hpp:821
detail::IBackend & wrapped() const noexcept
The wrapped backend.
Definition backend.hpp:731
void setReconnectHandler(std::function< void()> handler, ::morph::exec::IExecutor *exec) override
Queues the installation on the wrapped backend.
Definition backend.hpp:894
::morph::exec::detail::ModelId registerModelWithContext(const std::string &typeId, std::function< std::unique_ptr<::morph::model::detail::IModelHolder >()> factory, std::string_view contextKey) override
Runs the wrapped backend's registerModelWithContext on the strand and waits for it.
Definition backend.hpp:791
LowerCtx attached(DetachedLower const &held)
Puts a detached context's scope back.
"Am I on my owner?" for a component that belongs to one executor.
Compile-time profiler instrumentation: Tracy zones, plots, messages and thread names,...
#define MORPH_ZONE_TEXT(text)
Attaches text to the enclosing scope's MORPH_ZONE; an empty text attaches nothing.
Definition profiler.hpp:126
#define MORPH_ZONE(name)
Opens a profiler zone named name that ends with the enclosing scope. At most one per scope.
Definition profiler.hpp:118
void request(Store< ViewState, Msg > &store, bridge::BridgeHandler< Model, Sharing > &handler, async::CallbackScope const &scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg)
Executes action and sends its result, or its failure, to store as one message.
Definition store.hpp:202
morph's strands: one per model instance, from core-cpp's core::async::KeyedStrands,...
BackendChangedError()
Constructs the error with a canned diagnostic message.
Definition backend.hpp:602
BridgeDestroyedError()
Constructs the error with a canned diagnostic message.
Definition backend.hpp:608
ClientTimeoutError()
Constructs the error with a canned diagnostic message.
Definition backend.hpp:654
DisconnectedError()
Constructs the error with a canned diagnostic message.
Definition backend.hpp:623
HandlerDestroyedError()
Constructs the error with a canned diagnostic message.
Definition backend.hpp:615
TimeoutError()
Constructs the error with a canned diagnostic message.
Definition backend.hpp:638
Abstract executor interface.
Definition executor.hpp:70
virtual void post(std::function< void()> task)=0
Schedules task for asynchronous execution.
Per-call session context carried through the bridge to the model.
Definition session.hpp:26
The decoded body of an event envelope.
Definition wire.hpp:621
std::shared_ptr< NodeData const > Node
A view-tree node: immutable, shared data.
Definition view.hpp:403