Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GRDB 7: Sendable database accesses #1618

Merged
merged 14 commits into from
Sep 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 29 additions & 17 deletions GRDB/Core/DatabasePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ extension DatabasePool: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func read<T>(
public func read<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
Expand Down Expand Up @@ -390,7 +390,9 @@ extension DatabasePool: DatabaseReader {
}
}

public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
guard let readerPool else {
value(.failure(DatabaseError.connectionIsClosed()))
return
Expand Down Expand Up @@ -435,7 +437,7 @@ extension DatabasePool: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func unsafeRead<T>(
public func unsafeRead<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
guard let readerPool else {
Expand Down Expand Up @@ -469,7 +471,9 @@ extension DatabasePool: DatabaseReader {
}
}

public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncUnsafeRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
guard let readerPool else {
value(.failure(DatabaseError.connectionIsClosed()))
return
Expand Down Expand Up @@ -514,7 +518,9 @@ extension DatabasePool: DatabaseReader {
}
}

public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func spawnConcurrentRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
asyncConcurrentRead(value)
}

Expand Down Expand Up @@ -555,7 +561,9 @@ extension DatabasePool: DatabaseReader {
/// ```
///
/// - parameter value: A function that accesses the database.
public func asyncConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncConcurrentRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
// Check that we're on the writer queue...
writer.execute { db in
// ... and that no transaction is opened.
Expand Down Expand Up @@ -714,7 +722,9 @@ extension DatabasePool: DatabaseReader {
///
/// - important: The `completion` argument is executed in a serial
/// dispatch queue, so make sure you use the transaction asynchronously.
func asyncWALSnapshotTransaction(_ completion: @escaping (Result<WALSnapshotTransaction, Error>) -> Void) {
func asyncWALSnapshotTransaction(
_ completion: @escaping @Sendable (Result<WALSnapshotTransaction, Error>) -> Void
) {
guard let readerPool else {
completion(.failure(DatabaseError.connectionIsClosed()))
return
Expand All @@ -740,9 +750,8 @@ extension DatabasePool: DatabaseReader {
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
if configuration.readonly {
// The easy case: the database does not change
return _addReadOnly(
Expand Down Expand Up @@ -771,9 +780,8 @@ extension DatabasePool: DatabaseReader {
private func _addConcurrent<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
assert(!configuration.readonly, "Use _addReadOnly(observation:) instead")
assert(!observation.requiresWriteAccess, "Use _addWriteOnly(observation:) instead")
let observer = ValueConcurrentObserver(
Expand All @@ -796,7 +804,7 @@ extension DatabasePool: DatabaseWriter {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func writeWithoutTransaction<T>(
public func writeWithoutTransaction<T: Sendable>(
_ updates: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute(updates)
Expand All @@ -813,7 +821,7 @@ extension DatabasePool: DatabaseWriter {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func barrierWriteWithoutTransaction<T>(
public func barrierWriteWithoutTransaction<T: Sendable>(
_ updates: @escaping @Sendable (Database) throws -> T
) async throws -> T {
let dbAccess = CancellableDatabaseAccess()
Expand All @@ -833,7 +841,9 @@ extension DatabasePool: DatabaseWriter {
}
}

public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
public func asyncBarrierWriteWithoutTransaction(
_ updates: @escaping @Sendable (Result<Database, Error>) -> Void
) {
guard let readerPool else {
updates(.failure(DatabaseError.connectionIsClosed()))
return
Expand Down Expand Up @@ -887,7 +897,9 @@ extension DatabasePool: DatabaseWriter {
try writer.reentrantSync(updates)
}

public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
public func asyncWriteWithoutTransaction(
_ updates: @escaping @Sendable (Database) -> Void
) {
writer.async(updates)
}
}
Expand Down
33 changes: 21 additions & 12 deletions GRDB/Core/DatabaseQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ extension DatabaseQueue: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func read<T>(
public func read<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute { db in
Expand All @@ -244,7 +244,9 @@ extension DatabaseQueue: DatabaseReader {
}
}

public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
writer.async { db in
defer {
// Ignore error because we can not notify it.
Expand All @@ -271,21 +273,25 @@ extension DatabaseQueue: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func unsafeRead<T>(
public func unsafeRead<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute(value)
}

public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncUnsafeRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
writer.async { value(.success($0)) }
}

public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) rethrows -> T {
try writer.reentrantSync(value)
}

public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func spawnConcurrentRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
// Check that we're on the writer queue...
writer.execute { db in
// ... and that no transaction is opened.
Expand Down Expand Up @@ -315,9 +321,8 @@ extension DatabaseQueue: DatabaseReader {
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
if configuration.readonly {
// The easy case: the database does not change
return _addReadOnly(
Expand Down Expand Up @@ -382,7 +387,7 @@ extension DatabaseQueue: DatabaseWriter {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func writeWithoutTransaction<T>(
public func writeWithoutTransaction<T: Sendable>(
_ updates: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute(updates)
Expand All @@ -394,13 +399,15 @@ extension DatabaseQueue: DatabaseWriter {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func barrierWriteWithoutTransaction<T>(
public func barrierWriteWithoutTransaction<T: Sendable>(
_ updates: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await writer.execute(updates)
}

public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
public func asyncBarrierWriteWithoutTransaction(
_ updates: @escaping @Sendable (Result<Database, Error>) -> Void
) {
writer.async { updates(.success($0)) }
}

Expand Down Expand Up @@ -446,7 +453,9 @@ extension DatabaseQueue: DatabaseWriter {
try writer.reentrantSync(updates)
}

public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
public func asyncWriteWithoutTransaction(
_ updates: @escaping @Sendable (Database) -> Void
) {
writer.async(updates)
}
}
Expand Down
47 changes: 27 additions & 20 deletions GRDB/Core/DatabaseReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ public protocol DatabaseReader: AnyObject, Sendable {
/// database access, or the error thrown by `value`, or
/// `CancellationError` if the task is cancelled.
@available(iOS 13, macOS 10.15, tvOS 13, *)
func read<T>(
func read<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T

Expand Down Expand Up @@ -247,7 +247,9 @@ public protocol DatabaseReader: AnyObject, Sendable {
/// - parameter value: A closure which accesses the database. Its argument
/// is a `Result` that provides the database connection, or the failure
/// that would prevent establishing the read access to the database.
func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void)
func asyncRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
)

/// Executes database operations, and returns their result after they have
/// finished executing.
Expand Down Expand Up @@ -322,7 +324,7 @@ public protocol DatabaseReader: AnyObject, Sendable {
/// database access, or the error thrown by `value`, or
/// `CancellationError` if the task is cancelled.
@available(iOS 13, macOS 10.15, tvOS 13, *)
func unsafeRead<T>(
func unsafeRead<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T

Expand Down Expand Up @@ -357,7 +359,9 @@ public protocol DatabaseReader: AnyObject, Sendable {
/// - parameter value: A closure which accesses the database. Its argument
/// is a `Result` that provides the database connection, or the failure
/// that would prevent establishing the read access to the database.
func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void)
func asyncUnsafeRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
)

/// Executes database operations, and returns their result after they have
/// finished executing.
Expand Down Expand Up @@ -415,8 +419,8 @@ public protocol DatabaseReader: AnyObject, Sendable {
func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable
}

extension DatabaseReader {
Expand Down Expand Up @@ -534,9 +538,8 @@ extension DatabaseReader {
@available(iOS 13, macOS 10.15, tvOS 13, *)
public func readPublisher<Output>(
receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main,
value: @escaping (Database) throws -> Output)
-> DatabasePublishers.Read<Output>
{
value: @escaping @Sendable (Database) throws -> Output
) -> DatabasePublishers.Read<Output> {
OnDemandFuture { fulfill in
self.asyncRead { dbResult in
fulfill(dbResult.flatMap { db in Result { try value(db) } })
Expand Down Expand Up @@ -582,9 +585,8 @@ extension DatabaseReader {
func _addReadOnly<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
if scheduler.immediateInitialValue() {
do {
// Perform a reentrant read, in case the observation would be
Expand Down Expand Up @@ -659,13 +661,15 @@ extension AnyDatabaseReader: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func read<T>(
public func read<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await base.read(value)
}

public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
base.asyncRead(value)
}

Expand All @@ -675,13 +679,15 @@ extension AnyDatabaseReader: DatabaseReader {
}

@available(iOS 13, macOS 10.15, tvOS 13, *)
public func unsafeRead<T>(
public func unsafeRead<T: Sendable>(
_ value: @escaping @Sendable (Database) throws -> T
) async throws -> T {
try await base.unsafeRead(value)
}

public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncUnsafeRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
base.asyncUnsafeRead(value)
}

Expand All @@ -692,9 +698,8 @@ extension AnyDatabaseReader: DatabaseReader {
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
onChange: @escaping @Sendable (Reducer.Value) -> Void
) -> AnyDatabaseCancellable {
base._add(
observation: observation,
scheduling: scheduler,
Expand Down Expand Up @@ -753,7 +758,9 @@ extension DatabaseSnapshotReader {
}

// There is no such thing as an unsafe access to a snapshot.
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
public func asyncUnsafeRead(
_ value: @escaping @Sendable (Result<Database, Error>) -> Void
) {
asyncRead(value)
}
}
Loading