morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
morph::backend::RemoteServer Class Reference

Server-side message handler that owns model instances and dispatches actions. More...

#include <remote.hpp>

Public Types

using LogProvider = ::morph::backend::LogProvider
 Alias of morph::backend::LogProvider, the type of ServerConfig::logProvider.

Public Member Functions

 RemoteServer (::morph::exec::IExecutor &workerPool, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher(), ::morph::model::detail::ModelRegistryFactory &registry=::morph::model::detail::defaultRegistry())
 Constructs a server backed by workerPool with allow-all authorization and a default ServerConfig.
 RemoteServer (::morph::exec::IExecutor &workerPool, std::shared_ptr<::morph::session::IAuthorizer > authorizer, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher(), ::morph::model::detail::ModelRegistryFactory &registry=::morph::model::detail::defaultRegistry())
 Constructs a server with a custom authorizer and a default ServerConfig.
 RemoteServer (::morph::exec::IExecutor &workerPool, ServerConfig config, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher(), ::morph::model::detail::ModelRegistryFactory &registry=::morph::model::detail::defaultRegistry())
 Constructs a server with allow-all authorization and config.
 RemoteServer (::morph::exec::IExecutor &workerPool, std::shared_ptr<::morph::session::IAuthorizer > authorizer, ServerConfig config, ::morph::model::detail::ActionDispatcher &dispatcher=::morph::model::detail::defaultDispatcher(), ::morph::model::detail::ModelRegistryFactory &registry=::morph::model::detail::defaultRegistry())
 Constructs a server with a custom authorizer and config.
 RemoteServer (const RemoteServer &)=delete
RemoteServer & operator= (const RemoteServer &)=delete
 RemoteServer (RemoteServer &&)=delete
RemoteServer & operator= (RemoteServer &&)=delete
 ~RemoteServer ()
 Closes the server strand before any other member goes.
void handle (std::string msg, std::function< void(std::string)> reply)
 Asynchronously processes a JSON Envelope and calls reply with the response.
void handle (std::string msg, std::function< void(std::string)> reply, ConnectionId cid)
 Like handle(msg, reply), but additionally attributes any register decoded from msg to a connection scope.
std::string handleInline (const std::string &msg)
 Processes a control Envelope on the server strand and returns its reply, blocking the caller until it has one.
std::string handleInline (const std::string &msg, ConnectionId cid)
 Like handleInline(msg), but additionally attributes any register decoded from msg to a connection scope.
ConnectionId openConnection ()
 Opens a new connection scope and returns its id.
void closeConnection (ConnectionId cid)
 Reclaims every model still registered under cid, then drops the scope.
void setConnectionWriter (ConnectionId cid, ConnectionWriter writer)
 Hands the server the transport's writer for connection cid, through which post reaches it.
void post (ConnectionId cid, std::string envelope)
 Writes envelope to connection cid, unless its reader has fallen behind.
void connectionDrained (ConnectionId cid)
 Tells the server that connection cid's backlog has fallen below kEventBacklogLow.
bool eventsNegotiated (ConnectionId cid) const
 Whether connection cid negotiated server-sent events.
detail::WatchTable const & watchTable () const
 The topics each open connection watches.
void fanOut (::morph::effects::Changes changes, ConnectionId caller, std::string_view callerPrincipal)
 Tells every connection watching what changes names that it changed, as one changes event per connection.
::morph::exec::IExecutor & strand () noexcept
 The server strand, as an executor.
PayloadCompleteness payloadCompleteness () const noexcept
 Returns the configured payload-completeness rule.
::morph::async::Completion< HealthStatus > health (::morph::exec::IExecutor &replyExec)
 The server's health, answered on the server strand.
void beginShutdown ()
 Enters shutdown: from now on, register, attach and execute envelopes are rejected with err "server shutting down". deregister is still served so clients can tear down cleanly. attach is refused too, so a client cannot re-attach to a shared instance during the drain window.
