Skip to content

Commit 9af15a1

Browse files
etrclaude
andcommitted
perf: drop per-request weak_ptr lock() in gated hook-fire helpers
The three tail-end hook-fire helpers each locked the request's resource_weak_ BEFORE checking their any_hooks_ gate, so every matched request paid ~8 atomic RMWs on the resource's shared control block (3x weak_ptr::lock() CAS + 3x shared_ptr destruction, plus the weak assign /destroy) even with zero hooks registered -- the common case, and worse under thread-pool load where all workers hammer the same cache line. Master's dispatch used raw http_resource* with no refcount traffic; this also contradicted TASK-051's plan ("one pointer null-check per phase"). fire_after_handler_gated and fire_response_sent_gated both fire within finalize_answer's scope, where the owning shared_ptr (hrm) is still alive, so they now take the resolved http_resource* directly and read hook_table_raw_() from it -- no lock(). The resource is threaded through materialize_and_queue_response for the response_sent site (nullptr on the skip-handler / 404 paths, which both helpers already tolerate). fire_request_completed_gated genuinely fires from the MHD completion callback, after that shared_ptr is gone, so it keeps the weak_ptr lock() -- but gated behind a new modded_request::route_has_hook_table_ snapshot (taken in finalize_answer). When no per-route hook table existed at dispatch and no server-wide request_completed hook is registered, the lock is skipped entirely; when the snapshot is set it still locks and runs the precise any_hooks(request_completed) check, preserving the unregistration-skip contract. Net on the zero-per-route-hook path: from ~8 control-block atomics per matched request down to the 2 (resource_weak_ assign + destroy) that the handler_exception path independently requires. No behavior change beyond the same same-request-registration race the relaxed any_hooks_ gates already tolerate. Full suite green (109/109). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NpysYDDJac63yz2mZKKiDf
1 parent a3e53f3 commit 9af15a1

5 files changed

Lines changed: 92 additions & 29 deletions

File tree

