Skip to content

Commit 3f928bc

Browse files
authored
Merge pull request groue#1794 from groue/dev/actor-access
Access task locals from asynchronous database accesses
2 parents 9db14cb + c91c183 commit 3f928bc

File tree

16 files changed

+322
-281
lines changed

16 files changed

+322
-281
lines changed

Documentation/DemoApps/GRDBDemo/GRDBDemo/Database/AppDatabase.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import os.log
1212
/// let dbQueue = try DatabaseQueue(configuration: config)
1313
/// let appDatabase = try AppDatabase(dbQueue)
1414
/// ```
15-
final class AppDatabase: Sendable {
15+
struct AppDatabase: Sendable {
1616
/// Access to the database.
1717
///
1818
/// See <https://swiftpackageindex.com/groue/GRDB.swift/documentation/grdb/databaseconnections>
@@ -130,7 +130,7 @@ extension AppDatabase {
130130

131131
/// Refresh all players (by performing some random changes, for demo purpose).
132132
func refreshPlayers() async throws {
133-
try await dbWriter.write { [self] db in
133+
try await dbWriter.write { db in
134134
if try Player.all().isEmpty(db) {
135135
// When database is empty, insert new random players
136136
try createRandomPlayers(db)

GRDB.xcodeproj/project.pbxproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@
273273
56A5EF0F1EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A5EF0E1EF7F20B00F03071 /* ForeignKeyInfoTests.swift */; };
274274
56A8C2301D1914540096E9D4 /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C22F1D1914540096E9D4 /* UUID.swift */; };
275275
56AACAA822ACED7100A40F2A /* Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AACAA722ACED7100A40F2A /* Fetch.swift */; };
276+
56AC93DF2E291C4700DB6C74 /* DispatchQueueActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC93DE2E291C4700DB6C74 /* DispatchQueueActor.swift */; };
276277
56AE64122229A53700AD1B0B /* HasOneThroughAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE64112229A53700AD1B0B /* HasOneThroughAssociation.swift */; };
277278
56AE6424222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AE6423222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift */; };
278279
56AFEF2F29969F6E00CA1E51 /* TransactionClock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AFEF2E29969F6E00CA1E51 /* TransactionClock.swift */; };
@@ -769,6 +770,7 @@
769770
56A8C22F1D1914540096E9D4 /* UUID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UUID.swift; sourceTree = "<group>"; };
770771
56A8C2361D1914790096E9D4 /* FoundationNSUUIDTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSUUIDTests.swift; sourceTree = "<group>"; };
771772
56AACAA722ACED7100A40F2A /* Fetch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fetch.swift; sourceTree = "<group>"; };
773+
56AC93DE2E291C4700DB6C74 /* DispatchQueueActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueActor.swift; sourceTree = "<group>"; };
772774
56AE64112229A53700AD1B0B /* HasOneThroughAssociation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HasOneThroughAssociation.swift; sourceTree = "<group>"; };
773775
56AE6423222AAC9500AD1B0B /* AssociationHasOneThroughSQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationHasOneThroughSQLTests.swift; sourceTree = "<group>"; };
774776
56AF746A1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleEscapingTests.swift; sourceTree = "<group>"; };
@@ -1595,13 +1597,13 @@
15951597
56A238751B9C75030082EB20 /* DatabaseValue.swift */,
15961598
560D923E1C672C3E00F4F92B /* DatabaseValueConvertible.swift */,
15971599
563363C31C942C37000BE133 /* DatabaseWriter.swift */,
1600+
56AC93DE2E291C4700DB6C74 /* DispatchQueueActor.swift */,
15981601
5636E9BB1D22574100B9B05F /* FetchRequest.swift */,
15991602
56A238761B9C75030082EB20 /* Row.swift */,
16001603
567404871CEF84C8003ED5CC /* RowAdapter.swift */,
16011604
566B9C1F25C6CC24004542CF /* RowDecodingError.swift */,
16021605
56BB6EA81D3009B100A1CA52 /* SchedulingWatchdog.swift */,
16031606
560A37A61C8FF6E500949E71 /* SerializedDatabase.swift */,
1604-
56D3331F29C38D6700430680 /* WALSnapshotTransaction.swift */,
16051607
56E9FAD7221053DC00C703A8 /* SQL.swift */,
16061608
569D6DDD220EF9E100A058A9 /* SQLInterpolation.swift */,
16071609
56FBFED82210731A00945324 /* SQLRequest.swift */,
@@ -1611,6 +1613,7 @@
16111613
56AFEF2E29969F6E00CA1E51 /* TransactionClock.swift */,
16121614
566B91321FA4D3810012D5B0 /* TransactionObserver.swift */,
16131615
56B7EE822863781300C0525F /* WALSnapshot.swift */,
1616+
56D3331F29C38D6700430680 /* WALSnapshotTransaction.swift */,
16141617
5605F1471C672E4000235C62 /* Support */,
16151618
);
16161619
path = Core;
@@ -2180,6 +2183,7 @@
21802183
563B8FC524A1D3B9007A48C9 /* OnDemandFuture.swift in Sources */,
21812184
5656A8B02295BFD7001FF3FF /* TableRecord+QueryInterfaceRequest.swift in Sources */,
21822185
5613ED4421A95B2C00DC7A68 /* ValueReducer.swift in Sources */,
2186+
56AC93DF2E291C4700DB6C74 /* DispatchQueueActor.swift in Sources */,
21832187
5636E9BC1D22574100B9B05F /* FetchRequest.swift in Sources */,
21842188
566DDE0D288D763C0000DCFB /* Fixits.swift in Sources */,
21852189
56BB6EA91D3009B100A1CA52 /* SchedulingWatchdog.swift in Sources */,

