Copilot commented on code in PR #13898:
URL: https://github.com/apache/trafficserver/pull/13898#discussion_r4365481600
##########
src/iocore/net/UnixNetAccept.cc:
##########
@@ -375,6 +493,16 @@ NetAccept::do_blocking_accept(EThread *t)
int count = 0;
int additional_accepts = NetHandler::get_additional_accepts();
+ watchdog_apply_backlog(this);
+
+ if (watchdog_should_refuse(this)) {
Review Comment:
With the defaults (`accept_threads=1`, `additional_accepts=-1`), the
blocking accept loop can serve nearly INT32_MAX connections without rechecking
the watchdog, so later transitions neither resize its backlog nor stop accepts.
Move the checks inside the drain loop; the same issue affects `net_accept()` at
line 187 and `acceptFastEvent()` at line 664 under sustained arrivals. Preserve
mutex cleanup in the periodic path and the coalesced retry timer in the fast
path when refusal interrupts a drain.
##########
src/iocore/net/UnixNetAccept.cc:
##########
@@ -529,6 +657,30 @@ NetAccept::acceptFastEvent(int event, void *ep)
NetHandler *h = get_NetHandler(t);
int additional_accepts =
NetHandler::get_additional_accepts();
+ if (e == refuse_retry) {
+ refuse_retry = nullptr;
+ }
+
+ watchdog_apply_backlog(this);
+
+ // See the equivalent comment in net_accept(). The listen fd here is
registered edge triggered, so
+ // refusing must come back on a timer: returning without draining would not
produce another
+ // readiness event for the connections already queued. New arrivals still
wake us while refusing,
+ // so only arm the timer if one is not already pending.
+ if (watchdog_should_refuse(this)) {
+ Metrics::Counter::increment(net_rsb.watchdog_accepts_refused);
+ if (refuse_retry == nullptr) {
+ refuse_retry = t->schedule_in(this, HRTIME_MSECONDS(net_throttle_delay));
Review Comment:
During graceful shutdown with `proxy.config.restart.stop_listening` enabled,
`UnixNetProcessor::stop_accept()` deletes the acceptor without cancelling
`refuse_retry`. If refusal is active, that timer can later dispatch through a
freed continuation during the drain interval. Cancel the retry under the
acceptor's owning mutex and prevent further retries before deleting the
acceptor.
##########
src/iocore/eventsystem/Watchdog.cc:
##########
@@ -37,73 +41,576 @@
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};
+
+ /// Most aggressive release deadband the retain knob will produce. See
release_threshold().
+ constexpr int32_t min_release_permille = 500;
+
+ /** Peak awake span at or below which a scan counts as recovered, given an
action @a threshold.
+
+ Every action engages at its own threshold but releases only here, so the
two differ and the
+ band between them holds the current state - a Schmitt trigger rather
than a bare comparison.
+ Without it a peak one millisecond under the threshold reads as full
recovery when it is really
+ the boundary, which is exactly where the state chatters and where
releasing lets the queue climb
+ back into the next step of load.
+
+ Derived from backlog_decrease_permille rather than from a knob of its
own because it is the same
+ statement: retain 500 means halve the queue when saturated *and* wait
for the peak to halve
+ before believing the recovery. Aggressive shedding and late release
belong together.
+
+ Floored at @c min_release_permille so the coupling cannot make
backpressure effectively
+ unreleasable - at retain 1 the threshold would be two milliseconds on a
two second timeout,
+ which a merely busy server may never reach. Half the threshold is
already far outside
+ measurement noise, so nothing is lost by refusing to go deeper; below
the floor the knob keeps
+ sharpening the shed and stops affecting the release.
+ */
+ std::chrono::milliseconds
+ release_threshold(std::chrono::milliseconds threshold)
+ {
+ int32_t const retain = std::max(min_release_permille,
backlog_decrease_permille.load(std::memory_order_relaxed));
+ return threshold * retain / 1000;
+ }
Review Comment:
For a valid 1 ms timeout, integral-millisecond division makes the default
release threshold zero instead of 0.5 ms. Once backpressure engages, even
healthy 100 µs awake spans cannot release it; recovery requires a scan with no
recorded work. Calculate the threshold in a finer duration to preserve the
fractional-millisecond boundary.
##########
src/iocore/eventsystem/Watchdog.cc:
##########
@@ -37,73 +41,576 @@
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};
+
+ /// Most aggressive release deadband the retain knob will produce. See
release_threshold().
+ constexpr int32_t min_release_permille = 500;
+
+ /** Peak awake span at or below which a scan counts as recovered, given an
action @a threshold.
+
+ Every action engages at its own threshold but releases only here, so the
two differ and the
+ band between them holds the current state - a Schmitt trigger rather
than a bare comparison.
+ Without it a peak one millisecond under the threshold reads as full
recovery when it is really
+ the boundary, which is exactly where the state chatters and where
releasing lets the queue climb
+ back into the next step of load.
+
+ Derived from backlog_decrease_permille rather than from a knob of its
own because it is the same
+ statement: retain 500 means halve the queue when saturated *and* wait
for the peak to halve
+ before believing the recovery. Aggressive shedding and late release
belong together.
+
+ Floored at @c min_release_permille so the coupling cannot make
backpressure effectively
+ unreleasable - at retain 1 the threshold would be two milliseconds on a
two second timeout,
+ which a merely busy server may never reach. Half the threshold is
already far outside
+ measurement noise, so nothing is lost by refusing to go deeper; below
the floor the knob keeps
+ sharpening the shed and stops affecting the release.
+ */
+ std::chrono::milliseconds
+ release_threshold(std::chrono::milliseconds threshold)
+ {
+ int32_t const retain = std::max(min_release_permille,
backlog_decrease_permille.load(std::memory_order_relaxed));
+ return threshold * retain / 1000;
+ }
+
+ 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()
+{
+ return accept_refuse_state.load(std::memory_order_relaxed);
+}
+
+int
+backlog_scale_permille()
+{
+ return backlog_scale.load(std::memory_order_relaxed);
+}
Review Comment:
An in-progress scan can overwrite the configuration callbacks' resets using
its old mode or refuse-timeout snapshot. Disabling backpressure or setting the
refuse timeout to zero can therefore leave accepts throttled until the next
scan, up to five seconds later. Gate both public accessors on the current
configuration so stale scan results cannot re-enable a disabled action.
##########
src/iocore/net/Server.cc:
##########
@@ -158,6 +157,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:
If creating the supplementary socket fails, such as with EMFILE,
`Server::listen()` reaches `Lerror` with `res` still zero and this branch
reports success. The caller then keeps an invalid acceptor, which can cause a
fatal poll-registration or accept error instead of skipping the companion.
Capture a negative failure status before cleanup when no earlier operation
assigned `res`.
##########
src/iocore/net/UnixNetAccept.cc:
##########
@@ -85,6 +87,81 @@ count_client_connection(UnixNetVConnection *vc, bool exempt)
}
}
+/** Whether this acceptor should leave the connections queued on its listen
socket for now.
+ *
+ * True while the peak awake span has reached
+ * @c proxy.config.exec_thread.watchdog.timeout_refuse_ms, where the queued
connections cost nothing at
+ * all and the threads may yet catch up and serve them. Independent of the
accept queue shrink, which
+ * has its own threshold (see @c watchdog_apply_backlog) and lets the *kernel*
shed while ATS keeps
+ * accepting - a bounded queue only drains as fast as we call @c accept4,
which is what keeps inflow
+ * near the rate we can actually serve. Both can be in force at once, in which
case the queue is
+ * bounded and nothing is draining it.
+ *
+ * A listener marked @c f_watchdog_exempt is never subject to backpressure.
Those are the loopback
+ * companion listeners opened alongside each proxy port, so local connections
keep getting a
+ * truthful answer about the proxy however far behind the event threads are.
The volume of local
+ * connections is small against the inbound load that caused the saturation.
+ *
+ * @param[in] na The acceptor.
+ *
+ * @return true if the caller should not drain its listen socket on this pass.
+ */
+bool
+watchdog_should_refuse(NetAccept *na)
+{
+ return !na->opt.f_watchdog_exempt && Watchdog::accept_refuse();
+}
+
+/** Resize @a na's kernel accept queue to the depth the watchdog is currently
asking for.
+ *
+ * Calling @c listen again on a listening socket updates its backlog.
Shrinking it does not evict
+ * connections already queued - they drain normally - but it does stop the
kernel admitting more until
+ * the depth falls back under the new limit, which is the backpressure we
want. The kernel then sheds on
+ * our behalf at no per-connection cost to ATS, and keeps doing so even when
no ATS thread is scheduled
+ * to run an accept loop. On Linux that means silently dropping new SYNs
rather than refusing them, so
+ * shed clients retransmit with backoff instead of seeing an error.
+ *
+ * Called at the top of each accept pass rather than from the monitor thread:
the monitor has no access
+ * to the listen descriptors without taking @c naVecMutex, and it is
deliberately kept clear of every
+ * event system lock. One syscall per acceptor per transition, none in the
steady state.
+ *
+ * @param[in,out] na The acceptor whose listen socket should be resized.
+ */
+void
+watchdog_apply_backlog(NetAccept *na)
+{
+ int want_permille = 1000;
+ if (!na->opt.f_watchdog_exempt) {
+ want_permille = Watchdog::backlog_scale_permille();
+ }
+
+ if (want_permille == na->applied_backlog_permille ||
!na->server.sock.is_ok()) {
Review Comment:
Cloned acceptors can share one listening socket but have separate
`applied_backlog_permille` caches. If clone A shrinks the socket while B still
caches 1000, B skips restoring it after recovery or disabling backpressure,
leaving the queue reduced until A runs again. Share and synchronize the applied
scale per listening socket, or designate a single resize owner.
##########
src/iocore/eventsystem/Watchdog.cc:
##########
@@ -37,73 +41,576 @@
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};
+
+ /// Most aggressive release deadband the retain knob will produce. See
release_threshold().
+ constexpr int32_t min_release_permille = 500;
+
+ /** Peak awake span at or below which a scan counts as recovered, given an
action @a threshold.
+
+ Every action engages at its own threshold but releases only here, so the
two differ and the
+ band between them holds the current state - a Schmitt trigger rather
than a bare comparison.
+ Without it a peak one millisecond under the threshold reads as full
recovery when it is really
+ the boundary, which is exactly where the state chatters and where
releasing lets the queue climb
+ back into the next step of load.
+
+ Derived from backlog_decrease_permille rather than from a knob of its
own because it is the same
+ statement: retain 500 means halve the queue when saturated *and* wait
for the peak to halve
+ before believing the recovery. Aggressive shedding and late release
belong together.
+
+ Floored at @c min_release_permille so the coupling cannot make
backpressure effectively
+ unreleasable - at retain 1 the threshold would be two milliseconds on a
two second timeout,
+ which a merely busy server may never reach. Half the threshold is
already far outside
+ measurement noise, so nothing is lost by refusing to go deeper; below
the floor the knob keeps
+ sharpening the shed and stops affecting the release.
+ */
+ std::chrono::milliseconds
+ release_threshold(std::chrono::milliseconds threshold)
+ {
+ int32_t const retain = std::max(min_release_permille,
backlog_decrease_permille.load(std::memory_order_relaxed));
+ return threshold * retain / 1000;
+ }
+
+ 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()
+{
+ return accept_refuse_state.load(std::memory_order_relaxed);
+}
+
+int
+backlog_scale_permille()
+{
+ 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);
+ }
+
+ _hb.last_sleep.store(now, std::memory_order_relaxed);
+ // Enter the odd (sleeping) phase. Release so the monitor cannot observe the
increment before the
+ // timestamp store above.
+ _hb.seq.fetch_add(1, std::memory_order_release);
+}
+
+SleepScope::~SleepScope()
+{
+ _hb.last_wake.store(std::chrono::steady_clock::now(),
std::memory_order_relaxed);
+ // Back to even (awake).
+ _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 && _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());
+ t->heartbeat_state.warned_seq.store(seq_before,
std::memory_order_relaxed);
+ } else if (warned_seq < seq_before) {
+ // 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;
+}
+
+int
+Monitor::recovery_step_permille() const
+{
+ int const rate = backlog_increase_permille.load(std::memory_order_relaxed);
+
+ // Proportional to the depth currently in force, so the step is a small
number of connections while
+ // the queue is deep in the hole and grows as it climbs out - the absolute
count admitted per scan
+ // is what re-saturates the threads, and a flat step in points of the
baseline is the same count at
+ // every depth. The cap restates the invariant rather than binding: the
scale is at most 1000, so
+ // the proportional value is at most the configured points, which it reaches
only at full depth.
+ // The floor keeps the step positive against integer truncation - below a
scale of 1000/rate the
+ // product truncates to zero, and a zero step would leave the queue stranded
at that depth, because
+ // the caller only republishes when the value actually changes.
+ int const step = std::max(1, std::min(_backlog_scale * rate / 1000, rate));
+
+ // No mark yet - the first recovery of the process, or one that already
climbed back to full depth.
+ // Nothing to attenuate against, so the proportional step stands.
+ if (_trouble_scale <= 0) {
+ return step;
+ }
+
+ // Past the mark already - it has been proven survivable since, so it
carries no information about
+ // the rest of the climb. Full rate the rest of the way to 1000.
+ if (_backlog_scale >= _trouble_scale) {
+ return step;
+ }
+
+ // Within one full step of the mark: land exactly on it rather than tapering
into it. The attenuated
+ // step goes to zero as the gap closes, and the one point floor above would
otherwise turn the last
+ // stretch into a crawl of single-point scans - dozens of them at a large
backlog - for no benefit,
+ // since the mark is a depth to sit at and reassess, not one to creep up on.
Never returns more than
+ // @a step, because the branch condition is exactly that the remaining gap
fits inside one, and @a
+ // step is itself bounded by the configured rate - so making the step
proportional cannot turn this
+ // into an unbounded leap onto the mark.
+ if (_backlog_scale + step >= _trouble_scale) {
+ return _trouble_scale - _backlog_scale;
+ }
+
+ // Attenuate by the fraction of the way still to go, so the climb
decelerates as it approaches the
+ // depth that last caused trouble instead of arriving there at full speed.
Far below the mark this is
+ // ~1 and costs nothing. The floor keeps the step positive against integer
truncation; the snap above
+ // is what stops it from being reached repeatedly.
+ int const headroom = _trouble_scale - _backlog_scale;
+ return std::max(1, step * headroom / _trouble_scale);
}
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;
+ 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);
+
+ // Each action engages on the first scan at or above its own threshold and
releases only once the
+ // peak has fallen to the deadband below it (release_threshold()). A bare
comparison against a
+ // single threshold would treat a peak one millisecond under it as full
recovery, when it is really
+ // the boundary - the state chatters there, and releasing at that point
lets the queue climb back
+ // just as the next step of load arrives. Between the two thresholds the
current state simply holds.
+ bool const backpressure_mode = mode == Mode::BACKPRESSURE;
+ bool const saturated = peak >= _shrink_timeout;
+ bool const recovered = peak <= release_threshold(_shrink_timeout);
+
+ // 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.
+ if (saturated) {
+ Metrics::Counter::increment(_shrink_exceeded_count);
+ }
+
+ // Shrink stage state. Mode off releases now rather than waiting out the
deadband - the point of
+ // turning it off is to stop throttling immediately.
+ bool const was_shrinking = _shrink_engaged;
+ if (!backpressure_mode) {
+ _shrink_engaged = false;
+ } else if (saturated) {
+ _shrink_engaged = true;
+ } else if (recovered) {
+ _shrink_engaged = false;
+ }
+ // Otherwise inside the deadband: hold, neither engaging nor releasing.
+
+ if (_shrink_engaged != was_shrinking) {
+ Metrics::Gauge::store(_shrink_gauge, _shrink_engaged ? 1 : 0);
+ if (_shrink_engaged) {
+ Metrics::Counter::increment(_shrink_count);
+ // The depth still in force as this episode begins is, by
construction, a depth that let in
+ // more than the threads could serve. Remember it as the mark that
headroom-attenuated
+ // recovery slows down against. Captured before the decrease below
touches _backlog_scale.
+ _trouble_scale = _backlog_scale;
}
- // 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);
+ Dbg(dbg_ctl_watchdog, "backlog shrink -> %s - peak awake %lld ms, shrink
timeout %lld ms", _shrink_engaged ? "on" : "off",
+ peak_ms,
std::chrono::duration_cast<printable_milli>(_shrink_timeout).count());
+ }
- if (last_wake == std::chrono::steady_clock::time_point::min() ||
last_wake < last_sleep) {
- // not yet woken from last sleep
- continue;
+ // Accept queue sizing. Multiplicative decrease on a saturated scan,
proportional increase once
+ // released, and held steady inside the release deadband - a peak that has
come down but not far
+ // enough is not evidence for shedding harder, and without the @a
saturated term a long stay in the
+ // deadband would squeeze the queue toward the floor while it was
recovering.
+ if (backpressure_mode) {
+ int const floor_permille =
backlog_min_permille.load(std::memory_order_relaxed);
+ int want_permille = _backlog_scale;
+
+ if (_shrink_engaged && saturated) {
+ int const retain =
backlog_decrease_permille.load(std::memory_order_relaxed);
+ // Integer truncation would stall the decrease entirely at small
scales - 3 permille retained
+ // at 900 rounds back to 3 - so always give up at least one point
while shrinking.
+ int const next = std::min(_backlog_scale * retain / 1000,
_backlog_scale - 1);
+ want_permille = std::max(floor_permille, next);
+ } else if (!_shrink_engaged) {
+ want_permille = std::min(1000, _backlog_scale +
recovery_step_permille());
}
- 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);
+ if (want_permille != _backlog_scale) {
Review Comment:
Disabling and re-enabling the mode between scans resets the published scale
to 1000 but leaves `_backlog_scale` unchanged. If the member is already at the
floor, subsequent saturated scans compute that same floor and skip publication,
leaving accepts unthrottled throughout the overload. Compare against the
published atomic as well so the next scan repairs the mismatch.
--
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]