437 lines
15 KiB
Swift
437 lines
15 KiB
Swift
import XCTest
|
|
import GRDB
|
|
|
|
class ConcurrencyTests: GRDBTestCase {
|
|
var busyCallback: Database.BusyCallback?
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
|
|
self.busyCallback = nil
|
|
let busyCallback: Database.BusyCallback = { numberOfTries in
|
|
if let busyCallback = self.busyCallback {
|
|
return busyCallback(numberOfTries)
|
|
} else {
|
|
// Default give up
|
|
return false
|
|
}
|
|
}
|
|
|
|
dbConfiguration.busyMode = .callback(busyCallback)
|
|
}
|
|
|
|
func testWrappedReadWrite() throws {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
|
|
try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)")
|
|
}
|
|
let id = try dbQueue.inDatabase { db in
|
|
try Int.fetchOne(db, sql: "SELECT id FROM items")!
|
|
}
|
|
XCTAssertEqual(id, 1)
|
|
}
|
|
|
|
func testDeferredTransactionConcurrency() throws {
|
|
let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
#if GRDBCIPHER_USE_ENCRYPTION
|
|
// Work around SQLCipher bug when two connections are open to the
|
|
// same empty database: make sure the database is not empty before
|
|
// running this test
|
|
try dbQueue1.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)")
|
|
}
|
|
#endif
|
|
let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
|
|
// Queue 1 Queue 2
|
|
// BEGIN DEFERRED TRANSACTION
|
|
let s1 = DispatchSemaphore(value: 0)
|
|
// BEGIN DEFERRED TRANSACTION
|
|
let s2 = DispatchSemaphore(value: 0)
|
|
// INSERT INTO stuffs (id) VALUES (NULL)
|
|
let s3 = DispatchSemaphore(value: 0)
|
|
// INSERT INTO stuffs (id) VALUES (NULL) <--- Error
|
|
// ROLLBACK
|
|
let s4 = DispatchSemaphore(value: 0)
|
|
// COMMIT
|
|
|
|
try dbQueue1.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE stuffs (id INTEGER PRIMARY KEY)")
|
|
}
|
|
|
|
let queue = DispatchQueue(label: "GRDB", attributes: [.concurrent])
|
|
let group = DispatchGroup()
|
|
|
|
// Queue 1
|
|
queue.async(group: group) {
|
|
do {
|
|
try dbQueue1.inTransaction(.deferred) { db in
|
|
s1.signal()
|
|
_ = s2.wait(timeout: .distantFuture)
|
|
try db.execute(sql: "INSERT INTO stuffs (id) VALUES (NULL)")
|
|
s3.signal()
|
|
_ = s4.wait(timeout: .distantFuture)
|
|
return .commit
|
|
}
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
// Queue 2
|
|
var concurrencyError: DatabaseError? = nil
|
|
queue.async(group: group) {
|
|
do {
|
|
_ = s1.wait(timeout: .distantFuture)
|
|
try dbQueue2.inTransaction(.deferred) { db in
|
|
s2.signal()
|
|
_ = s3.wait(timeout: .distantFuture)
|
|
try db.execute(sql: "INSERT INTO stuffs (id) VALUES (NULL)")
|
|
return .commit
|
|
}
|
|
}
|
|
catch let error as DatabaseError {
|
|
s4.signal()
|
|
concurrencyError = error
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
_ = group.wait(timeout: .distantFuture)
|
|
|
|
if let concurrencyError {
|
|
XCTAssertEqual(concurrencyError.resultCode, .SQLITE_BUSY)
|
|
XCTAssertEqual(concurrencyError.sql, "INSERT INTO stuffs (id) VALUES (NULL)")
|
|
} else {
|
|
XCTFail("Expected concurrency error")
|
|
}
|
|
}
|
|
|
|
func testExclusiveTransactionConcurrency() throws {
|
|
let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
|
|
// Queue 1 Queue 2
|
|
// BEGIN EXCLUSIVE TRANSACTION
|
|
let s1 = DispatchSemaphore(value: 0)
|
|
// BEGIN EXCLUSIVE TRANSACTION <--- Error
|
|
let s2 = DispatchSemaphore(value: 0)
|
|
// COMMIT
|
|
|
|
let queue = DispatchQueue(label: "GRDB", attributes: [.concurrent])
|
|
let group = DispatchGroup()
|
|
|
|
// Queue 1
|
|
queue.async(group: group) {
|
|
do {
|
|
try dbQueue1.inTransaction(.exclusive) { db in
|
|
s1.signal()
|
|
_ = s2.wait(timeout: .distantFuture)
|
|
return .commit
|
|
}
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
// Queue 2
|
|
var concurrencyError: DatabaseError? = nil
|
|
queue.async(group: group) {
|
|
do {
|
|
_ = s1.wait(timeout: .distantFuture)
|
|
try dbQueue2.inTransaction(.exclusive) { db in
|
|
return .commit
|
|
}
|
|
}
|
|
catch let error as DatabaseError {
|
|
s2.signal()
|
|
concurrencyError = error
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
_ = group.wait(timeout: .distantFuture)
|
|
|
|
if let concurrencyError {
|
|
XCTAssertEqual(concurrencyError.resultCode, .SQLITE_BUSY)
|
|
XCTAssertEqual(concurrencyError.sql, "BEGIN EXCLUSIVE TRANSACTION")
|
|
} else {
|
|
XCTFail("Expected concurrency error")
|
|
}
|
|
}
|
|
|
|
func testImmediateTransactionConcurrency() throws {
|
|
let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
|
|
// Queue 1 Queue 2
|
|
// BEGIN IMMEDIATE TRANSACTION
|
|
let s1 = DispatchSemaphore(value: 0)
|
|
// BEGIN IMMEDIATE TRANSACTION <--- Error
|
|
let s2 = DispatchSemaphore(value: 0)
|
|
// COMMIT
|
|
|
|
let queue = DispatchQueue(label: "GRDB", attributes: [.concurrent])
|
|
let group = DispatchGroup()
|
|
|
|
// Queue 1
|
|
queue.async(group: group) {
|
|
do {
|
|
try dbQueue1.inTransaction(.immediate) { db in
|
|
s1.signal()
|
|
_ = s2.wait(timeout: .distantFuture)
|
|
return .commit
|
|
}
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
// Queue 2
|
|
var concurrencyError: DatabaseError? = nil
|
|
queue.async(group: group) {
|
|
do {
|
|
_ = s1.wait(timeout: .distantFuture)
|
|
try dbQueue2.inTransaction(.immediate) { db in
|
|
return .commit
|
|
}
|
|
}
|
|
catch let error as DatabaseError {
|
|
s2.signal()
|
|
concurrencyError = error
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
_ = group.wait(timeout: .distantFuture)
|
|
|
|
if let concurrencyError {
|
|
XCTAssertEqual(concurrencyError.resultCode, .SQLITE_BUSY)
|
|
XCTAssertEqual(concurrencyError.sql, "BEGIN IMMEDIATE TRANSACTION")
|
|
} else {
|
|
XCTFail("Expected concurrency error")
|
|
}
|
|
}
|
|
|
|
func testBusyCallback() throws {
|
|
let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
#if GRDBCIPHER_USE_ENCRYPTION
|
|
// Work around SQLCipher bug when two connections are open to the
|
|
// same empty database: make sure the database is not empty before
|
|
// running this test
|
|
try dbQueue1.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)")
|
|
}
|
|
#endif
|
|
let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
|
|
// Queue 1 Queue 2
|
|
// BEGIN EXCLUSIVE TRANSACTION
|
|
let s1 = DispatchSemaphore(value: 0)
|
|
// BEGIN EXCLUSIVE TRANSACTION <--- Busy
|
|
let s2 = DispatchSemaphore(value: 0)
|
|
// COMMIT
|
|
// BEGIN EXCLUSIVE TRANSACTION
|
|
// COMMIT
|
|
|
|
var busyCallbackCalled = false
|
|
self.busyCallback = { n in
|
|
busyCallbackCalled = true
|
|
s2.signal()
|
|
return true
|
|
}
|
|
|
|
let queue = DispatchQueue(label: "GRDB", attributes: [.concurrent])
|
|
let group = DispatchGroup()
|
|
|
|
// Queue 1
|
|
queue.async(group: group) {
|
|
do {
|
|
try dbQueue1.inTransaction(.exclusive) { db in
|
|
s1.signal()
|
|
_ = s2.wait(timeout: .distantFuture)
|
|
return .commit
|
|
}
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
// Queue 2
|
|
queue.async(group: group) {
|
|
do {
|
|
_ = s1.wait(timeout: .distantFuture)
|
|
try dbQueue2.inTransaction(.exclusive) { db in
|
|
return .commit
|
|
}
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
_ = group.wait(timeout: .distantFuture)
|
|
|
|
XCTAssertTrue(busyCallbackCalled)
|
|
}
|
|
|
|
func testReaderDuringDefaultTransaction() throws {
|
|
let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
#if GRDBCIPHER_USE_ENCRYPTION
|
|
// Work around SQLCipher bug when two connections are open to the
|
|
// same empty database: make sure the database is not empty before
|
|
// running this test
|
|
try dbQueue1.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)")
|
|
}
|
|
#endif
|
|
let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
|
|
// Here we test that a reader can read while a writer is writing.
|
|
|
|
// Queue 1 Queue 2
|
|
// BEGIN IMMEDIATE TRANSACTION
|
|
// INSERT INTO stuffs (id) VALUES (NULL)
|
|
let s1 = DispatchSemaphore(value: 0)
|
|
// SELECT * FROM stuffs <--- 0 result
|
|
let s2 = DispatchSemaphore(value: 0)
|
|
// COMMIT
|
|
let s3 = DispatchSemaphore(value: 0)
|
|
// SELECT * FROM stuffs <--- 1 result
|
|
|
|
try dbQueue1.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE stuffs (id INTEGER PRIMARY KEY)")
|
|
}
|
|
|
|
let queue = DispatchQueue(label: "GRDB", attributes: [.concurrent])
|
|
let group = DispatchGroup()
|
|
|
|
// Queue 1
|
|
queue.async(group: group) {
|
|
do {
|
|
try dbQueue1.inTransaction { db in
|
|
try db.execute(sql: "INSERT INTO stuffs (id) VALUES (NULL)")
|
|
s1.signal()
|
|
_ = s2.wait(timeout: .distantFuture)
|
|
return .commit
|
|
}
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
s3.signal()
|
|
}
|
|
|
|
// Queue 2
|
|
var rows1: [Row]?
|
|
var rows2: [Row]?
|
|
queue.async(group: group) {
|
|
try! dbQueue2.writeWithoutTransaction { db in
|
|
_ = s1.wait(timeout: .distantFuture)
|
|
rows1 = try Row.fetchAll(db, sql: "SELECT * FROM stuffs")
|
|
s2.signal()
|
|
_ = s3.wait(timeout: .distantFuture)
|
|
rows2 = try Row.fetchAll(db, sql: "SELECT * FROM stuffs")
|
|
}
|
|
}
|
|
|
|
_ = group.wait(timeout: .distantFuture)
|
|
|
|
XCTAssertEqual(rows1!.count, 0) // uncommitted changes are not visible
|
|
XCTAssertEqual(rows2!.count, 1) // committed changes are visible
|
|
}
|
|
|
|
func testReaderInDeferredTransactionDuringDefaultTransaction() throws {
|
|
let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
#if GRDBCIPHER_USE_ENCRYPTION
|
|
// Work around SQLCipher bug when two connections are open to the
|
|
// same empty database: make sure the database is not empty before
|
|
// running this test
|
|
try dbQueue1.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)")
|
|
}
|
|
#endif
|
|
let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite")
|
|
|
|
// The `SELECT * FROM stuffs` statement of Queue 2 prevents Queue 1
|
|
// from committing with SQLITE_BUSY.
|
|
//
|
|
// Without this select, the Queue 2 COMMIT succeeds right away.
|
|
//
|
|
// Both facts are unexpected.
|
|
//
|
|
//
|
|
// Queue 1 Queue 2
|
|
// BEGIN IMMEDIATE TRANSACTION
|
|
// INSERT INTO stuffs (id) VALUES (NULL)
|
|
let s1 = DispatchSemaphore(value: 0)
|
|
// BEGIN DEFERRED TRANSACTION
|
|
// SELECT * FROM stuffs
|
|
let s2 = DispatchSemaphore(value: 0)
|
|
// COMMIT <--- Busy
|
|
let s3 = DispatchSemaphore(value: 0)
|
|
// COMMIT
|
|
// COMMIT
|
|
|
|
var busyCallbackCalled = false
|
|
self.busyCallback = { n in
|
|
busyCallbackCalled = true
|
|
s3.signal()
|
|
return true
|
|
}
|
|
|
|
try dbQueue1.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE stuffs (id INTEGER PRIMARY KEY)")
|
|
}
|
|
|
|
let queue = DispatchQueue(label: "GRDB", attributes: [.concurrent])
|
|
let group = DispatchGroup()
|
|
|
|
// Queue 1
|
|
queue.async(group: group) {
|
|
do {
|
|
try dbQueue1.inTransaction { db in
|
|
try db.execute(sql: "INSERT INTO stuffs (id) VALUES (NULL)")
|
|
s1.signal()
|
|
_ = s2.wait(timeout: .distantFuture)
|
|
return .commit
|
|
}
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
// Queue 2
|
|
queue.async(group: group) {
|
|
do {
|
|
_ = s1.wait(timeout: .distantFuture)
|
|
try dbQueue2.inTransaction(.deferred) { db in
|
|
_ = try Row.fetchAll(db, sql: "SELECT * FROM stuffs")
|
|
s2.signal()
|
|
_ = s3.wait(timeout: .distantFuture)
|
|
return .commit
|
|
}
|
|
}
|
|
catch {
|
|
XCTFail("\(error)")
|
|
}
|
|
}
|
|
|
|
_ = group.wait(timeout: .distantFuture)
|
|
|
|
XCTAssertTrue(busyCallbackCalled)
|
|
}
|
|
}
|