-
-
Notifications
You must be signed in to change notification settings - Fork 191
Expand file tree
/
Copy pathhttp_resource.cpp
More file actions
273 lines (246 loc) · 10.8 KB
/
Copy pathhttp_resource.cpp
File metadata and controls
273 lines (246 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
/*
This file is part of libhttpserver
Copyright (C) 2011-2026 Sebastiano Merlino
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
USA
*/
// Out-of-line bodies for http_resource::add_hook overloads
// + the lazy hook_table_ allocator. The public header (http_resource.hpp)
// stays free of <vector>/<atomic>/<shared_mutex> transitives: only the
// shared_ptr<detail::resource_hook_table> PIMPL slot is visible there.
#include "httpserver/http_resource.hpp"
#include <atomic>
#include <functional>
#include <memory>
#include <shared_mutex>
#include <stdexcept>
#include <string>
#include <utility>
#include "httpserver/detail/method_utils.hpp"
#include "httpserver/detail/resource_hook_table.hpp"
#include "httpserver/hook_action.hpp"
#include "httpserver/hook_context.hpp"
#include "httpserver/hook_handle.hpp"
#include "httpserver/hook_phase.hpp"
namespace httpserver {
namespace {
[[noreturn]] void throw_wrong_phase(hook_phase requested,
hook_phase expected) {
throw std::invalid_argument(
std::string("http_resource::add_hook: invalid phase ") +
std::string(to_string(requested)) +
" -- per-route hooks accept only " +
std::string(to_string(expected)) +
" on this overload (and one of: before_handler, "
"handler_exception, after_handler, response_sent, "
"request_completed -- the post-route-resolution phases). "
"Earlier phases (connection_opened, accept_decision, "
"request_received, body_chunk, route_resolved, "
"connection_closed) are server-scope only because the resource "
"is not yet known when they fire.");
}
void check_phase(hook_phase requested, hook_phase expected) {
if (requested != expected) {
throw_wrong_phase(requested, expected);
}
}
void check_fn(bool empty) {
if (empty) {
throw std::invalid_argument(
"http_resource::add_hook: hook callable must not be empty");
}
}
// Lazy CAS-style allocator. Resources that never register a hook keep
// hook_table_ at nullptr and pay zero allocation cost. Concurrent first-
// callers each construct a fresh table; the CAS winner installs theirs,
// the loser discards its local. At most one allocation is wasted under
// contention (acceptable -- registration is rare).
// TODO(C++20 cleanup): migrate hook_table_ to std::atomic<std::shared_ptr<T>>
// and use the member functions. The free std::atomic_*_explicit overloads on
// std::shared_ptr were deprecated in C++20 but remain functional through at
// least C++23; clang -Wdeprecated-declarations flags them. Until the field
// type is changed, suppress the warning at the only two call sites.
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#elif defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
std::shared_ptr<detail::resource_hook_table>
ensure_table(std::shared_ptr<detail::resource_hook_table>& slot) {
auto existing = std::atomic_load_explicit(
&slot, std::memory_order_acquire);
if (existing) {
return existing;
}
auto fresh = std::make_shared<detail::resource_hook_table>();
std::shared_ptr<detail::resource_hook_table> expected;
if (std::atomic_compare_exchange_strong_explicit(
&slot, &expected, fresh,
std::memory_order_acq_rel,
std::memory_order_acquire)) {
return fresh;
}
// Lost the race; `expected` was updated to the winning shared_ptr.
return expected;
}
#if defined(__clang__)
#pragma clang diagnostic pop
#elif defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
} // namespace
// ---- copy / move special members ----------------------------------------
//
// The std::shared_mutex cached_allow_mutex_ member has no copy or
// move. The defaulted special members on the public
// class declaration would therefore be implicitly deleted. Implement
// them here by-hand, skipping the mutex. The copy / move target starts
// with a fresh, default-constructed mutex and an invalidated Allow-
// header cache (cached_allow_valid_ = false), which the next call to
// get_allow_header() repopulates lazily.
http_resource::http_resource(const http_resource& b) noexcept
: methods_allowed_(b.methods_allowed_),
hook_table_(b.hook_table_) {
// cached_allow_mutex_ default-constructs.
// cached_allow_header_ / cached_allow_mask_ default-construct.
// cached_allow_valid_ stays false (member default).
}
http_resource::http_resource(http_resource&& b) noexcept
: methods_allowed_(b.methods_allowed_),
hook_table_(std::move(b.hook_table_)) {
// Same rationale as the copy constructor: cache state is per-
// instance and is not transferred. std::shared_mutex has no move.
}
http_resource& http_resource::operator=(const http_resource& b) noexcept {
if (this != &b) {
hook_table_ = b.hook_table_;
methods_allowed_ = b.methods_allowed_;
// Invalidate the local cache; do NOT touch the mutex (still
// owned by *this).
std::unique_lock<std::shared_mutex> lock(cached_allow_mutex_);
cached_allow_valid_ = false;
cached_allow_header_.clear();
}
return *this;
}
http_resource& http_resource::operator=(http_resource&& b) noexcept {
if (this != &b) {
hook_table_ = std::move(b.hook_table_);
methods_allowed_ = b.methods_allowed_;
std::unique_lock<std::shared_mutex> lock(cached_allow_mutex_);
cached_allow_valid_ = false;
cached_allow_header_.clear();
}
return *this;
}
// ---- add_hook overloads -------------------------------------------------
hook_handle http_resource::add_hook(hook_phase phase,
std::function<hook_action(before_handler_ctx&)> fn) {
check_phase(phase, hook_phase::before_handler);
check_fn(!fn);
auto table = ensure_table(hook_table_);
const std::uint64_t id = table->append_before_handler(std::move(fn));
return hook_handle{std::weak_ptr<detail::resource_hook_table>(table),
hook_phase::before_handler, id};
}
hook_handle http_resource::add_hook(hook_phase phase,
std::function<hook_action(const handler_exception_ctx&)> fn) {
check_phase(phase, hook_phase::handler_exception);
check_fn(!fn);
auto table = ensure_table(hook_table_);
const std::uint64_t id = table->append_handler_exception(std::move(fn));
return hook_handle{std::weak_ptr<detail::resource_hook_table>(table),
hook_phase::handler_exception, id};
}
hook_handle http_resource::add_hook(hook_phase phase,
std::function<hook_action(after_handler_ctx&)> fn) {
check_phase(phase, hook_phase::after_handler);
check_fn(!fn);
auto table = ensure_table(hook_table_);
const std::uint64_t id = table->append_after_handler(std::move(fn));
return hook_handle{std::weak_ptr<detail::resource_hook_table>(table),
hook_phase::after_handler, id};
}
hook_handle http_resource::add_hook(hook_phase phase,
std::function<void(const response_sent_ctx&)> fn) {
check_phase(phase, hook_phase::response_sent);
check_fn(!fn);
auto table = ensure_table(hook_table_);
const std::uint64_t id = table->append_response_sent(std::move(fn));
return hook_handle{std::weak_ptr<detail::resource_hook_table>(table),
hook_phase::response_sent, id};
}
hook_handle http_resource::add_hook(hook_phase phase,
std::function<void(const request_completed_ctx&)> fn) {
check_phase(phase, hook_phase::request_completed);
check_fn(!fn);
auto table = ensure_table(hook_table_);
const std::uint64_t id = table->append_request_completed(std::move(fn));
return hook_handle{std::weak_ptr<detail::resource_hook_table>(table),
hook_phase::request_completed, id};
}
// ---- get_allow_header ---------------------------------------------------
//
// Lazy Allow-header cache. See the header-side declaration for the
// contract. Implementation:
//
// Warm path (cache hit, the common case after the first 405):
// 1. Take a shared lock.
// 2. Snapshot methods_allowed_ and compare with the cached mask;
// on match, return the cached string by reference while still
// holding the shared lock.
//
// Cold / stale path (cache miss or mask changed):
// 1. Release the shared lock.
// 2. Take an exclusive (unique) lock.
// 3. Re-check (double-checked locking) because another thread may
// have filled the cache between the two lock acquisitions.
// 4. Rebuild via detail::format_allow_header and update the snapshot.
// 5. Return the cached string by reference.
//
// The lock protects the cache fields only; it does not synchronise
// methods_allowed_ writes (the mutators are unlocked -- see the
// header-side comment). Snapshotting the mask inside the lock keeps
// each fill internally consistent: the cached string always matches
// the mask it was built from.
//
// std::shared_mutex allows N concurrent warm-path readers without
// serialisation; only the cold fill path serialises.
//
// The returned reference is stable until the next mask mutation; the
// caller (dispatch_resource_handler) consumes it synchronously within
// the current dispatch, so the reference outlives use.
const std::string& http_resource::get_allow_header() const {
// Warm path: shared (read) lock.
{
std::shared_lock<std::shared_mutex> rlock(cached_allow_mutex_);
const method_set live = methods_allowed_; // read inside lock
if (cached_allow_valid_ && cached_allow_mask_ == live) {
return cached_allow_header_;
}
}
// Cold / stale path: exclusive (write) lock.
std::unique_lock<std::shared_mutex> wlock(cached_allow_mutex_);
// Double-checked: another thread may have filled the cache while we
// waited for the exclusive lock.
const method_set live = methods_allowed_; // re-read inside write lock
if (!cached_allow_valid_ || cached_allow_mask_ != live) {
cached_allow_header_ = detail::format_allow_header(live);
cached_allow_mask_ = live;
cached_allow_valid_ = true;
}
return cached_allow_header_;
}
} // namespace httpserver