add iOS
This commit is contained in:
+436
@@ -0,0 +1,436 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user