Skip to content

Commit c5a0598

Browse files
committed
DocC: Concurrency Guides
1 parent 5242fe1 commit c5a0598

14 files changed

+688
-853
lines changed

Documentation/Combine.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ let newPlayerCount = dbQueue.writePublisher { db -> Int in
191191

192192
The difference is that the last fetches are performed in the `thenRead` function. This function accepts two arguments: a readonly database connection, and the result of the `updates` function. This allows you to pass information from a function to the other (it is ignored in the sample code above).
193193

194-
When you use a [database pool], this method applies a scheduling optimization: the `thenRead` function sees the database in the state left by the `updates` function, and yet does not block any concurrent writes. This can reduce database write contention. See [Advanced DatabasePool](Concurrency.md#advanced-databasepool) for more information.
194+
When you use a [database pool], this method applies a scheduling optimization: the `thenRead` function sees the database in the state left by the `updates` function, and yet does not block any concurrent writes. This can reduce database write contention.
195195

196196
When you use a [database queue], the results are guaranteed to be identical, but no scheduling optimization is applied.
197197

@@ -402,5 +402,5 @@ let cancellable = hallOfFamePublisher.sink(
402402
[configured]: ../README.md#databasepool-configuration
403403
[database pool]: ../README.md#database-pools
404404
[database queue]: ../README.md#database-queues
405-
[database snapshot]: Concurrency.md#database-snapshots
405+
[database snapshot]: https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/databasesnapshot
406406
[scheduler]: https://developer.apple.com/documentation/combine/scheduler

Documentation/Concurrency.md

Lines changed: 1 addition & 531 deletions
Large diffs are not rendered by default.

Documentation/GoodPracticesForDesigningRecordTypes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -807,8 +807,8 @@ Instead, have a look at [Database Observation]:
807807
[migrations]: https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/migrations
808808
[migration]: https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/migrations
809809
[Foreign Key Actions]: https://sqlite.org/foreignkeys.html#fk_actions
810-
[Concurrency Guide]: Concurrency.md
811-
[GRDB concurrency rules]: Concurrency.md#concurrency-rules
810+
[Concurrency Guide]: https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/concurrency
811+
[GRDB concurrency rules]: https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/concurrency
812812
[PersistableRecord]: ../README.md#persistablerecord-protocol
813813
[Database Observation]: ../README.md#database-changes-observation
814814
[ValueObservation]: ../README.md#valueobservation

Documentation/SharingADatabase.md

Lines changed: 1 addition & 291 deletions
Original file line numberDiff line numberDiff line change
@@ -1,294 +1,4 @@
11
Sharing a Database
22
==================
33

4-
**This guide describes a recommended setup that applies as soon as several processes want to access the same SQLite database.** It complements the [Concurrency](Concurrency.md) guide, that you should read first.
5-
6-
On iOS for example, you can share database files between multiple processes by storing them in an [App Group Container](https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati). On macOS as well, several processes may want to open the same database, according to their particular sandboxing contexts.
7-
8-
Accessing a shared database from several SQLite connections, from several processes, creates challenges at various levels:
9-
10-
1. **Database setup** may be attempted by multiple processes, concurrently.
11-
2. **SQLite** may throw `SQLITE_BUSY` errors, code 5, "database is locked".
12-
3. **iOS** may kill your application with a `0xDEAD10CC` exception.
13-
4. **GRDB** database observation misses changes performed by external processes.
14-
15-
We'll address all of those challenges below.
16-
17-
- [Use a Database Pool]
18-
- [How to limit the `SQLITE_BUSY` error]
19-
- [How to limit the `0xDEAD10CC` exception]
20-
- [How to perform cross-process database observation]
21-
22-
23-
## Use a Database Pool
24-
25-
In order to access a shared database, use a [Database Pool]. It opens the database in the [WAL mode](https://www.sqlite.org/wal.html), which helps sharing a database.
26-
27-
Since several processes may open the database at the same time, protect the creation of the database pool with an [NSFileCoordinator].
28-
29-
- In a process that can create and write in the database, use this sample code:
30-
31-
```swift
32-
/// Returns an initialized database pool at the shared location databaseURL
33-
func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool {
34-
let coordinator = NSFileCoordinator(filePresenter: nil)
35-
var coordinatorError: NSError?
36-
var dbPool: DatabasePool?
37-
var dbError: Error?
38-
coordinator.coordinate(writingItemAt: databaseURL, options: .forMerging, error: &coordinatorError, byAccessor: { url in
39-
do {
40-
dbPool = try openDatabase(at: url)
41-
} catch {
42-
dbError = error
43-
}
44-
})
45-
if let error = dbError ?? coordinatorError {
46-
throw error
47-
}
48-
return dbPool!
49-
}
50-
51-
private func openDatabase(at databaseURL: URL) throws -> DatabasePool {
52-
var configuration = Configuration()
53-
configuration.prepareDatabase { db in
54-
// Activate the persistent WAL mode so that
55-
// readonly processes can access the database.
56-
//
57-
// See https://www.sqlite.org/walformat.html#operations_that_require_locks_and_which_locks_those_operations_use
58-
// and https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpersistwal
59-
if db.configuration.readonly == false {
60-
var flag: CInt = 1
61-
let code = withUnsafeMutablePointer(to: &flag) { flagP in
62-
sqlite3_file_control(db.sqliteConnection, nil, SQLITE_FCNTL_PERSIST_WAL, flagP)
63-
}
64-
guard code == SQLITE_OK else {
65-
throw DatabaseError(resultCode: ResultCode(rawValue: code))
66-
}
67-
}
68-
}
69-
let dbPool = try DatabasePool(path: databaseURL.path, configuration: configuration)
70-
71-
// Perform here other database setups, such as defining
72-
// the database schema with a DatabaseMigrator, and
73-
// checking if the application can open the file:
74-
try migrator.migrate(dbPool)
75-
if try dbPool.read(migrator.hasBeenSuperseded) {
76-
// Database is too recent
77-
throw /* some error */
78-
}
79-
80-
return dbPool
81-
}
82-
```
83-
84-
- In a process that only reads in the database, use this sample code:
85-
86-
```swift
87-
/// Returns an initialized database pool at the shared location databaseURL,
88-
/// or nil if the database is not created yet, or does not have the required
89-
/// schema version.
90-
func openSharedReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? {
91-
let coordinator = NSFileCoordinator(filePresenter: nil)
92-
var coordinatorError: NSError?
93-
var dbPool: DatabasePool?
94-
var dbError: Error?
95-
coordinator.coordinate(readingItemAt: databaseURL, options: .withoutChanges, error: &coordinatorError, byAccessor: { url in
96-
do {
97-
dbPool = try openReadOnlyDatabase(at: url)
98-
} catch {
99-
dbError = error
100-
}
101-
})
102-
if let error = dbError ?? coordinatorError {
103-
throw error
104-
}
105-
return dbPool
106-
}
107-
108-
private func openReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? {
109-
do {
110-
var configuration = Configuration()
111-
configuration.readonly = true
112-
let dbPool = try DatabasePool(path: databaseURL.path, configuration: configuration)
113-
114-
// Check here if the database schema is the expected one,
115-
// for example with a DatabaseMigrator:
116-
return try dbPool.read { db in
117-
if try migrator.hasBeenSuperseded(db) {
118-
// Database is too recent
119-
return nil
120-
} else if try migrator.hasCompletedMigrations(db) == false {
121-
// Database is too old
122-
return nil
123-
}
124-
return dbPool
125-
}
126-
} catch {
127-
if FileManager.default.fileExists(atPath: databaseURL.path) {
128-
throw error
129-
} else {
130-
return nil
131-
}
132-
}
133-
}
134-
```
135-
136-
137-
#### The specific case of readonly connections
138-
139-
Readonly connections will fail unless two extra files ending in `-shm` and `-wal` are present next to the database file ([source](https://www.sqlite.org/walformat.html#operations_that_require_locks_and_which_locks_those_operations_use)). Those files are regular companions of databases in the [WAL mode]. But they are deleted, under regular operations, when database connections are closed. Precisely speaking, they *may* be deleted: it depends on the SQLite and the operating system versions ([source](https://github.com/groue/GRDB.swift/issues/739#issuecomment-604363998)). And when they are deleted, readonly connections fail.
140-
141-
The solution is to enable the "persistent WAL mode", as shown in the sample code above, by setting the [SQLITE_FCNTL_PERSIST_WAL](https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpersistwal) flag. This mode makes sure the `-shm` and `-wal` files are never deleted, and guarantees a database access to readonly connections.
142-
143-
144-
## How to limit the `SQLITE_BUSY` error
145-
146-
> The SQLITE_BUSY result code indicates that the database file could not be written (or in some cases read) because of concurrent activity by some other database connection, usually a database connection in a separate process.
147-
148-
See https://www.sqlite.org/rescode.html#busy for more information about this error.
149-
150-
If several processes want to write in the database, configure the database pool of each process that wants to write:
151-
152-
```swift
153-
var configuration = Configuration()
154-
configuration.busyMode = .timeout(/* a TimeInterval */)
155-
let dbPool = try DatabasePool(path: ..., configuration: configuration)
156-
```
157-
158-
With such a setup, you may still get `SQLITE_BUSY` (5, "database is locked") errors from all write operations. They will occur if the database remains locked by another process for longer than the specified timeout.
159-
160-
```swift
161-
do {
162-
try dbPool.write { db in ... }
163-
} catch DatabaseError.SQLITE_BUSY {
164-
// Another process won't let you write. Deal with it.
165-
}
166-
```
167-
168-
> :bulb: **Tip**: In order to be nice to other processes, measure the duration of your longest writes, and attempt at optimizing the ones that last for too long. See the [How do I monitor the duration of database statements execution?] FAQ.
169-
170-
171-
## How to limit the `0xDEAD10CC` exception
172-
173-
> The exception code 0xDEAD10CC indicates that an application has been terminated by the OS because it held on to a file lock or sqlite database lock during suspension.
174-
175-
See https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report for more information about this exception.
176-
177-
1. If you use SQLCipher, use SQLCipher 4+, and call the [`cipher_plaintext_header_size` pragma](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_plaintext_header_size) from your database preparation function:
178-
179-
```swift
180-
var configuration = Configuration()
181-
configuration.prepareDatabase { (db: Database) in
182-
try db.usePassphrase("secret")
183-
try db.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
184-
}
185-
let dbPool = try DatabasePool(path: ..., configuration: configuration)
186-
```
187-
188-
This will avoid this issue: https://github.com/sqlcipher/sqlcipher/issues/255.
189-
190-
This will also disable a SQLCipher security feature: the salt. As described by https://www.zetetic.net/sqlcipher/design/:
191-
192-
> The salt is used for key derivation and it ensures that even if two databases are created using the same password, they will not have the same encryption key.
193-
194-
Applications are responsible for managing the salt themselves and providing it to SQLCipher. See https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_plaintext_header_size for instructions.
195-
196-
2. [**:fire: EXPERIMENTAL**](../README.md#what-are-experimental-features) In each process that wants to write in the database:
197-
198-
Set the `observesSuspensionNotifications` configuration flag:
199-
200-
```swift
201-
var configuration = Configuration()
202-
configuration.observesSuspensionNotifications = true
203-
let dbPool = try DatabasePool(path: ..., configuration: configuration)
204-
```
205-
206-
Post `Database.suspendNotification` when the application is about to be [suspended](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle). You can for example post this notification from `UIApplicationDelegate.applicationDidEnterBackground(_:)`, or in the expiration handler of a [background task](https://forums.developer.apple.com/thread/85066).
207-
208-
```swift
209-
@UIApplicationMain
210-
class AppDelegate: UIResponder, UIApplicationDelegate {
211-
func applicationDidEnterBackground(_ application: UIApplication) {
212-
// Suspend databases
213-
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
214-
}
215-
}
216-
```
217-
218-
Post `Database.resumeNotification` from `UIApplicationDelegate.applicationWillEnterForeground(_:)` (or `SceneDelegate.sceneWillEnterForeground(_:)` for scene-based applications):
219-
220-
```swift
221-
@UIApplicationMain
222-
class AppDelegate: UIResponder, UIApplicationDelegate {
223-
func applicationWillEnterForeground(_ application: UIApplication) {
224-
// Resume databases
225-
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
226-
}
227-
}
228-
```
229-
230-
If the application uses the background modes supported by iOS, post `Database.resumeNotification` method from each and every background mode callback that may use the database. For example, if your application supports background fetches:
231-
232-
```swift
233-
@UIApplicationMain
234-
class AppDelegate: UIResponder, UIApplicationDelegate {
235-
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
236-
// Resume databases
237-
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
238-
// Proceed with background fetch
239-
...
240-
}
241-
}
242-
```
243-
244-
Suspended databases greatly reduce the odds of `0xDEAD10CC` exception occurring. If you see one in your crash logs, please open an issue!
245-
246-
In exchange, you will get `SQLITE_INTERRUPT` (9) or `SQLITE_ABORT` (4) errors, with messages "Database is suspended", "Transaction was aborted", or "interrupted", for any attempt at writing in the database when it is suspended.
247-
248-
You can catch those errors:
249-
250-
```swift
251-
do {
252-
try dbPool.write { db in ... }
253-
} catch DatabaseError.SQLITE_INTERRUPT, DatabaseError.SQLITE_ABORT {
254-
// Oops, the database is suspended.
255-
// Maybe try again after database is resumed?
256-
}
257-
```
258-
259-
260-
## How to perform cross-process database observation
261-
262-
GRDB [Database Observation] features, as well as its [Combine publishers] and [RxGRDB], are not able to notify database changes performed by other processes.
263-
264-
Whenever you need to notify other processes that the database has been changed, you will have to use a cross-process notification mechanism such as [NSFileCoordinator] or [CFNotificationCenterGetDarwinNotifyCenter].
265-
266-
You can trigger those notifications automatically with [DatabaseRegionObservation]:
267-
268-
```swift
269-
// Notify all changes made to the "player" and "team" database tables
270-
let observation = DatabaseRegionObservation(tracking: Player.all(), Team.all())
271-
let observer = try observation.start(in: dbPool) { (db: Database) in
272-
// Notify other processes
273-
}
274-
275-
// Notify all changes made to the database
276-
let observation = DatabaseRegionObservation(tracking: DatabaseRegion.fullDatabase)
277-
let observer = try observation.start(in: dbPool) { (db: Database) in
278-
// Notify other processes
279-
}
280-
```
281-
282-
[Use a Database Pool]: #use-a-database-pool
283-
[How to limit the `SQLITE_BUSY` error]: #how-to-limit-the-sqlite_busy-error
284-
[How to limit the `0xDEAD10CC` exception]: #how-to-limit-the-0xdead10cc-exception
285-
[How to perform cross-process database observation]: #how-to-perform-cross-process-database-observation
286-
[Database Pool]: ../README.md#database-pools
287-
[Database Observation]: ../README.md#database-changes-observation
288-
[RxGRDB]: https://github.com/RxSwiftCommunity/RxGRDB
289-
[NSFileCoordinator]: https://developer.apple.com/documentation/foundation/nsfilecoordinator
290-
[CFNotificationCenterGetDarwinNotifyCenter]: https://developer.apple.com/documentation/corefoundation/1542572-cfnotificationcentergetdarwinnot
291-
[DatabaseRegionObservation]: ../README.md#databaseregionobservation
292-
[WAL mode]: https://www.sqlite.org/wal.html
293-
[How do I monitor the duration of database statements execution?]: ../README.md#how-do-i-monitor-the-duration-of-database-statements-execution
294-
[Combine publishers]: Combine.md
4+
This guide [has moved](https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/databasesharing).

Documentation/WhyAdoptGRDB.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ If this little tour of GRDB has convinced you, the real trip starts here: [GRDB]
285285

286286
Happy GRDB! :gift:
287287

288-
[Concurrency Guide]: Concurrency.md
288+
[Concurrency Guide]: https://groue.github.io/GRDB.swift/docs/6.3/documentation/grdb/concurrency
289289
[Core Data]: https://developer.apple.com/documentation/coredata
290290
[DatabasePool]: ../README.md#database-pools
291291
[Diesel]: http://diesel.rs

GRDB/Core/DatabasePool.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import UIKit
1515
/// read accesses are executed in **reader dispatch queues** (one per read-only
1616
/// SQLite connection).
1717
///
18+
/// See <doc:Concurrency> for more information about concurrent
19+
/// database accesses.
20+
///
1821
/// A database pool inherits most of its database access methods from the
1922
/// ``DatabaseReader`` and ``DatabaseWriter`` protocols. It defines a few
2023
/// specific database access methods as well.

GRDB/Core/DatabaseReader.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import Dispatch
1010
/// valid conforming types.
1111
///
1212
/// The protocol comes with isolation guarantees that describe the behavior of
13-
/// adopting types in a multithreaded application.
13+
/// conforming types in a multithreaded application. See <doc:Concurrency> for
14+
/// more information.
1415
///
1516
/// ## Topics
1617
///

GRDB/Core/DatabaseWriter.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import Dispatch
1414
/// Read accesses are defined by ``DatabaseReader``, the protocol all database
1515
/// writers conform to.
1616
///
17+
/// See <doc:Concurrency> for more information about the behavior of conforming
18+
/// types in a multithreaded application.
19+
///
1720
/// ## Topics
1821
///
1922
/// ### Writing into the Database

0 commit comments

Comments
 (0)