Added a quick integrity check during startup, fixes #679

This commit is contained in:
emeric
2025-06-22 22:25:24 +02:00
parent b90c5aa35a
commit 6bfc769cab
3 changed files with 138 additions and 0 deletions
+4
View File
@@ -11,7 +11,11 @@ ffmpeg-file = "/usr/bin/ffmpeg";
log-file = ""; log-file = "";
access-log-file = ""; access-log-file = "";
# Minimum severity, can be "debug", "info", "warning", "error" or "fatal" # Minimum severity, can be "debug", "info", "warning", "error" or "fatal"
# "debug" is useful for debugging purposes, but it will also generate a lot of log data and slow down the application
log-min-severity = "info"; log-min-severity = "info";
# Database consistency check to run at startup.
# Can be "none", "quick", or "full"
db-integrity-check = "quick";
# Output db queries on stdout # Output db queries on stdout
db-show-queries = false; db-show-queries = false;
+129
View File
@@ -19,6 +19,9 @@
#include "database/Db.hpp" #include "database/Db.hpp"
#include <functional>
#include <memory>
#include <Wt/Dbo/FixedSqlConnectionPool.h> #include <Wt/Dbo/FixedSqlConnectionPool.h>
#include <Wt/Dbo/backend/Sqlite3.h> #include <Wt/Dbo/backend/Sqlite3.h>
@@ -70,21 +73,98 @@ namespace lms::db
std::filesystem::path _dbPath; std::filesystem::path _dbPath;
}; };
enum class IntegrityCheckType
{
Quick,
Full
};
bool checkDbIntegrity(Wt::Dbo::SqlConnection& connection, IntegrityCheckType checkType, std::function<void(std::string_view error)> errorCallback)
{
bool integrityCheckPassed{};
auto statement = connection.prepareStatement(checkType == IntegrityCheckType::Full ? "PRAGMA integrity_check" : "PRAGMA quick_check");
statement->execute();
std::string result;
result.reserve(32);
while (statement->nextRow())
{
result.clear();
statement->getResult(0, &result, result.capacity());
if (result == "ok")
{
integrityCheckPassed = true;
break;
}
errorCallback(result);
}
return integrityCheckPassed;
}
bool checkDbForeignKeyConstraints(Wt::Dbo::SqlConnection& connection, std::function<void(std::string_view table, long long rowId, std::string_view referredTable)> errorCallback)
{
bool foreignKeyConstraintsPassed{ true };
auto statement = connection.prepareStatement("PRAGMA foreign_key_check");
statement->execute();
std::string table;
std::string foreignTable;
// see https://www.sqlite.org/pragma.html#pragma_foreign_key_check for exepcted result
while (statement->nextRow())
{
foreignKeyConstraintsPassed = false;
table.clear();
foreignTable.clear();
long long rowId{};
statement->getResult(0, &table, static_cast<int>(table.capacity()));
statement->getResult(1, &rowId);
statement->getResult(2, &foreignTable, static_cast<int>(foreignTable.capacity()));
errorCallback(table, rowId, foreignTable);
}
return foreignKeyConstraintsPassed;
}
} // namespace } // namespace
// Session living class handling the database and the login // Session living class handling the database and the login
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount) Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
{ {
std::string checkType{ "quick" };
LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath); LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath);
auto connection{ std::make_unique<Connection>(dbPath) }; auto connection{ std::make_unique<Connection>(dbPath) };
if (core::IConfig * config{ core::Service<core::IConfig>::get() }) // may not be here on testU if (core::IConfig * config{ core::Service<core::IConfig>::get() }) // may not be here on testU
{
connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false"); connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false");
checkType = config->getString("db-integrity-check", "quick");
}
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) }; auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
connectionPool->setTimeout(std::chrono::seconds{ 10 }); connectionPool->setTimeout(std::chrono::seconds{ 10 });
_connectionPool = std::move(connectionPool); _connectionPool = std::move(connectionPool);
if (checkType == "quick")
{
performQuickCheck();
}
else if (checkType == "full")
{
performIntegrityCheck();
performForeignKeyConstraintsCheck();
}
else if (checkType != "none")
{
throw Exception("Invalid 'db-integrity-check' value: '" + checkType + "'. Expected 'quick', 'full' or 'none'.");
}
} }
void Db::executeSql(const std::string& sql) void Db::executeSql(const std::string& sql)
@@ -114,6 +194,55 @@ namespace lms::db
return *tlsSession; return *tlsSession;
} }
void Db::performQuickCheck()
{
ScopedConnection connection{ *_connectionPool };
LMS_LOG(DB, INFO, "Performing quick database check...");
// Quick check is just a simple integrity check
bool quickCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Quick, [&](std::string_view error) {
LMS_LOG(DB, ERROR, "Quick check error: " << error);
}) };
if (quickCheckPassed)
LMS_LOG(DB, INFO, "Quick database check passed!");
else
LMS_LOG(DB, ERROR, "Quick database check done with errors!");
}
void Db::performIntegrityCheck()
{
ScopedConnection connection{ *_connectionPool };
LMS_LOG(DB, INFO, "Checking database integrity...");
bool integrityCheckPassed{ checkDbIntegrity(*connection, IntegrityCheckType::Full, [&](std::string_view error) {
LMS_LOG(DB, ERROR, "Integrity check error: " << error);
}) };
if (integrityCheckPassed)
LMS_LOG(DB, INFO, "Database integrity check passed!");
else
LMS_LOG(DB, ERROR, "Database integrity check done with errors!");
}
void Db::performForeignKeyConstraintsCheck()
{
ScopedConnection connection{ *_connectionPool };
LMS_LOG(DB, INFO, "Checking foreign key constraints...");
bool foreignKeyConstraintsPassed{ checkDbForeignKeyConstraints(*connection, [&](std::string_view table, long long rowId, std::string_view referredTable) {
LMS_LOG(DB, ERROR, "Foreign key constraint failed in table '" << table << "', rowid = " << rowId << ", referred table = '" << referredTable << "'");
}) };
if (!foreignKeyConstraintsPassed)
throw Exception("Foreign key constraints check failed! Please restore from a backup or recreate the database.");
LMS_LOG(DB, INFO, "Foreign key constraints check passed!");
}
Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool) Db::ScopedConnection::ScopedConnection(Wt::Dbo::SqlConnectionPool& pool)
: _connectionPool{ pool } : _connectionPool{ pool }
, _connection{ _connectionPool.getConnection() } , _connection{ _connectionPool.getConnection() }
@@ -46,6 +46,10 @@ namespace lms::db
core::RecursiveSharedMutex& getMutex() { return _sharedMutex; } core::RecursiveSharedMutex& getMutex() { return _sharedMutex; }
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; } Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
void performQuickCheck();
void performIntegrityCheck();
void performForeignKeyConstraintsCheck();
class ScopedConnection class ScopedConnection
{ {
public: public:
@@ -53,6 +57,7 @@ namespace lms::db
~ScopedConnection(); ~ScopedConnection();
Wt::Dbo::SqlConnection* operator->() const; Wt::Dbo::SqlConnection* operator->() const;
Wt::Dbo::SqlConnection& operator*() const { return *_connection; }
private: private:
ScopedConnection(const ScopedConnection&) = delete; ScopedConnection(const ScopedConnection&) = delete;