Skip to content

Commit

Permalink
Fix 'database locked' error upon logout.
Browse files Browse the repository at this point in the history
SQLite ROLLBACK TO command for savepoints does NOT actually
release the savepoint (transaction) which effectively keeps a lock on the database.

(cherry picked from commit 4d2ef9c)
  • Loading branch information
aforge committed Feb 28, 2023
1 parent e1f1397 commit f375fff
Show file tree
Hide file tree
Showing 8 changed files with 99 additions and 18 deletions.
5 changes: 2 additions & 3 deletions Tinodios/UiUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,8 @@ class UiUtils {
public static func logoutAndRouteToLoginVC() {
Cache.log.info("UiUtils - Invalidating cache and logging out.")
SharedUtils.removeAuthToken()
UiUtils.routeToLoginVC() {
Cache.invalidate()
}
Cache.invalidate()
UiUtils.routeToLoginVC()
}

private static func routeToLoginVC(completion: (() -> (Void))? = nil) {
Expand Down
13 changes: 12 additions & 1 deletion TinodiosDB/AccountDb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ public class AccountDb {
try! self.db.run(self.table.createIndex(uid, unique: true, ifNotExists: true))
try! self.db.run(self.table.createIndex(active, ifNotExists: true))
}

// Deletes all records from `accounts` table.
public func truncateTable() {
try! self.db.run(self.table.delete())
}

@discardableResult
func deactivateAll() throws -> Int {
return try self.db.run(self.table.update(self.active <- 0))
Expand All @@ -68,9 +74,10 @@ public class AccountDb {
return nil
}
func addOrActivateAccount(for uid: String, withCredMethods meth: [String]?) -> StoredAccount? {
let savepointName = "AccountDb.addOrActivateAccount"
var result: StoredAccount?
do {
try db.savepoint("AccountDb.addOrActivateAccount") {
try db.savepoint(savepointName) {
try self.deactivateAll()
result = self.getByUid(uid: uid)
let serializedCredMeth = meth?.joined(separator: ",")
Expand All @@ -93,9 +100,13 @@ public class AccountDb {
}
}
} catch SQLite.Result.error(message: let err, code: let code, statement: _) {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("Failed to add account for uid %@: SQLite code = %d, error = %@", uid, code, err)
result = nil
} catch {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("Failed to add account '%@'", error.localizedDescription)
result = nil
}
Expand Down
23 changes: 21 additions & 2 deletions TinodiosDB/BaseDb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ public class BaseDb {
self.account = self.accountDb!.getActiveAccount()
}

private func clearDb() {
BaseDb.log.info("Clearing local store (SQLite db).")
try! self.db!.transaction {
self.messageDb?.truncateTable()
self.subscriberDb?.truncateTable()
self.topicDb?.truncateTable()
self.userDb?.truncateTable()
self.accountDb?.truncateTable()
}
}

private func dropDb() {
BaseDb.log.info("Dropping local store (SQLite db).")
self.messageDb?.destroyTable()
Expand Down Expand Up @@ -174,7 +185,7 @@ public class BaseDb {
// _ = try? self.accountDb?.deactivateAll()
// self.setUid(uid: nil, credMethods: nil)
BaseDb.accessQueue.sync {
self.dropDb()
self.clearDb()
BaseDb.default = nil
}
}
Expand All @@ -191,8 +202,9 @@ public class BaseDb {
BaseDb.log.error("Could not find account for uid [%@]", uid)
return false
}
let savepointName = "BaseDb.deleteUid"
do {
try self.db?.savepoint("BaseDb.deleteUid") {
try self.db?.savepoint(savepointName) {
if !(self.topicDb?.deleteAll(forAccount: acc2.id) ?? true) {
BaseDb.log.error("Failed to clear topics/messages/subscribers for account id [%lld]", acc2.id)
}
Expand All @@ -204,6 +216,8 @@ public class BaseDb {
}
}
} catch {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db?.releaseSavepoint(withName: savepointName)
BaseDb.log.error("BaseDb - deleteUid operation failed: uid = %@, error = %@", uid, error.localizedDescription)
return false
}
Expand Down Expand Up @@ -238,4 +252,9 @@ extension SQLite.Connection {
get { return Int32((try? scalar("PRAGMA user_version") as? Int64) ?? -1) }
set { try! run("PRAGMA user_version = \(newValue)") }
}

/// Releases a savepoint explicitly.
public func releaseSavepoint(withName savepointName: String) {
try! self.execute("RELEASE '\(savepointName)'")
}
}
28 changes: 24 additions & 4 deletions TinodiosDB/MessageDb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ public class MessageDb {
self.content = Expression<String?>("content")
}
func destroyTable() {
if self.db.locked
try! self.db.run(self.table.dropIndex(topicId, effectiveSeq.desc, ifExists: true))
try! self.db.run(self.table.dropIndex(topicId, seq.desc, ifExists: true))
try! self.db.run(self.table.drop(ifExists: true))
Expand Down Expand Up @@ -100,6 +99,12 @@ public class MessageDb {
try! self.db.run([partIdx, "WHERE", (self.effectiveSeq != nil).asSQL()].joined(separator: " "))
}

// Deletes all records from `messages` table.
func truncateTable() {
// Note: SQLite optimizes 'DELETE FROM table' to behave like `TRUNCATE TABLE`.
try! self.db.run(self.table.delete())
}

@discardableResult
private func insertRaw(topic: TopicProto, msg: StoredMessage, withEffectiveSeq effectiveSeqId: Int?,
withEffectiveTs effectiveTs: Date?) throws -> Int64 {
Expand Down Expand Up @@ -186,8 +191,9 @@ public class MessageDb {
if (msg.topicId ?? -1) <= 0 {
msg.topicId = topicId
}
let savepointName = "MessageDb.insert"
do {
try db.savepoint("MessageDb.insert") {
try db.savepoint(savepointName) {
var effSeq: Int?
var effTs: Date?
if let replaceSeq = msg.replacesSeq {
Expand Down Expand Up @@ -221,9 +227,13 @@ public class MessageDb {
}
return msg.msgId
} catch SQLite.Result.error(message: let errMsg, code: let code, statement: _) {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("MessageDb[topic: %@, seq: %d] - insert SQLite error: code = %d, error = %@", topic.name, msg.seqId, code, errMsg)
return -1
} catch {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("MessageDb - insert operation failed: %@", error.localizedDescription.debugDescription)
return -1
}
Expand Down Expand Up @@ -309,8 +319,9 @@ public class MessageDb {
@discardableResult
func deleteOrMarkDeleted(topicId: Int64, delId: Int?, inRanges ranges: [MsgRange], hard: Bool) -> Bool {
var success = false
let savepointName = "MessageDb.deleteOrMarkDeleted-ranges"
do {
try db.savepoint("MessageDb.deleteOrMarkDeleted-ranges") {
try db.savepoint(savepointName) {
for r in ranges {
if !deleteOrMarkDeleted(topicId: topicId, delId: delId, from: r.lower, to: r.upper, hard: hard) {
throw MessageDbError.dbError("Failed to process: delId \(delId ?? -1) range: \(r)")
Expand All @@ -319,8 +330,12 @@ public class MessageDb {
}
success = true
} catch SQLite.Result.error(message: let errMsg, code: let code, statement: _) {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("MessageDb[topicId = %lld] - deleteOrMarkDeleted SQLite error: code = %d, error = %@", topicId, code, errMsg)
} catch {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("MessageDb - deleteOrMarkDeleted2 with ranges failed: topicId = %lld, error = %@", topicId, error.localizedDescription)
}
return success
Expand All @@ -333,10 +348,11 @@ public class MessageDb {
if endId == 0 {
endId = startId + 1
}
let savepointName = "MessageDb.deleteOrMarkDeleted-plain"
// 1. Delete all messages in the range.
do {
var updateResult = false
try db.savepoint("MessageDb.deleteOrMarkDeleted-plain") {
try db.savepoint(savepointName) {
// Message selector: all messages in a given topic with seq between fromId and toId [inclusive, exclusive).
let messageSelector = self.table.filter(
self.topicId == topicId && startId <= self.seq && self.seq < endId && self.status <= BaseDb.Status.synced.rawValue)
Expand Down Expand Up @@ -395,9 +411,13 @@ public class MessageDb {
}
return updateResult
} catch SQLite.Result.error(message: let errMsg, code: let code, statement: _) {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("MessageDb[topicId = %lld] - markDeleted SQLite error: code = %d, error = %@", topicId, code, errMsg)
return false
} catch {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("MessageDb - markDeleted operation failed: topicId = %lld, error = %@", topicId, error.localizedDescription)
return false
}
Expand Down
22 changes: 16 additions & 6 deletions TinodiosDB/SqlStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,20 @@ public class SqlStore: Storage {

public func topicDelete(topic: TopicProto, hard: Bool) -> Bool {
guard let st = topic.payload as? StoredTopic, let topicId = st.id else { return false }
let savepointName = "SqlStore.topicDelete"
do {
if hard {
try dbh?.db?.savepoint("SqlStore.topicDelete") {
try dbh?.db?.savepoint(savepointName) {
self.dbh?.messageDb?.deleteAll(forTopic: topicId)
self.dbh?.subscriberDb?.deleteForTopic(topicId: topicId)
self.dbh?.topicDb?.delete(recordId: topicId)

}
} else {
self.dbh?.topicDb?.markDeleted(recordId: topicId)
}
return true
} catch {
dbh?.db?.releaseSavepoint(withName: savepointName)
BaseDb.log.error("SqlStore - topicDelete operation failed: topicId = %lld, error = %@", topicId, error.localizedDescription)
return false
}
Expand Down Expand Up @@ -204,15 +205,17 @@ public class SqlStore: Storage {
sm.topicId = topicId
sm.userId = userId
sm.dbStatus = .synced
let savepointName = "SqlStore.msgReceived"
do {
try dbh?.db?.savepoint("SqlStore.msgReceived") {
try dbh?.db?.savepoint(savepointName) {
sm.msgId = self.dbh?.messageDb?.insert(topic: topic, msg: sm) ?? -1
if sm.msgId <= 0 || !(self.dbh?.topicDb?.msgReceived(topic: topic, ts: sm.ts ?? Date(), seq: sm.seqId) ?? false) {
throw SqlStoreError.dbError("Could not handle received message: msgId = \(sm.msgId), topicId = \(topicId), userId = \(userId)")
}
}
return sm
} catch {
dbh?.db?.releaseSavepoint(withName: savepointName)
BaseDb.log.error("SqlStore - msgReceived operation failed: %@", error.localizedDescription)
return nil
}
Expand Down Expand Up @@ -286,8 +289,9 @@ public class SqlStore: Storage {
}

public func msgDelivered(topic: TopicProto, dbMessageId: Int64, timestamp: Date, seq: Int) -> Bool {
let savepointName = "SqlStore.msgDelivered"
do {
try dbh?.db?.savepoint("SqlStore.msgDelivered") {
try dbh?.db?.savepoint(savepointName) {
let messageDbSuccessful = self.dbh?.messageDb?.delivered(msgId: dbMessageId, ts: timestamp, seq: seq) ?? false
let topicDbSuccessful = self.dbh?.topicDb?.msgReceived(topic: topic, ts: timestamp, seq: seq) ?? false
if !(messageDbSuccessful && topicDbSuccessful) {
Expand All @@ -296,6 +300,7 @@ public class SqlStore: Storage {
}
return true
} catch {
dbh?.db?.releaseSavepoint(withName: savepointName)
BaseDb.log.error("SqlStore - msgDelivered operation failed %@", error.localizedDescription)
return false
}
Expand All @@ -316,12 +321,14 @@ public class SqlStore: Storage {
guard let st = topic.payload as? StoredTopic, let topicId = st.id else { return false }
let idHi = idHi <= 0 ? (st.maxLocalSeq ?? 0) + 1 : idHi
var success = false
let savepointName = "SqlStore.msgDelete-bounds"
do {
try dbh?.db?.savepoint("SqlStore.msgDelete-bounds") {
try dbh?.db?.savepoint(savepointName) {
success = (dbh?.topicDb?.msgDeleted(topic: topic, delId: delId, from: idLo, to: idHi) ?? false) &&
(dbh?.messageDb?.delete(topicId: topicId, deleteId: delId, from: idLo, to: idHi) ?? false)
}
} catch {
dbh?.db?.releaseSavepoint(withName: savepointName)
BaseDb.log.error("SqlStore - msgDelete operation failed %@", error.localizedDescription)
}
return success
Expand All @@ -333,12 +340,15 @@ public class SqlStore: Storage {
let collapsedRanges = MsgRange.collapse(ranges)
let enclosing = MsgRange.enclosing(for: collapsedRanges)
var success = false
let savepointName = "SqlStore.msgDelete-ranges"
do {
try dbh?.db?.savepoint("SqlStore.msgDelete-ranges") {
try dbh?.db?.savepoint(savepointName) {
success = (dbh?.topicDb?.msgDeleted(topic: topic, delId: delId, from: enclosing!.lower, to: enclosing!.upper) ?? false) &&
(dbh?.messageDb?.deleteOrMarkDeleted(topicId: topicId, delId: delId, inRanges: collapsedRanges, hard: false) ?? false)
}
} catch {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
dbh?.db?.releaseSavepoint(withName: savepointName)
BaseDb.log.error("SqlStore - msgDelete operation failed %@", error.localizedDescription)
}
return success
Expand Down
16 changes: 14 additions & 2 deletions TinodiosDB/SubscriberDb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,17 @@ public class SubscriberDb {
})
try! self.db.run(self.table.createIndex(topicId, ifNotExists: true))
}

// Deletes all records from `subscribers` table.
func truncateTable() {
try! self.db.run(self.table.delete())
}

func insert(for topicId: Int64, with status: BaseDb.Status, using sub: SubscriptionProto) -> Int64 {
var rowId: Int64 = -1
let savepointName = "SubscriberDb.insert"
do {
try self.db.savepoint("SubscriberDb.insert") {
try self.db.savepoint(savepointName) {
let ss = StoredSubscription()
let userDb = baseDb.userDb!
ss.userId = userDb.getId(for: sub.user)
Expand Down Expand Up @@ -125,6 +132,8 @@ public class SubscriberDb {
sub.payload = ss
}
} catch {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("SubscriberDb - insert operation failed: topicId = %lld, error = %@", topicId, error.localizedDescription)
return -1
}
Expand All @@ -136,8 +145,9 @@ public class SubscriberDb {
}
let record = self.table.filter(self.id == recordId)
var updated = 0
let savepointName = "SubscriberDb.update"
do {
try self.db.savepoint("SubscriberDb.update") {
try self.db.savepoint(savepointName) {
var status = ss.status!
_ = baseDb.userDb!.update(sub: sub)
var setters = [Setter]()
Expand All @@ -159,6 +169,8 @@ public class SubscriberDb {
ss.status = status
}
} catch {
// Explicitly releasing savepoint since ROLLBACK TO (SQLite.swift behavior) won't release the savepoint transaction.
self.db.releaseSavepoint(withName: savepointName)
BaseDb.log.error("SubscriberDb - update operation failed: subId = %lld, error = %@", recordId, error.localizedDescription)
return false
}
Expand Down
5 changes: 5 additions & 0 deletions TinodiosDB/TopicDb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ public class TopicDb {
try! db.run(self.table.createIndex(accountId, topic, unique: true, ifNotExists: true))
}

// Deletes all records from `topics` table.
public func truncateTable() {
try! self.db.run(self.table.delete())
}

public static func isUnsentSeq(seq: Int) -> Bool {
return seq >= TopicDb.kUnsentIdStart
}
Expand Down
5 changes: 5 additions & 0 deletions TinodiosDB/UserDb.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public class UserDb {
try! self.db.run(self.table.createIndex(accountId, uid, ifNotExists: true))
}

// Deletes all records from `users` table.
public func truncateTable() {
try! self.db.run(self.table.delete())
}

public func insert(user: UserProto?) -> Int64 {
guard let user = user else { return 0 }
let id = self.insert(uid: user.uid, updated: user.updated, serializedPub: user.serializePub())
Expand Down

0 comments on commit f375fff

Please # to comment.