Skip to content

Commit 43598db

Browse files
Fix Potential crash on db close (#72)
1 parent 7438ef8 commit 43598db

33 files changed

+1006
-770
lines changed

.changeset/purple-drinks-allow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@journeyapps/react-native-quick-sqlite': minor
3+
---
4+
5+
Improved behaviour for closing a database connection. This should prevent some crash issues.

.github/workflows/test.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ jobs:
5858
cd tests
5959
yarn install --frozen-lockfile
6060
61+
- name: Initialize Android Folder
62+
run: mkdir -p ~/.android/avd
63+
6164
- name: create AVD and generate snapshot for caching
6265
if: steps.avd-cache.outputs.cache-hit != 'true'
6366
uses: reactivecircus/android-emulator-runner@v2.28.0

cpp/ConnectionPool.cpp

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ ConnectionPool::ConnectionPool(std::string dbName, std::string docPath,
1919

2020
onContextCallback = nullptr;
2121
isConcurrencyEnabled = maxReads > 0;
22+
isClosed = false;
2223

2324
readConnections = new ConnectionState *[maxReads];
2425
// Open the read connections
@@ -94,7 +95,14 @@ ConnectionPool::queueInContext(ConnectionLockId contextId,
9495
};
9596
}
9697

97-
state->queueWork(task);
98+
try {
99+
state->queueWork(task);
100+
} catch (const std::exception &e) {
101+
return SQLiteOPResult{
102+
.errorMessage = e.what(),
103+
.type = SQLiteError,
104+
};
105+
}
98106

99107
return SQLiteOPResult{
100108
.type = SQLiteOk,
@@ -162,6 +170,14 @@ void ConnectionPool::closeContext(ConnectionLockId contextId) {
162170
}
163171

164172
void ConnectionPool::closeAll() {
173+
isClosed = true;
174+
// Stop any callbacks
175+
sqlite3_commit_hook(writeConnection.connection,
176+
NULL, NULL);
177+
sqlite3_rollback_hook(writeConnection.connection,
178+
NULL, NULL);
179+
sqlite3_update_hook(writeConnection.connection,
180+
NULL, NULL);
165181
writeConnection.close();
166182
for (int i = 0; i < maxReads; i++) {
167183
readConnections[i]->close();

cpp/ConnectionPool.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ class ConnectionPool {
7575
bool isConcurrencyEnabled;
7676

7777
public:
78+
bool isClosed;
79+
7880
ConnectionPool(std::string dbName, std::string docPath,
7981
unsigned int numReadConnections);
8082
~ConnectionPool();

cpp/ConnectionState.cpp

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,17 @@ SQLiteOPResult genericSqliteOpenDb(string const dbName, string const docPath,
1010
ConnectionState::ConnectionState(const std::string dbName,
1111
const std::string docPath, int SQLFlags) {
1212
auto result = genericSqliteOpenDb(dbName, docPath, &connection, SQLFlags);
13-
14-
this->clearLock();
15-
threadDone = false;
16-
thread = new std::thread(&ConnectionState::doWork, this);
13+
if (result.type != SQLiteOk) {
14+
throw std::runtime_error("Failed to open SQLite database: " + result.errorMessage);
15+
}
16+
thread = std::thread(&ConnectionState::doWork, this);
17+
this->clearLock();
1718
}
1819

1920
ConnectionState::~ConnectionState() {
20-
// So threads know it's time to shut down
21-
threadDone = true;
22-
23-
// Wake up all the threads, so they can finish and be joined
24-
workQueueConditionVariable.notify_all();
25-
if (thread->joinable()) {
26-
thread->join();
21+
if (!isClosed) {
22+
close();
2723
}
28-
29-
delete thread;
3024
}
3125

3226
void ConnectionState::clearLock() {
@@ -64,21 +58,41 @@ std::future<void> ConnectionState::refreshSchema() {
6458
}
6559

6660
void ConnectionState::close() {
61+
{
62+
std::unique_lock<std::mutex> g(workQueueMutex);
63+
// prevent any new work from being queued
64+
isClosed = true;
65+
}
66+
67+
// Wait for the work queue to empty
6768
waitFinished();
68-
// So that the thread can stop (if not already)
69-
threadDone = true;
69+
70+
{
71+
// Now signal the thread to stop and notify it
72+
std::unique_lock<std::mutex> g(workQueueMutex);
73+
threadDone = true;
74+
workQueueConditionVariable.notify_all();
75+
}
76+
77+
// Join the worker thread
78+
if (thread.joinable()) {
79+
thread.join();
80+
}
81+
82+
// Safely close the SQLite connection
7083
sqlite3_close_v2(connection);
7184
}
7285

7386
void ConnectionState::queueWork(std::function<void(sqlite3 *)> task) {
74-
// Grab the mutex
75-
std::lock_guard<std::mutex> g(workQueueMutex);
76-
77-
// Push the request to the queue
78-
workQueue.push(task);
87+
{
88+
std::unique_lock<std::mutex> g(workQueueMutex);
89+
if (isClosed) {
90+
throw std::runtime_error("Connection is not open. Connection has been closed before queueing work.");
91+
}
92+
workQueue.push(task);
93+
}
7994

80-
// Notify one thread that there are requests to process
81-
workQueueConditionVariable.notify_all();
95+
workQueueConditionVariable.notify_all();
8296
}
8397

8498
void ConnectionState::doWork() {
@@ -104,9 +118,9 @@ void ConnectionState::doWork() {
104118
workQueue.pop();
105119
}
106120

107-
++threadBusy;
121+
threadBusy = true;
108122
task(connection);
109-
--threadBusy;
123+
threadBusy = false;
110124
// Need to notify in order for waitFinished to be updated when
111125
// the queue is empty and not busy
112126
{
@@ -118,11 +132,8 @@ void ConnectionState::doWork() {
118132

119133
void ConnectionState::waitFinished() {
120134
std::unique_lock<std::mutex> g(workQueueMutex);
121-
if (workQueue.empty()) {
122-
return;
123-
}
124135
workQueueConditionVariable.wait(
125-
g, [&] { return workQueue.empty() && (threadBusy == 0); });
136+
g, [&] { return workQueue.empty() && !threadBusy; });
126137
}
127138

128139
SQLiteOPResult genericSqliteOpenDb(string const dbName, string const docPath,

cpp/ConnectionState.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ class ConnectionState {
2525
// Mutex to protect workQueue
2626
std::mutex workQueueMutex;
2727
// Store thread in order to stop it gracefully
28-
std::thread *thread;
28+
std::thread thread;
2929
// This condition variable is used for the threads to wait until there is work
3030
// to do
3131
std::condition_variable_any workQueueConditionVariable;
32-
unsigned int threadBusy;
33-
bool threadDone;
32+
std::atomic<bool> threadBusy{false};
33+
std::atomic<bool> threadDone{false};
3434

3535
public:
36+
std::atomic<bool> isClosed{false};
37+
3638
ConnectionState(const std::string dbName, const std::string docPath,
3739
int SQLFlags);
3840
~ConnectionState();

cpp/bindings.cpp

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,30 @@ void transactionFinalizerHandler(const TransactionCallbackPayload *payload) {
7777
* This function triggers an async invocation to call watch callbacks,
7878
* avoiding holding SQLite up.
7979
*/
80-
invoker->invokeAsync([payload] {
80+
81+
// Make a copy of the payload data, this avoids a potential race condition
82+
// where the async invocation might occur after closing a connection
83+
auto dbName = std::make_shared<std::string>(*payload->dbName);
84+
int event = payload->event;
85+
invoker->invokeAsync([dbName, event] {
8186
try {
87+
88+
ConnectionPool* connection = getConnection(*dbName);
89+
if (connection == nullptr || connection->isClosed) {
90+
return;
91+
}
92+
8293
auto global = runtime->global();
8394
jsi::Function handlerFunction = global.getPropertyAsFunction(
8495
*runtime, "triggerTransactionFinalizerHook");
8596

86-
auto jsiDbName = jsi::String::createFromAscii(*runtime, *payload->dbName);
87-
auto jsiEventType = jsi::Value((int)payload->event);
97+
auto jsiDbName = jsi::String::createFromAscii(*runtime, *dbName);
98+
auto jsiEventType = jsi::Value(event);
8899
handlerFunction.call(*runtime, move(jsiDbName), move(jsiEventType));
89100
} catch (jsi::JSINativeException e) {
90101
std::cout << e.what() << std::endl;
102+
} catch (const std::exception& e) {
103+
std::cout << "Standard Exception: " << e.what() << std::endl;
91104
} catch (...) {
92105
std::cout << "Unknown error" << std::endl;
93106
}
@@ -384,7 +397,14 @@ void osp::install(jsi::Runtime &rt,
384397
}
385398
};
386399

387-
sqliteQueueInContext(dbName, contextLockId, task);
400+
auto response = sqliteQueueInContext(dbName, contextLockId, task);
401+
if (response.type == SQLiteError) {
402+
auto errorCtr = rt.global().getPropertyAsFunction(rt, "Error");
403+
auto error = errorCtr.callAsConstructor(
404+
rt, jsi::String::createFromUtf8(
405+
rt, response.errorMessage));
406+
reject->asObject(rt).asFunction(rt).call(rt, error);
407+
}
388408
return {};
389409
}));
390410

cpp/sqliteBridge.cpp

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ SQLiteOPResult generateNotOpenResult(std::string const &dbName) {
3232
};
3333
}
3434

35+
ConnectionPool *getConnection(std::string const dbName) {
36+
if (dbMap.count(dbName) == 0) {
37+
// Connection is already closed
38+
return nullptr;
39+
}
40+
41+
return dbMap[dbName];
42+
}
43+
44+
3545
/**
3646
* Opens SQL database with default settings
3747
*/
@@ -50,10 +60,18 @@ sqliteOpenDb(string const dbName, string const docPath,
5060
};
5161
}
5262

53-
dbMap[dbName] = new ConnectionPool(dbName, docPath, numReadConnections);
54-
dbMap[dbName]->setOnContextAvailable(contextAvailableCallback);
55-
dbMap[dbName]->setTableUpdateHandler(updateTableCallback);
56-
dbMap[dbName]->setTransactionFinalizerHandler(onTransactionFinalizedCallback);
63+
try {
64+
// Open the database
65+
dbMap[dbName] = new ConnectionPool(dbName, docPath, numReadConnections);
66+
dbMap[dbName]->setOnContextAvailable(contextAvailableCallback);
67+
dbMap[dbName]->setTableUpdateHandler(updateTableCallback);
68+
dbMap[dbName]->setTransactionFinalizerHandler(onTransactionFinalizedCallback);
69+
} catch (const std::exception &e) {
70+
return SQLiteOPResult{
71+
.type = SQLiteError,
72+
.errorMessage = e.what(),
73+
};
74+
}
5775

5876
return SQLiteOPResult{
5977
.type = SQLiteOk,
@@ -126,13 +144,6 @@ SQLiteOPResult sqliteRequestLock(std::string const dbName,
126144

127145
ConnectionPool *connection = dbMap[dbName];
128146

129-
if (connection == nullptr) {
130-
return SQLiteOPResult{
131-
.type = SQLiteOk,
132-
133-
};
134-
}
135-
136147
switch (lockType) {
137148
case ConcurrentLockType::ReadLock:
138149
connection->readLock(contextId);
@@ -147,6 +158,7 @@ SQLiteOPResult sqliteRequestLock(std::string const dbName,
147158

148159
return SQLiteOPResult{
149160
.type = SQLiteOk,
161+
150162
};
151163
}
152164

cpp/sqliteBridge.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ SQLiteOPResult sqliteCloseDb(string const dbName);
4040

4141
void sqliteCloseAll();
4242

43+
ConnectionPool *getConnection(std::string const dbName);
44+
4345
SQLiteOPResult sqliteRemoveDb(string const dbName, string const docPath);
4446

4547
/**

src/DBListenerManager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export interface DBListener extends BaseListener {
3939
* is started, committed or rolled back.
4040
*/
4141
writeTransaction: (event: WriteTransactionEvent) => void;
42+
43+
closed: () => void;
4244
}
4345

4446
export class DBListenerManager extends BaseObserver<DBListener> {}

0 commit comments

Comments
 (0)