Skip to content

Commit cbe986a

Browse files
authored
Fix journal mode related lock issue (#15)
⚠️ API breaking change. - remove setting default journal mode - remove setting default timeout - use `System.coreCount` for max connection number
1 parent e38509d commit cbe986a

3 files changed

Lines changed: 102 additions & 13 deletions

File tree

Sources/SQLiteNIOExtras/SQLiteClient.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Logging
9+
import NIOCore
910
import SQLiteNIO
1011

1112
/// A SQLite client backed by a connection pool.
@@ -49,9 +50,9 @@ public final class SQLiteClient: Sendable {
4950
/// Logger used for pool operations.
5051
public let logger: Logger
5152
/// Journal mode applied to each pooled connection.
52-
public let journalMode: JournalMode
53+
public let journalMode: JournalMode?
5354
/// Busy timeout, in milliseconds, applied to each pooled connection.
54-
public let busyTimeoutMilliseconds: Int
55+
public let busyTimeoutMilliseconds: Int?
5556
/// Foreign key enforcement mode applied to each pooled connection.
5657
public let foreignKeysMode: ForeignKeysMode
5758

@@ -68,15 +69,17 @@ public final class SQLiteClient: Sendable {
6869
storage: SQLiteConnection.Storage,
6970
logger: Logger,
7071
minimumConnections: Int = 1,
71-
maximumConnections: Int = 8,
72-
journalMode: JournalMode = .wal,
72+
maximumConnections: Int = System.coreCount,
73+
journalMode: JournalMode? = nil,
7374
foreignKeysMode: ForeignKeysMode = .on,
74-
busyTimeoutMilliseconds: Int = 1000
75+
busyTimeoutMilliseconds: Int? = nil
7576
) {
7677
precondition(minimumConnections >= 0)
7778
precondition(maximumConnections >= 1)
7879
precondition(minimumConnections <= maximumConnections)
79-
precondition(busyTimeoutMilliseconds >= 0)
80+
if let busyTimeoutMilliseconds {
81+
precondition(busyTimeoutMilliseconds >= 0)
82+
}
8083
self.storage = storage
8184
self.minimumConnections = minimumConnections
8285
self.maximumConnections = maximumConnections

Sources/SQLiteNIOExtras/SQLiteConnectionPool.swift

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,21 @@ actor SQLiteConnectionPool {
191191
logger: configuration.logger
192192
)
193193
do {
194+
let foreignKeys = configuration.foreignKeysMode
195+
194196
_ = try await connection.query(
195-
"PRAGMA journal_mode = \(configuration.journalMode.rawValue);"
196-
)
197-
_ = try await connection.query(
198-
"PRAGMA busy_timeout = \(configuration.busyTimeoutMilliseconds);"
199-
)
200-
_ = try await connection.query(
201-
"PRAGMA foreign_keys = \(configuration.foreignKeysMode.rawValue);"
197+
"PRAGMA foreign_keys = \(foreignKeys.rawValue);"
202198
)
199+
if let busyTimeout = configuration.busyTimeoutMilliseconds {
200+
_ = try await connection.query(
201+
"PRAGMA busy_timeout = \(busyTimeout);"
202+
)
203+
}
204+
if let journalMode = configuration.journalMode {
205+
_ = try await connection.query(
206+
"PRAGMA journal_mode = \(journalMode.rawValue);"
207+
)
208+
}
203209
}
204210
catch {
205211
await closeConnection(connection)

Tests/SQLiteNIOExtrasTests/SQLiteNIOExtrasTestSuite.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,84 @@ struct SQLiteNIOExtrasTestSuite {
229229
#expect(result[0].column("is_valid")?.bool == true)
230230
}
231231
}
232+
233+
// MARK: - lock
234+
235+
private actor LockBarrier {
236+
private var ready = false
237+
private var waiters: [CheckedContinuation<Void, Never>] = []
238+
239+
func waitUntilLocked() async {
240+
if ready { return }
241+
await withCheckedContinuation { continuation in
242+
waiters.append(continuation)
243+
}
244+
}
245+
246+
func markLocked() {
247+
guard !ready else { return }
248+
ready = true
249+
let pending = waiters
250+
waiters.removeAll(keepingCapacity: false)
251+
for continuation in pending {
252+
continuation.resume()
253+
}
254+
}
255+
}
256+
257+
@Test
258+
func warmupWaitsForTransientExclusiveLock() async throws {
259+
let dbPath =
260+
"/tmp/feather-lock-\(UInt64.random(in: 0...UInt64.max)).sqlite"
261+
262+
var logger = Logger(label: "test.sqlite.lock.warmup")
263+
logger.logLevel = .info
264+
265+
let config = SQLiteClient.Configuration(
266+
storage: .file(path: dbPath),
267+
logger: logger,
268+
minimumConnections: 1,
269+
maximumConnections: 1,
270+
journalMode: .delete,
271+
busyTimeoutMilliseconds: 5_000
272+
)
273+
274+
let clientA = SQLiteClient(configuration: config)
275+
let clientB = SQLiteClient(configuration: config)
276+
277+
try await clientA.run()
278+
defer {
279+
Task {
280+
await clientB.shutdown()
281+
await clientA.shutdown()
282+
}
283+
}
284+
285+
let barrier = LockBarrier()
286+
287+
let holder = Task {
288+
try await clientA.withConnection { connection in
289+
_ = try await connection.query("BEGIN EXCLUSIVE;")
290+
await barrier.markLocked()
291+
try await Task.sleep(for: .milliseconds(1200))
292+
_ = try await connection.query("COMMIT;")
293+
}
294+
}
295+
296+
await barrier.waitUntilLocked()
297+
298+
let clock = ContinuousClock()
299+
let start = clock.now
300+
301+
try await clientB.run()
302+
try await clientB.withConnection { connection in
303+
_ = try await connection.query("SELECT 1;")
304+
}
305+
306+
let elapsed = start.duration(to: clock.now)
307+
#expect(elapsed >= .milliseconds(900))
308+
309+
_ = try await holder.value
310+
}
311+
232312
}

0 commit comments

Comments
 (0)