Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Database to keep sql operations in single thread #122

Merged
merged 1 commit into from
Oct 10, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
227 changes: 159 additions & 68 deletions src/database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,60 +18,47 @@

#include "database.h"
#include "models/playlistmodel.h"
#include "mainwindow.h"
#include <QtSql>
#include <QStandardPaths>
#include <QDir>
#include <QMutexLocker>
#include <QtDebug>

struct DatabaseJob {
enum Type {
PutThumbnail,
GetThumbnail
} type;

QImage image;
QString hash;
bool result;
bool completed;
DatabaseJob()
: result(false)
, completed(false)
{}
};

static Database* instance = 0;

Database::Database(QObject *parent) :
QObject(parent)
QThread(parent)
, m_commitTimer(0)
{
QDir dir(QStandardPaths::standardLocations(QStandardPaths::DataLocation).first());
if (!dir.exists())
dir.mkpath(dir.path());

QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(dir.filePath("db.sqlite3"));
db.open();

// Initialize version table, if needed.
int version = 0;
QSqlQuery query;
if (query.exec("CREATE TABLE version (version INTEGER);")) {
if (!query.exec("INSERT INTO version VALUES (0);"))
qCritical() << __PRETTY_FUNCTION__ << "Failed to create version table.";
} else if (query.exec("SELECT version FROM version")) {
query.next();
version = query.value(0).toInt();
} else {
qCritical() << __PRETTY_FUNCTION__ << "Failed to get version.";
}
if (version < 1 && upgradeVersion1())
version = 1;
qDebug() << "Database version is" << version;
}

Database &Database::singleton(QWidget *parent)
{
if (!instance)
if (!instance) {
instance = new Database(parent);
instance->start();
}
return *instance;
}

Database::~Database()
{
QString connection = QSqlDatabase::database().connectionName();
QSqlDatabase::database().close();
QSqlDatabase::removeDatabase(connection);
instance = 0;
}

