Files

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)
}
}