Skip to content

Commit 8226d1e

Browse files
close pending locks when database is closed
1 parent 421acd6 commit 8226d1e

File tree

5 files changed

+114
-39
lines changed

5 files changed

+114
-39
lines changed

cpp/ConnectionState.cpp

+11-8
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,19 @@ std::future<void> ConnectionState::refreshSchema() {
5858
}
5959

6060
void ConnectionState::close() {
61-
// prevent any new work from being queued
62-
isClosed = true;
61+
{
62+
// Now signal the thread to stop and notify it
63+
std::unique_lock<std::mutex> g(workQueueMutex);
64+
// prevent any new work from being queued
65+
isClosed = true;
66+
}
6367

6468
// Wait for the work queue to empty
6569
waitFinished();
6670

6771
{
6872
// Now signal the thread to stop and notify it
69-
std::lock_guard<std::mutex> g(workQueueMutex);
73+
std::unique_lock<std::mutex> g(workQueueMutex);
7074
threadDone = true;
7175
workQueueConditionVariable.notify_all();
7276
}
@@ -81,12 +85,11 @@ void ConnectionState::close() {
8185
}
8286

8387
void ConnectionState::queueWork(std::function<void(sqlite3 *)> task) {
84-
if (isClosed) {
85-
throw std::runtime_error("Connection is not open. Connection has been closed before queueing work.");
86-
}
87-
8888
{
89-
std::lock_guard<std::mutex> g(workQueueMutex);
89+
std::unique_lock<std::mutex> g(workQueueMutex);
90+
if (isClosed) {
91+
throw std::runtime_error("Connection is not open. Connection has been closed before queueing work.");
92+
}
9093
workQueue.push(task);
9194
}
9295

cpp/sqliteBridge.cpp

+1-7
Original file line numberDiff line numberDiff line change
@@ -134,13 +134,6 @@ SQLiteOPResult sqliteRequestLock(std::string const dbName,
134134

135135
ConnectionPool *connection = dbMap[dbName];
136136

137-
if (connection == nullptr) {
138-
return SQLiteOPResult{
139-
.type = SQLiteOk,
140-
141-
};
142-
}
143-
144137
switch (lockType) {
145138
case ConcurrentLockType::ReadLock:
146139
connection->readLock(contextId);
@@ -155,6 +148,7 @@ SQLiteOPResult sqliteRequestLock(std::string const dbName,
155148

156149
return SQLiteOPResult{
157150
.type = SQLiteOk,
151+
158152
};
159153
}
160154

src/setup-open.ts

+43-14
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import {
2-
ISQLite,
32
ConcurrentLockType,
4-
QuickSQLiteConnection,
53
ContextLockID,
4+
ISQLite,
65
LockContext,
76
LockOptions,
8-
TransactionContext,
9-
UpdateCallback,
10-
SQLBatchTuple,
117
OpenOptions,
12-
QueryResult
8+
QueryResult,
9+
QuickSQLiteConnection,
10+
SQLBatchTuple,
11+
TransactionContext,
12+
UpdateCallback
1313
} from './types';
1414

15-
import { enhanceQueryResult } from './utils';
1615
import { DBListenerManagerInternal } from './DBListenerManager';
1716
import { LockHooks } from './lock-hooks';
17+
import { enhanceQueryResult } from './utils';
1818

1919
type LockCallbackRecord = {
2020
callback: (context: LockContext) => Promise<any>;
@@ -39,11 +39,16 @@ const getRequestId = () => {
3939
const LockCallbacks: Record<ContextLockID, LockCallbackRecord> = {};
4040
let proxy: ISQLite;
4141

42+
/**
43+
* Creates a unique identifier for all a database's lock requests
44+
*/
45+
const lockKey = (dbName: string, id: ContextLockID) => `${dbName}:${id}`;
46+
4247
/**
4348
* Closes the context in JS and C++
4449
*/
4550
function closeContextLock(dbName: string, id: ContextLockID) {
46-
delete LockCallbacks[id];
51+
delete LockCallbacks[lockKey(dbName, id)];
4752

4853
// This is configured by the setupOpen function
4954
proxy.releaseLock(dbName, id);
@@ -59,7 +64,11 @@ global.onLockContextIsAvailable = async (dbName: string, lockId: ContextLockID)
5964
// Don't hold C++ bridge side up waiting to complete
6065
setImmediate(async () => {
6166
try {
62-
const record = LockCallbacks[lockId];
67+
const key = lockKey(dbName, lockId);
68+
const record = LockCallbacks[key];
69+
// clear record after fetching, the hash should only contain pending requests
70+
delete LockCallbacks[key];
71+
6372
if (record?.timeout) {
6473
clearTimeout(record.timeout);
6574
}
@@ -116,12 +125,12 @@ export function setupOpen(QuickSQLite: ISQLite) {
116125
// Wrap the callback in a promise that will resolve to the callback result
117126
return new Promise<T>((resolve, reject) => {
118127
// Add callback to the queue for timing
119-
const record = (LockCallbacks[id] = {
128+
const key = lockKey(dbName, id);
129+
const record = (LockCallbacks[key] = {
120130
callback: async (context: LockContext) => {
121131
try {
122132
await hooks?.lockAcquired?.();
123133
const res = await callback(context);
124-
125134
closeContextLock(dbName, id);
126135
resolve(res);
127136
} catch (ex) {
@@ -134,18 +143,19 @@ export function setupOpen(QuickSQLite: ISQLite) {
134143
} as LockCallbackRecord);
135144

136145
try {
146+
// throws if lock could not be requested
137147
QuickSQLite.requestLock(dbName, id, type);
138148
const timeout = options?.timeoutMs;
139149
if (timeout) {
140150
record.timeout = setTimeout(() => {
141151
// The callback won't be executed
142-
delete LockCallbacks[id];
152+
delete LockCallbacks[key];
143153
reject(new Error(`Lock request timed out after ${timeout}ms`));
144154
}, timeout);
145155
}
146156
} catch (ex) {
147157
// Remove callback from the queue
148-
delete LockCallbacks[id];
158+
delete LockCallbacks[key];
149159
reject(ex);
150160
}
151161
});
@@ -224,7 +234,26 @@ export function setupOpen(QuickSQLite: ISQLite) {
224234

225235
// Return the concurrent connection object
226236
return {
227-
close: () => QuickSQLite.close(dbName),
237+
close: () => {
238+
QuickSQLite.close(dbName);
239+
// Reject any pending lock requests
240+
Object.entries(LockCallbacks).forEach(([key, record]) => {
241+
const recordDBName = key.split(':')[0];
242+
if (dbName !== recordDBName) {
243+
return;
244+
}
245+
// A bit of a hack, let the callbacks run with an execute method that will fail
246+
record
247+
.callback({
248+
execute: async () => {
249+
throw new Error('Connection is closed');
250+
}
251+
})
252+
.catch(() => {});
253+
254+
delete LockCallbacks[key];
255+
});
256+
},
228257
refreshSchema: () => QuickSQLite.refreshSchema(dbName),
229258
execute: (sql: string, args?: any[]) => writeLock((context) => context.execute(sql, args)),
230259
readLock,

tests/android/build.gradle

-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22

3-
task printEnv() {
4-
System.out.println( System.getenv().toString())
5-
}
6-
73
buildscript {
84
ext {
95
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'

tests/tests/sqlite/rawQueries.spec.ts

+59-6
Original file line numberDiff line numberDiff line change
@@ -651,29 +651,82 @@ export function registerBaseTests() {
651651
});
652652

653653
it('Should handle multiple closes', async () => {
654-
// populate test data
655654
const dbName = 'test-close';
655+
656+
// populate test data
656657
const db = open(dbName);
657658
await db.execute('CREATE TABLE IF NOT EXISTS t1(id INTEGER PRIMARY KEY, c TEXT)');
658659
await db.execute('DELETE FROM t1');
659-
// Bulk insert 50000 rows without using a transaction
660+
// // Bulk insert 50000 rows without using a transaction
660661
const bulkInsertCommands: SQLBatchTuple[] = [];
661662
for (let i = 0; i < 50000; i++) {
662663
bulkInsertCommands.push(['INSERT INTO t1(id, c) VALUES(?, ?)', [i + 1, `value${i + 1}`]]);
663664
}
664665
await db.executeBatch(bulkInsertCommands);
665666
db.close();
666667

667-
for (let i = 1; i < 1_000; i++) {
668+
for (let i = 1; i < 100; i++) {
668669
const db = open(dbName, {
669670
numReadConnections: NUM_READ_CONNECTIONS
670671
});
671672

672-
// don't await this on purpose
673-
const pExecute = db.execute(`SELECT * FROM t1} `);
673+
// ensure a regular query works
674+
const pExecute = await db.execute(`SELECT * FROM t1 `);
675+
expect(pExecute.rows?.length).to.equal(50000);
676+
677+
// Queue a bunch of write locks, these will fail due to the db being closed
678+
// before they are accepted.
679+
const tests = [
680+
db.execute(`SELECT * FROM t1 `),
681+
db.execute(`SELECT * FROM t1 `),
682+
db.execute(`SELECT * FROM t1 `),
683+
db.execute(`SELECT * FROM t1 `)
684+
];
685+
686+
db.close();
687+
688+
const results = await Promise.allSettled(tests);
689+
expect(results.map((r) => r.status)).deep.equal(Array(tests.length).fill('rejected'));
690+
}
691+
});
692+
693+
it('Should wait for locks before close', async () => {
694+
const dbName = 'test-lock-close';
695+
696+
// populate test data
697+
const db = open(dbName);
698+
await db.execute('CREATE TABLE IF NOT EXISTS t1(id INTEGER PRIMARY KEY, c TEXT)');
699+
await db.execute('DELETE FROM t1');
700+
// // Bulk insert 50000 rows without using a transaction
701+
const bulkInsertCommands: SQLBatchTuple[] = [];
702+
for (let i = 0; i < 50000; i++) {
703+
bulkInsertCommands.push(['INSERT INTO t1(id, c) VALUES(?, ?)', [i + 1, `value${i + 1}`]]);
704+
}
705+
await db.executeBatch(bulkInsertCommands);
706+
db.close();
707+
708+
for (let i = 1; i < 10; i++) {
709+
const db = open(dbName, {
710+
numReadConnections: NUM_READ_CONNECTIONS
711+
});
712+
713+
const promises: Promise<QueryResult>[] = [];
714+
// ensure a regular query
715+
db.writeLock(async (tx) => {
716+
await tx.execute(`SELECT * FROM t1 `);
717+
// Don't await these
718+
promises.push(
719+
tx.execute(`SELECT * FROM t1 `),
720+
tx.execute(`SELECT * FROM t1 `),
721+
tx.execute(`SELECT * FROM t1 `),
722+
tx.execute(`SELECT * FROM t1 `)
723+
);
724+
});
725+
674726
db.close();
675727

676-
await expect(pExecute).to.eventually.rejectedWith('is not open');
728+
const results = await Promise.all(promises);
729+
expect(results.map((r) => r.rows?.length)).deep.equal(Array(promises.length).fill(50000));
677730
}
678731
});
679732
});

0 commit comments

Comments
 (0)