This is an automated email from the ASF dual-hosted git repository.
mochen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 019790806b watchdog: alert on ET_NET thread stalls beyond threshold
(#12524)
019790806b is described below
commit 019790806b54d6bb0729616dc7e508fb56076765
Author: Mo Chen <[email protected]>
AuthorDate: Mon Nov 10 12:56:20 2025 -0600
watchdog: alert on ET_NET thread stalls beyond threshold (#12524)
Run a watchdog thread to find blocking events.
---
doc/admin-guide/files/records.yaml.en.rst | 12 ++++
include/iocore/eventsystem/EThread.h | 3 +
include/iocore/eventsystem/Watchdog.h | 70 ++++++++++++++++++++
src/iocore/eventsystem/CMakeLists.txt | 1 +
src/iocore/eventsystem/UnixEThread.cc | 9 +++
src/iocore/eventsystem/Watchdog.cc | 104 ++++++++++++++++++++++++++++++
src/records/RecordsConfig.cc | 7 +-
src/traffic_server/traffic_server.cc | 14 ++++
8 files changed, 219 insertions(+), 1 deletion(-)
diff --git a/doc/admin-guide/files/records.yaml.en.rst
b/doc/admin-guide/files/records.yaml.en.rst
index 920ec5d7d4..59bf7f6181 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -419,6 +419,18 @@ 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
+ :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
+ 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.
+
.. ts:cv:: CONFIG proxy.config.system.file_max_pct FLOAT 0.9
Set the maximum number of file handles for the traffic_server process as a
percentage of the fs.file-max proc value in Linux. The default is 90%.
diff --git a/include/iocore/eventsystem/EThread.h
b/include/iocore/eventsystem/EThread.h
index 9b6f64bfed..9400d6e892 100644
--- a/include/iocore/eventsystem/EThread.h
+++ b/include/iocore/eventsystem/EThread.h
@@ -33,6 +33,7 @@
#include "iocore/eventsystem/PriorityEventQueue.h"
#include "iocore/eventsystem/ProtectedQueue.h"
#include "tsutil/Histogram.h"
+#include "iocore/eventsystem/Watchdog.h"
#if TS_USE_HWLOC
struct hwloc_obj;
@@ -584,6 +585,8 @@ public:
Metrics metrics;
+ Watchdog::Heartbeat heartbeat_state;
+
private:
void cons_common();
};
diff --git a/include/iocore/eventsystem/Watchdog.h
b/include/iocore/eventsystem/Watchdog.h
new file mode 100644
index 0000000000..3f70667856
--- /dev/null
+++ b/include/iocore/eventsystem/Watchdog.h
@@ -0,0 +1,70 @@
+/** @file
+
+ A watchdog for event loops
+
+ Each event thread advertises its current state through a lightweight
+ "heartbeat" struct: the thread publishes the timestamps for the most recent
+ sleep/wake pair along with a monotonically increasing sequence number.
+ `Watchdog::Monitor`, started from `traffic_server.cc`, runs in its own
+ `std::thread` and periodically scans those heartbeats; if a thread has been
+ awake longer than the configured timeout it emits a warning (timeout values
+ come from `proxy.config.exec_thread.watchdog.timeout_ms`, where 0 disables
+ the monitor). The monitor never touches event-system locks, keeping the
+ runtime overhead in the hot loop confined to a handful of atomic updates.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ */
+
+#pragma once
+
+#include <atomic>
+#include <chrono>
+#include <vector>
+#include <thread>
+
+class EThread;
+
+namespace Watchdog
+{
+struct Heartbeat {
+ std::atomic<std::chrono::time_point<std::chrono::steady_clock>> last_sleep{
+ std::chrono::steady_clock::time_point::min()}; // set right before
sleeping (e.g. before epoll_wait)
+ std::atomic<std::chrono::time_point<std::chrono::steady_clock>> last_wake{
+ std::chrono::steady_clock::time_point::min()}; // set right after waking
from sleep (e.g. epoll_wait returns)
+ std::atomic<uint64_t> seq{0}; // increment on each loop -
used to deduplicate warnings
+ std::atomic<uint64_t> warned_seq{0}; // last seq we logged a
warning about
+};
+
+class Monitor
+{
+public:
+ explicit Monitor(EThread *threads[], size_t n_threads,
std::chrono::milliseconds timeout_ms);
+ ~Monitor();
+ Monitor() = delete;
+
+private:
+ const std::vector<EThread *> _threads;
+ std::thread _watchdog_thread;
+ const std::chrono::milliseconds _timeout;
+ std::atomic<bool> _shutdown = false;
+ void monitor_loop() const;
+};
+
+} // namespace Watchdog
diff --git a/src/iocore/eventsystem/CMakeLists.txt
b/src/iocore/eventsystem/CMakeLists.txt
index 9b014571a5..ab19fdcca6 100644
--- a/src/iocore/eventsystem/CMakeLists.txt
+++ b/src/iocore/eventsystem/CMakeLists.txt
@@ -35,6 +35,7 @@ add_library(
ConfigProcessor.cc
RecRawStatsImpl.cc
RecProcess.cc
+ Watchdog.cc
)
add_library(ts::inkevent ALIAS inkevent)
diff --git a/src/iocore/eventsystem/UnixEThread.cc
b/src/iocore/eventsystem/UnixEThread.cc
index cfcd889fbe..210ac51482 100644
--- a/src/iocore/eventsystem/UnixEThread.cc
+++ b/src/iocore/eventsystem/UnixEThread.cc
@@ -304,8 +304,17 @@ EThread::execute_regular()
ink_hrtime post_drain = ink_get_hrtime();
ink_hrtime drain_queue = post_drain - loop_start_time;
+ // watchdog kick - pre-sleep
+ // Relaxed store because this EThread is the only writer and the watchdog
only needs a coherent timestamp.
+ this->heartbeat_state.last_sleep.store(std::chrono::steady_clock::now(),
std::memory_order_relaxed);
+
tail_cb->waitForActivity(sleep_time);
+ // watchdog kick - post-wake
+ // Relaxed store/fetch because the monitor thread is the single reader and
per-field coherence is sufficient.
+ this->heartbeat_state.last_wake.store(std::chrono::steady_clock::now(),
std::memory_order_relaxed);
+ this->heartbeat_state.seq.fetch_add(1, std::memory_order_relaxed);
+
// loop cleanup
loop_finish_time = ink_get_hrtime();
// @a delta can be negative due to time of day adjustments (which
apparently happen quite frequently). I
diff --git a/src/iocore/eventsystem/Watchdog.cc
b/src/iocore/eventsystem/Watchdog.cc
new file mode 100644
index 0000000000..4c37e51bb3
--- /dev/null
+++ b/src/iocore/eventsystem/Watchdog.cc
@@ -0,0 +1,104 @@
+/** @file
+
+ A watchdog for event loops
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ */
+
+#include "iocore/eventsystem/Watchdog.h"
+#include "iocore/eventsystem/EThread.h"
+#include "tscore/Diags.h"
+#include "tscore/ink_assert.h"
+#include "tscore/ink_thread.h"
+#include "tsutil/DbgCtl.h"
+
+#include <atomic>
+#include <chrono>
+#include <thread>
+#include <functional>
+
+namespace Watchdog
+{
+
+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}
+{
+ // Precondition: 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);
+ _watchdog_thread = std::thread(std::bind_front(&Monitor::monitor_loop,
this));
+}
+
+Monitor::~Monitor()
+{
+ _shutdown.store(true, std::memory_order_release);
+ _watchdog_thread.join();
+}
+
+void
+Monitor::monitor_loop() const
+{
+ // Divide by a floating point 2 to avoid truncation to zero.
+ auto sleep_time = _timeout / 2.0;
+ ink_release_assert(sleep_time.count() > 0);
+ Dbg(dbg_ctl_watchdog, "Starting watchdog with timeout %" PRIu64 " ms on %zu
threads. sleep_time = %" PRIu64 " us",
+ _timeout.count(), _threads.size(),
std::chrono::duration_cast<std::chrono::microseconds>(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);
+
+ if (last_wake == std::chrono::steady_clock::time_point::min() ||
last_wake < last_sleep) {
+ // not yet woken from last sleep
+ continue;
+ }
+
+ 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 %" PRIu64 " ms",
i,
+
std::chrono::duration_cast<std::chrono::milliseconds>(awake_duration).count());
+ t->heartbeat_state.warned_seq.store(seq, std::memory_order_relaxed);
+ }
+ }
+ }
+
+ std::this_thread::sleep_for(sleep_time);
+ }
+ Dbg(dbg_ctl_watchdog, "Stopping watchdog");
+}
+} // namespace Watchdog
diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc
index fbc2d3eb65..318cbf8960 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -1529,7 +1529,12 @@ static constexpr RecordElement RecordsConfig[] =
{RECT_CONFIG, "proxy.config.io_uring.wq_workers_unbounded", RECD_INT, "0",
RECU_NULL, RR_NULL, RECC_NULL, nullptr, RECA_NULL},
{RECT_CONFIG, "proxy.config.aio.mode", RECD_STRING, "auto", RECU_DYNAMIC,
RR_NULL, RECC_STR, "(auto|io_uring|thread)", RECA_NULL},
#endif
-
+ //###########
+ //#
+ //# Thread watchdog
+ //#
+ //###########
+ {RECT_CONFIG, "proxy.config.exec_thread.watchdog.timeout_ms", RECD_INT, "0",
RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-10000]", RECA_NULL}
};
// clang-format on
diff --git a/src/traffic_server/traffic_server.cc
b/src/traffic_server/traffic_server.cc
index 9bf267fb8d..7ac906af10 100644
--- a/src/traffic_server/traffic_server.cc
+++ b/src/traffic_server/traffic_server.cc
@@ -32,6 +32,7 @@
#include "iocore/aio/AIO.h"
#include "iocore/cache/Store.h"
+#include "iocore/eventsystem/Watchdog.h"
#include "tscore/TSSystemState.h"
#include "tscore/Version.h"
#include "tscore/ink_platform.h"
@@ -214,6 +215,8 @@ int cmd_block = 0;
// -1: cache is already initialized, don't delay.
int delay_listen_for_cache = 0;
+std::unique_ptr<Watchdog::Monitor> watchdog = nullptr;
+
ArgumentDescription argument_descriptions[] = {
{"net_threads", 'n', "Number of Net Threads",
"I", &num_of_net_threads,
"PROXY_NET_THREADS", nullptr },
{"udp_threads", 'U', "Number of UDP Threads",
"I", &num_of_udp_threads,
"PROXY_UDP_THREADS", nullptr },
@@ -267,6 +270,9 @@ struct AutoStopCont : public Continuation {
int
mainEvent(int /* event */, Event * /* e */)
{
+ // Stop the watchdog before shutting threads down
+ watchdog.reset();
+
TSSystemState::stop_ssl_handshaking();
APIHook *hook = g_lifecycle_hooks->get(TS_LIFECYCLE_SHUTDOWN_HOOK);
@@ -2131,6 +2137,14 @@ main(int /* argc ATS_UNUSED */, const char **argv)
RecRegisterConfigUpdateCb("proxy.config.dump_mem_info_frequency",
init_memory_tracker, nullptr);
init_memory_tracker(nullptr, RECD_NULL, RecData(), nullptr);
+ // Start the watchdog
+ int watchdog_timeout_ms =
RecGetRecordInt("proxy.config.exec_thread.watchdog.timeout_ms").value_or(0);
+ if (watchdog_timeout_ms > 0) {
+ watchdog =
std::make_unique<Watchdog::Monitor>(eventProcessor.thread_group[ET_NET]._thread,
+
static_cast<size_t>(eventProcessor.thread_group[ET_NET]._count),
+
std::chrono::milliseconds{watchdog_timeout_ms});
+ }
+
{
auto s{RecGetRecordStringAlloc("proxy.config.diags.debug.client_ip")};
if (auto p{ats_as_c_str(s)}; p) {