|
| 1 | +### DR-014: Behavior decomposition of `webserver_impl` into request-pipeline services |
| 2 | + |
| 3 | +**Status:** Accepted |
| 4 | +**Date:** 2026-07-18 |
| 5 | +**Context:** The v2.0 refactor extracted five *state* clusters out of the `webserver_impl` god-object, each into its own collaborator that owns a mutex plus data: `daemon_lifecycle`, `route_table`, `hook_bus`, `ip_access_control`, `ws_registry` (recorded to date only as implementation-status prose in the §4.x component docs — this DR is also the missing decision record for that work). What remained un-extracted is *behavior*: ~80 `webserver_impl::` member functions, spread across ~20 `src/detail/webserver_*.cpp` translation units, that drive the libmicrohttpd callback sequence and read/write the per-request `detail::modded_request`. The physical file split (forced by the 500-SLOC per-file gate) gave the *appearance* of decomposition without the ownership structure behind it: every one of those methods is still a method of one class, free to reach any field via the `parent` back-pointer. This is the residual god-object. |
| 6 | + |
| 7 | +**Two kinds of collaborator.** This DR names the distinction the refactor had been making implicitly: |
| 8 | +- **State collaborators** own a mutex + mutable data (the five above). Extracted by DR-007/DR-012 and predecessors. |
| 9 | +- **Behavior services** own *logic*, not state. They are per-server, effectively stateless, operate on a `modded_request&` passed in, and hold only `const&` references to their dependencies. This DR introduces them. |
| 10 | + |
| 11 | +When both kinds are extracted, `webserver_impl` becomes a **composition root**: a constructor, the five state members, the behavior-service members, and the static MHD trampolines. It contains no request logic of its own. |
| 12 | + |
| 13 | +**Options considered:** |
| 14 | +1. **Eight behavior services over `modded_request`, wired as a DAG, held by the composition root.** Config-hungry services take `const webserver_config&`; the rest take specific collaborator/service references. |
| 15 | +2. **Fewer, coarser services (4–5).** Merge the pipeline/dispatch/materialize stages. |
| 16 | +3. **A single `request_processor` mediator** owning all the logic, sub-methods still on one class. |
| 17 | +4. **Leave it as files; add a `request_context` bundle** passed to each method for testability. |
| 18 | + |
| 19 | +**Decision:** Option 1. Eight services: |
| 20 | + |
| 21 | +| Service | Responsibility | Ctor dependencies | |
| 22 | +|---|---|---| |
| 23 | +| `error_pages` | synthesise 404/405/500 responses | `const webserver_config&` | |
| 24 | +| `response_materializer` | `http_response` → `MHD_Response` + queue | `error_pages&`, digest opaque | |
| 25 | +| `hook_dispatcher` | dispatch-time hook gating + firing | `hook_bus&` | |
| 26 | +| `upload_pipeline` | multipart / file-upload handling | `const webserver_config&` | |
| 27 | +| `websocket_upgrader` | RFC-6455 handshake (`HAVE_WEBSOCKET`) | `ws_registry&` | |
| 28 | +| `connection_callbacks` | MHD connection/daemon lifecycle mapping | `const webserver_config&`, `ip_access_control&`, `hook_dispatcher&` | |
| 29 | +| `request_dispatcher` | route + auth + handler invocation | `route_table&`, `hook_dispatcher&`, `error_pages&`, `response_materializer&`, `const webserver_config&` | |
| 30 | +| `request_pipeline` | MHD re-entrant body accumulation | `const webserver_config&`, `hook_dispatcher&`, `request_dispatcher&` | |
| 31 | + |
| 32 | +**Rationale:** |
| 33 | +- **Eight, by reason-to-change (Option 1 over 2).** Each service has a distinct change axis — MHD body protocol vs routing/auth vs wire translation vs hook gating vs upload vs ws handshake vs connection lifecycle vs synthetic pages. The 500-SLOC and CCN-10 gates already reward this granularity; coarser services would re-hit the gates and re-fragment. The one defensible merge (`request_pipeline`+`request_dispatcher`, the most-coupled pair) was considered and rejected: they have genuinely different reasons to change. |
| 34 | +- **DAG, no mediator (over Option 3).** The dependency graph is acyclic — `request_pipeline → request_dispatcher → {response_materializer, error_pages, hook_dispatcher, route_table}`, `response_materializer → {error_pages, hook_dispatcher}`, `error_pages` is a leaf. Plain member references in the composition root suffice; no mediator/service-locator is needed. Because services only *store* references at construction (never invoke a dependency during their own ctor), binding a reference to a sibling member is well-defined regardless of member-declaration order; `-Wreorder -Werror` still guards accidental reorders. |
| 35 | +- **`const webserver_config&`, not the `parent` back-pointer (rejecting Option 4's bundle).** The `webserver*` back-pointer is exactly the "reach into anything, including sibling impl state" anti-pattern that made the class a god-object; a `request_context` bundle is a service-locator that re-grants it. A `const webserver_config&` states at the type level "I read configuration, I mutate nothing." The `parent` pointer stays on the composition root for the few members that genuinely need the owning `webserver*` (e.g. `modded_request::ws`), not on the services. |
| 36 | + |
| 37 | +**MHD adapter layer.** libmicrohttpd calls in through fixed-signature C trampolines with a `void* cls` closure (`answer_to_connection`, `request_completed`, `connection_notify`, `policy_callback`, `post_iterator`, `uri_log`, `error_log`, `unescaper_func`, `upgrade_handler`, and the GnuTLS `psk_cred_handler_func` / `sni_cert_callback_func`). These remain `static`/free functions — they unpack `cls` and forward into the appropriate service. They are the C-ABI boundary, not behavior. |
| 38 | + |
| 39 | +**Free functions, not classes.** Pure statics with no instance state become free functions in `httpserver::detail` rather than spurious one-method classes: `log_dispatch_error(const webserver_config&, std::string_view)` (shared by every error path, so a free function avoids a dependency edge to `error_pages`), `serialize_allow_methods`/`format_allow_header`, `resolve_method_callback`, `materialize_response`, `decorate_mhd_response`, `handle_post_form_arg`, `manage_upload_stream`. |
| 40 | + |
| 41 | +**Route registration is not request behavior.** `prepare_or_create_lambda_shim` / `commit_handlers_to_shim` are write-path shim lifecycle already run under `route_table::lock_for_write()`; they fold into `route_table`'s writer side, not the request path. |
| 42 | + |
| 43 | +**Threading / lifetime.** The eight services are per-server, constructed once at `webserver_impl` construction, destroyed with it. They own no mutable state and take no locks, so they are inherently shareable across MHD worker threads — the decomposition adds **zero mutexes and is race-detector-neutral**. `modded_request` remains the per-request context, arena/PMR-allocated per DR-003b; the services take `modded_request&` and never allocate it, so DR-003b is untouched. Config is read-only (`const&`), matching the single-writer-at-construction posture. |
| 44 | + |
| 45 | +**Scope boundaries.** |
| 46 | +- **`http_request` is out of scope.** Its backing `detail::http_request_impl` is a fat impl (~40 members over five concerns, file-split into `_args`/`_tls`/`_auth`), but it is handled as a single impl object and the sprawl is a minor, tolerable issue. Rather than decompose it, its structure and file layout are documented (§4.2) so tooling and contributors can navigate it. A future collaborator split would be constrained by DR-003b's per-connection arena/PMR allocation and is deferred. |
| 47 | +- **`http_response`, `http_resource`, `webserver_config`, and the `webserver` facade need no decomposition** — a sealed single-concern value type, a cohesive handler base (per-route hooks already in `resource_hook_table`), an intentional passive data bag, and a thin pimpl facade respectively. |
| 48 | + |
| 49 | +**Consequences:** |
| 50 | +- No public-surface change: all eight services are internal `detail` types under the `HTTPSERVER_COMPILATION` gate; the public headers and ABI are untouched. |
| 51 | +- `webserver_impl` shrinks to a composition root; the ~20 `webserver_*.cpp` TUs consolidate into eight service TUs plus the MHD-adapter and free-function TUs. |
| 52 | +- White-box tests reaching in via `webserver_test_access` update their access points per extraction step. |
| 53 | +- The `helgrind`/`drd` race-detector lanes regressed during the state-decomposition window (green through 2026-07-15); the fix is sequenced *after* this behavior decomposition lands, still within v2.0 (the decomposition is race-detector-neutral, so it neither fixes nor worsens the lanes). |
| 54 | + |
| 55 | +**Migration.** Leaf-first, one service per commit into `feature/v2.0`, each keeping CI green — mirroring how the five state collaborators landed: free-function extractions → `error_pages` → `response_materializer` → `hook_dispatcher` → `upload_pipeline` → `websocket_upgrader` → `connection_callbacks` → `request_dispatcher` → `request_pipeline` → fold route registration into `route_table`. |
| 56 | + |
| 57 | +**Verification:** existing `make check` (113 tests) stays green at every step; the per-file 500-SLOC and per-function CCN-10 gates are the structural acceptance criteria; `basic`/`file_upload`/`ws_start_stop`/`deferred` under helgrind/drd return to green in the follow-on lane fix. |
| 58 | + |
| 59 | +--- |
0 commit comments