Copilot commented on code in PR #13898:
URL: https://github.com/apache/trafficserver/pull/13898#discussion_r4459118867
##########
src/iocore/eventsystem/Watchdog.cc:
##########
@@ -37,73 +42,448 @@
namespace Watchdog
{
+using ts::Metrics;
+
DbgCtl dbg_ctl_watchdog("watchdog");
-Monitor::Monitor(EThread *threads[], size_t n_threads,
std::chrono::milliseconds timeout_ms)
- : _threads(threads, threads + n_threads), _timeout{timeout_ms}
+namespace
+{
+ // this ensures `long long` duration for older compilers to match format
specifiers
+ using printable_micro = std::chrono::duration<long long, std::micro>;
+ using printable_milli = std::chrono::duration<long long, std::milli>;
+
+ /** Current value of accept_refuse(). Written only by the monitor thread and
the config callback.
+
+ Read from the accept paths on every accept loop.
+ */
+ std::atomic<bool> accept_refuse_state{false};
+
+ /** Current value of proxy.config.exec_thread.watchdog.mode.
+
+ File scope rather than a Monitor member on purpose:
RecRegisterConfigUpdateCb has no
+ unregister counterpart, so a callback that captured the Monitor would
outlive it and fire
+ against freed memory at shutdown. Keeping the mode here means the
callback touches only static
+ storage and remains valid for the life of the process.
+ */
+ std::atomic<Mode> current_mode{Mode::DISABLED};
+
+ /** proxy.config.exec_thread.watchdog.timeout_refuse_ms - threshold for
accept_refuse().
+
+ Zero disables it entirely. Independent of the shrink timeout that drives
the scan period: the
+ accept paths stopping and the accept queues shrinking are separate
decisions on the same peak,
+ so this can be reloaded freely without disturbing the scan rate.
+ */
+ std::atomic<int64_t> refuse_timeout_ms{0};
+
+ /// proxy.config.exec_thread.watchdog.backlog_min_permille - floor for
backlog_scale_permille().
+ std::atomic<int32_t> backlog_min_permille{50};
+
+ /// Published listen backlog scale, in permille of the configured value.
1000 == unthrottled.
+ std::atomic<int32_t> backlog_scale{1000};
+
+ /** proxy.config.exec_thread.watchdog.backlog_decrease_permille -
multiplicative decrease.
+
+ Expressed as the permille of the *current* scale retained on each
saturated scan, rather than
+ as a divisor, so the whole range is reachable rather than just 1/2, 1/3,
1/4: 500 is the
+ classic halving, 667 is a divide by 1.5, 900 is a slow squeeze, 975 is a
crawl.
+
+ Doubles as the release deadband - see release_threshold(). One number
for how much margin is
+ wanted against the overload, applied to the queue depth and to the
signal alike.
+ */
+ std::atomic<int32_t> backlog_decrease_permille{500};
+
+ /** proxy.config.exec_thread.watchdog.backlog_increase_permille -
proportional increase.
+
+ Permille of the scale *currently in force* offered as the step on each
recovered scan, before
+ the headroom attenuation in recovery_step_permille(). Equivalently,
permille points of the
+ configured backlog at full depth, which is where the two readings
coincide and why one number
+ serves as both the rate and the ceiling on the step - the scale never
exceeds 1000, so the
+ proportional value can never exceed the configured points.
+
+ Proportional to the current depth rather than a flat number of points
because what re-saturates
+ the threads is the count of connections admitted, not the fraction of
the baseline they
+ represent, and a flat step is a fixed count whatever depth is in force.
At the bottom of a hard
+ shed that count is a multiple of the whole queue: at 100 points of a
65,535 baseline, the first
+ recovered scan after a shed to 20 permille would readmit 6,553
connections onto a queue holding
+ 1,310. The same 100 here readmits 131, and the step grows with the queue
as it climbs out.
+ */
+ std::atomic<int32_t> backlog_increase_permille{100};
+
+ int
+ update_watchdog_mode(const char * /* name */, RecDataT /* type */, RecData
data, void * /* cookie */)
+ {
+ auto mode = static_cast<Mode>(data.rec_int);
+ if (mode != Mode::DISABLED && mode != Mode::BACKPRESSURE) {
+ Warning("Watchdog: ignoring out of range
proxy.config.exec_thread.watchdog.mode %" PRId64, data.rec_int);
+ return REC_ERR_OKAY;
+ }
+
+ Mode const prev = current_mode.exchange(mode, std::memory_order_relaxed);
+ if (prev == mode) {
+ return REC_ERR_OKAY;
+ }
+
+ // Drop any backpressure the outgoing mode had engaged. The monitor
re-derives everything from
+ // the new mode on its next scan, so the worst case is up to half a
timeout of not shedding -
+ // strictly the safe direction. Leaving it set would be the unsafe one:
switching to mode 0
+ // would otherwise strand the accept queues shrunk forever.
+ //
+ // The reporting gauges are left to the monitor rather than cleared here.
It owns them, and
+ // sync_state_gauges() corrects them on its next scan; writing them from
this thread would race
+ // its stores.
+ if (mode == Mode::DISABLED) {
+ accept_refuse_state.store(false, std::memory_order_relaxed);
+ backlog_scale.store(1000, std::memory_order_relaxed);
+ }
+
+ Note("Watchdog: mode changed from %d to %d", static_cast<int>(prev),
static_cast<int>(mode));
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_refuse_timeout(const char * /* name */, RecDataT /* type */, RecData
data, void * /* cookie */)
+ {
+ int64_t const want = std::max<int64_t>(0, data.rec_int);
+ refuse_timeout_ms.store(want, std::memory_order_relaxed);
+
+ // Turning it off must resume accepting here rather than at the next scan
- the whole point of
+ // setting it to zero is to stop refusing immediately. Turning it on is
left to the monitor, which
+ // has an actual peak to compare against.
+ if (want == 0) {
+ accept_refuse_state.store(false, std::memory_order_relaxed);
+ }
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_backlog_min_permille(const char * /* name */, RecDataT /* type */,
RecData data, void * /* cookie */)
+ {
+
backlog_min_permille.store(static_cast<int32_t>(std::clamp<int64_t>(data.rec_int,
1, 1000)), std::memory_order_relaxed);
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_backlog_decrease_permille(const char * /* name */, RecDataT /* type
*/, RecData data, void * /* cookie */)
+ {
+
backlog_decrease_permille.store(static_cast<int32_t>(std::clamp<int64_t>(data.rec_int,
1, 999)), std::memory_order_relaxed);
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_backlog_increase_permille(const char * /* name */, RecDataT /* type
*/, RecData data, void * /* cookie */)
+ {
+
backlog_increase_permille.store(static_cast<int32_t>(std::clamp<int64_t>(data.rec_int,
1, 1000)), std::memory_order_relaxed);
+ return REC_ERR_OKAY;
+ }
+
+} // namespace
+
+void
+register_config_callbacks()
+{
+ RecInt mode =
RecGetRecordInt("proxy.config.exec_thread.watchdog.mode").value_or(0);
+ current_mode.store(static_cast<Mode>(mode), std::memory_order_relaxed);
+ RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.mode",
update_watchdog_mode, nullptr);
+
+ RecInt refuse_timeout =
RecGetRecordInt("proxy.config.exec_thread.watchdog.timeout_refuse_ms").value_or(0);
+ refuse_timeout_ms.store(std::max<int64_t>(0, refuse_timeout),
std::memory_order_relaxed);
+
RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.timeout_refuse_ms",
update_refuse_timeout, nullptr);
+
+ RecInt min_permille =
RecGetRecordInt("proxy.config.exec_thread.watchdog.backlog_min_permille").value_or(50);
+
backlog_min_permille.store(static_cast<int32_t>(std::clamp<int64_t>(min_permille,
1, 1000)), std::memory_order_relaxed);
+
RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.backlog_min_permille",
update_backlog_min_permille, nullptr);
+
+ RecInt dec_permille =
RecGetRecordInt("proxy.config.exec_thread.watchdog.backlog_decrease_permille").value_or(500);
+
backlog_decrease_permille.store(static_cast<int32_t>(std::clamp<int64_t>(dec_permille,
1, 999)), std::memory_order_relaxed);
+
RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.backlog_decrease_permille",
update_backlog_decrease_permille,
+ nullptr);
+
+ RecInt inc_permille =
RecGetRecordInt("proxy.config.exec_thread.watchdog.backlog_increase_permille").value_or(100);
+
backlog_increase_permille.store(static_cast<int32_t>(std::clamp<int64_t>(inc_permille,
1, 1000)), std::memory_order_relaxed);
+
RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.backlog_increase_permille",
update_backlog_increase_permille,
+ nullptr);
+}
+
+void
+record_awake_span(Heartbeat &hb, std::chrono::steady_clock::duration span)
+{
+ auto ns = static_cast<uint64_t>(std::max<int64_t>(0,
std::chrono::duration_cast<std::chrono::nanoseconds>(span).count()));
+ // Only the owning EThread raises this; the monitor's exchange to zero is
the only other writer, so a
+ // plain load/store pair could lose a reset. Use CAS to keep the max
semantics under that race.
+ uint64_t prev = hb.peak_awake.load(std::memory_order_relaxed);
+ while (prev < ns && !hb.peak_awake.compare_exchange_weak(prev, ns,
std::memory_order_relaxed, std::memory_order_relaxed)) {
+ ;
+ }
+}
+
+bool
+accept_refuse()
+{
+ // Gated on the live configuration so a scan that raced a config callback
and published a stale
+ // result cannot re-enable an action that has since been turned off.
+ return accept_refuse_state.load(std::memory_order_relaxed) &&
+ current_mode.load(std::memory_order_relaxed) == Mode::BACKPRESSURE &&
+ refuse_timeout_ms.load(std::memory_order_relaxed) > 0;
+}
+
+int
+backlog_scale_permille()
+{
+ if (current_mode.load(std::memory_order_relaxed) != Mode::BACKPRESSURE) {
+ return 1000;
+ }
+ return backlog_scale.load(std::memory_order_relaxed);
+}
+
+SleepScope::SleepScope(Heartbeat &hb) : _hb(hb)
+{
+ auto now = std::chrono::steady_clock::now();
+
+ // Publish the span that just ended, so a burst finishing between two
monitor scans is not lost.
+ auto last_wake = _hb.last_wake.load(std::memory_order_relaxed);
+ if (last_wake != std::chrono::steady_clock::time_point::min()) {
+ record_awake_span(_hb, now - last_wake);
+ }
+
+ // Enter the odd (sleeping) phase.
+ _hb.seq.fetch_add(1, std::memory_order_relaxed);
+}
+
+SleepScope::~SleepScope()
+{
+ _hb.last_wake.store(std::chrono::steady_clock::now(),
std::memory_order_relaxed);
+ // Back to even (awake). Release so the monitor cannot observe the even
sequence before the
+ // timestamp store above.
+ _hb.seq.fetch_add(1, std::memory_order_release);
+}
+
+Monitor::Monitor(EThread *threads[], size_t n_threads,
std::chrono::milliseconds shrink_timeout_ms, bool legacy)
+ : _threads(threads, threads + n_threads),
_shrink_timeout{shrink_timeout_ms}, _legacy{legacy}
{
- // Precondition: timeout_ms must be > 0. A timeout of 0 indicates the
watchdog is disabled
+ // Precondition: shrink_timeout_ms must be > 0. A timeout of 0 indicates the
watchdog is disabled
// and the caller should not instantiate the Monitor (see traffic_server.cc).
- ink_release_assert(timeout_ms.count() > 0);
+ ink_release_assert(shrink_timeout_ms.count() > 0);
+ if (!_legacy) {
+ create_metrics();
+ }
_watchdog_thread = std::thread(std::bind_front(&Monitor::monitor_loop,
this));
}
+void
+Monitor::create_metrics()
+{
+ _max_awake_ms =
Metrics::Gauge::createPtr("proxy.process.watchdog.max_awake_ms");
+ _max_awake_ms_hwm =
Metrics::Gauge::createPtr("proxy.process.watchdog.max_awake_ms_hwm");
+ _shrink_exceeded_count =
Metrics::Counter::createPtr("proxy.process.watchdog.timeout_shrink_exceeded_count");
+ _refuse_exceeded_count =
Metrics::Counter::createPtr("proxy.process.watchdog.timeout_refuse_exceeded_count");
+ _shrink_gauge =
Metrics::Gauge::createPtr("proxy.process.watchdog.accept_shrink");
+ _shrink_count =
Metrics::Counter::createPtr("proxy.process.watchdog.accept_shrink_engaged_count");
+ _refuse_gauge =
Metrics::Gauge::createPtr("proxy.process.watchdog.accept_refuse");
+ _refuse_count =
Metrics::Counter::createPtr("proxy.process.watchdog.accept_refuse_engaged_count");
+ _backlog_permille =
Metrics::Gauge::createPtr("proxy.process.watchdog.listen_backlog_permille");
+ Metrics::Gauge::store(_backlog_permille, 1000);
+}
+
Monitor::~Monitor()
{
_shutdown.store(true, std::memory_order_release);
_watchdog_thread.join();
+ // Never leave the accept paths throttled behind a monitor that is no longer
scanning.
+ accept_refuse_state.store(false, std::memory_order_relaxed);
+ backlog_scale.store(1000, std::memory_order_relaxed);
+}
+
+std::chrono::steady_clock::duration
+Monitor::scan(std::chrono::steady_clock::time_point now)
+{
+ std::chrono::steady_clock::duration peak =
std::chrono::steady_clock::duration::zero();
+
+ for (size_t i = 0; i < _threads.size(); ++i) {
+ EThread *t = _threads[i];
+
+ // Completed spans the thread finished since the previous scan. Draining
to zero here is what makes
+ // this a per-scan peak rather than a lifetime high water mark.
+ auto recorded =
std::chrono::nanoseconds{t->heartbeat_state.peak_awake.exchange(0,
std::memory_order_relaxed)};
+ peak = std::max<std::chrono::steady_clock::duration>(peak,
recorded);
+
+ // Seqlock read of the sleep/wake pair. An odd sequence means the thread
is inside the blocking
+ // poll, and a changed sequence means it transitioned while we were
reading - either way the pair
+ // is not a coherent snapshot and the live reading is skipped. peak_awake
above already accounts
+ // for that thread's completed work, so skipping loses nothing.
+ uint64_t const seq_before =
t->heartbeat_state.seq.load(std::memory_order_acquire);
+ if (seq_before & 1) {
+ continue; // sleeping
+ }
+
+ auto last_wake =
t->heartbeat_state.last_wake.load(std::memory_order_relaxed);
+
+ std::atomic_thread_fence(std::memory_order_acquire);
+ if (t->heartbeat_state.seq.load(std::memory_order_relaxed) != seq_before) {
+ continue; // torn - the thread slept or woke mid-read
+ }
+
+ if (last_wake == std::chrono::steady_clock::time_point::min()) {
+ continue; // event loop hasn't completed its first poll
+ }
+
+ // Live snapshot of the span in progress. A thread stuck in a single long
span never reaches the
+ // top of its next loop to record itself in peak_awake, so it only shows
up here.
+ auto awake_duration = now - last_wake;
+ if (awake_duration < std::chrono::steady_clock::duration::zero()) {
+ // @a now is sampled once at the top of the scan, before this thread's
seqlock read, so a thread
+ // that woke in between can report a last_wake fractionally newer than
it. Harmless and always
+ // sub-millisecond, but it must not be fed to std::max as a duration.
+ continue;
+ }
+ peak = std::max(peak, awake_duration);
+
+ if (awake_duration > _shrink_timeout) {
+ // Monitor thread is the sole reader (and warned_seq writer), so relaxed
accesses are race-free.
+ uint64_t warned_seq =
t->heartbeat_state.warned_seq.load(std::memory_order_relaxed);
+ if (warned_seq < seq_before) {
+ if (_legacy) {
+ // The deprecated timeout_ms warned here, and a config that still
sets it gets what it asked for.
+ Warning("Watchdog: [ET_NET %zu] has been awake for %lld ms", i,
+
std::chrono::duration_cast<printable_milli>(awake_duration).count());
+ } else {
+ // Debug rather than Warning: on a busy proxy the timeout is crossed
routinely, and a log line
+ // per event loop that runs long is noise an operator cannot act on.
The condition is already
+ // reported continuously and cheaply as
proxy.process.watchdog.max_awake_ms, and counted per
+ // scan as proxy.process.watchdog.timeout_shrink_exceeded_count,
which is the right thing to
+ // alert on.
+ // This is here for the case where you need to know *which* thread.
+ //
+ // Reported once per loop iteration, not once per scan.
+ Dbg(dbg_ctl_watchdog, "[ET_NET %zu] has been awake for %lld ms
(shrink timeout %lld ms)", i,
+
std::chrono::duration_cast<printable_milli>(awake_duration).count(),
+
std::chrono::duration_cast<printable_milli>(_shrink_timeout).count());
+ }
+ t->heartbeat_state.warned_seq.store(seq_before,
std::memory_order_relaxed);
+ }
+ }
+ }
+
+ return peak;
}
void
-Monitor::monitor_loop() const
+Monitor::sync_state_gauges()
{
- // this ensures `long long` duration for older compilers to match format
specifiers
- using printable_micro = std::chrono::duration<long long, std::micro>;
- using printable_milli = std::chrono::duration<long long, std::milli>;
+ bool const refusing = accept_refuse_state.load(std::memory_order_relaxed);
+ if (refusing != _refuse_reported) {
+ _refuse_reported = refusing;
+ Metrics::Gauge::store(_refuse_gauge, refusing ? 1 : 0);
+ }
+}
+void
+Monitor::monitor_loop()
+{
// Divide by a floating point 2 to avoid truncation to zero.
- auto sleep_time = _timeout / 2.0;
+ auto sleep_time = _shrink_timeout / 2.0;
ink_release_assert(sleep_time.count() > 0);
- Dbg(dbg_ctl_watchdog, "Starting watchdog with timeout %lld ms on %zu
threads. sleep_time = %lld us",
- std::chrono::duration_cast<printable_milli>(_timeout).count(),
_threads.size(),
+ Dbg(dbg_ctl_watchdog, "Starting watchdog with shrink timeout %lld ms, mode
%d, on %zu threads. sleep_time = %lld us",
+ std::chrono::duration_cast<printable_milli>(_shrink_timeout).count(),
+ static_cast<int>(current_mode.load(std::memory_order_relaxed)),
_threads.size(),
std::chrono::duration_cast<printable_micro>(sleep_time).count());
ink_set_thread_name("[WATCHDOG]");
while (!_shutdown.load(std::memory_order_acquire)) {
- std::chrono::time_point<std::chrono::steady_clock> now =
std::chrono::steady_clock::now();
- for (size_t i = 0; i < _threads.size(); ++i) {
- EThread *t = _threads[i];
- // Relaxed load: each heartbeat field has a single writer (its EThread)
so per-object coherence suffices.
- std::chrono::time_point<std::chrono::steady_clock> last_sleep =
t->heartbeat_state.last_sleep.load(std::memory_order_relaxed);
- if (last_sleep == std::chrono::steady_clock::time_point::min()) {
- // initial value sentinel - event loop hasn't started
- continue;
- }
- // Same reasoning for relaxed load on wake timestamp.
- std::chrono::time_point<std::chrono::steady_clock> last_wake =
t->heartbeat_state.last_wake.load(std::memory_order_relaxed);
+ auto peak = scan(std::chrono::steady_clock::now());
+
+ if (_legacy) {
+ std::this_thread::sleep_for(sleep_time);
+ continue;
+ }
+
+ auto peak_ms = std::chrono::duration_cast<printable_milli>(peak).count();
+ Metrics::Gauge::store(_max_awake_ms, peak_ms);
+
+ // Monotonic high water mark alongside the per-scan gauge above.
max_awake_ms covers only the
+ // interval since the previous scan and is overwritten by the next one, so
a spike that happens to
+ // land between two scrapes of the metric is never seen at all - and the
scan period is half the
+ // shrink timeout, which is far shorter than any realistic scrape
interval. This one only ever
+ // rises, so the worst span the process has served is still readable after
the fact.
+ if (peak_ms > _hwm_ms) {
+ _hwm_ms = peak_ms;
+ Metrics::Gauge::store(_max_awake_ms_hwm, peak_ms);
+ }
+
+ // Re-read the mode every scan - it is reloadable, so it can change under
us at any point.
+ Mode const mode = current_mode.load(std::memory_order_relaxed);
- if (last_wake == std::chrono::steady_clock::time_point::min() ||
last_wake < last_sleep) {
- // not yet woken from last sleep
- continue;
+ // The arithmetic lives in advance() so it can be exercised without a
running monitor. This loop gathers
+ // its inputs, applies the result to the published atomics and metrics,
and reports the edges. The refuse
+ // state is read back from the published atomic rather than mirrored in a
member, because
+ // update_refuse_timeout() and update_watchdog_mode() also clear it - a
local mirror would go stale against
+ // those and suppress the store that re-engages.
+ PolicyConfig config;
+ config.backpressure = mode == Mode::BACKPRESSURE;
+ config.shrink_timeout = _shrink_timeout;
+ config.refuse_timeout =
std::chrono::milliseconds{refuse_timeout_ms.load(std::memory_order_relaxed)};
+ config.decrease_permille =
backlog_decrease_permille.load(std::memory_order_relaxed);
+ config.increase_permille =
backlog_increase_permille.load(std::memory_order_relaxed);
+ config.min_permille =
backlog_min_permille.load(std::memory_order_relaxed);
+
+ PolicyState const before{_shrink_engaged,
accept_refuse_state.load(std::memory_order_relaxed), _backlog_scale,
_trouble_scale};
+ auto const result = advance(config, before, peak);
+ PolicyState const &after = result.state;
+
+ // Counted per saturated scan and deliberately independent of @c mode, so
mode 0 - report only -
+ // still gets a monotonic record of how often the threads crossed the
shrink threshold. The engaged
+ // counters below only move on a state edge, which says nothing about how
long or how often, and
+ // the max_awake_ms gauge can miss the crossing entirely between scrapes.
Dividing this by the scan
+ // rate gives the fraction of time spent over the threshold. The refuse
threshold is counted the same way.
+ if (result.saturated) {
+ Metrics::Counter::increment(_shrink_exceeded_count);
+ }
+ if (result.refuse_exceeded) {
+ Metrics::Counter::increment(_refuse_exceeded_count);
+ }
+
+ _shrink_engaged = after.shrink_engaged;
+ _trouble_scale = after.trouble_scale;
+
+ if (after.shrink_engaged != before.shrink_engaged) {
+ Metrics::Gauge::store(_shrink_gauge, after.shrink_engaged ? 1 : 0);
+ if (after.shrink_engaged) {
+ Metrics::Counter::increment(_shrink_count);
}
+ Dbg(dbg_ctl_watchdog, "backlog shrink -> %s - peak awake %lld ms, shrink
timeout %lld ms",
+ after.shrink_engaged ? "on" : "off", peak_ms,
std::chrono::duration_cast<printable_milli>(_shrink_timeout).count());
+ }
- auto awake_duration = now - last_wake;
- if (awake_duration > _timeout) {
- // Monitor thread is the sole reader (and warned_seq writer), so
relaxed accesses are race-free.
- uint64_t seq =
t->heartbeat_state.seq.load(std::memory_order_relaxed);
- uint64_t warned_seq =
t->heartbeat_state.warned_seq.load(std::memory_order_relaxed);
- if (warned_seq < seq) {
- // Warn once per loop iteration
- Warning("Watchdog: [ET_NET %zu] has been awake for %lld ms", i,
-
std::chrono::duration_cast<printable_milli>(awake_duration).count());
- t->heartbeat_state.warned_seq.store(seq, std::memory_order_relaxed);
- }
+ // Also compare against the published value: a config callback resets it
without touching
+ // _backlog_scale, and the two must be reconciled even when the monitor's
own scale is unchanged.
+ if (after.scale != before.scale || after.scale !=
backlog_scale.load(std::memory_order_relaxed)) {
+ _backlog_scale = after.scale;
+ backlog_scale.store(after.scale, std::memory_order_relaxed);
+ Metrics::Gauge::store(_backlog_permille, after.scale);
+ Dbg(dbg_ctl_watchdog, "listen backlog scale -> %d permille - peak awake
%lld ms", after.scale, peak_ms);
+ }
+
+ if (after.refuse != before.refuse) {
+ accept_refuse_state.store(after.refuse, std::memory_order_relaxed);
+ if (after.refuse) {
+ Metrics::Counter::increment(_refuse_count);
}
+ Dbg(dbg_ctl_watchdog, "accept refuse -> %s - peak awake %lld ms, refuse
timeout %lld ms", after.refuse ? "on" : "off",
+ peak_ms, static_cast<long long>(config.refuse_timeout.count()));
}
+ sync_state_gauges();
+
std::this_thread::sleep_for(sleep_time);
}
+
+ if (accept_refuse_state.load(std::memory_order_relaxed) || _backlog_scale !=
1000) {
+ accept_refuse_state.store(false, std::memory_order_relaxed);
+ backlog_scale.store(1000, std::memory_order_relaxed);
+ Metrics::Gauge::store(_shrink_gauge, 0);
+ Metrics::Gauge::store(_backlog_permille, 1000);
+ }
+ sync_state_gauges();
Review Comment:
Legacy-mode monitors (`_legacy == true`) never call `create_metrics()`,
leaving metric pointers (e.g. `_shrink_gauge`, `_backlog_permille`,
`_refuse_gauge`) null. When the loop exits, the unconditional cleanup block and
`sync_state_gauges()` will dereference those null pointers and can crash on
shutdown. Fix by guarding all metric stores/`sync_state_gauges()` behind `if
(!_legacy)` (or early-return before the cleanup section when `_legacy` is
true), so legacy warn-only mode never touches metric pointers.
##########
src/iocore/net/Server.cc:
##########
@@ -158,6 +160,13 @@ Server::listen(bool non_blocking, const
NetProcessor::AcceptOptions &opt)
close();
}
+ if (opt.f_non_fatal_listen) {
+ // A supplementary listener. Losing it degrades behavior but must not take
the proxy down.
+ Warning("Could not bind or listen to port %d, mptcp enabled: %d (error:
%d) %s %d", ats_ip_port_host_order(&addr),
+ prot == IPPROTO_MPTCP, errno, strerror(errno), res);
+ return res;
+ }
Review Comment:
This warning logs `errno`/`strerror(errno)` at `Lerror`, but `errno` may no
longer correspond to the failing syscall after cleanup work (e.g., `close()` or
other calls that can mutate `errno`). Capture the failing `errno` into a local
(e.g., `saved_errno`) at the point of failure and log that instead. Also, the
message says 'bind or listen' even for failures from `socket()`, `chmod()`, or
`chown()` paths shown in this diff; consider broadening phrasing (e.g., 'Could
not set up listener') to keep the warning accurate.
##########
src/iocore/eventsystem/Watchdog.cc:
##########
@@ -37,73 +42,448 @@
namespace Watchdog
{
+using ts::Metrics;
+
DbgCtl dbg_ctl_watchdog("watchdog");
-Monitor::Monitor(EThread *threads[], size_t n_threads,
std::chrono::milliseconds timeout_ms)
- : _threads(threads, threads + n_threads), _timeout{timeout_ms}
+namespace
+{
+ // this ensures `long long` duration for older compilers to match format
specifiers
+ using printable_micro = std::chrono::duration<long long, std::micro>;
+ using printable_milli = std::chrono::duration<long long, std::milli>;
+
+ /** Current value of accept_refuse(). Written only by the monitor thread and
the config callback.
+
+ Read from the accept paths on every accept loop.
+ */
+ std::atomic<bool> accept_refuse_state{false};
+
+ /** Current value of proxy.config.exec_thread.watchdog.mode.
+
+ File scope rather than a Monitor member on purpose:
RecRegisterConfigUpdateCb has no
+ unregister counterpart, so a callback that captured the Monitor would
outlive it and fire
+ against freed memory at shutdown. Keeping the mode here means the
callback touches only static
+ storage and remains valid for the life of the process.
+ */
+ std::atomic<Mode> current_mode{Mode::DISABLED};
+
+ /** proxy.config.exec_thread.watchdog.timeout_refuse_ms - threshold for
accept_refuse().
+
+ Zero disables it entirely. Independent of the shrink timeout that drives
the scan period: the
+ accept paths stopping and the accept queues shrinking are separate
decisions on the same peak,
+ so this can be reloaded freely without disturbing the scan rate.
+ */
+ std::atomic<int64_t> refuse_timeout_ms{0};
+
+ /// proxy.config.exec_thread.watchdog.backlog_min_permille - floor for
backlog_scale_permille().
+ std::atomic<int32_t> backlog_min_permille{50};
+
+ /// Published listen backlog scale, in permille of the configured value.
1000 == unthrottled.
+ std::atomic<int32_t> backlog_scale{1000};
+
+ /** proxy.config.exec_thread.watchdog.backlog_decrease_permille -
multiplicative decrease.
+
+ Expressed as the permille of the *current* scale retained on each
saturated scan, rather than
+ as a divisor, so the whole range is reachable rather than just 1/2, 1/3,
1/4: 500 is the
+ classic halving, 667 is a divide by 1.5, 900 is a slow squeeze, 975 is a
crawl.
+
+ Doubles as the release deadband - see release_threshold(). One number
for how much margin is
+ wanted against the overload, applied to the queue depth and to the
signal alike.
+ */
+ std::atomic<int32_t> backlog_decrease_permille{500};
+
+ /** proxy.config.exec_thread.watchdog.backlog_increase_permille -
proportional increase.
+
+ Permille of the scale *currently in force* offered as the step on each
recovered scan, before
+ the headroom attenuation in recovery_step_permille(). Equivalently,
permille points of the
+ configured backlog at full depth, which is where the two readings
coincide and why one number
+ serves as both the rate and the ceiling on the step - the scale never
exceeds 1000, so the
+ proportional value can never exceed the configured points.
+
+ Proportional to the current depth rather than a flat number of points
because what re-saturates
+ the threads is the count of connections admitted, not the fraction of
the baseline they
+ represent, and a flat step is a fixed count whatever depth is in force.
At the bottom of a hard
+ shed that count is a multiple of the whole queue: at 100 points of a
65,535 baseline, the first
+ recovered scan after a shed to 20 permille would readmit 6,553
connections onto a queue holding
+ 1,310. The same 100 here readmits 131, and the step grows with the queue
as it climbs out.
+ */
+ std::atomic<int32_t> backlog_increase_permille{100};
+
+ int
+ update_watchdog_mode(const char * /* name */, RecDataT /* type */, RecData
data, void * /* cookie */)
+ {
+ auto mode = static_cast<Mode>(data.rec_int);
+ if (mode != Mode::DISABLED && mode != Mode::BACKPRESSURE) {
+ Warning("Watchdog: ignoring out of range
proxy.config.exec_thread.watchdog.mode %" PRId64, data.rec_int);
+ return REC_ERR_OKAY;
+ }
+
+ Mode const prev = current_mode.exchange(mode, std::memory_order_relaxed);
+ if (prev == mode) {
+ return REC_ERR_OKAY;
+ }
+
+ // Drop any backpressure the outgoing mode had engaged. The monitor
re-derives everything from
+ // the new mode on its next scan, so the worst case is up to half a
timeout of not shedding -
+ // strictly the safe direction. Leaving it set would be the unsafe one:
switching to mode 0
+ // would otherwise strand the accept queues shrunk forever.
+ //
+ // The reporting gauges are left to the monitor rather than cleared here.
It owns them, and
+ // sync_state_gauges() corrects them on its next scan; writing them from
this thread would race
+ // its stores.
+ if (mode == Mode::DISABLED) {
+ accept_refuse_state.store(false, std::memory_order_relaxed);
+ backlog_scale.store(1000, std::memory_order_relaxed);
+ }
+
+ Note("Watchdog: mode changed from %d to %d", static_cast<int>(prev),
static_cast<int>(mode));
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_refuse_timeout(const char * /* name */, RecDataT /* type */, RecData
data, void * /* cookie */)
+ {
+ int64_t const want = std::max<int64_t>(0, data.rec_int);
+ refuse_timeout_ms.store(want, std::memory_order_relaxed);
+
+ // Turning it off must resume accepting here rather than at the next scan
- the whole point of
+ // setting it to zero is to stop refusing immediately. Turning it on is
left to the monitor, which
+ // has an actual peak to compare against.
+ if (want == 0) {
+ accept_refuse_state.store(false, std::memory_order_relaxed);
+ }
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_backlog_min_permille(const char * /* name */, RecDataT /* type */,
RecData data, void * /* cookie */)
+ {
+
backlog_min_permille.store(static_cast<int32_t>(std::clamp<int64_t>(data.rec_int,
1, 1000)), std::memory_order_relaxed);
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_backlog_decrease_permille(const char * /* name */, RecDataT /* type
*/, RecData data, void * /* cookie */)
+ {
+
backlog_decrease_permille.store(static_cast<int32_t>(std::clamp<int64_t>(data.rec_int,
1, 999)), std::memory_order_relaxed);
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_backlog_increase_permille(const char * /* name */, RecDataT /* type
*/, RecData data, void * /* cookie */)
+ {
+
backlog_increase_permille.store(static_cast<int32_t>(std::clamp<int64_t>(data.rec_int,
1, 1000)), std::memory_order_relaxed);
+ return REC_ERR_OKAY;
+ }
+
+} // namespace
+
+void
+register_config_callbacks()
+{
+ RecInt mode =
RecGetRecordInt("proxy.config.exec_thread.watchdog.mode").value_or(0);
+ current_mode.store(static_cast<Mode>(mode), std::memory_order_relaxed);
+ RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.mode",
update_watchdog_mode, nullptr);
+
+ RecInt refuse_timeout =
RecGetRecordInt("proxy.config.exec_thread.watchdog.timeout_refuse_ms").value_or(0);
+ refuse_timeout_ms.store(std::max<int64_t>(0, refuse_timeout),
std::memory_order_relaxed);
+
RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.timeout_refuse_ms",
update_refuse_timeout, nullptr);
+
+ RecInt min_permille =
RecGetRecordInt("proxy.config.exec_thread.watchdog.backlog_min_permille").value_or(50);
+
backlog_min_permille.store(static_cast<int32_t>(std::clamp<int64_t>(min_permille,
1, 1000)), std::memory_order_relaxed);
+
RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.backlog_min_permille",
update_backlog_min_permille, nullptr);
+
+ RecInt dec_permille =
RecGetRecordInt("proxy.config.exec_thread.watchdog.backlog_decrease_permille").value_or(500);
+
backlog_decrease_permille.store(static_cast<int32_t>(std::clamp<int64_t>(dec_permille,
1, 999)), std::memory_order_relaxed);
+
RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.backlog_decrease_permille",
update_backlog_decrease_permille,
+ nullptr);
+
+ RecInt inc_permille =
RecGetRecordInt("proxy.config.exec_thread.watchdog.backlog_increase_permille").value_or(100);
+
backlog_increase_permille.store(static_cast<int32_t>(std::clamp<int64_t>(inc_permille,
1, 1000)), std::memory_order_relaxed);
+
RecRegisterConfigUpdateCb("proxy.config.exec_thread.watchdog.backlog_increase_permille",
update_backlog_increase_permille,
+ nullptr);
+}
+
+void
+record_awake_span(Heartbeat &hb, std::chrono::steady_clock::duration span)
+{
+ auto ns = static_cast<uint64_t>(std::max<int64_t>(0,
std::chrono::duration_cast<std::chrono::nanoseconds>(span).count()));
+ // Only the owning EThread raises this; the monitor's exchange to zero is
the only other writer, so a
+ // plain load/store pair could lose a reset. Use CAS to keep the max
semantics under that race.
+ uint64_t prev = hb.peak_awake.load(std::memory_order_relaxed);
+ while (prev < ns && !hb.peak_awake.compare_exchange_weak(prev, ns,
std::memory_order_relaxed, std::memory_order_relaxed)) {
+ ;
+ }
+}
+
+bool
+accept_refuse()
+{
+ // Gated on the live configuration so a scan that raced a config callback
and published a stale
+ // result cannot re-enable an action that has since been turned off.
+ return accept_refuse_state.load(std::memory_order_relaxed) &&
+ current_mode.load(std::memory_order_relaxed) == Mode::BACKPRESSURE &&
+ refuse_timeout_ms.load(std::memory_order_relaxed) > 0;
+}
+
+int
+backlog_scale_permille()
+{
+ if (current_mode.load(std::memory_order_relaxed) != Mode::BACKPRESSURE) {
+ return 1000;
+ }
+ return backlog_scale.load(std::memory_order_relaxed);
+}
+
+SleepScope::SleepScope(Heartbeat &hb) : _hb(hb)
+{
+ auto now = std::chrono::steady_clock::now();
+
+ // Publish the span that just ended, so a burst finishing between two
monitor scans is not lost.
+ auto last_wake = _hb.last_wake.load(std::memory_order_relaxed);
+ if (last_wake != std::chrono::steady_clock::time_point::min()) {
+ record_awake_span(_hb, now - last_wake);
+ }
+
+ // Enter the odd (sleeping) phase.
+ _hb.seq.fetch_add(1, std::memory_order_relaxed);
+}
+
+SleepScope::~SleepScope()
+{
+ _hb.last_wake.store(std::chrono::steady_clock::now(),
std::memory_order_relaxed);
+ // Back to even (awake). Release so the monitor cannot observe the even
sequence before the
+ // timestamp store above.
+ _hb.seq.fetch_add(1, std::memory_order_release);
+}
+
+Monitor::Monitor(EThread *threads[], size_t n_threads,
std::chrono::milliseconds shrink_timeout_ms, bool legacy)
+ : _threads(threads, threads + n_threads),
_shrink_timeout{shrink_timeout_ms}, _legacy{legacy}
{
- // Precondition: timeout_ms must be > 0. A timeout of 0 indicates the
watchdog is disabled
+ // Precondition: shrink_timeout_ms must be > 0. A timeout of 0 indicates the
watchdog is disabled
// and the caller should not instantiate the Monitor (see traffic_server.cc).
- ink_release_assert(timeout_ms.count() > 0);
+ ink_release_assert(shrink_timeout_ms.count() > 0);
+ if (!_legacy) {
+ create_metrics();
+ }
_watchdog_thread = std::thread(std::bind_front(&Monitor::monitor_loop,
this));
}
+void
+Monitor::create_metrics()
+{
+ _max_awake_ms =
Metrics::Gauge::createPtr("proxy.process.watchdog.max_awake_ms");
+ _max_awake_ms_hwm =
Metrics::Gauge::createPtr("proxy.process.watchdog.max_awake_ms_hwm");
+ _shrink_exceeded_count =
Metrics::Counter::createPtr("proxy.process.watchdog.timeout_shrink_exceeded_count");
+ _refuse_exceeded_count =
Metrics::Counter::createPtr("proxy.process.watchdog.timeout_refuse_exceeded_count");
+ _shrink_gauge =
Metrics::Gauge::createPtr("proxy.process.watchdog.accept_shrink");
+ _shrink_count =
Metrics::Counter::createPtr("proxy.process.watchdog.accept_shrink_engaged_count");
+ _refuse_gauge =
Metrics::Gauge::createPtr("proxy.process.watchdog.accept_refuse");
+ _refuse_count =
Metrics::Counter::createPtr("proxy.process.watchdog.accept_refuse_engaged_count");
+ _backlog_permille =
Metrics::Gauge::createPtr("proxy.process.watchdog.listen_backlog_permille");
+ Metrics::Gauge::store(_backlog_permille, 1000);
+}
+
Monitor::~Monitor()
{
_shutdown.store(true, std::memory_order_release);
_watchdog_thread.join();
+ // Never leave the accept paths throttled behind a monitor that is no longer
scanning.
+ accept_refuse_state.store(false, std::memory_order_relaxed);
+ backlog_scale.store(1000, std::memory_order_relaxed);
+}
+
+std::chrono::steady_clock::duration
+Monitor::scan(std::chrono::steady_clock::time_point now)
+{
+ std::chrono::steady_clock::duration peak =
std::chrono::steady_clock::duration::zero();
+
+ for (size_t i = 0; i < _threads.size(); ++i) {
+ EThread *t = _threads[i];
+
+ // Completed spans the thread finished since the previous scan. Draining
to zero here is what makes
+ // this a per-scan peak rather than a lifetime high water mark.
+ auto recorded =
std::chrono::nanoseconds{t->heartbeat_state.peak_awake.exchange(0,
std::memory_order_relaxed)};
+ peak = std::max<std::chrono::steady_clock::duration>(peak,
recorded);
+
+ // Seqlock read of the sleep/wake pair. An odd sequence means the thread
is inside the blocking
+ // poll, and a changed sequence means it transitioned while we were
reading - either way the pair
+ // is not a coherent snapshot and the live reading is skipped. peak_awake
above already accounts
+ // for that thread's completed work, so skipping loses nothing.
+ uint64_t const seq_before =
t->heartbeat_state.seq.load(std::memory_order_acquire);
+ if (seq_before & 1) {
+ continue; // sleeping
+ }
+
+ auto last_wake =
t->heartbeat_state.last_wake.load(std::memory_order_relaxed);
+
+ std::atomic_thread_fence(std::memory_order_acquire);
+ if (t->heartbeat_state.seq.load(std::memory_order_relaxed) != seq_before) {
+ continue; // torn - the thread slept or woke mid-read
+ }
+
+ if (last_wake == std::chrono::steady_clock::time_point::min()) {
+ continue; // event loop hasn't completed its first poll
+ }
+
+ // Live snapshot of the span in progress. A thread stuck in a single long
span never reaches the
+ // top of its next loop to record itself in peak_awake, so it only shows
up here.
+ auto awake_duration = now - last_wake;
+ if (awake_duration < std::chrono::steady_clock::duration::zero()) {
+ // @a now is sampled once at the top of the scan, before this thread's
seqlock read, so a thread
+ // that woke in between can report a last_wake fractionally newer than
it. Harmless and always
+ // sub-millisecond, but it must not be fed to std::max as a duration.
+ continue;
+ }
+ peak = std::max(peak, awake_duration);
+
+ if (awake_duration > _shrink_timeout) {
+ // Monitor thread is the sole reader (and warned_seq writer), so relaxed
accesses are race-free.
+ uint64_t warned_seq =
t->heartbeat_state.warned_seq.load(std::memory_order_relaxed);
+ if (warned_seq < seq_before) {
+ if (_legacy) {
+ // The deprecated timeout_ms warned here, and a config that still
sets it gets what it asked for.
+ Warning("Watchdog: [ET_NET %zu] has been awake for %lld ms", i,
+
std::chrono::duration_cast<printable_milli>(awake_duration).count());
+ } else {
+ // Debug rather than Warning: on a busy proxy the timeout is crossed
routinely, and a log line
+ // per event loop that runs long is noise an operator cannot act on.
The condition is already
+ // reported continuously and cheaply as
proxy.process.watchdog.max_awake_ms, and counted per
+ // scan as proxy.process.watchdog.timeout_shrink_exceeded_count,
which is the right thing to
+ // alert on.
+ // This is here for the case where you need to know *which* thread.
+ //
+ // Reported once per loop iteration, not once per scan.
+ Dbg(dbg_ctl_watchdog, "[ET_NET %zu] has been awake for %lld ms
(shrink timeout %lld ms)", i,
+
std::chrono::duration_cast<printable_milli>(awake_duration).count(),
+
std::chrono::duration_cast<printable_milli>(_shrink_timeout).count());
+ }
+ t->heartbeat_state.warned_seq.store(seq_before,
std::memory_order_relaxed);
+ }
+ }
+ }
+
+ return peak;
}
void
-Monitor::monitor_loop() const
+Monitor::sync_state_gauges()
{
- // this ensures `long long` duration for older compilers to match format
specifiers
- using printable_micro = std::chrono::duration<long long, std::micro>;
- using printable_milli = std::chrono::duration<long long, std::milli>;
+ bool const refusing = accept_refuse_state.load(std::memory_order_relaxed);
+ if (refusing != _refuse_reported) {
+ _refuse_reported = refusing;
+ Metrics::Gauge::store(_refuse_gauge, refusing ? 1 : 0);
+ }
+}
+void
+Monitor::monitor_loop()
+{
// Divide by a floating point 2 to avoid truncation to zero.
- auto sleep_time = _timeout / 2.0;
+ auto sleep_time = _shrink_timeout / 2.0;
ink_release_assert(sleep_time.count() > 0);
- Dbg(dbg_ctl_watchdog, "Starting watchdog with timeout %lld ms on %zu
threads. sleep_time = %lld us",
- std::chrono::duration_cast<printable_milli>(_timeout).count(),
_threads.size(),
+ Dbg(dbg_ctl_watchdog, "Starting watchdog with shrink timeout %lld ms, mode
%d, on %zu threads. sleep_time = %lld us",
+ std::chrono::duration_cast<printable_milli>(_shrink_timeout).count(),
+ static_cast<int>(current_mode.load(std::memory_order_relaxed)),
_threads.size(),
std::chrono::duration_cast<printable_micro>(sleep_time).count());
ink_set_thread_name("[WATCHDOG]");
while (!_shutdown.load(std::memory_order_acquire)) {
- std::chrono::time_point<std::chrono::steady_clock> now =
std::chrono::steady_clock::now();
- for (size_t i = 0; i < _threads.size(); ++i) {
- EThread *t = _threads[i];
- // Relaxed load: each heartbeat field has a single writer (its EThread)
so per-object coherence suffices.
- std::chrono::time_point<std::chrono::steady_clock> last_sleep =
t->heartbeat_state.last_sleep.load(std::memory_order_relaxed);
- if (last_sleep == std::chrono::steady_clock::time_point::min()) {
- // initial value sentinel - event loop hasn't started
- continue;
- }
- // Same reasoning for relaxed load on wake timestamp.
- std::chrono::time_point<std::chrono::steady_clock> last_wake =
t->heartbeat_state.last_wake.load(std::memory_order_relaxed);
+ auto peak = scan(std::chrono::steady_clock::now());
+
+ if (_legacy) {
+ std::this_thread::sleep_for(sleep_time);
+ continue;
+ }
+
+ auto peak_ms = std::chrono::duration_cast<printable_milli>(peak).count();
+ Metrics::Gauge::store(_max_awake_ms, peak_ms);
+
+ // Monotonic high water mark alongside the per-scan gauge above.
max_awake_ms covers only the
+ // interval since the previous scan and is overwritten by the next one, so
a spike that happens to
+ // land between two scrapes of the metric is never seen at all - and the
scan period is half the
+ // shrink timeout, which is far shorter than any realistic scrape
interval. This one only ever
+ // rises, so the worst span the process has served is still readable after
the fact.
+ if (peak_ms > _hwm_ms) {
+ _hwm_ms = peak_ms;
+ Metrics::Gauge::store(_max_awake_ms_hwm, peak_ms);
+ }
Review Comment:
Legacy-mode monitors (`_legacy == true`) never call `create_metrics()`,
leaving metric pointers (e.g. `_shrink_gauge`, `_backlog_permille`,
`_refuse_gauge`) null. When the loop exits, the unconditional cleanup block and
`sync_state_gauges()` will dereference those null pointers and can crash on
shutdown. Fix by guarding all metric stores/`sync_state_gauges()` behind `if
(!_legacy)` (or early-return before the cleanup section when `_legacy` is
true), so legacy warn-only mode never touches metric pointers.
##########
doc/admin-guide/files/records.yaml.en.rst:
##########
@@ -434,18 +434,451 @@ Thread Variables
This option only has an affect when |TS| has been compiled with
``--enable-hwloc``.
-.. ts:cv:: CONFIG proxy.config.exec_thread.watchdog.timeout_ms INT 0
+.. ts:cv:: CONFIG proxy.config.exec_thread.watchdog.timeout_shrink_ms INT 0
:units: milliseconds
- Set the timeout for the exec thread watchdog in milliseconds. If an exec
thread
- does not heartbeat within this time period, the watchdog will log a warning
message.
- If this value is zero, the watchdog is disabled.
-
- The default of this watchdot timeout is set to 0 (disabled) for ATS 10.2 for
+ Peak awake span at which the exec thread watchdog considers the event
threads saturated,
+ in milliseconds. If this value is zero, the watchdog is disabled entirely
and
+ ``proxy.process.watchdog.max_awake_ms`` is not reported.
+
+ Under :ts:cv:`proxy.config.exec_thread.watchdog.mode` ``1`` this is also
the threshold at
+ which the accept queues start to shrink - hence the name. See that setting
for what the
+ shrinking does and
:ts:cv:`proxy.config.exec_thread.watchdog.timeout_refuse_ms` for the
+ other, independent action available.
+
+ An event thread awake longer than this is reported under the ``watchdog``
debug tag,
+ once per loop iteration. It is deliberately not a warning - on a busy proxy
the
+ threshold is crossed routinely, and a log line per long event loop is noise
that
+ cannot be acted on. Alert on
``proxy.process.watchdog.timeout_shrink_exceeded_count``
+ instead - a counter of the scans that crossed this threshold, maintained in
every
+ mode - and enable the debug tag when you need to know which thread is
responsible.
+ ``proxy.process.watchdog.max_awake_ms`` shows the current peak, but it is a
gauge
+ republished every half of this value, so a spike between two scrapes never
reaches your
+ metrics pipeline; ``proxy.process.watchdog.max_awake_ms_hwm`` retains the
worst one.
+
+ This value fixes the watchdog's scan period at half of it, so it is read
once at startup
+ and requires a restart to change. Every other watchdog setting, including
the other two
+ thresholds, is reloadable.
+
+ The default of this watchdog timeout is set to 0 (disabled) for ATS 10.2 for
compatibility. We recommend that administrators set a reasonable
value, such as 1000, for production configurations, in order to
catch hung plugins, or server overload scenarios.
+ See :ts:cv:`proxy.config.exec_thread.watchdog.mode` for making the watchdog
act on
+ this threshold rather than only reporting it.
+
+.. ts:cv:: CONFIG proxy.config.exec_thread.watchdog.timeout_ms INT 0
+ :units: milliseconds
+ :deprecated:
+
+ This setting is deprecated in favor of
:ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms`.
+ It keeps its previous behavior: when it is non-zero and
+ :ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms` is zero, the
watchdog runs and logs a
+ warning for each event loop iteration that stays awake longer than this
value, and a deprecation
+ warning naming the replacement is logged at startup. In that case the
watchdog does nothing else -
+ it publishes no ``proxy.process.watchdog`` metrics and ignores
+ :ts:cv:`proxy.config.exec_thread.watchdog.mode` and every other watchdog
setting. If
+ :ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms` is non-zero,
this setting is ignored
+ and a warning is logged.
+
+.. ts:cv:: CONFIG proxy.config.exec_thread.watchdog.mode INT 0
+ :reloadable:
+
+ Select whether the watchdog acts on the peak awake span reported by
+ ``proxy.process.watchdog.max_awake_ms`` or merely reports it. This setting
has no effect
+ when :ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms` is zero,
since the
+ watchdog is then not running at all.
+
+ =====
=======================================================================
+ Value Description
+ =====
=======================================================================
+ ``0`` Report only. Warnings and metrics, no change in behavior.
+ ``1`` Apply inbound backpressure while the event threads are saturated.
+ =====
=======================================================================
+
+ A change takes effect on the watchdog's next scan, at most half of
+ :ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms` later. Setting
the mode to ``0``
+ releases any backpressure immediately and restores the accept queues, so it
is always
+ safe to disable under load without waiting for a scan.
+
+ Mode ``1`` offers two *independent* actions, each with its own threshold on
the same peak
+ awake span. Neither escalates into the other and neither requires the
other, so they can be
+ used together or one at a time. Requests already in flight are never
touched by either.
+
+ **Shrink the accept queues** - at
+ :ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms`. |TS|
re-listens on each
+ non-exempt listen socket at a progressively smaller fraction of
+ :ts:cv:`proxy.config.net.listen_backlog` and lets the *kernel* shed the
excess, decreasing
+ multiplicatively for as long as the threads stay saturated and recovering
proportionally once
+ they do not, floored at
:ts:cv:`proxy.config.exec_thread.watchdog.backlog_min_permille`. The
+ fraction in force is published as
``proxy.process.watchdog.listen_backlog_permille`` and the
+ absolute depth as ``proxy.process.net.watchdog_listen_backlog``.
+
+ Accepting continues throughout. A bounded accept queue only drains as fast
as the acceptors
+ that read it call ``accept()``, so the kernel ends up admitting new
connections at approximately the
+ rate |TS| can actually serve - a closed loop that needs no per-connection
decision and costs
+ nothing per connection. With :ts:cv:`proxy.config.accept_threads` ``0``
every event thread accepts, so
+ the queue drains at the combined rate of the threads that are keeping up
and a single stalled
+ thread does not hold it back. It also keeps working when no event thread is
scheduled at all, which
+ a userspace refusal cannot. The connections shed this way are not refused,
though - see
+ *What shed clients see* below.
+
+ Scaling the queue dynamically also generalizes a statically tuned backlog.
A fixed low
+ :ts:cv:`proxy.config.net.listen_backlog` protects against overload but
cannot absorb a
+ legitimate burst such as a restart thundering-herd; scaling keeps a
generous queue when
+ healthy and tightens it only while saturated.
+
+ **Stop reading accepts** - at
+ :ts:cv:`proxy.config.exec_thread.watchdog.timeout_refuse_ms`, ``0`` and
therefore off by
+ default. |TS| stops taking connections off its listen sockets altogether.
Pending
+ connections stay in the kernel accept queue, and once that queue fills the
kernel drops new
+ connection attempts on its own, exactly as it does for a shrunk queue. This
costs |TS| nothing at all - no ``accept()``, no
+ descriptor, no work charged to the event threads - and a client whose
connection is picked up
+ once the threads recover sees no error whatsoever. That is the advantage
over
+ :ts:cv:`proxy.config.net.connections_throttle`, where a shed connection is
still accepted
+ and closed.
+
+ The decision to stop reading accepts is **global**, not per thread. It is
made from the peak awake
+ span across all event threads, and every acceptor consults that one value.
With
+ :ts:cv:`proxy.config.accept_threads` ``0``, where each event thread accepts
for itself, one stalled
+ thread therefore makes *every* thread stop accepting, including those that
are idle and could have
+ served the connections. Only the loopback companion listener, which is
exempt, keeps accepting.
+
+ Choose the action to match the accept configuration:
+
+ - With dedicated accept threads (:ts:cv:`proxy.config.accept_threads`
greater than ``0``), either
+ action is suitable. The accept threads are not the ones being saturated,
so pausing them keeps new
+ work off the event threads, and shrinking the queue lets them keep
draining at a bounded rate.
+ - With :ts:cv:`proxy.config.accept_threads` ``0``, prefer shrinking the
accept queues. Threads that are
+ keeping up continue to accept, so a stalled thread does not stall
admission. Use stopping accepts
+ only if halting every thread on one thread's stall is what you want.
+
+ Set the refuse threshold *above* the shrink threshold if you use both: the
cheap,
+ self-limiting control then acts first, and the blunt one only at a depth of
saturation where
+ handing the queue to the kernel has not been enough. They compose - with
both engaged the
+ queue is bounded and nothing is draining it.
+
+ **Protecting local connections.** Backpressure applies to a whole listen
socket, so a
+ local connection arriving on the same port as other traffic would be
affected along with
+ it. To avoid that, whenever the watchdog is enabled
+ |TS| additionally binds a **loopback companion listener** for each
wildcard-bound TCP
+ proxy port, and exempts only that listener from backpressure:
+
+ .. code-block:: yaml
+
+ # configured:
+ proxy.config.http.server_ports: >-
+ 80
+ 443:ssl
+
+ # additionally bound when watchdog.timeout_shrink_ms is non-zero:
+ # 127.0.0.1:80 (exempt from backpressure)
+ # 127.0.0.1:443 (exempt from backpressure)
+
+ The kernel delivers a connection to the most specific matching bind, so a
connection to
+ ``localhost`` lands on the exempt listener while everything arriving from
off box lands
+ on the original one and is subject to backpressure as normal. No change to
local
+ clients is required, and because the exemption is a property of the socket
+ rather than of an individual connection it can be honored *before*
``accept()`` - which
+ is what makes not reading accepts free. Exempt listeners are never resized.
+
+ .. important::
+
+ Two listening sockets overlapping on one port requires ``SO_REUSEPORT``
on **both**,
+ so |TS| sets it on the port's wildcard socket as well as the loopback
one whenever a
+ companion listener is opened. (``SO_REUSEADDR`` is not sufficient on
Linux.) A side
+ effect is that any process running with the same effective uid can then
bind that port
+ and receive a share of its connections. If that is unacceptable in your
environment,
+ leave :ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms` at
``0`` to disable the
+ watchdog, or bind your proxy ports to explicit addresses with ``ip-in=``
so no
+ companion is opened.
+
+ Companion listeners are opened only for ports that are plain TCP and bound
to a wildcard
+ address. A port already pinned to a specific address with ``ip-in=`` was
deliberately
+ scoped, and adding a loopback bind would be exposure that was not asked
for. They are
+ opened when the watchdog is *enabled*, not when mode ``1`` is selected,
since the mode is
+ reloadable and binding sockets in response to a config change would be risk
for no
+ benefit; an exempt listener behaves identically to any other when no
backpressure is
+ engaged. A companion that fails to bind logs a warning and is skipped - it
never prevents
+ |TS| from starting.
+
+ .. note::
+
+ The companion listener ignores :ts:cv:`proxy.config.exec_thread.listen`
and always uses a
+ single socket shared by every event thread, even when the proxy port it
shadows uses one
+ listen socket per thread.
+
+ Per-thread listeners exist to remove contention on a single accept
queue, which matters at
+ high connection rates but not for the low volume of local connections.
More importantly,
+ the ``SO_REUSEPORT`` group they form selects a socket by hashing the
connection's four-tuple,
+ which is blind to what the owning thread is doing - a connection could
be steered to a thread that
+ is mid-stall while every other thread sits idle. One shared socket
behaves the opposite way:
+ ``EPOLLEXCLUSIVE`` wakes a thread that is currently blocked polling, and
a thread that is
+ behind on work is not, so the connection lands on one that is free.
+
+ This also means the companion adds exactly one socket per port rather
than one per thread.
+
+ **What shed clients see.** On Linux a full accept queue does not refuse a
new connection - it
+ *silently drops its SYN*. No SYN-ACK and no RST is sent. The client's
``connect()`` does not
+ fail; it retransmits the SYN with exponential backoff (1s, 2s, 4s, 8s, ...
about 127 seconds in
+ total with the default ``net.ipv4.tcp_syn_retries``) and either fails when
its own connect
+ timeout fires, or gets in once the queue has room and sees the wait as a
latency step of a second
+ or more. Both actions shed this way. Whether that is acceptable depends on
the clients: anything
+ with a short connect timeout reports a connection error rather than failing
over quickly, and
+ nothing gets a fast, explicit refusal.
+
+ Shrinking the backlog does not reset connections already queued beyond the
new depth. They stay
+ queued and are accepted normally; only new SYNs are dropped, until the
queue falls below the new
+ depth.
+
+ .. important::
+
+ ``net.ipv4.tcp_abort_on_overflow`` does **not** turn this shedding into
resets. It applies only
+ to a narrower race: the queue had room when the SYN arrived but filled
before the client's
+ final ACK. With ``0`` the kernel drops that ACK and retransmits the
SYN-ACK, while the client
+ already believes it is connected and may have sent its request or TLS
ClientHello; with ``1``
+ it sends an RST instead. Under sustained overload the queue is already
full when the SYN
+ arrives, so almost every shed connection takes the silent-drop path
whatever this is set to.
+
+ .. important::
+
+ Kernel-side drops are invisible to |TS|. Watch the listen-queue overflow
counters
+ (``ListenOverflows`` and ``ListenDrops``, reported by ``netstat -s`` and
``nstat``) alongside
+ ``proxy.process.net.watchdog_listen_backlog``.
+
+ .. note::
+
+ On Linux the SYN (half-open) queue limit is derived from the same
backlog, so shrinking toward
+ :ts:cv:`proxy.config.exec_thread.watchdog.backlog_min_permille` can make
the kernel start
+ sending SYN cookies (with the default ``net.ipv4.tcp_syncookies=1``)
without any real SYN
+ flood. The kernel then logs ``Possible SYN flooding on port X. Sending
cookies.``, and a
+ connection established through a cookie can lose TCP options - window
scaling and SACK when
+ TCP timestamps are not in use - and has its MSS rounded down to one of a
few fixed values. The
+ details vary by kernel version, so check against yours before relying on
a small floor.
+
+ .. note::
+
+ Resizing a listening socket's backlog by calling ``listen()`` again is
not specified by
+ POSIX. It works on Linux. Verify on your platform before relying on mode
``1`` - if the
+ resize fails, |TS| warns and the listener keeps its previous depth.
+
+ .. note::
+
+ Mode ``1`` only affects *new connections*. Requests arriving on existing
keepalive
+ connections and new HTTP/2 streams on existing sessions pass through
untouched. On a
+ workload where most requests are served over reused connections it will
engage and have
+ almost no effect on load. It is the right choice for connection-churn
overload - TLS
+ handshake storms, thundering-herd reconnects after an upstream failover
- where the cost
+ is in accepting connections rather than in serving requests.
+
+ .. note::
+
+ Neither action helps when a thread is stalled rather than busy - a
plugin blocking on a
+ synchronous DNS lookup or disk read is not going to finish sooner
because |TS| stopped
+ taking work. Compare ``proxy.process.watchdog.max_awake_ms`` against
+ :ts:stat:`proxy.process.eventloop.events.10s` before enabling mode
``1``: a high awake
+ span with a *low* event count means a stall, not load.
+
+ Mode ``1`` does not replace :ts:cv:`proxy.config.net.connections_throttle`,
which remains
+ in effect underneath. That limit protects file descriptors, a hard
resource; this is a
+ latency heuristic layered above it.
+
+.. ts:cv:: CONFIG proxy.config.exec_thread.watchdog.timeout_refuse_ms INT 0
+ :units: milliseconds
+ :reloadable:
+
+ Peak awake span at which :ts:cv:`proxy.config.exec_thread.watchdog.mode`
``1`` stops reading
+ accepts altogether - new inbound connections are left in the kernel accept
queue instead of
+ being taken off it. ``0``, the default, disables this action; the accept
queue shrink at
+ :ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms` then runs on
its own. See
+ :ts:cv:`proxy.config.exec_thread.watchdog.mode` for what each action does.
+
+ This is a threshold on saturation *depth*, not a duration, and it is
independent of the shrink
+ threshold in both directions: it neither requires it to be crossed first
nor stops it acting.
+ The watchdog's scan period does not depend on this value, which is why it
is reloadable while
+ :ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms` is not.
Setting it back to ``0``
+ resumes accepting immediately rather than at the next scan.
+
+ Normally set this *above*
+ :ts:cv:`proxy.config.exec_thread.watchdog.timeout_shrink_ms`. Shrinking the
queue is
+ self-limiting - |TS| keeps accepting, so inflow settles near what it can
serve - whereas not
+ reading accepts is blunt, and is worth reaching for only at a depth of
saturation where the
+ first control has visibly not been enough. Setting it below the shrink
threshold means the
+ accepts stop before the queue is ever bounded.
+
+ .. important::
+
+ Not reading accepts is only free while the queue can hold what arrives.
|TS|'s drain rate on
+ a listener in this state is exactly zero, so the accept queue can only
fill - it absorbs
+ roughly the arrival rate multiplied by the time spent above this
threshold, and past that
+ the kernel drops new SYNs at the queue's own depth, which the shrink
threshold may already
+ have reduced - see :ts:cv:`proxy.config.exec_thread.watchdog.mode` for
what those clients
+ see. On a small :ts:cv:`proxy.config.net.listen_backlog` that point
arrives quickly, and
+ while it lasts |TS| is serving nothing at all off that socket - strictly
worse than the
+ shrink action, which keeps serving at the rate it can. Size this
threshold so that reaching
+ it really is worse than not accepting.
+
+ Between engaging and releasing there is a deadband, as for every watchdog
action: accepts
+ resume only once the peak has fallen to
+ :ts:cv:`proxy.config.exec_thread.watchdog.backlog_decrease_permille` of
this value, floored at
+ half. The state is published as ``proxy.process.watchdog.accept_refuse``,
each pass that declined to
+ drain a listen socket is counted in
``proxy.process.net.watchdog_accepts_refused``, and every scan
+ over the threshold - in any mode - is counted in
+ ``proxy.process.watchdog.timeout_refuse_exceeded_count``.
+
+.. ts:cv:: CONFIG proxy.config.exec_thread.watchdog.backlog_min_permille INT 50
+ :reloadable:
+
+ Floor for the dynamic accept queue depth, in permille of
+ :ts:cv:`proxy.config.net.listen_backlog`, under
+ :ts:cv:`proxy.config.exec_thread.watchdog.mode` ``1``. The default of
``50`` is 5%.
+
+ The floor exists because a backlog of zero does not stop new connections
outright - the
+ kernel still admits at least one - while an extremely small queue drops so
many SYNs that
+ recovery is slow and bursty: every dropped client backs off for a second or
more before it
+ retries, and a small backlog also makes SYN cookies more likely (see
+ :ts:cv:`proxy.config.exec_thread.watchdog.mode`). It also guarantees the
node never becomes
+ completely unreachable through this mechanism.
+
+ Permille rather than percent throughout, because the interesting range is
compressed near the
+ bottom: on a large backlog the step from 1% to 2% is thousands of
connections, and percent
+ cannot express anything between them.
+
+ The resulting depth depends on the baseline. With
:ts:cv:`proxy.config.net.listen_backlog` left
+ at ``-1`` the baseline is the resolved system ``somaxconn``, so the same
permille yields very
+ different depths on different hosts. Set
:ts:cv:`proxy.config.net.listen_backlog` explicitly if
+ you want a predictable floor.
+
+.. ts:cv:: CONFIG proxy.config.exec_thread.watchdog.backlog_decrease_permille
INT 500
+ :reloadable:
+
+ How much of the current accept queue depth is *retained* on each saturated
scan under
+ :ts:cv:`proxy.config.exec_thread.watchdog.mode` ``1``. Lower values shed
capacity faster.
+
+ Expressed as permille retained rather than as a divisor so the whole range
is reachable, not
+ just 1/2, 1/3, 1/4. Starting from ``1000`` with a floor of ``250``:
+
+ ========= ===============================================================
+ Value Trajectory (one step per scan)
+ ========= ===============================================================
+ ``500`` 1000 → 500 → 250
+ ``667`` 1000 → 667 → 444 → 296 → 250
+ ``800`` 1000 → 800 → 640 → 512 → 409 → 327 → 261 → 250
+ ``900`` 1000 → 900 → 810 → 729 → 656 → 590 → 531 → 477 → …
+ ``975`` 1000 → 975 → 950 → 926 → 902 → 879 → 857 → 835 → …
+ ========= ===============================================================
+
+ A scan always gives up at least one point while refusing, so integer
truncation cannot stall
+ the decrease at small scales.
Review Comment:
This section is documenting
`proxy.config.exec_thread.watchdog.backlog_decrease_permille` (the
shrink/backlog decrease). The sentence 'gives up at least one point while
refusing' appears to be a copy/paste mismatch and should refer to
shrinking/decreasing the backlog scale (not refusing accepts). Updating wording
here avoids confusing shrink vs refuse behaviors.
##########
src/iocore/eventsystem/Watchdog.cc:
##########
@@ -37,73 +42,448 @@
namespace Watchdog
{
+using ts::Metrics;
+
DbgCtl dbg_ctl_watchdog("watchdog");
-Monitor::Monitor(EThread *threads[], size_t n_threads,
std::chrono::milliseconds timeout_ms)
- : _threads(threads, threads + n_threads), _timeout{timeout_ms}
+namespace
+{
+ // this ensures `long long` duration for older compilers to match format
specifiers
+ using printable_micro = std::chrono::duration<long long, std::micro>;
+ using printable_milli = std::chrono::duration<long long, std::milli>;
+
+ /** Current value of accept_refuse(). Written only by the monitor thread and
the config callback.
+
+ Read from the accept paths on every accept loop.
+ */
+ std::atomic<bool> accept_refuse_state{false};
+
+ /** Current value of proxy.config.exec_thread.watchdog.mode.
+
+ File scope rather than a Monitor member on purpose:
RecRegisterConfigUpdateCb has no
+ unregister counterpart, so a callback that captured the Monitor would
outlive it and fire
+ against freed memory at shutdown. Keeping the mode here means the
callback touches only static
+ storage and remains valid for the life of the process.
+ */
+ std::atomic<Mode> current_mode{Mode::DISABLED};
+
+ /** proxy.config.exec_thread.watchdog.timeout_refuse_ms - threshold for
accept_refuse().
+
+ Zero disables it entirely. Independent of the shrink timeout that drives
the scan period: the
+ accept paths stopping and the accept queues shrinking are separate
decisions on the same peak,
+ so this can be reloaded freely without disturbing the scan rate.
+ */
+ std::atomic<int64_t> refuse_timeout_ms{0};
+
+ /// proxy.config.exec_thread.watchdog.backlog_min_permille - floor for
backlog_scale_permille().
+ std::atomic<int32_t> backlog_min_permille{50};
+
+ /// Published listen backlog scale, in permille of the configured value.
1000 == unthrottled.
+ std::atomic<int32_t> backlog_scale{1000};
+
+ /** proxy.config.exec_thread.watchdog.backlog_decrease_permille -
multiplicative decrease.
+
+ Expressed as the permille of the *current* scale retained on each
saturated scan, rather than
+ as a divisor, so the whole range is reachable rather than just 1/2, 1/3,
1/4: 500 is the
+ classic halving, 667 is a divide by 1.5, 900 is a slow squeeze, 975 is a
crawl.
+
+ Doubles as the release deadband - see release_threshold(). One number
for how much margin is
+ wanted against the overload, applied to the queue depth and to the
signal alike.
+ */
+ std::atomic<int32_t> backlog_decrease_permille{500};
+
+ /** proxy.config.exec_thread.watchdog.backlog_increase_permille -
proportional increase.
+
+ Permille of the scale *currently in force* offered as the step on each
recovered scan, before
+ the headroom attenuation in recovery_step_permille(). Equivalently,
permille points of the
+ configured backlog at full depth, which is where the two readings
coincide and why one number
+ serves as both the rate and the ceiling on the step - the scale never
exceeds 1000, so the
+ proportional value can never exceed the configured points.
+
+ Proportional to the current depth rather than a flat number of points
because what re-saturates
+ the threads is the count of connections admitted, not the fraction of
the baseline they
+ represent, and a flat step is a fixed count whatever depth is in force.
At the bottom of a hard
+ shed that count is a multiple of the whole queue: at 100 points of a
65,535 baseline, the first
+ recovered scan after a shed to 20 permille would readmit 6,553
connections onto a queue holding
+ 1,310. The same 100 here readmits 131, and the step grows with the queue
as it climbs out.
+ */
+ std::atomic<int32_t> backlog_increase_permille{100};
+
+ int
+ update_watchdog_mode(const char * /* name */, RecDataT /* type */, RecData
data, void * /* cookie */)
+ {
+ auto mode = static_cast<Mode>(data.rec_int);
+ if (mode != Mode::DISABLED && mode != Mode::BACKPRESSURE) {
+ Warning("Watchdog: ignoring out of range
proxy.config.exec_thread.watchdog.mode %" PRId64, data.rec_int);
+ return REC_ERR_OKAY;
+ }
+
+ Mode const prev = current_mode.exchange(mode, std::memory_order_relaxed);
+ if (prev == mode) {
+ return REC_ERR_OKAY;
+ }
+
+ // Drop any backpressure the outgoing mode had engaged. The monitor
re-derives everything from
+ // the new mode on its next scan, so the worst case is up to half a
timeout of not shedding -
+ // strictly the safe direction. Leaving it set would be the unsafe one:
switching to mode 0
+ // would otherwise strand the accept queues shrunk forever.
+ //
+ // The reporting gauges are left to the monitor rather than cleared here.
It owns them, and
+ // sync_state_gauges() corrects them on its next scan; writing them from
this thread would race
+ // its stores.
+ if (mode == Mode::DISABLED) {
+ accept_refuse_state.store(false, std::memory_order_relaxed);
+ backlog_scale.store(1000, std::memory_order_relaxed);
+ }
+
+ Note("Watchdog: mode changed from %d to %d", static_cast<int>(prev),
static_cast<int>(mode));
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_refuse_timeout(const char * /* name */, RecDataT /* type */, RecData
data, void * /* cookie */)
+ {
+ int64_t const want = std::max<int64_t>(0, data.rec_int);
+ refuse_timeout_ms.store(want, std::memory_order_relaxed);
+
+ // Turning it off must resume accepting here rather than at the next scan
- the whole point of
+ // setting it to zero is to stop refusing immediately. Turning it on is
left to the monitor, which
+ // has an actual peak to compare against.
+ if (want == 0) {
+ accept_refuse_state.store(false, std::memory_order_relaxed);
+ }
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_backlog_min_permille(const char * /* name */, RecDataT /* type */,
RecData data, void * /* cookie */)
+ {
+
backlog_min_permille.store(static_cast<int32_t>(std::clamp<int64_t>(data.rec_int,
1, 1000)), std::memory_order_relaxed);
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_backlog_decrease_permille(const char * /* name */, RecDataT /* type
*/, RecData data, void * /* cookie */)
+ {
+
backlog_decrease_permille.store(static_cast<int32_t>(std::clamp<int64_t>(data.rec_int,
1, 999)), std::memory_order_relaxed);
+ return REC_ERR_OKAY;
+ }
+
+ int
+ update_backlog_increase_permille(const char * /* name */, RecDataT /* type
*/, RecData data, void * /* cookie */)
+ {
+
backlog_increase_permille.store(static_cast<int32_t>(std::clamp<int64_t>(data.rec_int,
1, 1000)), std::memory_order_relaxed);
+ return REC_ERR_OKAY;
+ }
+
+} // namespace
+
+void
+register_config_callbacks()
+{
+ RecInt mode =
RecGetRecordInt("proxy.config.exec_thread.watchdog.mode").value_or(0);
+ current_mode.store(static_cast<Mode>(mode), std::memory_order_relaxed);
Review Comment:
`register_config_callbacks()` stores the startup
`proxy.config.exec_thread.watchdog.mode` directly into `current_mode` without
validating it, while the dynamic callback validates and clamps to known values.
If an out-of-range value is present at startup, `current_mode` can be set to an
invalid enum and the watchdog gating logic can behave unpredictably until the
next config reload. Validate/clamp `mode` on startup the same way
`update_watchdog_mode()` does (and emit the same warning) before storing to
`current_mode`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]