Skip to content

Commit

Permalink
Merge pull request #659 from groue/dev/interrupt
Browse files Browse the repository at this point in the history
Database interruption
  • Loading branch information
groue authored Dec 1, 2019
2 parents 047ada7 + e3d25bd commit 1fb12a2
Show file tree
Hide file tree
Showing 22 changed files with 622 additions and 60 deletions.
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

0 comments on commit 1fb12a2

Please sign in to comment.