::morph::async::Completion< bool > drainedWithin (std::chrono::milliseconds deadline, ::morph::exec::IExecutor &replyExec)
 Answers whether every in-flight execute delivers its reply within deadline.

Detailed Description

Server-side message handler that owns model instances and dispatches actions.

RemoteServer receives JSON envelopes (morph::wire::Envelope) from any transport (WebSocket, in-process simulation, …) and executes the corresponding model operations via an ActionDispatcher. Authorization is delegated to an IAuthorizer that defaults to allow-all.

One owner
The server's state — the instance registry, the connection scopes, the in-flight count, readiness — belongs to one strand over the worker pool, the server strand, and is touched only in its tasks. Every public verb posts to it. An execute is admitted there and then posted to its model's own strand, so for one model the order handle() was called in is the order the model runs them: both hops are strands, and a strand runs in post order. Configuration is a ServerConfig, fixed at construction.
Heap allocation requirement
RemoteServer must be heap-allocated via std::make_shared. Every task it posts captures shared_from_this(), so the server outlives its queued work however the last external reference is dropped.
Wire format
All requests and replies are encoded as morph::wire::Envelope JSON. See wire.hpp for the field semantics. The kind field is the discriminator.

Definition at line 365 of file remote.hpp.

Member Typedef Documentation

◆ LogProvider

using morph::backend::RemoteServer::LogProvider = ::morph::backend::LogProvider

Alias of morph::backend::LogProvider, the type of ServerConfig::logProvider.

Definition at line 369 of file remote.hpp.

Constructor & Destructor Documentation

◆ RemoteServer() [1/4]

morph::backend::RemoteServer::RemoteServer ( ::morph::exec::IExecutor & workerPool,
::morph::model::detail::ActionDispatcher & dispatcher = ::morph::model::detail::defaultDispatcher(),
::morph::model::detail::ModelRegistryFactory & registry = ::morph::model::detail::defaultRegistry() )
inlineexplicit

Constructs a server backed by workerPool with allow-all authorization and a default ServerConfig.

Parameters
workerPoolPool the server strand and every model strand run on. Borrowed, not owned: it must outlive this server and keep running until teardown completes (see docs/spec/concurrency_and_lifetimes.md, "Destruction ordering").
dispatcherAction dispatcher; defaults to the process-level singleton. Borrowed: it must outlive this server.
registryModel factory registry; defaults to the process-level singleton. Borrowed: it must outlive this server.
Exceptions
morph::model::EffectsRegistrationErrorif the process's record and effects declarations are invalid: the server reads them here, so such a server is never constructed.

Definition at line 386 of file remote.hpp.

◆ RemoteServer() [2/4]

morph::backend::RemoteServer::RemoteServer ( ::morph::exec::IExecutor & workerPool,
std::shared_ptr<::morph::session::IAuthorizer > authorizer,
::morph::model::detail::ActionDispatcher & dispatcher = ::morph::model::detail::defaultDispatcher(),
::morph::model::detail::ModelRegistryFactory & registry = ::morph::model::detail::defaultRegistry() )
inline

Constructs a server with a custom authorizer and a default ServerConfig.

Parameters
workerPoolPool the server's strands run on. Borrowed, on the same terms as the constructor above.
authorizerAuthorizer consulted for every envelope that makes an authorization decision; null means allow-all.
dispatcherAction dispatcher; defaults to the process-level singleton. Borrowed: it must outlive this server.
registryModel factory registry; defaults to the process-level singleton. Borrowed: it must outlive this server.
Exceptions
morph::model::EffectsRegistrationErrorif the process's record and effects declarations are invalid: the server reads them here, so such a server is never constructed.

Definition at line 407 of file remote.hpp.

◆ RemoteServer() [3/4]

morph::backend::RemoteServer::RemoteServer ( ::morph::exec::IExecutor & workerPool,
ServerConfig config,
::morph::model::detail::ActionDispatcher & dispatcher = ::morph::model::detail::defaultDispatcher(),
::morph::model::detail::ModelRegistryFactory & registry = ::morph::model::detail::defaultRegistry() )
inline

