Merge PR #241: Improve log DB housekeeping performance

This commit is contained in:
Jenkins nymea 2020-01-22 23:47:15 +01:00
commit 2d08e7a028
4 changed files with 114 additions and 77 deletions

View File

@ -155,14 +155,10 @@ LogEngine::LogEngine(const QString &driver, const QString &dbName, const QString
m_db = QSqlDatabase::addDatabase(driver, "logs"); m_db = QSqlDatabase::addDatabase(driver, "logs");
m_db.setDatabaseName(dbName); m_db.setDatabaseName(dbName);
m_db.setHostName(hostname); m_db.setHostName(hostname);
m_overflow = 100; m_trimSize = qRound(0.01 * m_dbMaxSize);
m_maxQueueLength = 1000;
if (QCoreApplication::instance()->organizationName() == "nymea-test") { qCDebug(dcLogEngine) << "Opening logging database" << m_db.databaseName() << "(Max size:" << m_dbMaxSize << "trim size:" << m_trimSize << ")";
m_dbMaxSize = 20;
qCDebug(dcLogEngine) << "Set logging dab max size to" << m_dbMaxSize << "for testing.";
}
qCDebug(dcLogEngine) << "Opening logging database" << m_db.databaseName();
if (!m_db.isValid()) { if (!m_db.isValid()) {
qCWarning(dcLogEngine) << "Database not valid:" << m_db.lastError().driverText() << m_db.lastError().databaseText(); qCWarning(dcLogEngine) << "Database not valid:" << m_db.lastError().driverText() << m_db.lastError().databaseText();
@ -175,12 +171,12 @@ LogEngine::LogEngine(const QString &driver, const QString &dbName, const QString
rotate(m_db.databaseName()); rotate(m_db.databaseName());
if (!initDB(username, password)) { if (!initDB(username, password)) {
qCWarning(dcLogEngine()) << "Error fixing log database. Giving up. Logs can't be stored."; qCWarning(dcLogEngine()) << "Error fixing log database. Giving up. Logs can't be stored.";
return;
} }
} }
} }
connect(&m_jobWatcher, SIGNAL(finished()), this, SLOT(handleJobFinished())); connect(&m_jobWatcher, SIGNAL(finished()), this, SLOT(handleJobFinished()));
checkDBSize(); checkDBSize();
} }
@ -221,7 +217,7 @@ LogEntriesFetchJob *LogEngine::fetchLogEntries(const LogFilter &filter)
DatabaseJob *job = new DatabaseJob(m_db, queryString, filter.values()); DatabaseJob *job = new DatabaseJob(m_db, queryString, filter.values());
LogEntriesFetchJob *fetchJob = new LogEntriesFetchJob(this); LogEntriesFetchJob *fetchJob = new LogEntriesFetchJob(this);
connect(job, &DatabaseJob::finished, this, [this, job, fetchJob](){ connect(job, &DatabaseJob::finished, this, [job, fetchJob](){
fetchJob->deleteLater(); fetchJob->deleteLater();
if (job->error().isValid()) { if (job->error().isValid()) {
qCWarning(dcLogEngine) << "Error fetching log entries. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); qCWarning(dcLogEngine) << "Error fetching log entries. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText();
@ -247,7 +243,7 @@ LogEntriesFetchJob *LogEngine::fetchLogEntries(const LogFilter &filter)
fetchJob->finished(); fetchJob->finished();
}); });
enqueJob(job); enqueJob(job, true);
return fetchJob; return fetchJob;
} }
@ -258,7 +254,7 @@ DevicesFetchJob *LogEngine::fetchDevices()
DatabaseJob *job = new DatabaseJob(m_db, queryString); DatabaseJob *job = new DatabaseJob(m_db, queryString);
DevicesFetchJob *fetchJob = new DevicesFetchJob(this); DevicesFetchJob *fetchJob = new DevicesFetchJob(this);
connect(job, &DatabaseJob::finished, this, [this, job, fetchJob](){ connect(job, &DatabaseJob::finished, this, [job, fetchJob](){
fetchJob->deleteLater(); fetchJob->deleteLater();
if (job->error().type() != QSqlError::NoError) { if (job->error().type() != QSqlError::NoError) {
qCWarning(dcLogEngine()) << "Error fetching device entries from log database:" << job->error().driverText() << job->error().databaseText(); qCWarning(dcLogEngine()) << "Error fetching device entries from log database:" << job->error().driverText() << job->error().databaseText();
@ -271,6 +267,7 @@ DevicesFetchJob *LogEngine::fetchDevices()
} }
fetchJob->finished(); fetchJob->finished();
}); });
enqueJob(job, true);
return fetchJob; return fetchJob;
} }
@ -279,11 +276,11 @@ bool LogEngine::jobsRunning() const
return !m_jobQueue.isEmpty() || m_currentJob; return !m_jobQueue.isEmpty() || m_currentJob;
} }
void LogEngine::setMaxLogEntries(int maxLogEntries, int overflow) void LogEngine::setMaxLogEntries(int maxLogEntries, int trimSize)
{ {
m_dbMaxSize = maxLogEntries; m_dbMaxSize = maxLogEntries;
m_overflow = overflow; m_trimSize = trimSize;
checkDBSize(); trim();
} }
/*! Removes all entries from the database. This method will be used for the tests. */ /*! Removes all entries from the database. This method will be used for the tests. */
@ -298,7 +295,9 @@ void LogEngine::clearDatabase()
connect(job, &DatabaseJob::finished, this, [this, job](){ connect(job, &DatabaseJob::finished, this, [this, job](){
if (job->error().type() != QSqlError::NoError) { if (job->error().type() != QSqlError::NoError) {
qCWarning(dcLogEngine) << "Could not clear logging database. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); qCWarning(dcLogEngine) << "Could not clear logging database. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText();
return;
} }
m_entryCount = 0;
emit logDatabaseUpdated(); emit logDatabaseUpdated();
}); });
@ -319,6 +318,7 @@ void LogEngine::logEvent(const Event &event)
Logging::LoggingSource sourceType; Logging::LoggingSource sourceType;
if (event.isStateChangeEvent()) { if (event.isStateChangeEvent()) {
sourceType = Logging::LoggingSourceStates; sourceType = Logging::LoggingSourceStates;
// There should only be one param // There should only be one param
if (!event.params().isEmpty()) if (!event.params().isEmpty())
valueList << event.params().first().value(); valueList << event.params().first().value();
@ -430,9 +430,11 @@ void LogEngine::removeDeviceLogs(const DeviceId &deviceId)
connect(job, &DatabaseJob::finished, this, [this, job, deviceId](){ connect(job, &DatabaseJob::finished, this, [this, job, deviceId](){
if (job->error().type() != QSqlError::NoError) { if (job->error().type() != QSqlError::NoError) {
qCWarning(dcLogEngine) << "Error deleting log entries from device" << deviceId.toString() << ". Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); qCWarning(dcLogEngine) << "Error deleting log entries from device" << deviceId.toString() << ". Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText();
} else { return;
emit logDatabaseUpdated();
} }
emit logDatabaseUpdated();
checkDBSize();
}); });
enqueJob(job); enqueJob(job);
@ -450,9 +452,11 @@ void LogEngine::removeRuleLogs(const RuleId &ruleId)
if (job->error().type() != QSqlError::NoError) { if (job->error().type() != QSqlError::NoError) {
qCWarning(dcLogEngine) << "Error deleting log entries from rule" << ruleId.toString() << ". Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); qCWarning(dcLogEngine) << "Error deleting log entries from rule" << ruleId.toString() << ". Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText();
} else { return;
emit logDatabaseUpdated();
} }
emit logDatabaseUpdated();
checkDBSize();
}); });
enqueJob(job); enqueJob(job);
@ -473,8 +477,27 @@ void LogEngine::appendLogEntry(const LogEntry &entry)
DatabaseJob *job = new DatabaseJob(m_db, queryString); DatabaseJob *job = new DatabaseJob(m_db, queryString);
// Check for log flooding. If we are exceeding the queue we'll start flagging log events of a certain type.
// If we'll get more log events of the same type while the queue is still exceededd, we'll discard the old
// ones and queue up the new one instead. The most recent one is more important (i.e. we don't want to lose
// the last event in a series).
if (m_jobQueue.count() > m_maxQueueLength) {
qCDebug(dcLogEngine()) << "An excessive amount of data is being logged. (" << m_jobQueue.length() << "jobs in the queue)";
if (m_flaggedJobs.contains(entry.typeId().toString() + entry.deviceId().toString())) {
if (m_flaggedJobs.value(entry.typeId().toString() + entry.deviceId().toString()).count() > 10) {
qCWarning(dcLogEngine()) << "Discarding log entry because of excessive log flooding.";
DatabaseJob *job = m_flaggedJobs[entry.typeId().toString() + entry.deviceId().toString()].takeFirst();
int jobIdx = m_jobQueue.indexOf(job);
m_jobQueue.takeAt(jobIdx)->deleteLater();
}
}
m_flaggedJobs[entry.typeId().toString() + entry.deviceId().toString()].append(job);
}
connect(job, &DatabaseJob::finished, this, [this, job, entry](){ connect(job, &DatabaseJob::finished, this, [this, job, entry](){
m_flaggedJobs[entry.typeId().toString() + entry.deviceId().toString()].removeAll(job);
if (job->error().type() != QSqlError::NoError) { if (job->error().type() != QSqlError::NoError) {
qCWarning(dcLogEngine) << "Error writing log entry. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText(); qCWarning(dcLogEngine) << "Error writing log entry. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText();
qCWarning(dcLogEngine) << entry; qCWarning(dcLogEngine) << entry;
@ -482,11 +505,11 @@ void LogEngine::appendLogEntry(const LogEntry &entry)
return; return;
} }
emit logEntryAdded(entry); emit logEntryAdded(entry);
if (++m_entryCount > m_dbMaxSize + m_overflow) { m_entryCount++;
checkDBSize(); trim();
}
}); });
enqueJob(job); enqueJob(job);
@ -494,57 +517,52 @@ void LogEngine::appendLogEntry(const LogEntry &entry)
void LogEngine::checkDBSize() void LogEngine::checkDBSize()
{ {
if (m_dbMaxSize == -1) { DatabaseJob *job = new DatabaseJob(m_db, "SELECT COUNT(*) FROM entries;");
// No tripping required connect(job, &DatabaseJob::finished, this, [this, job](){
return;
}
QDateTime startTime = QDateTime::currentDateTime();
QString queryString = "SELECT COUNT(*) FROM entries;";
DatabaseJob *job = new DatabaseJob(m_db, queryString);
connect(job, &DatabaseJob::finished, this, [this, job, startTime](){
if (job->error().type() != QSqlError::NoError) { if (job->error().type() != QSqlError::NoError) {
qCWarning(dcLogEngine()) << "Failed to query entry count in db:" << job->error().databaseText(); qCWarning(dcLogEngine()) << "Error fetching log DB size. Driver error:" << job->error().driverText() << "Database error:" << job->error().databaseText();
return; m_entryCount = 0;
}
if (job->results().isEmpty()) {
qCWarning(dcLogEngine()) << "Failed retrieving entry count.";
return; return;
} }
m_entryCount = job->results().first().value(0).toInt(); m_entryCount = job->results().first().value(0).toInt();
if (m_entryCount <= m_dbMaxSize) {
return;
}
// keep only the latest m_dbMaxSize entries
if (!m_trimWarningPrinted) {
qCDebug(dcLogEngine) << "Deleting oldest entries" << (m_entryCount - m_dbMaxSize) << "and keep only the latest" << m_dbMaxSize << "entries.";
m_trimWarningPrinted = true;
}
QString queryDeleteString = QString("DELETE FROM entries WHERE ROWID IN (SELECT ROWID FROM entries ORDER BY timestamp DESC LIMIT -1 OFFSET %1);").arg(QString::number(m_dbMaxSize));
DatabaseJob *deleteJob = new DatabaseJob(m_db, queryDeleteString);
connect(deleteJob, &DatabaseJob::finished, this, [this, deleteJob,startTime](){
if (deleteJob->error().type() != QSqlError::NoError) {
qCWarning(dcLogEngine) << "Error deleting oldest log entries to keep size. Driver error:" << deleteJob->error().driverText() << "Database error:" << deleteJob->error().databaseText();
}
m_entryCount = m_dbMaxSize;
qCDebug(dcLogEngine()) << "Ran housekeeping on log database in" << startTime.msecsTo(QDateTime::currentDateTime()) << "ms.";
emit logDatabaseUpdated();
});
enqueJob(deleteJob);
}); });
enqueJob(job); enqueJob(job, true);
} }
void LogEngine::enqueJob(DatabaseJob *job) void LogEngine::trim()
{ {
m_jobQueue.append(job); if (m_dbMaxSize == -1 || m_entryCount < m_dbMaxSize) {
// No trimming required
return;
}
QDateTime startTime = QDateTime::currentDateTime();
QString queryDeleteString = QString("DELETE FROM entries WHERE ROWID IN (SELECT ROWID FROM entries ORDER BY timestamp DESC LIMIT -1 OFFSET %1);").arg(QString::number(m_dbMaxSize - m_trimSize));
DatabaseJob *deleteJob = new DatabaseJob(m_db, queryDeleteString);
connect(deleteJob, &DatabaseJob::finished, this, [this, deleteJob, startTime](){
if (deleteJob->error().type() != QSqlError::NoError) {
qCWarning(dcLogEngine) << "Error deleting oldest log entries to keep size. Driver error:" << deleteJob->error().driverText() << "Database error:" << deleteJob->error().databaseText();
}
qCDebug(dcLogEngine()) << "Ran housekeeping on log database in" << startTime.msecsTo(QDateTime::currentDateTime()) << "ms. (Deleted" << m_entryCount - (m_dbMaxSize - m_trimSize) << "entries)";
m_entryCount = m_dbMaxSize - m_trimSize;
emit logDatabaseUpdated();
});
qCDebug(dcLogEngine()) << "Scheduling housekeeping job.";
enqueJob(deleteJob, true);
}
void LogEngine::enqueJob(DatabaseJob *job, bool priority)
{
if (priority) {
m_jobQueue.prepend(job);
} else {
m_jobQueue.append(job);
}
qCDebug(dcLogEngine()) << "Scheduled job at position" << (priority ? 0 : m_jobQueue.count() - 1) << "(" << m_jobQueue.count() << "jobs in the queue)";
processQueue(); processQueue();
} }
@ -569,7 +587,9 @@ void LogEngine::processQueue()
m_dbMalformed = false; m_dbMalformed = false;
} }
DatabaseJob *job = m_jobQueue.takeFirst(); DatabaseJob *job = m_jobQueue.takeFirst();
qCDebug(dcLogEngine()) << "Processing DB queue. (" << m_jobQueue.count() << "jobs left in queue," << m_entryCount << "entries in DB)";
m_currentJob = job; m_currentJob = job;
QFuture<DatabaseJob*> future = QtConcurrent::run([job](){ QFuture<DatabaseJob*> future = QtConcurrent::run([job](){
@ -603,6 +623,8 @@ void LogEngine::handleJobFinished()
job->finished(); job->finished();
job->deleteLater(); job->deleteLater();
m_currentJob = nullptr; m_currentJob = nullptr;
qCDebug(dcLogEngine()) << "DB job finished. (" << m_entryCount << "entries in DB)";
processQueue(); processQueue();
} }

View File

@ -56,7 +56,7 @@ public:
bool jobsRunning() const; bool jobsRunning() const;
void setMaxLogEntries(int maxLogEntries, int overflow); void setMaxLogEntries(int maxLogEntries, int trimSize);
void clearDatabase(); void clearDatabase();
void logSystemEvent(const QDateTime &dateTime, bool active, Logging::LoggingLevel level = Logging::LoggingLevelInfo); void logSystemEvent(const QDateTime &dateTime, bool active, Logging::LoggingLevel level = Logging::LoggingLevelInfo);
@ -88,8 +88,9 @@ private:
private slots: private slots:
void checkDBSize(); void checkDBSize();
void trim();
void enqueJob(DatabaseJob *job); void enqueJob(DatabaseJob *job, bool priority = false);
void processQueue(); void processQueue();
void handleJobFinished(); void handleJobFinished();
@ -98,11 +99,14 @@ private:
QString m_username; QString m_username;
QString m_password; QString m_password;
int m_dbMaxSize; int m_dbMaxSize;
int m_overflow; int m_trimSize;
bool m_trimWarningPrinted = false;
int m_entryCount = 0; int m_entryCount = 0;
bool m_dbMalformed = false; bool m_dbMalformed = false;
// When maxQueueLength is exceeded, jobs will be flagged and discarded if this source logs more events
int m_maxQueueLength;
QHash<QString, QList<DatabaseJob*>> m_flaggedJobs;
QList<DatabaseJob*> m_jobQueue; QList<DatabaseJob*> m_jobQueue;
DatabaseJob *m_currentJob = nullptr; DatabaseJob *m_currentJob = nullptr;
QFutureWatcher<DatabaseJob*> m_jobWatcher; QFutureWatcher<DatabaseJob*> m_jobWatcher;

View File

@ -946,7 +946,7 @@ void NymeaCore::deviceManagerLoaded()
qCDebug(dcApplication()) << "Starting housekeeping..."; qCDebug(dcApplication()) << "Starting housekeeping...";
QDateTime startTime = QDateTime::currentDateTime(); QDateTime startTime = QDateTime::currentDateTime();
DevicesFetchJob *job = m_logger->fetchDevices(); DevicesFetchJob *job = m_logger->fetchDevices();
connect(job, &DevicesFetchJob::finished, this, [this, job, startTime](){ connect(job, &DevicesFetchJob::finished, m_deviceManager, [this, job, startTime](){
foreach (const DeviceId &deviceId, job->results()) { foreach (const DeviceId &deviceId, job->results()) {
if (!m_deviceManager->findConfiguredDevice(deviceId)) { if (!m_deviceManager->findConfiguredDevice(deviceId)) {
qCDebug(dcApplication()) << "Cleaning stale device entries from log DB for device id" << deviceId; qCDebug(dcApplication()) << "Cleaning stale device entries from log DB for device id" << deviceId;

View File

@ -41,6 +41,11 @@ private:
inline void verifyDeviceError(const QVariant &response, Device::DeviceError error = Device::DeviceErrorNoError) { inline void verifyDeviceError(const QVariant &response, Device::DeviceError error = Device::DeviceErrorNoError) {
verifyError(response, "deviceError", enumValueName(error)); verifyError(response, "deviceError", enumValueName(error));
} }
inline void waitForDBSync() {
while (NymeaCore::instance()->logEngine()->jobsRunning()) {
qApp->processEvents();
}
}
private slots: private slots:
void initTestCase(); void initTestCase();
@ -80,7 +85,8 @@ void TestLogging::initTestCase()
"LogEngine.debug=true\n" "LogEngine.debug=true\n"
"Tests.debug=true\n" "Tests.debug=true\n"
"MockDevice.debug=true\n" "MockDevice.debug=true\n"
"DeviceManager.debug=true\n"); "DeviceManager.debug=true\n"
"Application.debug=true\n");
} }
void TestLogging::initLogs() void TestLogging::initLogs()
@ -101,6 +107,7 @@ void TestLogging::initLogs()
QVERIFY(logEntries.count() == 0); QVERIFY(logEntries.count() == 0);
restartServer(); restartServer();
NymeaCore::instance()->logEngine()->setMaxLogEntries(1000, 10);
} }
void TestLogging::databaseSerializationTest_data() void TestLogging::databaseSerializationTest_data()
@ -158,6 +165,8 @@ void TestLogging::systemLogs()
qWarning() << "Clearing logging DB"; qWarning() << "Clearing logging DB";
clearLoggingDatabase(); clearLoggingDatabase();
waitForDBSync();
QVariantMap params; QVariantMap params;
params.insert("loggingSources", QVariantList() << enumValueName(Logging::LoggingSourceSystem)); params.insert("loggingSources", QVariantList() << enumValueName(Logging::LoggingSourceSystem));
params.insert("eventTypes", QVariantList() << enumValueName(Logging::LoggingEventTypeActiveChange)); params.insert("eventTypes", QVariantList() << enumValueName(Logging::LoggingEventTypeActiveChange));
@ -610,6 +619,8 @@ void TestLogging::testHouseKeeping()
connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater())); connect(reply, SIGNAL(finished()), reply, SLOT(deleteLater()));
spy.wait(); spy.wait();
waitForDBSync();
params.clear(); params.clear();
params.insert("deviceIds", QVariantList() << deviceId); params.insert("deviceIds", QVariantList() << deviceId);
response = injectAndWait("Logging.GetLogEntries", params); response = injectAndWait("Logging.GetLogEntries", params);
@ -623,15 +634,13 @@ void TestLogging::testHouseKeeping()
restartServer(); restartServer();
// Wait for the mock device to complete the setup waitForDBSync();
do {
qApp->processEvents();
params.clear();
params.insert("deviceId", m_mockDeviceId);
response = injectAndWait("Devices.GetConfiguredDevices", params);
} while (!response.toMap().value("params").toMap().value("devices").toList().first().toMap().value("setupComplete").toBool());
params.clear();
params.insert("deviceIds", QVariantList() << deviceId);
response = injectAndWait("Logging.GetLogEntries", params); response = injectAndWait("Logging.GetLogEntries", params);
qCDebug(dcTests()) << qUtf8Printable(QJsonDocument::fromVariant(response).toJson());
QVERIFY2(response.toMap().value("status").toString() == QString("success"), "GetLogEntries failed");
QVERIFY2(response.toMap().value("params").toMap().value("logEntries").toList().count() == 0, "Device state change event still in log. Should've been cleaned by housekeeping."); QVERIFY2(response.toMap().value("params").toMap().value("logEntries").toList().count() == 0, "Device state change event still in log. Should've been cleaned by housekeeping.");
} }
@ -660,6 +669,8 @@ void TestLogging::testLimits()
verifyDeviceError(response); verifyDeviceError(response);
} }
waitForDBSync();
QVariantMap params; QVariantMap params;
QVariantMap response; QVariantMap response;