src/detail/webserver_finalize.cpp

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,13 @@ bool webserver_impl::fire_before_handler_gated(
135135
// Per §4.10: after_handler fires only when a handler conceptually ran.
136136
// The pre-handler short-circuit branch (mr->skip_handler) is handled
137137
// upstream in finalize_answer and never reaches this site.
138-
void webserver_impl::fire_after_handler_gated(detail::modded_request* mr) {
138+
void webserver_impl::fire_after_handler_gated(detail::modded_request* mr,
139+
http_resource* resource) {
139140
const bool server_gate = has_hooks_for(hook_phase::after_handler);
140-
std::shared_ptr<http_resource> res;
141-
auto* rtable = per_route_table(mr, res);
141+
// resource is borrowed from finalize_answer's live shared_ptr; no
142+
// weak_ptr lock() needed (this fires within finalize_answer's scope).
143+
// nullptr on the 404 path -- no per-route table then.
144+
auto* rtable = resource != nullptr ? resource->hook_table_raw_() : nullptr;
142145
const bool route_gate = rtable != nullptr &&
143146
rtable->any_hooks(hook_phase::after_handler);
144147

@@ -175,10 +178,14 @@ void webserver_impl::fire_after_handler_gated(detail::modded_request* mr) {
175178
// webserver_impl::request_completed). bytes_queued is the logical body
176179
// size from http_response::body_->size(); for deferred/pipe bodies this
177180
// is 0 and consumers should fall back to the Content-Length header.
178-
void webserver_impl::fire_response_sent_gated(detail::modded_request* mr) {
181+
void webserver_impl::fire_response_sent_gated(detail::modded_request* mr,
182+
http_resource* resource) {
179183
const bool server_gate = has_hooks_for(hook_phase::response_sent);
180-
std::shared_ptr<http_resource> res;
181-
auto* rtable = per_route_table(mr, res);
184+
// resource is borrowed from finalize_answer's live shared_ptr (this
185+
// fires from materialize_and_queue_response, still within that scope),
186+
// so read the per-route table directly instead of locking the
187+
// weak_ptr. nullptr on the skip_handler / 404 paths.
188+
auto* rtable = resource != nullptr ? resource->hook_table_raw_() : nullptr;
182189
const bool route_gate = rtable != nullptr &&
183190
rtable->any_hooks(hook_phase::response_sent);
184191

@@ -226,12 +233,24 @@ void webserver_impl::fire_request_completed_gated(
226233
enum MHD_RequestTerminationCode toe) {
227234
const bool server_gate = has_hooks_for(hook_phase::request_completed);
228235

229-
// TASK-051: per-route gate. per_route_table() returns null if the
230-
// resource was unregistered between dispatch and completion (per the
231-
// action-item contract: skip the per-route chain in that case).
232-
// res keeps the resource alive while rtable is in use.
236+
// TASK-051: per-route gate. Unlike after_handler/response_sent, this
237+
// fires from the MHD completion callback -- finalize_answer's owning
238+
// shared_ptr is gone, so we must lock() the weak_ptr to keep the
239+
// resource alive while rtable is in use. per_route_table() returns
240+
// null if the resource was unregistered between dispatch and
241+
// completion (action-item contract: skip the per-route chain).
242+
//
243+
// But skip the lock() entirely on the common path: mr->route_has_hook_table_
244+
// is a snapshot (taken in finalize_answer) of whether the resource
245+
// carried any per-route hook table. When it's false and no server-wide
246+
// request_completed hook is registered, no control-block atomics are
247+
// touched. When it's true we still lock() and run the precise
248+
// any_hooks(request_completed) check, preserving the skip contract.
233249
std::shared_ptr<http_resource> res;
234-
auto* rtable = per_route_table(mr, res);
250+
resource_hook_table* rtable = nullptr;
251+
if (server_gate || mr->route_has_hook_table_) {
252+
rtable = per_route_table(mr, res);
253+
}
235254
const bool per_route_present = rtable != nullptr &&
236255
rtable->any_hooks(hook_phase::request_completed);
237256

src/detail/webserver_request.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@ MHD_Result webserver_impl::finalize_answer(MHD_Connection* connection,
270270
// NOTE: after_handler is NOT fired on this path (no handler ran);
271271
// response_sent fires unconditionally in materialize_and_queue_response.
272272
if (mr->skip_handler) {
273-
return materialize_and_queue_response(connection, mr);
273+
// No resource was resolved on this path -- pass nullptr.
274+
return materialize_and_queue_response(connection, mr, nullptr);
274275
}
275276

276277
// TASK-023: hold a shared_ptr copy across dispatch. If a concurrent
@@ -289,6 +290,13 @@ MHD_Result webserver_impl::finalize_answer(MHD_Connection* connection,
289290
// the resource directly.
290291
if (found) {
291292
mr->resource_weak_ = hrm;
293+
// Snapshot whether this resource carries a per-route hook table,
294+
// so fire_request_completed_gated (which fires after this
295+
// shared_ptr is gone) can gate its weak_ptr lock() on the common
296+
// zero-per-route-hook path. Same acquire barrier as
297+
// fire_before_handler_gated relies on (hrm came from
298+
// resolve_resource_for_request under the route-table shared_lock).
299+
mr->route_has_hook_table_ = (hrm->hook_table_raw_() != nullptr);
292300
}
293301

294302
fire_route_resolved_gated(this, mr, found, hrm);
@@ -305,7 +313,7 @@ MHD_Result webserver_impl::finalize_answer(MHD_Connection* connection,
305313
// NOTE: after_handler was NOT fired on this path (before_handler
306314
// short-circuit); response_sent fires unconditionally in
307315
// materialize_and_queue_response below.
308-
return materialize_and_queue_response(connection, mr);
316+
return materialize_and_queue_response(connection, mr, hrm.get());
309317
}
310318

311319
if (found) {
@@ -323,9 +331,11 @@ MHD_Result webserver_impl::finalize_answer(MHD_Connection* connection,
323331
// design choice: we fire because the dispatch site has produced a
324332
// response and the contract is "fires between response readiness
325333
// and queue", and that gives users a uniform observation point.
326-
fire_after_handler_gated(mr);
334+
// hrm is null on the 404 path (found == false); fire_after_handler_gated
335+
// and materialize_and_queue_response both tolerate a null resource.
336+
fire_after_handler_gated(mr, hrm.get());
327337

328-
return materialize_and_queue_response(connection, mr);
338+
return materialize_and_queue_response(connection, mr, hrm.get());
329339
}
330340

331341
MHD_Result webserver_impl::complete_request(MHD_Connection* connection, struct detail::modded_request* mr, const char* version, const char* method) {

src/detail/webserver_response_queue.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,8 @@ int webserver_impl::queue_response_dispatching_kind(
231231
}
232232

233233
MHD_Result webserver_impl::materialize_and_queue_response(MHD_Connection* connection,
234-
detail::modded_request* mr) {
234+
detail::modded_request* mr,
235+
http_resource* resource) {
235236
struct MHD_Response* raw_response = get_raw_response_with_fallback(mr);
236237
if (raw_response == nullptr) {
237238
// Belt-and-suspenders: even get_raw_response_with_fallback's
@@ -263,7 +264,7 @@ MHD_Result webserver_impl::materialize_and_queue_response(MHD_Connection* connec
263264
// queue, so destroying the MHD_Response below does not affect the
264265
// queued bytes -- only the http_response in mr->response matters
265266
// for the hook ctx pointer.
266-
fire_response_sent_gated(mr);
267+
fire_response_sent_gated(mr, resource);
267268
// MHD reference-counting note: for MHD_create_response_from_callback
268269
// responses (deferred/streaming), MHD increments its own internal
269270
// refcount during MHD_queue_response, so this MHD_destroy_response only

src/httpserver/detail/modded_request.hpp

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,17 +120,31 @@ struct modded_request {
120120

121121
// TASK-051: weak_ptr to the resource that handled this request.
122122
// Populated in finalize_answer when resolve_resource_for_request
123-
// returns a non-null hrm. Used by fire_response_sent_gated and
124-
// fire_request_completed_gated to fire the per-route phase chain
125-
// after the server-wide one. If the resource was unregistered
126-
// between dispatch and completion, lock() returns null and the
127-
// per-route chain is skipped (the action-item contract). The
128-
// weak_ptr also keeps a control-block reference into the resource
129-
// alive until ~modded_request, so the resource cannot be destroyed
130-
// mid-firing -- the hot-path firing helpers lock() into a local
131-
// shared_ptr before iterating.
123+
// returns a non-null hrm. Used by fire_request_completed_gated (which
124+
// fires from the MHD completion callback, after finalize_answer's
125+
// owning shared_ptr is gone) and the handler_exception path. If the
126+
// resource was unregistered between dispatch and completion, lock()
127+
// returns null and the per-route chain is skipped (the action-item
128+
// contract).
129+
//
130+
// The two in-scope firing helpers (fire_after_handler_gated /
131+
// fire_response_sent_gated) do NOT lock() this weak_ptr: they run
132+
// while finalize_answer's owning shared_ptr is still alive, so the
133+
// resolved resource is passed to them directly. Locking there cost a
134+
// control-block CAS per matched request even with zero hooks -- the
135+
// gate is checked first now instead.
132136
std::weak_ptr<http_resource> resource_weak_{};
133137

138+
// Snapshot, taken in finalize_answer, of whether the resolved
139+
// resource carried a per-route hook table at dispatch time. Lets
140+
// fire_request_completed_gated skip the weak_ptr lock() (and its
141+
// control-block atomics) on the overwhelmingly common zero-per-route-
142+
// hook path: when this is false and no server-wide request_completed
143+
// hook is registered, no lock is taken at all. The precise
144+
// any_hooks(request_completed) check still happens after locking when
145+
// this is true, preserving the unregistration-skip contract.
146+
bool route_has_hook_table_ = false;
147+
134148
modded_request() = default;
135149

136150
modded_request(const modded_request& b) = delete;

src/httpserver/detail/webserver_impl_dispatch.hpp

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,21 @@ void fire_request_completed(
224224
// TASK-050: gated-fire helpers, members so the http_response friendship
225225
// applies (response_sent_ctx::bytes_queued reads response.body_->size()).
226226
// Definitions live in src/detail/webserver_finalize.cpp.
227-
void fire_after_handler_gated(modded_request* mr);
228-
void fire_response_sent_gated(modded_request* mr);
227+
//
228+
// @p resource is the resolved resource borrowed from finalize_answer's
229+
// owning shared_ptr (nullptr on the 404 / short-circuit paths where no
230+
// resource was resolved). Passing it avoids a per-request weak_ptr
231+
// lock() -- and its control-block atomics -- on the zero-hook hot path;
232+
// the resource stays alive for the whole finalize_answer scope, which
233+
// both of these fire sites run within.
234+
void fire_after_handler_gated(modded_request* mr,
235+
::httpserver::http_resource* resource);
236+
void fire_response_sent_gated(modded_request* mr,
237+
::httpserver::http_resource* resource);
238+
// request_completed fires from the MHD completion callback, after
239+
// finalize_answer's shared_ptr is gone, so it keeps the weak_ptr lock()
240+
// -- but gated behind mr->route_has_hook_table_ so the lock is skipped
241+
// entirely on the common zero-per-route-hook path.
229242
void fire_request_completed_gated(modded_request* mr,
230243
enum MHD_RequestTerminationCode toe);
231244

@@ -330,8 +343,14 @@ std::string serialize_allow_methods(method_set allowed) const;
330343
// mr->response, decorate it, queue it on the connection. Handles
331344
// the belt-and-suspenders fallback when get_raw_response_with_fallback
332345
// itself fails to produce a response.
346+
//
347+
// @p resource is the resolved resource (nullptr when none was resolved),
348+
// forwarded to fire_response_sent_gated so it can reach the per-route
349+
// hook table without a weak_ptr lock(). Borrowed for the duration of the
350+
// call from finalize_answer's owning shared_ptr.
333351
MHD_Result materialize_and_queue_response(MHD_Connection* connection,
334-
modded_request* mr);
352+
modded_request* mr,
353+
::httpserver::http_resource* resource);
335354

336355
// TASK-062: kind-dispatched MHD queue entry-point. For
337356
// body_kind::digest_challenge, branches into the auth-required

0 commit comments

Comments
 (0)