Skip to content

Commit 1c7932c

Browse files
Protect database creation by a file lock.
Otherwise, due to SQLITE locking rules, database creation may fail late, disturbing integration tests among others.
1 parent aa68666 commit 1c7932c

File tree

3 files changed

+95
-31
lines changed

3 files changed

+95
-31
lines changed

src/common/database/db_connection.cpp

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
#include <unistd.h>
1212

1313
#include "db_connection.h"
14+
#include "cflock.h"
1415
#include "sqlite_database_scheme.h"
1516
#include "sqlite_database_scheme_updates.h"
1617
#include "qexcdatabase.h"
18+
#include "qfilethrow.h"
1719
#include "qsqlquerythrow.h"
1820
#include "logger.h"
1921
#include "app.h"
@@ -22,6 +24,12 @@
2224

2325
static QSqlDatabase* g_db = nullptr;
2426

27+
static bool versionTableExists(QSqlQueryThrow& query){
28+
logDebug << "checking for version table...";
29+
query.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='version'");
30+
return query.next();
31+
}
32+
2533
static QVersionNumber queryVersion(QSqlQueryThrow& query){
2634
query.exec("select ver from version");
2735
query.next(true);
@@ -88,55 +96,76 @@ static void handleDifferentVersions(const QVersionNumber& dbVersion,
8896

8997
}
9098

91-
/// @param versionAction: if true, update version, if needed
92-
/// @throws QExcDatabase
93-
static void openAndPrepareSqliteDb()
94-
{
95-
const QString appDataLoc = db_connection::mkDbPath();
96-
const QString dbPath = appDataLoc + "/database.db";
97-
98-
bool pathExisted = QFileInfo::exists(dbPath);
99-
g_db->setDatabaseName(dbPath);
100-
if(! g_db->open()) {
101-
throw QExcDatabase(__func__, g_db->lastError());
102-
}
103-
104-
QSqlQueryThrow query(*g_db);
105-
// Allow for delete queries with cascades
106-
99+
static void
100+
createOrUpDateDb(QSqlQueryThrow &query, CFlock& lock){
101+
logDebug << "about to lockExclusive database for scheme update...";
107102
// quoting sqlite.org/foreignkeys.html
108103
// "It is not possible to enable or disable foreign key constraints in the
109104
// middle of a multi-statement transaction (when SQLite is not in autocommit mode)"
110-
// The scheme-updates require foreign_keys=OFF, so enable below
105+
// The scheme-updates require foreign_keys=OFF, so call below pragma:
111106
query.exec("PRAGMA foreign_keys=OFF");
112-
107+
lock.lockExclusive();
113108
query.transaction();
114109

115-
if(! pathExisted){
116-
// Initially create the database
110+
if(! versionTableExists(query)){
117111
logInfo << qtr("Creating new sqlite database");
118-
QStringList statements = QString(SQLITE_DATABASE_SCHEME).split(';', QString::SkipEmptyParts);
119-
112+
QStringList statements = QString(
113+
SQLITE_DATABASE_SCHEME).split(';', QString::SkipEmptyParts);
120114
for(const QString& stmt : statements){
121115
query.exec(stmt);
122116
}
123-
QFile dbfile(appDataLoc);
124-
if(! dbfile.setPermissions(
125-
QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner)){
126-
logWarning << qtr("Failed to initially set permissions on the database-directory "
127-
"at %1: %2. Other users might be able "
117+
118+
QFile dbDir(db_connection::getDatabaseDir());
119+
if(! dbDir.setPermissions(
120+
QFileDevice::ReadOwner|QFileDevice::WriteOwner|QFileDevice::ExeOwner)){
121+
logWarning << qtr("Failed to initially set permissions on the database-"
122+
"directory at %1: %2. Other users might be able "
128123
"to browse your command history...")
129-
.arg(dbPath, dbfile.errorString());
124+
.arg(db_connection::getDatabaseDir(), dbDir.errorString());
130125
}
131126
}
132127
const auto dbVersion = queryVersion(query);
133-
logDebug << "current db-version" << dbVersion.toString();
134128
if(dbVersion != app::version()){
135129
handleDifferentVersions(dbVersion, query);
136130
}
137-
138131
query.commit();
139132
// outside of transaction (see above):
133+
// Allow for delete queries with cascades
134+
query.exec("PRAGMA foreign_keys=ON");
135+
}
136+
137+
/// @throws QExcDatabase
138+
static void openAndPrepareSqliteDb()
139+
{
140+
const QString appDataLoc = db_connection::mkDbPath();
141+
const QString dbPath = appDataLoc + "/database.db";
142+
QFileThrow lockfile(appDataLoc + "/.shournal-dblock");
143+
lockfile.open(QFile::OpenModeFlag::ReadWrite);
144+
// Lock to allow for concurrent database scheme updates. First, we lock shared,
145+
// upgrading to exclusive on scheme update. Note that for some reason concurrent
146+
// processes executing "PRAGMA locking_mode=EXCLUSIVE; BEGIN EXCLUSIVE;" deadlocked
147+
// during integration tests, so be careful with that directive.
148+
CFlock lock(lockfile.handle());
149+
lock.lockShared();
150+
g_db->setDatabaseName(dbPath);
151+
if(! g_db->open()) {
152+
throw QExcDatabase(__func__, g_db->lastError());
153+
}
154+
155+
QSqlQueryThrow query(*g_db);
156+
if(! versionTableExists(query)){
157+
logDebug << "version table did not exist yet..";
158+
createOrUpDateDb(query, lock);
159+
} else {
160+
const auto dbVersion = queryVersion(query);
161+
logDebug << "current db-version" << dbVersion.toString();
162+
if(dbVersion != app::version()){
163+
createOrUpDateDb(query, lock);
164+
}
165+
}
166+
lock.unlock();
167+
168+
// Allow for delete queries with cascades
140169
query.exec("PRAGMA foreign_keys=ON");
141170
}
142171

src/common/oscpp/cflock.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,33 @@
44
#include "cflock.h"
55
#include "os.h"
66

7+
#ifndef NDEBUG
8+
9+
static bool checkFdFlockFlags(int fd, int operation){
10+
int flags = os::getFdStatusFlags(fd);
11+
12+
switch (operation) {
13+
case LOCK_EX:
14+
if(flags & O_WRONLY || flags & O_RDWR) {
15+
return true;
16+
}
17+
std::cerr << "LOCK_EX: fd opened RDONLY\n";
18+
break;
19+
case LOCK_SH:
20+
if(!(flags & O_WRONLY)) {
21+
return true;
22+
}
23+
std::cerr << "LOCK_SH: fd opened WRONLY\n";
24+
break;
25+
default:
26+
std::cerr << "Bad fd operation " << operation << "\n";
27+
break;
28+
}
29+
return false;
30+
}
31+
32+
#endif
33+
734
CFlock::CFlock(int fd) :
835
m_fd(fd),
936
m_isLocked(false)
@@ -23,12 +50,14 @@ CFlock::~CFlock()
2350

2451
void CFlock::lockExclusive()
2552
{
53+
assert(checkFdFlockFlags(m_fd, LOCK_EX));
2654
os::flock(m_fd, LOCK_EX);
2755
m_isLocked = true;
2856
}
2957

3058
void CFlock::lockShared()
3159
{
60+
assert(checkFdFlockFlags(m_fd, LOCK_SH));
3261
os::flock(m_fd, LOCK_SH);
3362
m_isLocked = true;
3463
}

src/common/oscpp/cflock.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
#pragma once
22

33

4-
/// Warpper class for flock
4+
/// Wrapper class for flock.
5+
/// Due to NFS emulating flock as fcntl(2) byte-range locks
6+
/// the fd open mode must match the locking operations:
7+
/// In order to place a shared lock, fd must be open for reading,
8+
/// In order to place an exclusive lock, fd must be open for writing. To
9+
/// place both types of lock, open a file read-write.
510
class CFlock
611
{
712
public:
13+
/// fd should in general be opened read-write (see above)!
814
CFlock(int fd);
915
~CFlock();
1016

0 commit comments

Comments
 (0)