Skip to content

Consider: dynamic config / hot-reload for *Config types #117

Description

@Yaraslaut

Part of #115 (userver-vs-morph survey).

Observation

Every *Config type in morph (ReconnectCoordinatorConfig, NetworkMonitorConfig, QtWebSocketServerConfig, QtWebSocketBackendConfig, SocketBackendConfig/SocketServerConfig, Lightweight's PoolConfig) is a plain aggregate consumed once at construction time. Changing a value means reconstructing the object — no file-watching, no reload signal, no config-service client. userver's dynamic_config::Source/Snapshot lets a whole fleet of service instances pick up new config values (kill-switches, timeouts, experiment flags) without a redeploy, backed by a polling components::DynamicConfigClient against a reference config service.

Status: plain absence, not currently a scope gap

This is a genuine absence rather than a scope substitute worth stretching for right now — morph has no long-lived fleet of server processes to reconfigure without redeploy, which is the exact problem this subsystem solves.

Revisit only if

morph grows a long-lived, multi-instance server deployment story of its own (multiple RemoteServer processes that need centrally-controlled runtime behavior changes without a redeploy).

Reference

Full comparison: #115, §3.5 of the attached document (docs/superpowers/findings/userver-vs-morph-2026-08-17.md).


Deep analysis: #117 dynamic config / hot-reload

Verification against current source (2026-08-19). The deferral's factual basis still holds, and has strengthened:

  • All six in-repo *Config types remain plain aggregates consumed at construction, with no setter or reload path: SocketServerConfig/SocketBackendConfig (include/morph/net/socket_server.hpp, socket_backend.hpp), ReconnectCoordinatorConfig/NetworkMonitorConfig (include/morph/offline/), QtWebSocketServerConfig/QtWebSocketBackendConfig (include/morph/qt/). Lightweight's PoolConfig is a template non-type parameter (Pool<PoolConfig{...}>) in a third-party dependency — not reloadable even in principle, and not morph's code to change.
  • RemoteServer::setLimitPolicy() (include/morph/core/remote.hpp:361) is still the one production-proven hot-swap mechanism: mutex-guarded install, locked-copy read on every register/execute/shared-acquire. It is still called exactly once per server, from hardcoded constexpr literals (examples/polls/src/app/app.cpp:57, examples/bookmarks/src/app/app.cpp:90) — never from env.
  • The trigger ("long-lived multi-instance server deployment") remains unmet: the examples ladder has grown to eleven rungs, yet still only pastebin, bookmarks, and polls ship a server binary, each reading env vars once at the top of main(). docs/spec/core/backend.md still states restart-to-reconfigure as design: "there is no un-shutdown; a restarted service constructs a fresh RemoteServer."

Refinement of the sketched minimal design. Two findings argue against the SIGHUP half of the sketch:

  1. Platform fit. morph runs a first-class Windows CI job (.github/workflows/ci.yml), and SIGHUP does not exist on Windows. The Qt-safe signal pattern (volatile sig_atomic_t + QTimer poll, already used for SIGINT in examples/pastebin/src/server/main.cpp) works only on POSIX and only in the embedding app — a library-level SIGHUP story would be half-platform.
  2. A better in-tree trigger exists. Every execute already passes the IAuthorizer choke point (docs/spec/security.md), and the wire protocol carries typed actions with an action log. An operator-authorized admin action ("set limits") delivered over the existing wire is portable, testable with the existing harness, auditable, and multi-instance-ready — strictly dominating SIGHUP here.

Which values would actually benefit: only server-side, live-behavior-gating ones — LimitPolicy (mechanism already done) and QtWebSocketServerConfig's messagesPerSecond/maxConnections/idleTimeout, which the event-loop thread reads live per-message (src/qt/qt_websocket_server.cpp:194-297), so a queued setter needs no locking. The client-side reconnect/monitor configs are read lock-free on background threads; retrofitting setters there adds synchronization cost for values a client fixes at startup anyway.

Cost/benefit: S–M effort for the narrowed version (one setter on QtWebSocketServer, env-or-action plumbing in example servers), but non-trivial spec/docs burden per field under morph's spec-sync discipline, benefiting no current user.

Recommendation: defer, with the trigger sharpened. Revisit when a rung's server needs an operator-flippable value (rate-limit or kill-switch) without dropping connections — and implement it then as an authorized admin action over the existing wire protocol calling setLimitPolicy-style setters, not as a SIGHUP/env re-read.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions