fix: make timer_.on_expired survive (not crash) if the timer as been destroyed

This commit is contained in:
CJ van den Berg 2026-03-11 20:42:39 +01:00
parent bbcc800964
commit 367c497a38
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9

View file

@ -761,24 +761,40 @@ struct timeout_impl {
owner_.env_.trace(array("timeout", "set", m, us.count())); owner_.env_.trace(array("timeout", "set", m, us.count()));
timer_.expires_after(us); timer_.expires_after(us);
timer_.on_expired([this, start, m(move(m)), // Capture lifelock and dtor_cancelled instead of `this`.
lifelock{owner_.lifetime_}](const error_code &error) { // timeout_impl is owned by a unique_ptr and its destructor calls cancel();
if (is_trace_enabled()) // the ASIO callback is queued *after* ~timeout_impl returns, so `this`
owner_.env_.trace(array("timeout", "expired", m, // would be dangling. lifelock keeps the instance alive; dtor_cancelled
clk::now().time_since_epoch().count() - start, // distinguishes destructor-triggered cancellation (where no notification
error.value(), error.message())); // is desired) from an explicit user cancel() call.
timer_.on_expired([lifelock{owner_.lifetime_},
dtor_cancelled{dtor_cancelled_},
start, m(move(m))](const error_code &error) {
if (!lifelock) return;
if (lifelock->env_.enabled(channel::timer))
lifelock->env_.trace(array("timeout", "expired", m,
clk::now().time_since_epoch().count() - start,
error.value(), error.message()));
if (!error) if (!error)
auto _ = owner_.send_raw(m); auto _ = lifelock->send_raw(m);
else else if (!*dtor_cancelled)
auto _ = owner_.send_raw( // User called cancel() explicitly — preserve the existing timeout_error
// notification so callers can react (e.g. arm a different timer).
auto _ = lifelock->send_raw(
exit_message("timeout_error", error.value(), error.message())); exit_message("timeout_error", error.value(), error.message()));
}); });
} }
~timeout_impl() { cancel(); } ~timeout_impl() {
*dtor_cancelled_ = true;
cancel();
}
void cancel() { timer_.cancel(); } void cancel() { timer_.cancel(); }
instance &owner_; instance &owner_;
executor::timer timer_; executor::timer timer_;
// Shared with the timer callback so it can distinguish dtor cancellation
// from an explicit user cancel() without capturing `this`.
shared_ptr<bool> dtor_cancelled_{make_shared<bool>(false)};
}; };
void timeout::cancel() { void timeout::cancel() {
if (ref) if (ref)