Repository navigation
fix: keep only the database queue thread out of database logging - #5208
connortechnology wants to merge 2 commits into
Conversation
TL;DR: The direction looks right to us. With this PR, errors and retries from zmc's own synchronous queries (start-up, exit, reconnect) reach Suggested change and test evidenceThe change. Builds: master
Diff on top of this PR (`git diff pr-5208..alt/f-logrow -- src tests`)diff --git a/src/zm_db.cpp b/src/zm_db.cpp
index 1d0999726..de0ec9a27 100644
--- a/src/zm_db.cpp
+++ b/src/zm_db.cpp
@@ -379,12 +379,6 @@ void zmDbQueue::stop() {
}
void zmDbQueue::process() {
- // This thread runs the inserts into Logs. A line it logged would queue another
- // insert, and at a debug database level the "Success running sql query" line
- // of each insert would do so without end, so nothing it logs goes to the
- // database. Other threads log there as usual, including the errors and debug
- // lines of their own queries.
- Logger::DatabaseLogPause pause_db_logging;
std::unique_lock<std::mutex> lock(mMutex);
while (!mTerminate and !zm_terminate) {
@@ -393,7 +387,8 @@ void zmDbQueue::process() {
}
while (!mQueue.empty()) {
size_t backlog = mQueue.size();
- std::string sql = std::move(mQueue.front());
+ std::string sql = std::move(mQueue.front().first);
+ bool log_row = mQueue.front().second;
mQueue.pop();
// My idea for leaving the locking around each sql statement is to allow
// other db writers to get a chance
@@ -401,19 +396,30 @@ void zmDbQueue::process() {
if (backlog > 40) {
// Not while holding mMutex: logging takes the logger's mutex, and a
// thread already holding that one waits on mMutex to push its row.
+ // A row about the backlog would only add to it, so none is written.
+ Logger::DatabaseLogPause pause_db_logging;
Warning("db queue size has grown larger %zu than 40 entries", backlog);
}
- zmDbDo(sql);
+ if (log_row) {
+ // An insert into Logs. A line about it would queue another insert, and at
+ // a debug database level its "Success running sql query" line would do
+ // so without end, so lines about these inserts stay out of the database.
+ // Lines about the other queued writes (events, frames, stats) go there.
+ Logger::DatabaseLogPause pause_db_logging;
+ zmDbDo(sql);
+ } else {
+ zmDbDo(sql);
+ }
lock.lock();
}
}
} // end void zmDbQueue::process()
-void zmDbQueue::push(std::string &&sql) {
+void zmDbQueue::push(std::string &&sql, bool log_row) {
{
std::unique_lock<std::mutex> lock(mMutex);
if (mTerminate) return;
- mQueue.push(std::move(sql));
+ mQueue.emplace(std::move(sql), log_row);
}
mCondition.notify_all();
}
diff --git a/src/zm_db.h b/src/zm_db.h
index a9820f40b..b90178cf8 100644
--- a/src/zm_db.h
+++ b/src/zm_db.h
@@ -28,6 +28,7 @@
#include <queue>
#include <string>
#include <thread>
+#include <utility>
#include <unistd.h>
// How many times a query that lost a lock race is re-run before being
@@ -45,7 +46,8 @@ useconds_t zmDbContentionBackoff(int attempt);
class zmDbQueue {
private:
- std::queue<std::string> mQueue;
+ // The flag marks rows the logger queued (inserts into Logs).
+ std::queue<std::pair<std::string, bool>> mQueue;
std::thread mThread;
std::mutex mMutex;
std::condition_variable mCondition;
@@ -53,7 +55,7 @@ class zmDbQueue {
public:
zmDbQueue();
~zmDbQueue();
- void push(std::string &&sql);
+ void push(std::string &&sql, bool log_row = false);
void process();
void stop();
};
diff --git a/src/zm_logger.cpp b/src/zm_logger.cpp
index 0aa3ffaf4..ccdb16293 100644
--- a/src/zm_logger.cpp
+++ b/src/zm_logger.cpp
@@ -553,7 +553,7 @@ void Logger::logPrint(bool hex, const char *filepath, int line, int level, const
"( %jd.%06" PRIi64 ", '%s', %d, %d, %d, '%s', '%s', '%s', %d )",
static_cast<intmax_t>(now_sec), static_cast<int64>(now_frac.count()), mId.c_str(), staticConfig.SERVER_ID, tid, level, classString,
escapedString.c_str(), file, line);
- dbQueue.push(std::move(sql_string));
+ dbQueue.push(std::move(sql_string), true);
}
} // end if level <= mDatabaseLevel
diff --git a/src/zm_logger.h b/src/zm_logger.h
index f402e893b..f715844f0 100644
--- a/src/zm_logger.h
+++ b/src/zm_logger.h
@@ -153,7 +153,7 @@ class Logger {
// While one of these is alive, lines logged on the thread that created it
// are not written to the database. Every other thread keeps logging there.
//
- // The database queue's thread holds one for its whole life: at a debug
+ // The database queue holds one around each insert into Logs: at a debug
// database level, inserting a Logs row logs a line about that insert, which
// would queue another insert, and so on without end.
class DatabaseLogPause {
diff --git a/tests/zm_db_logging_live.cpp b/tests/zm_db_logging_live.cpp
index 57eb06f76..5a032ac20 100644
--- a/tests/zm_db_logging_live.cpp
+++ b/tests/zm_db_logging_live.cpp
@@ -133,6 +133,16 @@ TEST_CASE("an error from a query on another thread reaches Logs", "[notCI][dbliv
CHECK(WaitForLog("Can't run query%" + table + "%", std::chrono::seconds(5)));
}
+TEST_CASE("an error from a queued write reaches Logs", "[notCI][dblive]") {
+ // Events, frames and stats are written through dbQueue too. Only the queue's
+ // inserts into Logs are kept from logging; its other writes report errors.
+ LiveLogger live(Logger::INFO);
+
+ const std::string table = stringtf("zm_test_dblog_queued_missing_%d", getpid());
+ dbQueue.push(stringtf("UPDATE `%s` SET x=1", table.c_str()));
+ CHECK(WaitForLog("Can't run query%" + table + "%", std::chrono::seconds(5)));
+}
+
TEST_CASE("a backlog in dbQueue does not deadlock threads that log", "[notCI][dblive]") {
// With more than 40 rows waiting, the queue thread warns about the backlog.
// It used to do that while holding the queue's mutex, and logPrint takes the🤖 Generated with Claude Code |
zmDbDo() kept its calling thread out of the database for the whole query, which stopped its own "Success running sql query" line from queueing another Logs insert. But the loop only starts where the Logs rows are inserted, on the dbQueue thread, and pausing every zmDbDo() also kept the calling thread's errors about its query out of Logs: "Can't run query" and the lock contention retries were only in the log files. Hold one Logger::DatabaseLogPause on the dbQueue thread for its whole life, in zmDbQueue::process(), and none in zmDbDo(). Lines the queue thread logs, the backlog warning included, still stay out of the database; every other thread's query lines reach it, errors and, at a debug database level, the success lines too. Tests: the queue thread's inserts into Logs log nothing and the row count settles, a query on another thread logs its success line at debug, and an error from a failed query reaches Logs. The last two fail with the pause in zmDbDo(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Holding a DatabaseLogPause on the dbQueue thread for its whole life kept the errors of every queued write out of Logs too: Monitor_Status updates, Events, Frames and Stats rows. Under lock contention most "Giving up on query" errors were only in the log files. Mark each queued row with whether the logger queued it, and pause database logging only around those inserts into Logs and the backlog warning. The loop stays closed: a line about a queued write queues one Logs insert, which logs nothing to the database. Test: an error from a queued write reaches Logs. From review by @nabbi on #5208. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
53f2752 to
4bc4c6e
Compare
|
Thanks @nabbi, taken into this PR as 4bc4c6e: rows the logger queues are flagged, and only those inserts and the backlog warning run with the pause, so errors from the queued Events/Frames/Stats/Monitor_Status writes reach Logs. I also rebased onto current master. Here all 8 |
Follow-up to #5197, from its review.
#5197 replaced the process-wide NOLOG toggle with
Logger::DatabaseLogPause, held inside everyzmDbDo(). That keeps the calling thread out of the database for the whole query, so the thread's own errors about the query never reached Logs: "Can't run query ..." and the lock contention retries were only in the log files. It was the same before #5197, but now that the pause is per thread it doesn't have to be.The feedback loop the pause exists for only starts where Logs rows are inserted, on the dbQueue thread. So this holds one
DatabaseLogPauseon the dbQueue thread for its whole life, at the top ofzmDbQueue::process(), and none inzmDbDo().Logs" lines and the backlog warning, as before.Tradeoff to review: the queue also carries Event, Frame and Monitor_Status writes (
zm_event.cpp,zm_monitor.cpp), so an error in one of those async writes still stays out of Logs. Pausing only for the Logs inserts would get those too, but it would meanzmDbQueuerecognising the logger's SQL. I went with the whole thread.At a debug database level, ordinary queries' success lines now also land in Logs, one extra row per query. That only applies when someone has set the database level to debug.
Tests
tests/zm_db_logging_live.cpp([notCI][dblive]):Logs" rows and the row count must settle, whileDO 1run on the test's thread logs its success line.Both fail with #5197 as merged. All 7
[dblive]cases pass on master plus this change against MariaDB (run as www-data, zm.conf credentials), and so does the unit suite: 249 cases, 14718 assertions.🤖 Generated with Claude Code