bool Database::upgradeVersion1()
{
QMutexLocker lock(&m_mutex);
bool success = false;
QSqlQuery query;
if (query.exec("CREATE TABLE thumbnails (hash TEXT PRIMARY KEY NOT NULL, accessed DATETIME NOT NULL, image BLOB);")) {
Expand All @@ -84,46 +71,94 @@ bool Database::upgradeVersion1()
return success;
}

bool Database::putThumbnail(const QString& hash, const QImage& image)
void Database::doJob(DatabaseJob * job)
{
QMutexLocker lock(&m_mutex);
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG");
if (!m_commitTimer->isActive())
QSqlDatabase::database().transaction();
m_commitTimer->start();

QSqlQuery query;
query.prepare("DELETE FROM thumbnails WHERE hash = :hash;");
query.bindValue(":hash", hash);
query.exec();
query.prepare("INSERT INTO thumbnails VALUES (:hash, datetime('now'), :image);");
query.bindValue(":hash", hash);
query.bindValue(":image", ba);
bool result = query.exec();
if (!result)
qCritical() << __FUNCTION__ << query.lastError();
if (job->type == DatabaseJob::PutThumbnail) {
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
job->image.save(&buffer, "PNG");

QSqlQuery query;
query.prepare("DELETE FROM thumbnails WHERE hash = :hash;");
query.bindValue(":hash", job->hash);
query.exec();
query.prepare("INSERT INTO thumbnails VALUES (:hash, datetime('now'), :image);");
query.bindValue(":hash", job->hash);
query.bindValue(":image", ba);
job->result = query.exec();
if (!job->result)
qCritical() << __FUNCTION__ << query.lastError();
} else if (job->type == DatabaseJob::GetThumbnail) {
QImage result;
QSqlQuery query;
query.prepare("SELECT image FROM thumbnails WHERE hash = :hash;");
query.bindValue(":hash", job->hash);
if (query.exec() && query.first()) {
result.loadFromData(query.value(0).toByteArray(), "PNG");
QSqlQuery update;
update.prepare("UPDATE thumbnails SET accessed = datetime('now') WHERE hash = :hash ;");
update.bindValue(":hash", job->hash);
if (!update.exec())
qCritical() << __FUNCTION__ << update.lastError();
}
job->image = result;
}
deleteOldThumbnails();
return result;
job->completed = true;
}

QImage Database::getThumbnail(const QString &hash)
void Database::commitTransaction()
{
QMutexLocker lock(&m_mutex);
QImage result;
QSqlQuery query;
query.prepare("SELECT image FROM thumbnails WHERE hash = :hash;");
query.bindValue(":hash", hash);
if (query.exec() && query.first()) {
result.loadFromData(query.value(0).toByteArray(), "PNG");
QSqlQuery update;
update.prepare("UPDATE thumbnails SET accessed = datetime('now') WHERE hash = :hash ;");
update.bindValue(":hash", hash);
if (!update.exec())
qCritical() << __FUNCTION__ << update.lastError();
QSqlDatabase::database().commit();
}

bool Database::putThumbnail(const QString& hash, const QImage& image)
{
DatabaseJob job;
job.type = DatabaseJob::PutThumbnail;
job.hash = hash;
job.image = image;
submitAndWaitForJob(&job);
return job.result;
}

void Database::submitAndWaitForJob(DatabaseJob * job)
{
job->completed = false;
m_mutex.lock();
m_jobs.append(job);
if (m_jobs.size() == 1) {
//worker was idle until now
m_waitForNewJob.wakeAll();
}
// qDebug() << __FUNCTION__ << result.byteCount();
deleteOldThumbnails();
return result;
while (!job->completed) {
m_waitForFinished.wait(&m_mutex);
}
m_mutex.unlock();
}

QImage Database::getThumbnail(const QString &hash)
{
DatabaseJob job;
job.type = DatabaseJob::GetThumbnail;
job.hash = hash;
submitAndWaitForJob(&job);
return job.image;
}

void Database::shutdown()
{
requestInterruption();
wait();
QString connection = QSqlDatabase::database().connectionName();
QSqlDatabase::database().close();
QSqlDatabase::removeDatabase(connection);
instance = 0;
}

void Database::deleteOldThumbnails()
Expand All @@ -134,3 +169,59 @@ void Database::deleteOldThumbnails()
qCritical() << __FUNCTION__ << query.lastError();
}

void Database::run()
{
connect(&MAIN, SIGNAL(aboutToShutDown()),
this, SLOT(shutdown()), Qt::DirectConnection);

QDir dir(QStandardPaths::standardLocations(QStandardPaths::DataLocation).first());
if (!dir.exists())
dir.mkpath(dir.path());

QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(dir.filePath("db.sqlite3"));
db.open();

m_commitTimer = new QTimer();
m_commitTimer->setSingleShot(true);
m_commitTimer->setInterval(5000);
connect(m_commitTimer, SIGNAL(timeout()),
this, SLOT(commitTransaction()));

// Initialize version table, if needed.
int version = 0;
QSqlQuery query;
if (query.exec("CREATE TABLE version (version INTEGER);")) {
if (!query.exec("INSERT INTO version VALUES (0);"))
qCritical() << __PRETTY_FUNCTION__ << "Failed to create version table.";
} else if (query.exec("SELECT version FROM version")) {
query.next();
version = query.value(0).toInt();
} else {
qCritical() << __PRETTY_FUNCTION__ << "Failed to get version.";
}
if (version < 1 && upgradeVersion1())
version = 1;
qDebug() << "Database version is" << version;

while (true) {
DatabaseJob * newJob = 0;
m_mutex.lock();
if (m_jobs.isEmpty())
m_waitForNewJob.wait(&m_mutex, 1000);
else
newJob = m_jobs.takeFirst();
m_mutex.unlock();
QCoreApplication::processEvents();
if (newJob) {
doJob(newJob);
m_waitForFinished.wakeAll();
}
if (isInterruptionRequested())
break;
}
if (m_commitTimer->isActive())
commitTransaction();
delete m_commitTimer;
}

22 changes: 19 additions & 3 deletions src/database.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,42 @@
#ifndef DATABASE_H
#define DATABASE_H

#include <QObject>
#include <QThread>
#include <QImage>
#include <QMutex>
#include <QWaitCondition>

class Database : public QObject
struct DatabaseJob;
class QTimer;
class Database : public QThread
{
Q_OBJECT
explicit Database(QObject *parent = 0);

public:
static Database& singleton(QWidget* parent = 0);
~Database();

bool upgradeVersion1();
bool putThumbnail(const QString& hash, const QImage& image);
QImage getThumbnail(const QString& hash);

private slots:
void commitTransaction();

private slots:
void shutdown();

private:
void doJob(DatabaseJob * job);
void submitAndWaitForJob(DatabaseJob * job);
void deleteOldThumbnails();
void run();

QList<DatabaseJob*> m_jobs;
QMutex m_mutex;
QWaitCondition m_waitForFinished;
QWaitCondition m_waitForNewJob;
QTimer * m_commitTimer;
};

#define DB Database::singleton()
Expand Down
1 change: 1 addition & 0 deletions src/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,7 @@ void MainWindow::closeEvent(QCloseEvent* event)
if (!m_htmlEditor || m_htmlEditor->close()) {
writeSettings();
event->accept();
emit aboutToShutDown();
QApplication::exit(m_exitCode);
return;
}
Expand Down
1 change: 1 addition & 0 deletions src/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class MainWindow : public QMainWindow
void producerOpened();
void profileChanged();
void openFailed(QString);
void aboutToShutDown();

protected:
MainWindow();
Expand Down