Skip to content

Commit 5dbcb64

Browse files
committed
Use busy-timeout for database-level locks.
1 parent b665748 commit 5dbcb64

File tree

4 files changed

+67
-24
lines changed

4 files changed

+67
-24
lines changed

lib/src/sqlite_connection_impl.dart

+5-20
Original file line numberDiff line numberDiff line change
@@ -123,30 +123,15 @@ class SqliteConnectionImpl with SqliteQueries implements SqliteConnection {
123123
@override
124124
Future<T> writeLock<T>(Future<T> Function(SqliteWriteContext tx) callback,
125125
{Duration? lockTimeout, String? debugContext}) async {
126-
final stopWatch = lockTimeout == null ? null : (Stopwatch()..start());
127126
// Private lock to synchronize this with other statements on the same connection,
128127
// to ensure that transactions aren't interleaved.
129128
return await _connectionMutex.lock(() async {
130-
Duration? innerTimeout;
131-
if (lockTimeout != null && stopWatch != null) {
132-
innerTimeout = lockTimeout - stopWatch.elapsed;
133-
stopWatch.stop();
129+
final ctx = _TransactionContext(_isolateClient);
130+
try {
131+
return await callback(ctx);
132+
} finally {
133+
await ctx.close();
134134
}
135-
// DB lock so that only one write happens at a time
136-
return await _writeMutex.lock(() async {
137-
final ctx = _TransactionContext(_isolateClient);
138-
try {
139-
return await callback(ctx);
140-
} finally {
141-
await ctx.close();
142-
}
143-
}, timeout: innerTimeout).catchError((error, stackTrace) {
144-
if (error is TimeoutException) {
145-
return Future<T>.error(TimeoutException(
146-
'Failed to acquire global write lock', lockTimeout));
147-
}
148-
return Future<T>.error(error, stackTrace);
149-
});
150135
}, timeout: lockTimeout);
151136
}
152137
}

lib/src/sqlite_open_factory.dart

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'dart:async';
22

3-
import 'package:sqlite3/sqlite3.dart' as sqlite;
3+
import 'package:sqlite_async/sqlite3.dart' as sqlite;
44

55
import 'sqlite_options.dart';
66

@@ -29,6 +29,11 @@ class DefaultSqliteOpenFactory implements SqliteOpenFactory {
2929
List<String> pragmaStatements(SqliteOpenOptions options) {
3030
List<String> statements = [];
3131

32+
if (sqliteOptions.busyTimeout != null) {
33+
statements.add(
34+
'PRAGMA busy_timeout = ${sqliteOptions.busyTimeout!.inMilliseconds}');
35+
}
36+
3237
if (options.primaryConnection && sqliteOptions.journalMode != null) {
3338
// Persisted - only needed on the primary connection
3439
statements
@@ -51,8 +56,21 @@ class DefaultSqliteOpenFactory implements SqliteOpenFactory {
5156
final mode = options.openMode;
5257
var db = sqlite.sqlite3.open(path, mode: mode, mutex: false);
5358

59+
// Pragma statements don't have the same BUSY_TIMEOUT behavior as normal statements.
60+
// We add a manual retry loop for those.
5461
for (var statement in pragmaStatements(options)) {
55-
db.execute(statement);
62+
for (var tries = 0; tries < 30; tries++) {
63+
try {
64+
db.execute(statement);
65+
break;
66+
} on sqlite.SqliteException catch (e) {
67+
if (e.resultCode == sqlite.SqlError.SQLITE_BUSY && tries < 29) {
68+
continue;
69+
} else {
70+
rethrow;
71+
}
72+
}
73+
}
5674
}
5775
return db;
5876
}

lib/src/sqlite_options.dart

+9-2
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,22 @@ class SqliteOptions {
1111
/// attempt to truncate the file afterwards.
1212
final int? journalSizeLimit;
1313

14+
/// Timeout waiting for locks to be released by other write connections.
15+
/// Defaults to 30 seconds.
16+
/// Set to 0 to fail immediately when the database is locked.
17+
final Duration? busyTimeout;
18+
1419
const SqliteOptions.defaults()
1520
: journalMode = SqliteJournalMode.wal,
1621
journalSizeLimit = 6 * 1024 * 1024, // 1.5x the default checkpoint size
17-
synchronous = SqliteSynchronous.normal;
22+
synchronous = SqliteSynchronous.normal,
23+
busyTimeout = const Duration(seconds: 30);
1824

1925
const SqliteOptions(
2026
{this.journalMode = SqliteJournalMode.wal,
2127
this.journalSizeLimit = 6 * 1024 * 1024,
22-
this.synchronous = SqliteSynchronous.normal});
28+
this.synchronous = SqliteSynchronous.normal,
29+
this.busyTimeout = const Duration(seconds: 30)});
2330
}
2431

2532
/// SQLite journal mode. Set on the primary connection.

test/basic_test.dart

+33
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,39 @@ void main() {
6464
}
6565
});
6666

67+
test('Concurrency 2', () async {
68+
final db1 =
69+
SqliteDatabase.withFactory(testFactory(path: path), maxReaders: 3);
70+
71+
final db2 =
72+
SqliteDatabase.withFactory(testFactory(path: path), maxReaders: 3);
73+
await db1.initialize();
74+
await createTables(db1);
75+
await db2.initialize();
76+
print("${DateTime.now()} start");
77+
78+
var futures1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) {
79+
return db1.execute(
80+
"INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 1 ' || datetime() as connection RETURNING *",
81+
[
82+
i,
83+
5 + Random().nextInt(20)
84+
]).then((value) => print("${DateTime.now()} $value"));
85+
}).toList();
86+
87+
var futures2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) {
88+
return db2.execute(
89+
"INSERT OR REPLACE INTO test_data(id, description) SELECT ? as i, test_sleep(?) || ' ' || test_connection_name() || ' 2 ' || datetime() as connection RETURNING *",
90+
[
91+
i,
92+
5 + Random().nextInt(20)
93+
]).then((value) => print("${DateTime.now()} $value"));
94+
}).toList();
95+
await Future.wait(futures1);
96+
await Future.wait(futures2);
97+
print("${DateTime.now()} done");
98+
});
99+
67100
test('read-only transactions', () async {
68101
final db = await setupDatabase(path: path);
69102
await createTables(db);

0 commit comments

Comments
 (0)