3737#include < algorithm>
3838#include < atomic>
3939#include < chrono>
40+ #include < condition_variable>
4041#include < cstdint>
4142#include < cstdio>
4243#include < utility>
4344#include < cstring>
4445#include < memory>
46+ #include < mutex>
4547#include < string>
4648#include < thread>
4749
@@ -259,28 +261,45 @@ LT_END_AUTO_TEST(deferred_response_empty_content)
259261// ---------------------------------------------------------------------------
260262
261263namespace {
262- // Sentinel whose destructor flips an atomic; the destructor runs when the
263- // http_response (and its deferred_body) is destroyed inside
264- // ~modded_request() — which fires from webserver_impl::request_completed.
264+ // Sentinel whose destructor fires when the http_response (and its
265+ // deferred_body) is destroyed inside ~modded_request(), which fires from
266+ // webserver_impl::request_completed. On destruction it sets *destroyed to
267+ // true and notifies *cv so the test thread's timed_wait can return
268+ // immediately rather than spinning to a 1-second timeout.
265269//
266- // move_constructor leaves the source's pointer null so the moved-from
267- // temporary's destructor is a no-op. Otherwise std::make_shared's
268- // in-place copy/move of the temporary {&destroyed} aggregate would fire
269- // the destructor on the source temporary at the end of the full
270- // expression, flipping `destroyed` to true BEFORE the lambda even runs.
270+ // move_constructor leaves all pointers null so the moved-from temporary's
271+ // destructor is a no-op. Otherwise std::make_shared's in-place copy/move
272+ // of the temporary aggregate would flip `destroyed` BEFORE the lambda runs.
271273struct destruction_sentinel {
272- std::atomic<bool >* destroyed;
273- explicit destruction_sentinel (std::atomic<bool >* d) : destroyed(d) {}
274+ std::atomic<bool >* destroyed;
275+ std::mutex* cv_mu;
276+ std::condition_variable* cv;
277+
278+ explicit destruction_sentinel (std::atomic<bool >* d,
279+ std::mutex* mu,
280+ std::condition_variable* c)
281+ : destroyed(d), cv_mu(mu), cv(c) {}
274282 destruction_sentinel (const destruction_sentinel&) = delete ;
275283 destruction_sentinel& operator =(const destruction_sentinel&) = delete ;
276284 destruction_sentinel (destruction_sentinel&& o) noexcept
277- : destroyed(std::exchange(o.destroyed, nullptr )) {}
285+ : destroyed(std::exchange(o.destroyed, nullptr )),
286+ cv_mu (std::exchange(o.cv_mu, nullptr )),
287+ cv(std::exchange(o.cv, nullptr )) {}
278288 destruction_sentinel& operator =(destruction_sentinel&& o) noexcept {
279289 destroyed = std::exchange (o.destroyed , nullptr );
290+ cv_mu = std::exchange (o.cv_mu , nullptr );
291+ cv = std::exchange (o.cv , nullptr );
280292 return *this ;
281293 }
282294 ~destruction_sentinel () {
283- if (destroyed) destroyed->store (true );
295+ if (!destroyed) return ;
296+ destroyed->store (true );
297+ // Notify under the mutex so the waiting thread cannot miss the signal
298+ // if it is between loading destroyed and entering wait.
299+ {
300+ std::lock_guard<std::mutex> lk (*cv_mu);
301+ }
302+ cv->notify_one ();
284303 }
285304};
286305
@@ -346,34 +365,31 @@ LT_BEGIN_AUTO_TEST(deferred_suite, deferred_producer_destroyed_in_request_comple
346365 // capturing a shared_ptr<destruction_sentinel> inside the producer's
347366 // captures, then asserting the sentinel was destroyed after the server
348367 // is fully drained.
368+ //
369+ // Synchronization: sentinel destructor signals destroyed_cv so the test
370+ // thread wakes immediately rather than spinning. Upper bound is 5 s to
371+ // catch a genuine DR-010 regression without hanging the CI suite.
349372 std::atomic<bool > destroyed{false };
350- std::atomic<int > producer_calls{0 };
373+ std::atomic<int > producer_calls{0 };
374+ std::mutex destroyed_mu;
375+ std::condition_variable destroyed_cv;
351376
352377 // CRITICAL: the OUTER on_get lambda is stored long-term inside the
353- // registered lambda_resource. Anything it captures lives until the
354- // webserver itself is destroyed. So the outer lambda captures
355- // NOTHING but the atomic references — it allocates the
356- // destruction_sentinel inline (via make_shared) inside the INNER
357- // (producer) lambda's init-capture. The inner lambda's capture lives
358- // inside the http_response (and its deferred_body) anchored on
359- // mr->response_, so the sentinel is released exactly when
360- // ~modded_request fires from request_completed (DR-010). shared_ptr
361- // is required (not unique_ptr) because std::function — used as both
362- // lambda_handler and deferred_body::producer_type — requires its
363- // target to be CopyConstructible.
364- ws->on_get (" /lifetime" , [&producer_calls, &destroyed](
365- const http_request&) {
378+ // registered lambda_resource. Capture only lightweight references here;
379+ // the destruction_sentinel lives inside the INNER (producer) lambda so
380+ // it is released with the deferred_body when ~modded_request fires from
381+ // request_completed (DR-010). shared_ptr is required because
382+ // std::function requires its target to be CopyConstructible.
383+ ws->on_get (" /lifetime" ,
384+ [&producer_calls, &destroyed, &destroyed_mu, &destroyed_cv](
385+ const http_request&) {
366386 return http_response::deferred (
367- [sentinel = std::make_shared<destruction_sentinel>(&destroyed),
387+ [sentinel = std::make_shared<destruction_sentinel>(
388+ &destroyed, &destroyed_mu, &destroyed_cv),
368389 &producer_calls, &destroyed, served = 0 ](
369390 std::uint64_t , char * buf, std::size_t max) mutable -> ssize_t {
370- // Defensive: the producer should never run after the
371- // sentinel is destroyed; if it did, this read would be
372- // undefined behaviour (we'd be reading a destroyed atomic
373- // through the captured sentinel pointer). Assert via a
374- // separate side-channel.
391+ // Defensive: producer must not run after sentinel is gone.
375392 if (destroyed.load ()) {
376- // Would be UB anyway, but flag the regression.
377393 return -1 ;
378394 }
379395 producer_calls.fetch_add (1 );
@@ -397,9 +413,8 @@ LT_BEGIN_AUTO_TEST(deferred_suite, deferred_producer_destroyed_in_request_comple
397413 curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION , writefunc);
398414 curl_easy_setopt (curl, CURLOPT_WRITEDATA , &body);
399415 // Force HTTP/1.0 (no keep-alive) so MHD closes the connection as
400- // soon as curl finishes reading the body. That makes request_completed
401- // fire deterministically before ws->stop() — without relying on the
402- // keep-alive timeout.
416+ // soon as curl finishes reading the body, making request_completed
417+ // fire before ws->stop() in the common case.
403418 curl_easy_setopt (curl, CURLOPT_HTTP_VERSION , CURL_HTTP_VERSION_1_0 );
404419 CURLcode res = curl_easy_perform(curl);
405420 LT_ASSERT_EQ (res, 0 );
@@ -413,20 +428,19 @@ LT_BEGIN_AUTO_TEST(deferred_suite, deferred_producer_destroyed_in_request_comple
413428 LT_CHECK (producer_calls.load() >= 2);
414429
415430 // Force MHD to fire request_completed for every pending connection.
416- // MHD_stop_daemon (called by webserver::stop) joins the internal
417- // threads and drains the request_completed queue. After stop()
418- // returns, the modded_request (and its optional<http_response>
419- // holding our deferred_body, holding the inner-lambda's
420- // make_unique<destruction_sentinel>) MUST be gone. tear_down() will
421- // call stop() again — idempotent on an already-stopped server (see
422- // webserver::stop's running guard).
431+ // MHD_stop_daemon (called by stop()) joins internal threads and drains
432+ // the request_completed queue. tear_down() calls stop() again; that is
433+ // safe because webserver::stop() is idempotent once already stopped.
423434 ws->stop ();
424- // request_completed may run on a worker thread that completes a
425- // hair after stop() returns to us. Poll briefly for the destruction
426- // signal — bounded so a true regression (body anchor leaked past
427- // request_completed) fails fast instead of hanging.
428- for (int i = 0 ; i < 100 && !destroyed.load(); ++i) {
429- std::this_thread::sleep_for (std::chrono::milliseconds (10 ));
435+
436+ // Wait for the sentinel to be destroyed. request_completed may fire on
437+ // a worker thread that finishes a hair after stop() returns, so we use
438+ // a condition variable rather than a busy-poll. 5-second upper bound
439+ // catches a genuine DR-010 regression without hanging CI indefinitely.
440+ {
441+ std::unique_lock<std::mutex> lk (destroyed_mu);
442+ destroyed_cv.wait_for (lk, std::chrono::seconds (5 ),
443+ [&destroyed] { return destroyed.load (); });
430444 }
431445 LT_CHECK_EQ (destroyed.load(), true);
432446LT_END_AUTO_TEST (deferred_producer_destroyed_in_request_completed)
0 commit comments