Constructs a server with allow-all authorization and config.

Parameters
workerPoolPool the server's strands run on. Borrowed, on the same terms as the first constructor.
configLimits, log provider, health handler, protocol range and payload rule, fixed for the server's life.
dispatcherAction dispatcher; defaults to the process-level singleton. Borrowed: it must outlive this server.
registryModel factory registry; defaults to the process-level singleton. Borrowed: it must outlive this server.
Exceptions
std::invalid_argumentif config.minProtocolVersion exceeds config.maxProtocolVersion, or config.services holds a null entry.
morph::model::EffectsRegistrationErrorif the process's record and effects declarations are invalid: the server reads them here, so such a server is never constructed.

Definition at line 431 of file remote.hpp.

◆ RemoteServer() [4/4]

morph::backend::RemoteServer::RemoteServer ( ::morph::exec::IExecutor & workerPool,
std::shared_ptr<::morph::session::IAuthorizer > authorizer,
ServerConfig config,
::morph::model::detail::ActionDispatcher & dispatcher = ::morph::model::detail::defaultDispatcher(),
::morph::model::detail::ModelRegistryFactory & registry = ::morph::model::detail::defaultRegistry() )
inline

Constructs a server with a custom authorizer and config.

Calls config.healthHandler, if set, once, before returning, with the initial status (ready, nothing registered, nothing in flight).

Parameters
workerPoolPool the server's strands run on. Borrowed, on the same terms as the first constructor.
authorizerAuthorizer consulted for every envelope that makes an authorization decision; null means allow-all.
configLimits, log provider, health handler, protocol range and payload rule, fixed for the server's life.
dispatcherAction dispatcher; defaults to the process-level singleton. Borrowed: it must outlive this server.
registryModel factory registry; defaults to the process-level singleton. Borrowed: it must outlive this server.
Exceptions
std::invalid_argumentif config.minProtocolVersion exceeds config.maxProtocolVersion, or config.services holds a null entry.
morph::model::EffectsRegistrationErrorif the process's record and effects declarations are invalid: the server reads them here, so such a server is never constructed.

Definition at line 459 of file remote.hpp.

◆ ~RemoteServer()

morph::backend::RemoteServer::~RemoteServer ( )
inline

Closes the server strand before any other member goes.

Every task the server posts holds the server, so nothing of it is still queued here; closing waits for a task still running on another thread and returns at once from inside the server's own last task.

Definition at line 493 of file remote.hpp.

Member Function Documentation

◆ handle() [1/2]

void morph::backend::RemoteServer::handle ( std::string msg,
std::function< void(std::string)> reply )
inline

Asynchronously processes a JSON Envelope and calls reply with the response.

