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

Database interruption #659

Merged
merged 15 commits into from
Dec 1, 2019
12 changes: 8 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ All notable changes to this project will be documented in this file.
GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: APIs flagged [**:fire: EXPERIMENTAL**](README.md#what-are-experimental-features). Those are unstable, and may break between any two minor releases of the library.


<!--
[Next Release](#next-release)
-->


#### 4.x Releases
Expand Down Expand Up @@ -59,9 +57,15 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
- [0.110.0](#01100), ...


<!--
## Next Release
-->

**New**

- [#659](https://github.com/groue/GRDB.swift/pull/659): Database interruption

### Documentation Diff

A new [Interrupt a Database](README.md#interrupt-a-database) chapter documents the new `interrupt()` method.


## 4.6.2
Expand Down
8 changes: 8 additions & 0 deletions GRDB.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,9 @@
56DF001C228DDBA300D611F3 /* AssociationPrefetchingRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0019228DDBA200D611F3 /* AssociationPrefetchingRowTests.swift */; };
56DF001D228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF001A228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; };
56DF001E228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF001A228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; };
56E4F7EE2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */; };
56E4F7EF2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */; };
56E4F7F02392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */; };
56E5D7D41B4D3FEE00430942 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56E5D7CA1B4D3FED00430942 /* GRDB.framework */; };
56E5D7FE1B4D422E00430942 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC3773F319C8CBB3004FCF85 /* GRDB.framework */; };
56E5D8041B4D424400430942 /* GRDBTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */; };
Expand Down Expand Up @@ -1655,6 +1658,7 @@
56DE7B361C42BBBB00861EB8 /* PerformanceCoreDataTests.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = PerformanceCoreDataTests.sqlite; sourceTree = "<group>"; };
56DF0019228DDBA200D611F3 /* AssociationPrefetchingRowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingRowTests.swift; sourceTree = "<group>"; };
56DF001A228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingCodableRecordTests.swift; sourceTree = "<group>"; };
56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseAbortedTransactionTests.swift; sourceTree = "<group>"; };
56E5D7CA1B4D3FED00430942 /* GRDB.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GRDB.framework; sourceTree = BUILT_PRODUCTS_DIR; };
56E5D7D31B4D3FEE00430942 /* GRDBiOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBiOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
56E5D7F91B4D422D00430942 /* GRDBOSXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBOSXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -2218,6 +2222,7 @@
children = (
5665F85C203EE6030084C6C0 /* ColumnInfoTests.swift */,
56DAA2C41DE99D8D006E10C8 /* Cursor */,
56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */,
564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */,
564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */,
564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */,
Expand Down Expand Up @@ -3461,6 +3466,7 @@
56AF746F1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift in Sources */,
569BBA3722905FFA00478429 /* InflectionsTests.swift in Sources */,
5672DE6A1CDB751D0022BA81 /* DatabasePoolBackupTests.swift in Sources */,
56E4F7EF2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */,
561CFA992376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */,
5698AC4D1DA2D48A0056AF8C /* FTS3RecordTests.swift in Sources */,
56057C632291C7C600A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */,
Expand Down Expand Up @@ -3666,6 +3672,7 @@
5698AC491DA2D48A0056AF8C /* FTS3RecordTests.swift in Sources */,
569BBA3622905FFA00478429 /* InflectionsTests.swift in Sources */,
56D496931D81316E008276D7 /* FetchableRecordTests.swift in Sources */,
56E4F7EE2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */,
561CFA982376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */,
56D496881D81316E008276D7 /* DatabaseValueConversionTests.swift in Sources */,
56057C622291C7C600A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */,
Expand Down Expand Up @@ -4004,6 +4011,7 @@
AAA4DD3B230F262000C74B15 /* DatabaseValueConvertibleEscapingTests.swift in Sources */,
AAA4DD3C230F262000C74B15 /* InflectionsTests.swift in Sources */,
AAA4DD3D230F262000C74B15 /* DatabasePoolBackupTests.swift in Sources */,
56E4F7F02392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */,
561CFA9A2376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */,
AAA4DD3E230F262000C74B15 /* FTS3RecordTests.swift in Sources */,
AAA4DD3F230F262000C74B15 /* AssociationHasManyThroughRowScopeTests.swift in Sources */,
Expand Down
10 changes: 9 additions & 1 deletion GRDB/Core/Database+Statements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ extension Database {
extension Database {

func executeUpdateStatement(_ statement: UpdateStatement) throws {
// In aborted transaction, forbid all statements but statements that
// manage transaction.
if statement.transactionEffect == nil {
try assertNotInsideAbortedTransactionBlock(sql: statement.sql, arguments: statement.arguments)
}

let authorizer = observationBroker.updateStatementWillExecute(statement)
let sqliteStatement = statement.sqliteStatement
var code: Int32 = SQLITE_OK
Expand Down Expand Up @@ -289,7 +295,9 @@ extension Database {
}

@inline(__always)
func selectStatementWillExecute(_ statement: SelectStatement) {
func selectStatementWillExecute(_ statement: SelectStatement) throws {
try assertNotInsideAbortedTransactionBlock(sql: statement.sql, arguments: statement.arguments)

if _isRecordingSelectedRegion {
// Don't record schema introspection queries, which may be
// run, or not, depending on the state of the schema cache.
Expand Down
82 changes: 79 additions & 3 deletions GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,38 @@ public final class Database {
/// Use recordingSelectedRegion(_:), see `selectStatementWillExecute(_:)`
var _isRecordingSelectedRegion: Bool = false
var _selectedRegion = DatabaseRegion()

/// True inside `inTransaction { ... }` and `inSavepoint { ... }`
@usableFromInline var isInsideTransactionBlock = false

/// When true, a transaction has been aborted (for example, by
/// `sqlite3_interrupt`, or a `ON CONFLICT ROLLBACK` clause.
///
/// try db.inTransaction {
/// do {
/// // Aborted by sqlite3_interrupt or any other
/// // SQLite error which leaves transaction
/// ...
/// } catch { ... }
///
/// // <- Here we're inside an aborted transaction.
/// ...
///
/// return .commit
/// }
///
/// When a transaction has been aborted, we throw SQLITE_ABORT for all
/// database accesses, because we don't want the user to think they are
/// safely wrapped by a transaction.
///
/// We only monitor aborted transaction for `inTransaction` and
/// `inSavepoint` methods. When you use explicit methods like
/// `beginTransaction`, `commit`, or raw SQL statements, aborted
/// transactions are not monitored, and you won't get SQLITE_ABORT for
/// database accesses that run after a transaction has been aborted.
@usableFromInline var isInsideAbortedTransactionBlock: Bool {
return isInsideTransactionBlock && !isInsideTransaction
}

// MARK: - Private properties

Expand Down Expand Up @@ -635,11 +667,25 @@ extension Database {
throw DatabaseError(resultCode: code, message: lastErrorMessage)
}
}

func interrupt() {
sqlite3_interrupt(sqliteConnection)
}
}

extension Database {
// MARK: - Transactions & Savepoint

func assertNotInsideAbortedTransactionBlock(sql: String? = nil, arguments: StatementArguments? = nil) throws {
if isInsideAbortedTransactionBlock {
throw DatabaseError(
resultCode: SQLITE_ABORT,
message: "Transaction was aborted",
sql: sql,
arguments: arguments)
}
}

/// Executes a block inside a database transaction.
///
/// try dbQueue.inDatabase do {
Expand All @@ -666,6 +712,12 @@ extension Database {
// Begin transaction
try beginTransaction(kind)

let wasInsideTransactionBlock = isInsideTransactionBlock
isInsideTransactionBlock = true
defer {
isInsideTransactionBlock = wasInsideTransactionBlock
}

// Now that transaction has begun, we'll rollback in case of error.
// But we'll throw the first caught error, so that user knows
// what happened.
Expand All @@ -675,6 +727,15 @@ extension Database {
let completion = try block()
switch completion {
case .commit:
// In case of aborted transaction, throw SQLITE_ABORT instead
// of the generic SQLITE_ERROR "cannot commit - no transaction is active"
try assertNotInsideAbortedTransactionBlock()

// Leave transaction block now, so that transaction observers
// can execute statements without getting errors from
// assertNotInsideAbortedTransactionBlock.
isInsideTransactionBlock = wasInsideTransactionBlock

try commit()
needsRollback = false
case .rollback:
Expand Down Expand Up @@ -726,7 +787,8 @@ extension Database {
// So when the default GRDB transaction kind is not deferred, we open a
// transaction instead
if !isInsideTransaction && configuration.defaultTransactionKind != .deferred {
return try inTransaction(configuration.defaultTransactionKind, block)
try inTransaction(configuration.defaultTransactionKind, block)
return
}

// If the savepoint is top-level, we'll use ROLLBACK TRANSACTION in
Expand All @@ -742,6 +804,12 @@ extension Database {
// the user uses "grdb" as a savepoint name.
try execute(sql: "SAVEPOINT grdb")

let wasInsideTransactionBlock = isInsideTransactionBlock
isInsideTransactionBlock = true
defer {
isInsideTransactionBlock = wasInsideTransactionBlock
}

// Now that savepoint has begun, we'll rollback in case of error.
// But we'll throw the first caught error, so that user knows
// what happened.
Expand All @@ -751,6 +819,15 @@ extension Database {
let completion = try block()
switch completion {
case .commit:
// In case of aborted transaction, throw SQLITE_ABORT instead
// of the generic SQLITE_ERROR "cannot commit - no transaction is active"
try assertNotInsideAbortedTransactionBlock()

// Leave transaction block now, so that transaction observers
// can execute statements without getting errors from
// assertNotInsideAbortedTransactionBlock.
isInsideTransactionBlock = wasInsideTransactionBlock

try execute(sql: "RELEASE SAVEPOINT grdb")
assert(!topLevelSavepoint || !isInsideTransaction)
needsRollback = false
Expand Down Expand Up @@ -888,8 +965,7 @@ extension Database {
// The second technique is more robust, because we don't have to guess
// which rollback errors should be ignored, and which rollback errors
// should be exposed to the library user.
SchedulingWatchdog.preconditionValidQueue(self) // guard sqlite3_get_autocommit
if sqlite3_get_autocommit(sqliteConnection) == 0 {
if isInsideTransaction {
try execute(sql: "ROLLBACK TRANSACTION")
}
assert(!isInsideTransaction)
Expand Down
7 changes: 7 additions & 0 deletions GRDB/Core/DatabasePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,13 @@ extension DatabasePool {

extension DatabasePool: DatabaseReader {

// MARK: - Interrupting Database Operations

public func interrupt() {
writer.interrupt()
readerPool.forEach { $0.interrupt() }
}

// MARK: - Reading from Database

/// Synchronously executes a read-only block in a protected dispatch queue,
Expand Down
6 changes: 6 additions & 0 deletions GRDB/Core/DatabaseQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ extension DatabaseQueue {

extension DatabaseQueue {

// MARK: - Interrupting Database Operations

public func interrupt() {
writer.interrupt()
}

// MARK: - Reading from Database

/// Synchronously executes a read-only block in a protected dispatch queue,
Expand Down
52 changes: 52 additions & 0 deletions GRDB/Core/DatabaseReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,51 @@ public protocol DatabaseReader: AnyObject {
/// The database configuration
var configuration: Configuration { get }

// MARK: - Interrupting Database Operations

/// This method causes any pending database operation to abort and return at
/// its earliest opportunity.
///
/// It can be called from any thread.
///
/// A call to `interrupt()` that occurs when there are no running SQL
/// statements is a no-op and has no effect on SQL statements that are
/// started after `interrupt()` returns.
///
/// A database operation that is interrupted will throw a DatabaseError with
/// code SQLITE_INTERRUPT. If the interrupted SQL operation is an INSERT,
/// UPDATE, or DELETE that is inside an explicit transaction, then the
/// entire transaction will be rolled back automatically. If the rolled back
/// transaction was started by a transaction-wrapping method such as
/// `DatabaseWriter.write` or `Database.inTransaction`, then all database
/// accesses will throw a DatabaseError with code SQLITE_ABORT until the
/// wrapping method returns.
///
/// For example:
///
/// try dbQueue.write { db in
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// // not executed:
/// try Player(...).insert(db)
/// } // throws SQLITE_INTERRUPT
///
/// try dbQueue.write { db in
/// do {
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// } catch { }
/// try Player(...).insert(db) // throws SQLITE_ABORT
/// } // throws SQLITE_ABORT
///
/// try dbQueue.write { db in
/// do {
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// } catch { }
/// } // throws SQLITE_ABORT
func interrupt()

// MARK: - Read From Database

/// Synchronously executes a read-only block that takes a database
Expand Down Expand Up @@ -250,6 +295,13 @@ public final class AnyDatabaseReader: DatabaseReader {
return base.configuration
}

// MARK: - Interrupting Database Operations

/// :nodoc:
public func interrupt() {
base.interrupt()
}

// MARK: - Reading from Database

/// :nodoc:
Expand Down
6 changes: 6 additions & 0 deletions GRDB/Core/DatabaseSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public class DatabaseSnapshot: DatabaseReader {
// DatabaseReader
extension DatabaseSnapshot {

// MARK: - Interrupting Database Operations

public func interrupt() {
serializedDatabase.interrupt()
}

// MARK: - Reading from Database

/// Synchronously executes a read-only block that takes a database
Expand Down
4 changes: 2 additions & 2 deletions GRDB/Core/DatabaseValueConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public final class DatabaseValueCursor<Value: DatabaseValueConvertible>: Cursor
_statement.reset(withArguments: arguments)

// Assume cursor is created for iteration
statement.database.selectStatementWillExecute(statement)
try statement.database.selectStatementWillExecute(statement)
}

deinit {
Expand Down Expand Up @@ -123,7 +123,7 @@ public final class NullableDatabaseValueCursor<Value: DatabaseValueConvertible>:
_statement.reset(withArguments: arguments)

// Assume cursor is created for iteration
statement.database.selectStatementWillExecute(statement)
try statement.database.selectStatementWillExecute(statement)
}

deinit {
Expand Down
9 changes: 8 additions & 1 deletion GRDB/Core/DatabaseWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ extension DatabaseWriter {
if let value = observer.reducer.value(fetchedValue) {
startValue = value
}

db.add(transactionObserver: observer, extent: .observerLifetime)
}
} catch {
Expand Down Expand Up @@ -601,6 +601,13 @@ public final class AnyDatabaseWriter: DatabaseWriter {
return base.configuration
}

// MARK: - Interrupting Database Operations

/// :nodoc:
public func interrupt() {
base.interrupt()
}

// MARK: - Reading from Database

/// :nodoc:
Expand Down
Loading