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

    area: coreSubsystem: coreenhancementNew feature or requesttriage: parkedDeferred with an explicit re-entry condition

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions