Skip to content

Commit

Permalink
[BREAKING] Remove DatabaseFuture and concurrentRead
Browse files Browse the repository at this point in the history
That's one less DispatchSemaphore. We don't need this method anymore.
  • Loading branch information
groue committed Aug 25, 2024
1 parent 1a0403a commit 1de32c2
Show file tree
Hide file tree
Showing 6 changed files with 4 additions and 220 deletions.
18 changes: 0 additions & 18 deletions GRDB/Core/DatabasePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -442,24 +442,6 @@ extension DatabasePool: DatabaseReader {
}
}

public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
// The semaphore that blocks until futureResult is defined:
let futureSemaphore = DispatchSemaphore(value: 0)
var futureResult: Result<T, Error>? = nil

asyncConcurrentRead { dbResult in
// Fetch and release the future
futureResult = dbResult.flatMap { db in Result { try value(db) } }
futureSemaphore.signal()
}

return DatabaseFuture {
// Block the future until results are fetched
_ = futureSemaphore.wait(timeout: .distantFuture)
return try futureResult!.get()
}
}

public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
asyncConcurrentRead(value)
}
Expand Down
13 changes: 0 additions & 13 deletions GRDB/Core/DatabaseQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -266,19 +266,6 @@ extension DatabaseQueue: DatabaseReader {
try writer.reentrantSync(value)
}

public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
// DatabaseQueue can't perform parallel reads.
// Perform a blocking read instead.
return DatabaseFuture(Result {
// Check that we're on the writer queue, as documented
try writer.execute { db in
try db.isolated(readOnly: true) {
try value(db)
}
}
})
}

public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
// Check that we're on the writer queue...
writer.execute { db in
Expand Down
92 changes: 0 additions & 92 deletions GRDB/Core/DatabaseWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ import Dispatch
///
/// ### Reading from the Latest Committed Database State
///
/// - ``concurrentRead(_:)``
/// - ``spawnConcurrentRead(_:)``
/// - ``DatabaseFuture``
///
/// ### Unsafe Methods
///
Expand Down Expand Up @@ -251,49 +249,6 @@ public protocol DatabaseWriter: DatabaseReader {

// MARK: - Reading from Database

/// Schedules read-only database operations for execution, and returns a
/// future value.
///
/// This method must be called from the writer dispatch queue, outside of
/// any transaction. You'll get a fatal error otherwise.
///
/// Database operations performed by the `value` closure are isolated in a
/// transaction: they do not see changes performed by eventual concurrent
/// writes (even writes performed by other processes).
///
/// They see the database in the state left by the last updates performed
/// by the database writer.
///
/// To access the fetched results, you call the ``DatabaseFuture/wait()``
/// method of the returned future, on any dispatch queue.
///
/// In the example below, the number of players is fetched concurrently with
/// the player insertion. Yet the future is guaranteed to return zero:
///
/// ```swift
/// try writer.writeWithoutTransaction { db in
/// // Delete all players
/// try Player.deleteAll()
///
/// // Count players concurrently
/// let future = writer.concurrentRead { db in
/// return try Player.fetchCount()
/// }
///
/// // Insert a player
/// try Player(...).insert(db)
///
/// // Guaranteed to be zero
/// let count = try future.wait()
/// }
/// ```
///
/// - note: Usage of this method is discouraged, because waiting on the
/// returned ``DatabaseFuture`` blocks a thread. You may prefer
/// ``spawnConcurrentRead(_:)`` instead.
/// - parameter value: A closure which accesses the database.
func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T>

// Exposed for RxGRDB and GRBCombine. Naming is not stabilized.
/// Schedules read-only database operations for execution.
///
Expand Down Expand Up @@ -924,49 +879,6 @@ extension Publisher where Failure == Error {
}
#endif

/// A future database value.
///
/// You get instances of `DatabaseFuture` from the `DatabaseWriter`
/// ``DatabaseWriter/concurrentRead(_:)`` method. For example:
///
/// ```swift
/// let futureCount: Future<Int> = try writer.writeWithoutTransaction { db in
/// try Player(...).insert()
///
/// // Count players concurrently
/// return writer.concurrentRead { db in
/// return try Player.fetchCount()
/// }
/// }
///
/// let count: Int = try futureCount.wait()
/// ```
public class DatabaseFuture<Value> {
private var consumed = false
private let _wait: () throws -> Value

init(_ wait: @escaping () throws -> Value) {
_wait = wait
}

init(_ result: Result<Value, Error>) {
_wait = result.get
}

/// Blocks the current thread until the value is available, and returns it.
///
/// It is a programmer error to call this method several times.
///
/// - throws: Any error that prevented the value from becoming available.
public func wait() throws -> Value {
// Not thread-safe and quick and dirty.
// Goal is that users learn not to call this method twice.
GRDBPrecondition(consumed == false, "DatabaseFuture.wait() must be called only once")
consumed = true
return try _wait()
}
}

/// A type-erased database writer.
///
/// An instance of `AnyDatabaseWriter` forwards its operations to an underlying
Expand Down Expand Up @@ -1056,10 +968,6 @@ extension AnyDatabaseWriter: DatabaseWriter {
try base.unsafeReentrantWrite(updates)
}

public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
base.concurrentRead(value)
}

public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
base.spawnConcurrentRead(value)
}
Expand Down
29 changes: 2 additions & 27 deletions GRDB/Documentation.docc/Concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,31 +277,7 @@ let newPlayerCount = try dbPool.write { db in
}
```

➡️ The synchronous solution is the ``DatabaseWriter/concurrentRead(_:)`` method. It must be called from within a write access, outside of any transaction. It returns a ``DatabaseFuture`` which you consume any time later, with the ``DatabaseFuture/wait()`` method:

```swift
let future: DatabaseFuture<Int> = try dbPool.writeWithoutTransaction { db in
// Increment the number of players
try db.inTransaction {
try Player(...).insert(db)
return .commit
}

// <- Not in a transaction here
return dbPool.concurrentRead { db
try Player.fetchCount(db)
}
}

do {
// Handle the new player count - guaranteed greater than zero
let newPlayerCount = try future.wait()
} catch {
// Handle error
}
```

🔀 The asynchronous version of `concurrentRead` is ``DatabasePool/asyncConcurrentRead(_:)``:
🔀 The solution is ``DatabasePool/asyncConcurrentRead(_:)``. It must be called from within a write access, outside of any transaction:

```swift
try dbPool.writeWithoutTransaction { db in
Expand All @@ -324,11 +300,10 @@ try dbPool.writeWithoutTransaction { db in
}
```

Both ``DatabaseWriter/concurrentRead(_:)`` and ``DatabasePool/asyncConcurrentRead(_:)`` block until they can guarantee their closure argument an isolated access to the database, in the exact state left by the last transaction. It then asynchronously executes this closure.
The ``DatabasePool/asyncConcurrentRead(_:)`` method blocks until it can guarantee its closure argument an isolated access to the database, in the exact state left by the last transaction. It then asynchronously executes the closure.

In the illustration below, the striped band shows the delay needed for the reading thread to acquire isolation. Until then, no other thread can write:


![DatabasePool Concurrent Read](DatabasePoolConcurrentRead.png)

Types that conform to ``TransactionObserver`` can also use those methods in their ``TransactionObserver/databaseDidCommit(_:)`` method, in order to process database changes without blocking other threads that want to write into the database.
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
- [ ] GRDB7: Sendable: DatabaseDataDecodingStrategy (264d7fb5)
- [ ] GRDB7: Sendable: DatabaseDateDecodingStrategy (264d7fb5)
- [ ] GRDB7: Sendable: DatabaseColumnDecodingStrategy (264d7fb5)
- [ ] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8)
- [X] GRDB7/BREAKING: Remove DatabaseFuture and concurrentRead (05f7d3c8)
- [ ] GRDB7: Sendable: DatabaseFunction (6e691fe7)
- [ ] GRDB7: Sendable: DatabaseMigrator (22114ad4)
- [ ] GRDB7: Not Sendable: FilterCursor (b26e9709)
Expand Down
70 changes: 1 addition & 69 deletions Tests/GRDBTests/DatabasePoolConcurrencyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1074,74 +1074,6 @@ class DatabasePoolConcurrencyTests: GRDBTestCase {
try test(qos: .userInitiated)
}

// MARK: - ConcurrentRead

func testConcurrentReadOpensATransaction() throws {
let dbPool = try makeDatabasePool()
let future = dbPool.writeWithoutTransaction { db in
dbPool.concurrentRead { db in
XCTAssertTrue(db.isInsideTransaction)
do {
try db.execute(sql: "BEGIN DEFERRED TRANSACTION")
XCTFail("Expected error")
} catch {
}
}
}
try future.wait()
}

func testConcurrentReadOutsideOfTransaction() throws {
let dbPool = try makeDatabasePool()
try dbPool.write { db in
try db.create(table: "persons") { t in
t.primaryKey("id", .integer)
}
}

// Writer Reader
// dbPool.writeWithoutTransaction {
// >
// dbPool.concurrentRead {
// <
// INSERT INTO items (id) VALUES (NULL)
// >
let s1 = DispatchSemaphore(value: 0)
// } SELECT COUNT(*) FROM persons -> 0
// <
// }

let future: DatabaseFuture<Int> = try dbPool.writeWithoutTransaction { db in
let future: DatabaseFuture<Int> = dbPool.concurrentRead { db in
_ = s1.wait(timeout: .distantFuture)
return try! Int.fetchOne(db, sql: "SELECT COUNT(*) FROM persons")!
}
try db.execute(sql: "INSERT INTO persons DEFAULT VALUES")
s1.signal()
return future
}
XCTAssertEqual(try future.wait(), 0)
}

func testConcurrentReadError() throws {
// Necessary for this test to run as quickly as possible
dbConfiguration.readonlyBusyMode = .immediateError
let dbPool = try makeDatabasePool()
try dbPool.writeWithoutTransaction { db in
try db.execute(sql: "PRAGMA locking_mode=EXCLUSIVE")
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
let future = dbPool.concurrentRead { db in
fatalError("Should not run")
}
do {
try future.wait()
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_BUSY)
XCTAssertEqual(error.message!, "database is locked")
}
}
}

// MARK: - AsyncConcurrentRead

func testAsyncConcurrentReadOpensATransaction() throws {
Expand Down Expand Up @@ -1179,7 +1111,7 @@ class DatabasePoolConcurrencyTests: GRDBTestCase {
// Writer Reader
// dbPool.writeWithoutTransaction {
// >
// dbPool.concurrentRead {
// dbPool.asyncConcurrentRead {
// <
// INSERT INTO items (id) VALUES (NULL)
// >
Expand Down

0 comments on commit 1de32c2

Please sign in to comment.