Description
Since the 0.8.0 (4) version we have crashes coming up with the same exception:
Fatal error: 'try!' expression unexpectedly raised an error: Database.SQLiteDatabase.Error.failedToOpenDatabase
More debugging and enhancing of the thrown exception information led to the
Fatal error: 'try!' expression unexpectedly raised an error: Database.SQLiteDatabase.Error.failedToOpenDatabase("status: (23) (unknown error code)")
Which shows that SQLite returns SQLITE_AUTH error when a database connection is created.
The crashes mostly happen when the app is in the background.
Cause
The cause is that the database files are locked when the app is locked, and the sync process happening regularly in the background tries to access the file but gets an error from the operating system.
Remedy
Search through similar issues on GitHub found the problem described in GRDB.swift repository, which says the SQLITE_IOERR and SQLITE_AUTH errors could be thrown when the database uses files protected by OS's encryption. The corresponding APIs are in UIKit:
UIApplication.isProtectedDataAvailable
UIApplicationDelegate.applicationProtectedDataDidBecomeAvailable(_:)
UIApplicationDelegate.applicationProtectedDataWillBecomeUnavailable(_:)
NSNotification.Name.protectedDataDidBecomeAvailableNotification
NSNotification.Name.protectedDataWillBecomeUnavailableNotification
We would encounter this problem or be aware of it when developing the app if we would use the GRDB.swift
library to work with SQLite, but at this point, I'm reluctant to switch. Sorry, not invented here syndrome.
Proposed solution
After a short team discussion, the proposed treatment for the crash is the following.
Currently, the crash is happening in the implementation modules of repositories, more specifically, when an SQLite connection is created.
Our previous assumption was that any reason that makes a database request erroring (except busy state) is a programming error, meaning the app without working database is not worth functioning, that's why every kind of error on database opening was treated as crashable. This assumption proved to be wrong.
Considering the following points:
- We'd like to keep our repositories' interfaces clean from throwing errors (as it makes programming easier)
- We'd like to know if there are any other errors except SQLITE_IOERR and SQLITE_AUTH happening on the devices when a connection is created
- We'd like to keep the SQL database lib separate from UIKit lib
The proposed solution is:
- At the call site of opening the database file, if the error occurs:
- Retry the opening with 5-second interval 3 times or until
locked
event is received
- After 3 unsuccessful retries, crash the app
- If
lock
event is received, then sleep until unlock
event is received
- At the UIKit level (in sync service implementation):
- Publish
lock
and unlock
events upon protected data becoming unavailable and available
- The opening of the database file is a blocking operation, and the notifications from UIKit are assumed to be received on a different thread than the database access thread. This has to be verified.
As always, test that the crases are not happening in the internal beta.