Decodes on the calling thread and posts the envelope to the server strand. reply is called exactly once, from a pool thread: the server strand's, or, for an execute, the model strand's (or the I/O loop's, when a LimitPolicy::executeTimeout answers first).

Callable from any thread. For one connection, calls made one after the other on one thread are handled in that order.

Parameters
msgJSON-encoded morph::wire::Envelope (via wire::encode).
replyCallback invoked with the JSON-encoded reply envelope.

Definition at line 510 of file remote.hpp.

◆ handle() [2/2]

void morph::backend::RemoteServer::handle ( std::string msg,
std::function< void(std::string)> reply,
ConnectionId cid )
inline

Like handle(msg, reply), but additionally attributes any register decoded from msg to a connection scope.

A register processed under a non-zero cid records the new model in that connection's scope, so a later closeConnection(cid) reclaims it. Passing cid == 0 is exactly the unscoped, two-argument handle() — nothing is recorded and nothing is ever cleaned up automatically.

Callable from any thread, on the same terms as handle(msg, reply).

Parameters
msgJSON-encoded morph::wire::Envelope (via wire::encode).
replyCallback invoked with the JSON-encoded reply envelope.
cidConnection scope to attribute a register in msg to; 0 means unscoped.

Definition at line 528 of file remote.hpp.

◆ handleInline() [1/2]

std::string morph::backend::RemoteServer::handleInline ( const std::string & msg)
inline

Processes a control Envelope on the server strand and returns its reply, blocking the caller until it has one.

For register, deregister, attach, assign, instances, schemas and hello — the synchronous control path SimulatedRemoteBackend uses. Runs inline when the caller is already on the server strand; otherwise posts and waits. execute is rejected up front with an err reply: its reply is produced later, on the model's strand.

Callable from a pool thread, including one inside a running action (a handler registered from an action handler): the server strand then runs on another pool thread while this one waits. So the pool needs a thread free for it — a one-thread pool calling this from inside its own task, or every pool thread blocked here at once, never gets its reply. See docs/spec/concurrency_and_lifetimes.md, "Synchronous re-entry".

Parameters
msgJSON-encoded morph::wire::Envelope (via wire::encode).
Returns
JSON-encoded reply envelope.

Definition at line 550 of file remote.hpp.

◆ handleInline() [2/2]

std::string morph::backend::RemoteServer::handleInline ( const std::string & msg,
ConnectionId cid )
inline

Like handleInline(msg), but additionally attributes any register decoded from msg to a connection scope.

The synchronous counterpart to the scoped handle(msg, reply, cid) overload, on the same terms as the one-argument handleInline. Passing cid == 0 is exactly the unscoped overload above.

Parameters
msgJSON-encoded morph::wire::Envelope (via wire::encode).
cidConnection scope to attribute a register in msg to; 0 means unscoped.
Returns
JSON-encoded reply envelope.

Definition at line 563 of file remote.hpp.

◆ openConnection()

ConnectionId morph::backend::RemoteServer::openConnection ( )
inlinenodiscard

Opens a new connection scope and returns its id.

Call once per accepted transport connection (e.g. from a WebSocket server's "new connection" callback). The returned id is never 0, so it can always be distinguished from the reserved "unscoped" value. Pass it to the scoped handle(msg, reply, cid) overload for every message received on that connection, and to closeConnection(cid) once the connection is gone.

Callable from any thread. The id is drawn at once; the scope is opened on the server strand, before any handle() the same thread makes next.

Returns
A fresh, non-zero ConnectionId.

Definition at line 603 of file remote.hpp.

◆ closeConnection()

void morph::backend::RemoteServer::closeConnection ( ConnectionId cid)
inline

Reclaims every model still registered under cid, then drops the scope.

Call once the transport observes the connection is gone (disconnect, close, error). Releases exactly as many references as this connection held, exactly as an explicit deregister would, then drops the scope itself. A private instance (count 1, no directory entry) is erased outright, so a later execute against its id replies err "model not found"; a shared instance another connection is still attached to survives, and an execute against it still succeeds — see InstanceDirectory::release's retained outcome.

Idempotent: cid == 0, an unknown cid, or a cid already closed is a no-op. Deliberately does not consult IAuthorizer — this is the server's own housekeeping in reaction to a transport-level event, not an action attributable to any caller (see docs/spec/core/backend.md).

Safe while a model in the scope has an execute in flight: the model strand's task holds its own shared_ptr to the model holder, so erasing the registry entry here only prevents new lookups (see docs/spec/concurrency_and_lifetimes.md).

Callable from any thread; runs on the server strand, before any handle() the same thread makes next.

Parameters
cidConnection scope to close, as returned by openConnection().

Definition at line 637 of file remote.hpp.

◆ setConnectionWriter()

void morph::backend::RemoteServer::setConnectionWriter ( ConnectionId cid,
ConnectionWriter writer )
inline

Hands the server the transport's writer for connection cid, through which post reaches it.

Replaces any writer cid already has, keeping a resync the connection is owed. Kept only while the connection is open: a writer for 0, for an unknown cid or for one already closed is dropped, and closeConnection(cid) drops it. A writer whose write is empty removes the connection's writer.

Callable from any thread; runs on the server strand, after an openConnection() and before any post the same thread makes next.

Parameters
cidConnection, as returned by openConnection().
writerThe connection's writer and its backlog read.

Definition at line 657 of file remote.hpp.

◆ post()

void morph::backend::RemoteServer::post ( ConnectionId cid,
std::string envelope )
inline

Writes envelope to connection cid, unless its reader has fallen behind.

On the server strand, while the connection's backlog() exceeds kEventBacklogHigh, the envelope is dropped and the connection is owed one resync event, which connectionDrained(cid) sends. Until then every later envelope is dropped too, so the resync is the next thing the connection is sent after the hold-back began. Replies do not pass through here and are never held back.

Dropped as well: an envelope larger than wire::kMaxEnvelopeBytes (counted as Metric::eventOversize), and one for a connection with no writer — 0, unknown, or closed. A write or backlog that throws is caught on the strand, and the envelope is not written.

Callable from any thread. Envelopes one thread posts to one connection are written in that order.

Parameters
cidConnection to write to.
envelopeThe encoded envelope (via wire::encode).

Definition at line 696 of file remote.hpp.

◆ connectionDrained()

void morph::backend::RemoteServer::connectionDrained ( ConnectionId cid)
inline

Tells the server that connection cid's backlog has fallen below kEventBacklogLow.

What a transport calls when it has written enough of what it holds. If post held an event back since the last drain, the connection is sent one resync event (wire::makeEvent(wire::kResyncTopic, "{}")), meaning everything it watches may have changed, and events flow again. Otherwise nothing is sent. The drain, not a later write, sends it, so a connection that only listens still receives it.

Callable from any thread, the transport's included; it only posts to the server strand.

Parameters
cidConnection whose backlog drained.

Definition at line 721 of file remote.hpp.

◆ eventsNegotiated()

bool morph::backend::RemoteServer::eventsNegotiated ( ConnectionId cid) const
inlinenodiscard

Whether connection cid negotiated server-sent events.

Server strand only: call it from a task on strand(); a debug build asserts that. The answer is what the UI service recorded when the connection's ui-hello offered events, and stays until the connection closes; a reconnect is a new connection and negotiates again.

Parameters
cidConnection, as returned by openConnection().
Returns
true once the connection negotiated events; false for 0, an unknown connection or a closed one.

Definition at line 737 of file remote.hpp.

◆ watchTable()

detail::WatchTable const & morph::backend::RemoteServer::watchTable ( ) const
inlinenodiscard

The topics each open connection watches.

Server strand only: call it from a task on strand(); a debug build asserts that. Filled by the watch and unwatch kinds, and emptied for a connection when it closes or a write to it fails.

Returns
The watch table, valid for the server's life.

Definition at line 749 of file remote.hpp.

◆ fanOut()

void morph::backend::RemoteServer::fanOut ( ::morph::effects::Changes changes,
ConnectionId caller,
std::string_view callerPrincipal )
inline

Tells every connection watching what changes names that it changed, as one changes event per connection.

Server strand only: call it from a task on strand(); a debug build asserts that. A successful execute posts its changes here.

A record topic, and the instance topic of a keyed shared instance, go to every connection watching them, caller included: the caller may never see its own reply (it timed out, the client discarded it, or it went to an offline replay). An instance is matched by the directory key it holds now, so one an assign keyed while its execute ran reaches that key's watchers. A private instance's topic goes to every connection whose scope holds that instance, which needs no watch, and to no other. A connection that has not negotiated events is sent nothing.

Each recipient's watches are then re-authorised by the rule that admitted them (reauthorise): a topic whose watch fails is dropped from the table and from the event, and a connection that lost any watch is sent one resync (sendResync), after whatever it had queued. A record registered with morph::record::privateKeys reaches a principal other than callerPrincipal without its keys.

What is left is queued on the connection (detail::ChangeCoalescer), not sent: the queue is merged for ServerConfig::coalesceWindow from its first change, or until detail::kCoalesceKeyLimit keys are queued, and then sent as one changes event through post's backlog hold-back and size cap. Nothing is queued for a connection with no writer, or one held back behind its backlog, which its drain's resync covers.

Parameters
changesWhat one execute changed, from effects::deriveChanges.
callerConnection the execute arrived on; 0 for unscoped.
callerPrincipalThe execute's verified principal; empty when none was.

Definition at line 786 of file remote.hpp.

◆ strand()

::morph::exec::IExecutor & morph::backend::RemoteServer::strand ( )
inlinenodiscardnoexcept

The server strand, as an executor.

What runningOn(server.strand()) asks about: true inside every task the server runs on its own state, false elsewhere. A task posted to it runs in order with the server's own, never beside one.

Returns
The strand every envelope, verb and execute admission runs on.

Definition at line 821 of file remote.hpp.

◆ payloadCompleteness()

PayloadCompleteness morph::backend::RemoteServer::payloadCompleteness ( ) const
inlinenodiscardnoexcept

Returns the configured payload-completeness rule.

Returns
ServerConfig::payloadCompleteness, as given at construction.

Definition at line 825 of file remote.hpp.

◆ health()

::morph::async::Completion< HealthStatus > morph::backend::RemoteServer::health ( ::morph::exec::IExecutor & replyExec)
inlinenodiscard

The server's health, answered on the server strand.

Posted, so the answer reflects every envelope and verb this thread handed the server before calling. Callable from any thread; the caller attaches to the answer on replyExec, the owner it names.

Parameters
replyExecExecutor the answer is delivered on, and the one the caller attaches its callbacks from. Borrowed: it must outlive the returned Completion's delivery.
Returns
A Completion settled on the server strand with ready, the live model count and the in-flight execute count; its callbacks run on replyExec.

Definition at line 838 of file remote.hpp.

◆ beginShutdown()

void morph::backend::RemoteServer::beginShutdown ( )
inline

Enters shutdown: from now on, register, attach and execute envelopes are rejected with err "server shutting down". deregister is still served so clients can tear down cleanly. attach is refused too, so a client cannot re-attach to a shared instance during the drain window.

Posted to the server strand: an envelope this thread hands handle() after this call returns is refused. Idempotent. There is no way back: a restarted service constructs a fresh RemoteServer.

Also flips health()'s ready to false and calls ServerConfig::healthHandler, if set, with the post-shutdown status, on the server strand — what lets an orchestrator stop routing to this server while drainedWithin()'s drain runs.

Definition at line 861 of file remote.hpp.

◆ drainedWithin()

::morph::async::Completion< bool > morph::backend::RemoteServer::drainedWithin ( std::chrono::milliseconds deadline,
::morph::exec::IExecutor & replyExec )
inlinenodiscard

Answers whether every in-flight execute delivers its reply within deadline.

"In-flight" is one count, kept on the server strand: incremented when an execute is admitted for dispatch and decremented, on the strand, when its reply is sent — on every resolving path (ok, err, or a LimitPolicy::executeTimeout firing first). The same count LimitPolicy::maxInFlightExecutes gates and health()'s inFlight reads. Independent of beginShutdown(): it observes, it does not stop new work from arriving. Callable from any thread; nothing blocks. The caller attaches to the answer on replyExec, the owner it names.

Parameters
deadlineLongest time to wait; 0 answers from the count as it stands.
replyExecExecutor the answer is delivered on, and the one the caller attaches its callbacks from. Borrowed: it must outlive the returned Completion's delivery.
Returns
A Completion settled on the server strand: true once the count is zero, false if deadline elapses first. Its callbacks run on replyExec.

Definition at line 892 of file remote.hpp.


The documentation for this class was generated from the following file: