Repository navigation
Expand file tree
/
Copy pathdrain_scope_test.cpp
More file actions
247 lines (223 loc) · 10.1 KB
/
Copy pathdrain_scope_test.cpp
File metadata and controls
247 lines (223 loc) · 10.1 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
/*
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 file
LICENSE in the distribution; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA
*/
// TASK-110 step 1: the counted drain scope (PRD-V3N-REQ-032,
// DR-V3-008) in isolation -- no engines, no transport. Units count,
// the deadline-bound wait reports completed or deadline_expired, the
// cancel hook fires at most once across concurrent waiters, and a
// wait entered from work the drain itself counts is rejected with
// would_deadlock unless the count already reached zero. Every wait is
// deadline-bound; a pass condition is an observed result, never a
// sleep.
#include <atomic>
#include <chrono>
#include <cstddef>
#include <thread>
#include <utility>
#include <httpserver/concurrency/executor.hpp>
#include <httpserver/detail/drain_scope.hpp>
#include <httpserver/http/outcome.hpp>
#include <httpserver/server/server.hpp>
#include "./littletest.hpp"
namespace {
namespace srv = httpserver::server;
namespace http = httpserver::http;
using httpserver::detail::drain_scope;
using clock = std::chrono::steady_clock;
constexpr std::chrono::milliseconds kBudget{2000};
template<typename Pred>
bool wait_until(Pred pred) {
const auto deadline = clock::now() + kBudget;
while (!pred()) {
if (clock::now() >= deadline) return false;
std::this_thread::sleep_for(std::chrono::microseconds(200));
}
return true;
}
} // namespace
LT_BEGIN_SUITE(drain_scope_suite)
void set_up() {
}
void tear_down() {
}
LT_END_SUITE(drain_scope_suite)
// Units count up and down, the zero transition is idempotent, and the
// RAII bracket holds exactly one unit.
LT_BEGIN_AUTO_TEST(drain_scope_suite, units_count_and_notify_zero)
drain_scope scope;
LT_CHECK_EQ(scope.active(), std::size_t{0});
scope.enter();
scope.enter();
LT_CHECK_EQ(scope.active(), std::size_t{2});
scope.leave();
LT_CHECK_EQ(scope.active(), std::size_t{1});
scope.leave();
LT_CHECK_EQ(scope.active(), std::size_t{0});
// A stray leave cannot drive the count negative.
scope.leave();
LT_CHECK_EQ(scope.active(), std::size_t{0});
{
drain_scope::unit held{scope};
LT_CHECK_EQ(scope.active(), std::size_t{1});
}
LT_CHECK_EQ(scope.active(), std::size_t{0});
LT_END_AUTO_TEST(units_count_and_notify_zero)
// An armed scope with no live units completes immediately.
LT_BEGIN_AUTO_TEST(drain_scope_suite, wait_completes_when_idle)
drain_scope scope;
scope.arm(clock::now() + kBudget, {}, {});
LT_CHECK(scope.armed());
srv::drain_result out;
const http::outcome done = scope.wait(out);
LT_CHECK(done.ok());
LT_CHECK(out.status == srv::drain_status::completed);
LT_CHECK_EQ(out.remaining, std::size_t{0});
LT_END_AUTO_TEST(wait_completes_when_idle)
// An unarmed scope has no drain to wait on.
LT_BEGIN_AUTO_TEST(drain_scope_suite, wait_unarmed_is_invalid_state)
drain_scope scope;
LT_CHECK(!scope.armed());
srv::drain_result out;
const http::outcome refused = scope.wait(out);
LT_CHECK(!refused.ok());
LT_CHECK(refused.code() == http::outcome_code::invalid_state);
LT_END_AUTO_TEST(wait_unarmed_is_invalid_state)
// A hanging unit past a tight deadline expires the wait with the
// pre-cancel remaining count, and the cancel hook fires exactly once
// no matter how many waiters report the same expiry.
LT_BEGIN_AUTO_TEST(drain_scope_suite, wait_deadline_expires_cancels_once)
drain_scope scope;
std::atomic<int> cancels{0};
scope.enter(); // the hanging unit
scope.arm(clock::now() + std::chrono::milliseconds(150), {},
[&cancels] { cancels.fetch_add(1); });
srv::drain_result first_out;
srv::drain_result second_out;
std::thread second(
[&] { static_cast<void>(scope.wait(second_out)); });
const http::outcome first = scope.wait(first_out);
second.join();
LT_CHECK(first.ok());
LT_CHECK(first_out.status == srv::drain_status::deadline_expired);
LT_CHECK_EQ(first_out.remaining, std::size_t{1});
LT_CHECK(second_out.status == srv::drain_status::deadline_expired);
LT_CHECK_EQ(second_out.remaining, std::size_t{1});
LT_CHECK_EQ(cancels.load(), 1);
scope.leave(); // unwind the hanging unit before teardown
LT_END_AUTO_TEST(wait_deadline_expires_cancels_once)
// A unit released while the wait is parked wakes it to completion
// (observed inside the wait's own deadline -- never a timed pass).
LT_BEGIN_AUTO_TEST(drain_scope_suite, wait_completes_after_leave_during_wait)
drain_scope scope;
scope.enter();
scope.arm(clock::now() + kBudget, {}, {});
srv::drain_result out;
std::atomic<bool> started{false};
std::atomic<bool> ok{false};
std::thread waiter([&] {
started.store(true);
ok.store(scope.wait(out).ok());
});
LT_CHECK(wait_until([&] { return started.load(); }));
// Arrangement only: let the waiter park. The pass condition is the
// observed completion inside the 2 s deadline, so a lost wake fails
// this as a deadline expiry, not a hang.
std::this_thread::sleep_for(std::chrono::milliseconds(150));
scope.leave();
waiter.join();
LT_CHECK(ok.load());
LT_CHECK(out.status == srv::drain_status::completed);
LT_CHECK_EQ(out.remaining, std::size_t{0});
LT_END_AUTO_TEST(wait_completes_after_leave_during_wait)
// A wait from work the drain itself counts is rejected with
// would_deadlock; the scope stays usable afterwards.
LT_BEGIN_AUTO_TEST(drain_scope_suite, wait_from_counted_thread_rejects)
drain_scope scope;
scope.enter();
scope.arm(clock::now() + kBudget, [] { return true; }, {});
srv::drain_result out;
const http::outcome rejected = scope.wait(out);
LT_CHECK(!rejected.ok());
LT_CHECK(rejected.code() == http::outcome_code::would_deadlock);
// The same caller completes once the counted work ends.
scope.leave();
srv::drain_result done;
const http::outcome complete = scope.wait(done);
LT_CHECK(complete.ok());
LT_CHECK(done.status == srv::drain_status::completed);
LT_CHECK_EQ(done.remaining, std::size_t{0});
LT_END_AUTO_TEST(wait_from_counted_thread_rejects)
// The counted-thread rejection never fires once the count reached
// zero: a pool thread between counted items completes instead.
LT_BEGIN_AUTO_TEST(drain_scope_suite,
late_wait_after_units_gone_from_counted_thread_completes)
drain_scope scope;
scope.enter();
scope.leave();
scope.arm(clock::now() + kBudget, [] { return true; }, {});
srv::drain_result out;
const http::outcome done = scope.wait(out);
LT_CHECK(done.ok());
LT_CHECK(out.status == srv::drain_status::completed);
LT_CHECK_EQ(out.remaining, std::size_t{0});
LT_END_AUTO_TEST(late_wait_after_units_gone_from_counted_thread_completes)
LT_BEGIN_AUTO_TEST(drain_scope_suite, last_leave_after_deadline_retains_expiry_and_pre_cancel_count)
drain_scope scope;
int cancels = 0;
scope.enter();
scope.arm(clock::now() - std::chrono::milliseconds(1), {}, [&] { ++cancels; });
scope.leave();
srv::drain_result result;
LT_CHECK(scope.wait(result).ok());
LT_CHECK(result.status == srv::drain_status::deadline_expired);
LT_CHECK_EQ(result.remaining, std::size_t{1});
LT_CHECK_EQ(cancels, 1);
LT_END_AUTO_TEST(last_leave_after_deadline_retains_expiry_and_pre_cancel_count)
LT_BEGIN_AUTO_TEST(drain_scope_suite, cancel_hook_leaving_units_cannot_erase_expiry)
drain_scope scope;
int cancels = 0;
scope.enter(); scope.enter();
scope.arm(clock::now(), {}, [&] { ++cancels; scope.leave(); scope.leave(); });
srv::drain_result first, late;
LT_CHECK(scope.wait(first).ok()); LT_CHECK(scope.wait(late).ok());
LT_CHECK(first.status == srv::drain_status::deadline_expired);
LT_CHECK(late.status == srv::drain_status::deadline_expired);
LT_CHECK_EQ(first.remaining, std::size_t{2});
LT_CHECK_EQ(late.remaining, std::size_t{2}); LT_CHECK_EQ(cancels, 1);
LT_END_AUTO_TEST(cancel_hook_leaving_units_cannot_erase_expiry)
LT_BEGIN_AUTO_TEST(drain_scope_suite, external_expiry_claim_is_shared_with_concurrent_waiters)
drain_scope scope; std::atomic<int> cancels{0}; scope.enter();
const auto deadline = clock::now() + kBudget;
scope.arm(deadline, {}, [&] { ++cancels; scope.leave(); });
LT_CHECK(!scope.expire_if_due(deadline - std::chrono::nanoseconds(1)));
LT_CHECK(scope.expire_if_due(deadline));
srv::drain_result one, two;
std::thread waiter([&] { scope.wait(one); }); scope.wait(two); waiter.join();
LT_CHECK(one.status == srv::drain_status::deadline_expired);
LT_CHECK(two.status == srv::drain_status::deadline_expired);
LT_CHECK_EQ(one.remaining, std::size_t{1}); LT_CHECK_EQ(two.remaining, std::size_t{1});
LT_CHECK_EQ(cancels.load(), 1);
LT_END_AUTO_TEST(external_expiry_claim_is_shared_with_concurrent_waiters)
LT_BEGIN_AUTO_TEST(drain_scope_suite, last_leave_before_deadline_remains_completed_after_external_expiry)
drain_scope scope; int cancels = 0; scope.enter(); const auto deadline = clock::now() + kBudget;
scope.arm(deadline, {}, [&] { ++cancels; }); scope.leave();
LT_CHECK(!scope.expire_if_due(deadline)); srv::drain_result result; LT_CHECK(scope.wait(result).ok());
LT_CHECK(result.status == srv::drain_status::completed); LT_CHECK_EQ(cancels, 0);
LT_END_AUTO_TEST(last_leave_before_deadline_remains_completed_after_external_expiry)
LT_BEGIN_AUTO_TEST_ENV()
AUTORUN_TESTS()
LT_END_AUTO_TEST_ENV()