You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TASK-046: fire connection_opened / accept_decision / connection_closed
Wires the three connection-level lifecycle phases of the hook bus into
the existing MHD callback sites. Closes long-standing feature request
#332 (banned-IP log entry).
Production code
- accept_ctx extended from {peer} to {peer, accepted, reason}; `reason`
is a std::optional<std::string_view> pointing at a string literal
("banned" / "not-allowed") with static storage duration.
- Three new noexcept fire_* helpers on webserver_impl (declared in the
dispatch sibling header, defined in src/hook_handle.cpp): each takes
a shared_lock, snapshots the phase vector with reserve(8), releases
the lock, then iterates with try/catch routed through
log_dispatch_error (DR-009 §5.2). Mirrors TASK-027's route-cache
promotion pattern.
- connection_notify + policy_callback split out of
webserver_callbacks.cpp into a new webserver_callbacks_lifecycle.cpp
TU. The original would have overshot FILE_LOC_MAX after the firing-
site code landed. webserver_callbacks.cpp shrinks to 432 lines.
- MHD_OPTION_NOTIFY_CONNECTION closure pointer switched from nullptr to
the owning webserver* so connection_notify can reach
impl_->any_hooks_ / fire_connection_opened / fire_connection_closed.
- policy_callback gains decision-derivation logic (accept_ctx.reason);
extracted into anon-ns classify_decision() helper to stay under the
CCN gate.
- All three firing sites are gated by a relaxed atomic load on
any_hooks_[phase] so the zero-hook path stays one branch + one atomic
load (PRD-HOOK-REQ-008).
- accept_decision's throwing-hook semantics are a structural
guarantee: fire_accept_decision returns void and `decision` is
captured in a local before the fire call.
Pre-existing build fix
- src/detail/webserver_dispatch.cpp was missing `using std::map` and
`using httpserver::http::http_utils` directives (left out of the
TASK-15f8083 7-way split). Added so fresh worktree builds succeed.
Tests (+4)
- test/unit/hooks_accept_ctx_shape_test.cpp: compile-time pin for the
extended accept_ctx shape.
- test/integ/hooks_connection_lifecycle.cpp: drives one curl round-trip
and asserts all three lifecycle hooks fire with valid peer + correct
decision/reason; pins lifecycle ordering (closed is last; opened OR
accept is first — MHD callback order is platform-dependent).
- test/integ/hooks_accept_decision_banned.cpp: ACCEPT policy +
block_ip("127.0.0.1") -> hook observes accepted=false reason="banned".
- test/integ/hooks_accept_decision_throwing.cpp: two sub-tests pin that
a throwing accept_decision hook does not flip the decision (banned
still rejected; unbanned still accepted).
- test/integ/hooks_no_firing.cpp narrowed: still asserts zero
invocations on the eight phases TASK-047..051 will wire; the three
lifecycle phases are now expected to fire.
Example
- examples/banned_ip_log.cpp demonstrates the solution to issue #332:
ACCEPT policy + block_ip + accept_decision hook logging every
rejection to stderr with peer + reason. Wired into examples/Makefile.am.
Docs
- RELEASE_NOTES.md: one-line note under "What's new" describing the
M5 hook bus landing.
Verification
- 55/55 tests pass (was 51, +4 new).
- check-file-size, check-examples, check-readme, check-release-notes,
check-doxygen, check-install-layout, check-hygiene, check-duplication
all pass. check-complexity surfaces only pre-existing TASK-045
warnings (hook_phase::to_string, hook_handle::remove).
- cpplint clean on all modified/new files.
- Debug build (-Werror -Wextra -pedantic) compiles and tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: specs/tasks/M5-routing-lifecycle/TASK-046.md
+18-6Lines changed: 18 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,11 +8,23 @@
8
8
Wire the three connection-level phases into the existing MHD callback sites. Closes long-standing feature request #332 (banned-IP log entry).
9
9
10
10
**Action Items:**
11
-
-[ ] In `webserver_impl::connection_notify` (`webserver.cpp:1295`), fire `connection_opened` on `MHD_CONNECTION_NOTIFY_STARTED` and `connection_closed` on `MHD_CONNECTION_NOTIFY_CLOSED`. Context: `peer_address`, the `connection_state*` already allocated on STARTED.
12
-
-[ ] In `webserver_impl::policy_callback` (`webserver.cpp:1450`), at the bottom (after the YES/NO decision is fixed), fire `accept_decision` with `{peer_address, bool accepted, optional<std::string_view> reason}`. `reason` is `"banned"` / `"not-allowed"` / `std::nullopt` for ACCEPT.
13
-
-[ ] Use the per-phase `any_hooks_` short-circuit: a relaxed atomic load, branch out if zero. The body of each firing site is a single `if (impl_->any_hooks_[hook_phase::X].load(std::memory_order_relaxed)) impl_->fire_X(...);` to keep the inline code path tiny.
14
-
-[ ]`fire_*` helpers take a `shared_lock` on `hook_table_mutex_`, copy the phase vector into a small `std::vector` on the stack (typical N is small; reserve(8) is fine), release the lock, iterate. Mirror the TASK-027 pattern for route-cache promotion.
15
-
-[ ] Catch any exception thrown by a hook callable; route it through the existing `log_dispatch_error` helper (DR-009 §5.2). For `accept_decision` specifically, a throwing hook does NOT change the accept/reject decision — the decision was made before the hook fired.
11
+
-[x] In `webserver_impl::connection_notify`, fire `connection_opened` on `MHD_CONNECTION_NOTIFY_STARTED` and `connection_closed` on `MHD_CONNECTION_NOTIFY_CLOSED`. Lives in the new `src/detail/webserver_callbacks_lifecycle.cpp` (split out of `webserver_callbacks.cpp` to stay under FILE_LOC_MAX). Closure pointer for `MHD_OPTION_NOTIFY_CONNECTION` switched from `nullptr` to `parent` (the owning `webserver*`) so the callback can reach `impl_->any_hooks_`.
12
+
-[x] In `webserver_impl::policy_callback` (also in `webserver_callbacks_lifecycle.cpp`), at the bottom (after the YES/NO decision is fixed), fire `accept_decision` with `{peer_address, bool accepted, optional<std::string_view> reason}`. `reason` is `"banned"` / `"not-allowed"` / `std::nullopt` for ACCEPT. Decision derivation extracted into anon-ns helper `classify_decision` to stay under CCN.
13
+
-[x] Use the per-phase `any_hooks_` short-circuit: a relaxed atomic load, branch out if zero. Inline pattern: `if (impl->any_hooks_[hook_phase::X].load(std::memory_order_relaxed)) impl->fire_X(ctx);` — context is constructed only inside the if-body so the zero-hook path costs one atomic load.
14
+
-[x]`fire_*` helpers take a `shared_lock` on `hook_table_mutex_`, snapshot the phase vector into a stack-local `std::vector<phase_entry<Sig>>` with `reserve(8)`, release the lock, iterate with try/catch. Implemented as three `noexcept` members of `webserver_impl` in `src/hook_handle.cpp`. Mirrors the TASK-027 route-cache promotion pattern.
15
+
-[x] Catch any exception thrown by a hook callable; route it through `log_dispatch_error` with a `"hook[<phase>] threw: ..."` prefix. Non-`std::exception` caught via `catch (...)`. For `accept_decision` specifically, the structural guarantee holds: `fire_accept_decision` returns void and `decision` is captured in a local before the fire call, so a throwing hook cannot reach the `return decision;` branch.
16
+
17
+
**Public-header change:**`accept_ctx` extended to `{peer_address peer, bool accepted, std::optional<std::string_view> reason}` (TASK-045 had only `peer`). `reason` always references a string literal with static storage duration.
18
+
19
+
**New tests (4):**
20
+
-`test/unit/hooks_accept_ctx_shape_test.cpp` — compile-time pin for the extended shape.
21
+
-`test/integ/hooks_connection_lifecycle.cpp` — drives a curl round-trip and asserts the three lifecycle hooks fire (accepted=true, valid peer).
-`test/integ/hooks_accept_decision_throwing.cpp` — two sub-tests pinning that a throwing hook does not flip the decision.
24
+
25
+
**New example:**`examples/banned_ip_log.cpp` — minimal program demonstrating the solution to issue #332.
26
+
27
+
**Updated tests:**`test/integ/hooks_no_firing.cpp` narrowed: still asserts zero invocations on the eight phases TASK-047..051 will wire, but lets the three lifecycle phases fire (they must, per the new tests).
16
28
17
29
**Dependencies:**
18
30
- Blocked by: TASK-045
@@ -30,4 +42,4 @@ Wire the three connection-level phases into the existing MHD callback sites. Clo
0 commit comments