ios, core: better notifications processing to avoid contention for database (#3485)

* core: forward notifications about message processing (for iOS notifications)

* simplexmq

* the option to keep database key, to allow re-opening the database

* export new init with keepKey and reopen DB api

* stop remote ctrl when suspending chat

* ios: close/re-open db on suspend/activate

* allow activating chat without restoring (for NSE)

* update NSE to suspend/activate (does not work)

* simplexmq

* suspend chat and close database when last notification in the process is processed

* stop reading notifications on message markers

* replace async stream with cancellable concurrent queue

* better synchronization of app and NSE

* remove outside of task

* remove unused var

* whitespace

* more debug logging, handle cancelled read after dequeue

* comments

* more comments
This commit is contained in:
Evgeny Poberezkin
2023-12-09 21:59:40 +00:00
committed by GitHub
parent 2f7632a70f
commit d3059afc99
29 changed files with 661 additions and 224 deletions
+4 -3
View File
@@ -15,7 +15,7 @@ private let receiveTaskId = "chat.simplex.app.receive"
// TCP timeout + 2 sec
private let waitForMessages: TimeInterval = 6
private let bgRefreshInterval: TimeInterval = 450
private let bgRefreshInterval: TimeInterval = 600
private let maxTimerCount = 9
@@ -55,7 +55,7 @@ class BGManager {
}
logger.debug("BGManager.handleRefresh")
schedule()
if appStateGroupDefault.get().inactive {
if allowBackgroundRefresh() {
let completeRefresh = completionHandler {
task.setTaskCompleted(success: true)
}
@@ -92,18 +92,19 @@ class BGManager {
DispatchQueue.main.async {
let m = ChatModel.shared
if (!m.chatInitialized) {
setAppState(.bgRefresh)
do {
try initializeChat(start: true)
} catch let error {
fatalError("Failed to start or load chats: \(responseError(error))")
}
}
activateChat(appState: .bgRefresh)
if m.currentUser == nil {
completeReceiving("no current user")
return
}
logger.debug("BGManager.receiveMessages: starting chat")
activateChat(appState: .bgRefresh)
let cr = ChatReceiver()
self.chatReceiver = cr
cr.start()
+4 -2
View File
@@ -105,11 +105,13 @@ final class ChatModel: ObservableObject {
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
var ntfEnableLocal: Bool {
notificationMode == .off || ntfEnableLocalGroupDefault.get()
true
// notificationMode == .off || ntfEnableLocalGroupDefault.get()
}
var ntfEnablePeriodic: Bool {
notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get()
notificationMode != .off
// notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get()
}
var activeRemoteCtrl: Bool {
+2 -1
View File
@@ -228,7 +228,8 @@ func apiStopChat() async throws {
}
func apiActivateChat() {
let r = chatSendCmdSync(.apiActivateChat)
chatReopenStore()
let r = chatSendCmdSync(.apiActivateChat(restoreChat: true))
if case .cmdOk = r { return }
logger.error("apiActivateChat error: \(String(describing: r))")
}
+27 -10
View File
@@ -18,6 +18,8 @@ let bgSuspendTimeout: Int = 5 // seconds
let terminationTimeout: Int = 3 // seconds
let activationDelay: Double = 1.5 // seconds
private func _suspendChat(timeout: Int) {
// this is a redundant check to prevent logical errors, like the one fixed in this PR
let state = appStateGroupDefault.get()
@@ -47,8 +49,6 @@ func suspendBgRefresh() {
}
}
private var terminating = false
func terminateChat() {
logger.debug("terminateChat")
suspendLockQueue.sync {
@@ -64,7 +64,6 @@ func terminateChat() {
case .stopped:
chatCloseStore()
default:
terminating = true
// the store will be closed in _chatSuspended when event is received
_suspendChat(timeout: terminationTimeout)
}
@@ -85,14 +84,17 @@ private func _chatSuspended() {
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.stop()
}
if terminating {
chatCloseStore()
chatCloseStore()
}
func setAppState(_ appState: AppState) {
suspendLockQueue.sync {
appStateGroupDefault.set(appState)
}
}
func activateChat(appState: AppState = .active) {
logger.debug("DEBUGGING: activateChat")
terminating = false
suspendLockQueue.sync {
appStateGroupDefault.set(appState)
if ChatModel.ok { apiActivateChat() }
@@ -101,7 +103,6 @@ func activateChat(appState: AppState = .active) {
}
func initChatAndMigrate(refreshInvitations: Bool = true) {
terminating = false
let m = ChatModel.shared
if (!m.chatInitialized) {
do {
@@ -113,16 +114,32 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
}
}
func startChatAndActivate() {
terminating = false
func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ completion: @escaping () -> Void) {
logger.debug("DEBUGGING: startChatAndActivate")
if ChatModel.shared.chatRunning == true {
ChatReceiver.shared.start()
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
}
if .active != appStateGroupDefault.get() {
if .active == appStateGroupDefault.get() {
completion()
} else if nseStateGroupDefault.get().inactive {
activate()
} else {
suspendLockQueue.sync {
appStateGroupDefault.set(.activating)
}
// TODO can be replaced with Mach messenger to notify the NSE to terminate and continue after reply, with timeout
dispatchQueue.asyncAfter(deadline: .now() + activationDelay) {
if appStateGroupDefault.get() == .activating {
activate()
}
}
}
func activate() {
logger.debug("DEBUGGING: startChatAndActivate: before activateChat")
activateChat()
completion()
logger.debug("DEBUGGING: startChatAndActivate: after activateChat")
}
}