/*
* This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or (at your
* option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see .
*/
// Lines logged while a database query runs, and whether they reach the Logs
// table. The insert into Logs is asynchronous (dbQueue) and has to be read back
// from the server, so this needs a live database.
//
// Tagged [notCI] because it needs a reachable database and credentials in
// /etc/zm/zm.conf. It only deletes the Logs rows it wrote itself.
#include "zm_catch2.h"
#include "zm_config.h"
#include "zm_db.h"
#include "zm_logger.h"
#include "zm_utils.h"
#include
#include
#include
#include
#include
#include
#include
#include
namespace {
constexpr const char *kComponent = "zm_test_dblog";
std::string Marker(const char *what) {
return stringtf("dblog-probe-%d-%s", getpid(), what);
}
int CountLogs(const std::string &like) {
zmDbRow row;
MYSQL_RES *res = row.fetch(stringtf(
"SELECT COUNT(*) FROM `Logs` WHERE `Component`='%s' AND `Message` LIKE '%s'",
kComponent, zmDbEscapeString(like).c_str()));
REQUIRE(res != nullptr);
return atoi(row[0]);
}
// The insert goes through dbQueue on its own thread, so poll for it.
bool WaitForLog(const std::string &like, std::chrono::milliseconds budget) {
auto deadline = std::chrono::steady_clock::now() + budget;
while (std::chrono::steady_clock::now() < deadline) {
if (CountLogs(like) > 0) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return false;
}
struct LiveLogger {
explicit LiveLogger(Logger::Level db_level) {
zmLoadStaticConfig();
REQUIRE(zmDbConnect());
// The database level only sticks while connected.
logInit(kComponent, Logger::Options(Logger::NOLOG, db_level, Logger::NOLOG, Logger::NOLOG));
// Without ZM_LOG_DEBUG the overall level is capped at Info.
Logger::fetch()->level(db_level);
REQUIRE(Logger::fetch()->databaseLevel() == db_level);
}
~LiveLogger() {
zmDbDo(stringtf("DELETE FROM `Logs` WHERE `Component`='%s'", kComponent));
logTerm();
}
};
} // namespace
TEST_CASE("a line logged while another thread runs a query reaches Logs", "[notCI][dblive]") {
LiveLogger live(Logger::INFO);
Info("%s", Marker("before").c_str());
REQUIRE(WaitForLog(Marker("before"), std::chrono::seconds(5)));
// Holds db_mutex and the connection for two seconds.
std::thread query([] { zmDbDo("DO SLEEP(2)"); });
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// Another thread, same process, nothing to do with the query.
INFO("database level seen by this thread: " << Logger::fetch()->databaseLevel());
CHECK(Logger::fetch()->databaseLevel() == Logger::INFO);
Info("%s", Marker("during").c_str());
query.join();
CHECK(WaitForLog(Marker("during"), std::chrono::seconds(5)));
}
TEST_CASE("the queue thread's inserts into Logs do not log themselves", "[notCI][dblive]") {
// With the database level at debug, every insert into Logs would log its own
// "Success running sql query INSERT INTO `Logs` ..." line, which would be
// inserted in turn, and so on without end. The queue thread keeps its lines
// out; a query on any other thread still logs to the database.
LiveLogger live(Logger::DEBUG9);
REQUIRE(zmDbDo("DO 1") == 1);
Debug(1, "%s", Marker("debug").c_str());
REQUIRE(WaitForLog(Marker("debug"), std::chrono::seconds(5)));
CHECK(WaitForLog("Success running sql query DO 1%", std::chrono::seconds(5)));
// Give a loop, if there were one, time to show.
std::this_thread::sleep_for(std::chrono::milliseconds(500));
CHECK(CountLogs("Success running sql query INSERT INTO `Logs`%") == 0);
const int rows = CountLogs("%");
std::this_thread::sleep_for(std::chrono::milliseconds(500));
CHECK(CountLogs("%") == rows);
}
TEST_CASE("an error from a query on another thread reaches Logs", "[notCI][dblive]") {
// zmDbDo used to keep the calling thread out of the database for the whole
// query, so "Can't run query" errors were only in the log files.
LiveLogger live(Logger::INFO);
const std::string table = stringtf("zm_test_dblog_missing_%d", getpid());
// zmDbDo returns mysql_query's error code on failure, which is also 1, so
// the log line is the only sign of the failure.
zmDbDo(stringtf("SELECT 1 FROM `%s`", table.c_str()));
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
// logger's mutex and then the queue's mutex to push a row: two threads taking
// the same two locks in opposite order.
LiveLogger live(Logger::INFO);
// Keep the queue thread off the connection for a second so rows pile up.
std::thread holder([] {
std::lock_guard lck(db_mutex);
std::this_thread::sleep_for(std::chrono::seconds(1));
});
constexpr int kThreads = 4;
std::atomic done(0);
std::atomic logged(0);
auto stop_at = std::chrono::steady_clock::now() + std::chrono::seconds(2);
std::vector loggers;
for (int t = 0; t < kThreads; t++) {
loggers.emplace_back([t, stop_at, &done, &logged] {
for (int i = 0; std::chrono::steady_clock::now() < stop_at; i++) {
Info("%s %d %d", Marker("flood").c_str(), t, i);
logged++;
std::this_thread::sleep_for(std::chrono::microseconds(200));
}
done++;
});
}
// A deadlock leaves these threads blocked for good; nothing can unblock them
// to let the test fail cleanly, so give up on the process.
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
while (done < kThreads and std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (done < kThreads) {
fprintf(stderr, "dbQueue deadlock: %d of %d logging threads finished\n", done.load(), kThreads);
std::_Exit(2);
}
for (std::thread &t : loggers) t.join();
holder.join();
// Every line reaches the table once the queue drains. That can take a while
// on a busy host: one insert at a time.
int rows = 0;
auto drain = std::chrono::steady_clock::now() + std::chrono::seconds(120);
while (std::chrono::steady_clock::now() < drain) {
rows = CountLogs(Marker("flood") + "%");
if (rows >= logged) break;
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
INFO("lines logged: " << logged.load());
CHECK(rows == logged);
}