GRDB/Core/Database.swift

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,7 +1149,9 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
11491149

11501150
// See <https://www.sqlite.org/c3ref/interrupt.html>
11511151
func interrupt() {
1152-
sqlite3_interrupt(sqliteConnection)
1152+
if let sqliteConnection {
1153+
sqlite3_interrupt(sqliteConnection)
1154+
}
11531155
}
11541156

11551157
// MARK: - Database Suspension
@@ -1223,22 +1225,31 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
12231225
}
12241226
}
12251227

1226-
/// Cancels the current database access. All statements but ROLLBACK
1227-
/// will throw `CancellationError`, until `uncancel()` is called.
1228-
///
1229-
/// This method can be called from any thread.
1230-
func cancel() {
1231-
let needsInterrupt = suspensionMutex.withLock { suspension in
1232-
if suspension.isCancelled {
1233-
return false
1234-
}
1235-
1236-
suspension.isCancelled = true
1237-
return suspension.interruptsWhenCancelled
1228+
/// Returns a closure that cancels the current database access.
1229+
/// Most statements will throw `CancellationError`, until `uncancel()`
1230+
/// is called.
1231+
var cancel: @Sendable () -> Void {
1232+
// Workaround the fact that SQLiteConnection is not Sendable.
1233+
struct Connection: @unchecked Sendable {
1234+
var sqliteConnection: SQLiteConnection?
12381235
}
1236+
let connection = Connection(sqliteConnection: sqliteConnection)
12391237

1240-
if needsInterrupt {
1241-
interrupt()
1238+
return { [suspensionMutex, connection] in
1239+
let needsInterrupt = suspensionMutex.withLock { suspension in
1240+
if suspension.isCancelled {
1241+
return false
1242+
}
1243+
1244+
suspension.isCancelled = true
1245+
return suspension.interruptsWhenCancelled
1246+
}
1247+
1248+
if needsInterrupt {
1249+
if let sqliteConnection = connection.sqliteConnection {
1250+
sqlite3_interrupt(sqliteConnection)
1251+
}
1252+
}
12421253
}
12431254
}
12441255

@@ -1318,6 +1329,11 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
13181329
return
13191330
}
13201331

1332+
// Commits when read-only are just like rollbacks above.
1333+
if statement.transactionEffect == .commitTransaction && isReadOnly {
1334+
return
1335+
}
1336+
13211337
// Suspension should not prevent adjusting the read-only mode.
13221338
// See <https://github.com/groue/GRDB.swift/issues/1715>.
13231339
if statement.isQueryOnlyPragma {

GRDB/Core/DatabasePool.swift

Lines changed: 46 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -352,40 +352,24 @@ extension DatabasePool: DatabaseReader {
352352
}
353353

354354
public func read<T: Sendable>(
355-
_ value: @escaping @Sendable (Database) throws -> T
355+
_ value: @Sendable (Database) throws -> T
356356
) async throws -> T {
357-
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
358357
guard let readerPool else {
359358
throw DatabaseError.connectionIsClosed()
360359
}
361360

362-
let dbAccess = CancellableDatabaseAccess()
363-
return try await dbAccess.withCancellableContinuation { continuation in
364-
readerPool.asyncGet { result in
365-
do {
366-
let (reader, releaseReader) = try result.get()
367-
// Second async jump because that's how `Pool.async` has to be used.
368-
reader.async { db in
369-
defer {
370-
try? db.commit() // Ignore commit error
371-
releaseReader(.reuse)
372-
}
373-
do {
374-
let result = try dbAccess.inDatabase(db) {
375-
// The block isolation comes from the DEFERRED transaction.
376-
try db.beginTransaction(.deferred)
377-
try db.clearSchemaCacheIfNeeded()
378-
return try value(db)
379-
}
380-
continuation.resume(returning: result)
381-
} catch {
382-
continuation.resume(throwing: error)
383-
}
384-
}
385-
} catch {
386-
continuation.resume(throwing: error)
387-
}
388-
}
361+
return try await readerPool.get { reader in
362+
try await reader.execute { db in
363+
defer {
364+
// Ignore commit error, but make sure we leave the transaction
365+
try? db.commit()
366+
assert(!db.isInsideTransaction)
367+
}
368+
// The block isolation comes from the DEFERRED transaction.
369+
try db.beginTransaction(.deferred)
370+
try db.clearSchemaCacheIfNeeded()
371+
return try value(db)
372+
}
389373
}
390374
}
391375

@@ -403,7 +387,9 @@ extension DatabasePool: DatabaseReader {
403387
// Second async jump because that's how `Pool.async` has to be used.
404388
reader.async { db in
405389
defer {
406-
try? db.commit() // Ignore commit error
390+
// Ignore commit error, but make sure we leave the transaction
391+
try? db.commit()
392+
assert(!db.isInsideTransaction)
407393
releaseReader(.reuse)
408394
}
409395
do {
@@ -436,35 +422,16 @@ extension DatabasePool: DatabaseReader {
436422
}
437423

438424
public func unsafeRead<T: Sendable>(
439-
_ value: @escaping @Sendable (Database) throws -> T
425+
_ value: @Sendable (Database) throws -> T
440426
) async throws -> T {
441427
guard let readerPool else {
442428
throw DatabaseError.connectionIsClosed()
443429
}
444430

445-
let dbAccess = CancellableDatabaseAccess()
446-
return try await dbAccess.withCancellableContinuation { continuation in
447-
readerPool.asyncGet { result in
448-
do {
449-
let (reader, releaseReader) = try result.get()
450-
// Second async jump because that's how `Pool.async` has to be used.
451-
reader.async { db in
452-
defer {
453-
releaseReader(.reuse)
454-
}
455-
do {
456-
let result = try dbAccess.inDatabase(db) {
457-
try db.clearSchemaCacheIfNeeded()
458-
return try value(db)
459-
}
460-
continuation.resume(returning: result)
461-
} catch {
462-
continuation.resume(throwing: error)
463-
}
464-
}
465-
} catch {
466-
continuation.resume(throwing: error)
467-
}
431+
return try await readerPool.get { reader in
432+
try await reader.execute { db in
433+
try db.clearSchemaCacheIfNeeded()
434+
return try value(db)
468435
}
469436
}
470437
}
@@ -584,7 +551,9 @@ extension DatabasePool: DatabaseReader {
584551
let (reader, releaseReader) = try readerPool.get()
585552
reader.async { db in
586553
defer {
587-
try? db.commit() // Ignore commit error
554+
// Ignore commit error, but make sure we leave the transaction
555+
try? db.commit()
556+
assert(!db.isInsideTransaction)
588557
releaseReader(.reuse)
589558
}
590559
do {
@@ -802,7 +771,7 @@ extension DatabasePool: DatabaseWriter {
802771
}
803772

804773
public func writeWithoutTransaction<T: Sendable>(
805-
_ updates: @escaping @Sendable (Database) throws -> T
774+
_ updates: @Sendable (Database) throws -> T
806775
) async throws -> T {
807776
try await writer.execute(updates)
808777
}
@@ -818,22 +787,31 @@ extension DatabasePool: DatabaseWriter {
818787
}
819788

820789
public func barrierWriteWithoutTransaction<T: Sendable>(
821-
_ updates: @escaping @Sendable (Database) throws -> T
790+
_ updates: @Sendable (Database) throws -> T
822791
) async throws -> T {
823-
let dbAccess = CancellableDatabaseAccess()
824-
return try await dbAccess.withCancellableContinuation { continuation in
825-
asyncBarrierWriteWithoutTransaction { dbResult in
826-
do {
827-
try dbAccess.checkCancellation()
828-
let db = try dbResult.get()
829-
let result = try dbAccess.inDatabase(db) {
830-
try updates(db)
792+
guard let readerPool else {
793+
throw DatabaseError.connectionIsClosed()
794+
}
795+
796+
// Pool.barrier does not support async calls (yet?).
797+
// So we perform cancellation checks just as in
798+
// the async version of SerializedDatabase.execute().
799+
let cancelMutex = Mutex<(@Sendable () -> Void)?>(nil)
800+
return try await withTaskCancellationHandler {
801+
try Task.checkCancellation()
802+
return try await readerPool.barrier {
803+
try Task.checkCancellation()
804+
return try writer.sync { db in
805+
defer {
806+
db.uncancel()
831807
}
832-
continuation.resume(returning: result)
833-
} catch {
834-
continuation.resume(throwing: error)
808+
cancelMutex.store(db.cancel)
809+
try Task.checkCancellation()
810+
return try updates(db)
835811
}
836812
}
813+
} onCancel: {
814+
cancelMutex.withLock { $0?() }
837815
}
838816
}
839817

GRDB/Core/DatabaseQueue.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ extension DatabaseQueue: DatabaseReader {
234234
}
235235

236236
public func read<T: Sendable>(
237-
_ value: @escaping @Sendable (Database) throws -> T
237+
_ value: @Sendable (Database) throws -> T
238238
) async throws -> T {
239239
try await writer.execute { db in
240240
try db.isolated(readOnly: true) {
@@ -248,8 +248,9 @@ extension DatabaseQueue: DatabaseReader {
248248
) {
249249
writer.async { db in
250250
defer {
251-
// Ignore error because we can not notify it.
251+
// Ignore commit error (we can not notify it), but make sure we leave the transaction
252252
try? db.commit()
253+
assert(!db.isInsideTransaction)
253254
try? db.endReadOnly()
254255
}
255256

@@ -272,7 +273,7 @@ extension DatabaseQueue: DatabaseReader {
272273
}
273274

274275
public func unsafeRead<T: Sendable>(
275-
_ value: @escaping @Sendable (Database) throws -> T
276+
_ value: @Sendable (Database) throws -> T
276277
) async throws -> T {
277278
try await writer.execute(value)
278279
}
@@ -296,8 +297,9 @@ extension DatabaseQueue: DatabaseReader {
296297
GRDBPrecondition(!db.isInsideTransaction, "must not be called from inside a transaction.")
297298

298299
defer {
299-
// Ignore error because we can not notify it.
300+
// Ignore commit error (we can not notify it), but make sure we leave the transaction
300301
try? db.commit()
302+
assert(!db.isInsideTransaction)
301303
try? db.endReadOnly()
302304
}
303305

@@ -385,7 +387,7 @@ extension DatabaseQueue: DatabaseWriter {
385387
}
386388

387389
public func writeWithoutTransaction<T: Sendable>(
388-
_ updates: @escaping @Sendable (Database) throws -> T
390+
_ updates: @Sendable (Database) throws -> T
389391
) async throws -> T {
390392
try await writer.execute(updates)
391393
}
@@ -396,7 +398,7 @@ extension DatabaseQueue: DatabaseWriter {
396398
}
397399

398400
public func barrierWriteWithoutTransaction<T: Sendable>(
399-
_ updates: @escaping @Sendable (Database) throws -> T
401+
_ updates: @Sendable (Database) throws -> T
400402
) async throws -> T {
401403
try await writer.execute(updates)
402404
}

0 commit comments

Comments
 (0)