mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Switch the clientProxy's start/stop sync semantics to service pausing and resuming to better match underlying behaviors
This commit is contained in:
@@ -42,7 +42,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
configureNotificationManager()
|
configureNotificationManager()
|
||||||
observeUserSessionChanges()
|
observeUserSessionChanges()
|
||||||
Task {
|
Task {
|
||||||
await startSync()
|
await resumeServices()
|
||||||
await appHooks.configure(with: userSession)
|
await appHooks.configure(with: userSession)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,11 +185,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
self?.handleAppRoute(.call(roomID: roomID, isVoiceCall: isVoiceCall), windowType: nil)
|
self?.handleAppRoute(.call(roomID: roomID, isVoiceCall: isVoiceCall), windowType: nil)
|
||||||
case .receivedIncomingCallRequest:
|
case .receivedIncomingCallRequest:
|
||||||
// When reporting a VoIP call through the CXProvider's `reportNewIncomingVoIPPushPayload`
|
// When reporting a VoIP call through the CXProvider's `reportNewIncomingVoIPPushPayload`
|
||||||
// the UIApplication states don't change and syncing is neither started nor ran on
|
// the UIApplication states don't change and services are neither started nor ran on
|
||||||
// a background task. Handle both manually here.
|
// a background task. Handle both manually here.
|
||||||
Task {
|
Task {
|
||||||
await self?.startSync()
|
await self?.resumeServices()
|
||||||
self?.scheduleDelayedSyncStop()
|
self?.scheduleDelayedPauseServices()
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -813,7 +813,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
showLoadingIndicator()
|
showLoadingIndicator()
|
||||||
|
|
||||||
stopSync(isBackgroundTask: false)
|
pauseServices(isBackgroundTask: false)
|
||||||
userSessionFlowCoordinator?.stop()
|
userSessionFlowCoordinator?.stop()
|
||||||
|
|
||||||
guard !isSoft else {
|
guard !isSoft else {
|
||||||
@@ -943,7 +943,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
navigationRootCoordinator.setRootCoordinator(PlaceholderScreenCoordinator(hideBrandChrome: appSettings.hideBrandChrome))
|
navigationRootCoordinator.setRootCoordinator(PlaceholderScreenCoordinator(hideBrandChrome: appSettings.hideBrandChrome))
|
||||||
|
|
||||||
stopSync(isBackgroundTask: false)
|
pauseServices(isBackgroundTask: false)
|
||||||
userSessionFlowCoordinator?.stop()
|
userSessionFlowCoordinator?.stop()
|
||||||
|
|
||||||
// Allow for everything to deallocate properly
|
// Allow for everything to deallocate properly
|
||||||
@@ -1064,24 +1064,24 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
// MARK: - Application State
|
// MARK: - Application State
|
||||||
|
|
||||||
private func stopSync(isBackgroundTask: Bool, completion: (() -> Void)? = nil) {
|
private func pauseServices(isBackgroundTask: Bool, completion: (() -> Void)? = nil) {
|
||||||
if isBackgroundTask, UIApplication.shared.applicationState == .active {
|
if isBackgroundTask, UIApplication.shared.applicationState == .active {
|
||||||
// Attempt to stop the background task sync loop cleanly, only if the app not already running
|
// Attempt to pause the background task services cleanly, only if the app not already running
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
MainActor.assumeIsolated {
|
MainActor.assumeIsolated {
|
||||||
userSession?.clientProxy.stopSync(completion: completion)
|
userSession?.clientProxy.pauseServices(completion: completion)
|
||||||
clientProxyObserver = nil
|
clientProxyObserver = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startSync() async {
|
private func resumeServices() async {
|
||||||
guard let userSession else { return }
|
guard let userSession else { return }
|
||||||
|
|
||||||
analyticsService.signpost.startTransaction(.upToDateRoomList)
|
analyticsService.signpost.startTransaction(.upToDateRoomList)
|
||||||
|
|
||||||
await userSession.clientProxy.startSync()
|
await userSession.clientProxy.resumeServices()
|
||||||
|
|
||||||
guard clientProxyObserver == nil else {
|
guard clientProxyObserver == nil else {
|
||||||
return
|
return
|
||||||
@@ -1147,14 +1147,14 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
@objc
|
@objc
|
||||||
private func applicationWillTerminate() {
|
private func applicationWillTerminate() {
|
||||||
MXLog.info("Application will terminate")
|
MXLog.info("Application will terminate")
|
||||||
stopSync(isBackgroundTask: false)
|
pauseServices(isBackgroundTask: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
private func applicationDidEnterBackground() {
|
private func applicationDidEnterBackground() {
|
||||||
MXLog.info("Application did enter background")
|
MXLog.info("Application did enter background")
|
||||||
|
|
||||||
scheduleDelayedSyncStop()
|
scheduleDelayedPauseServices()
|
||||||
scheduleBackgroundAppRefresh()
|
scheduleBackgroundAppRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1163,7 +1163,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
MXLog.info("Application will resign active")
|
MXLog.info("Application will resign active")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func scheduleDelayedSyncStop() {
|
private func scheduleDelayedPauseServices() {
|
||||||
guard backgroundTask == nil else {
|
guard backgroundTask == nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1174,7 +1174,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
// We're intentionally strongly retaining self here to an EXC_BAD_ACCESS
|
// We're intentionally strongly retaining self here to an EXC_BAD_ACCESS
|
||||||
// `backgroundTask` will be eventually released in `endActiveBackgroundTask`
|
// `backgroundTask` will be eventually released in `endActiveBackgroundTask`
|
||||||
// https://sentry.tools.element.io/organizations/element/issues/4477794/events/9cfd04e4d045440f87498809cf718de5/
|
// https://sentry.tools.element.io/organizations/element/issues/4477794/events/9cfd04e4d045440f87498809cf718de5/
|
||||||
self.stopSync(isBackgroundTask: true) {
|
self.pauseServices(isBackgroundTask: true) {
|
||||||
self.endActiveBackgroundTask()
|
self.endActiveBackgroundTask()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1185,7 +1185,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
MXLog.info("Application will enter foreground")
|
MXLog.info("Application will enter foreground")
|
||||||
endActiveBackgroundTask()
|
endActiveBackgroundTask()
|
||||||
Task {
|
Task {
|
||||||
await startSync()
|
await resumeServices()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1242,17 +1242,17 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
// This is important for the app to keep refreshing in the background
|
// This is important for the app to keep refreshing in the background
|
||||||
scheduleBackgroundAppRefresh()
|
scheduleBackgroundAppRefresh()
|
||||||
|
|
||||||
// We have a lot of crashes stemming here which we previously believed are caused by stopSync not being async
|
// We have a lot of crashes stemming here which we previously believed are caused by pauseServices not being async
|
||||||
// on the client proxy side (see the comment on that method). We have now realised that will likely not fix anything but
|
// on the client proxy side (see the comment on that method). We have now realised that will likely not fix anything but
|
||||||
// we also noticed this does not crash on the main thread, even though the whole AppCoordinator is on the Main actor.
|
// we also noticed this does not crash on the main thread, even though the whole AppCoordinator is on the Main actor.
|
||||||
// As such, we introduced a MainActor conformance on the expirationHandler but we are also assuming main actor
|
// As such, we introduced a MainActor conformance on the expirationHandler but we are also assuming main actor
|
||||||
// isolated in the `stopSync` method above.
|
// isolated in the `pauseServices` method above.
|
||||||
// https://sentry.tools.element.io/organizations/element/issues/4477794/
|
// https://sentry.tools.element.io/organizations/element/issues/4477794/
|
||||||
task.expirationHandler = { @Sendable [weak self] in
|
task.expirationHandler = { @Sendable [weak self] in
|
||||||
MXLog.info("Background app refresh task is about to expire.")
|
MXLog.info("Background app refresh task is about to expire.")
|
||||||
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self?.stopSync(isBackgroundTask: true) {
|
self?.pauseServices(isBackgroundTask: true) {
|
||||||
MXLog.info("Marking Background app refresh task as complete.")
|
MXLog.info("Marking Background app refresh task as complete.")
|
||||||
task.setTaskCompleted(success: true)
|
task.setTaskCompleted(success: true)
|
||||||
}
|
}
|
||||||
@@ -1263,7 +1263,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await startSync()
|
await resumeServices()
|
||||||
|
|
||||||
// Be a good citizen, run for a max of 10 SS responses or 10 seconds
|
// Be a good citizen, run for a max of 10 SS responses or 10 seconds
|
||||||
// An SS request will time out after 30 seconds if no new data is available
|
// An SS request will time out after 30 seconds if no new data is available
|
||||||
@@ -1278,7 +1278,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
// Make sure we stop the sync loop, otherwise the ongoing request is immediately
|
// Make sure we stop the sync loop, otherwise the ongoing request is immediately
|
||||||
// handled the next time the app refreshes, which can trigger timeout failures.
|
// handled the next time the app refreshes, which can trigger timeout failures.
|
||||||
stopSync(isBackgroundTask: true) {
|
pauseServices(isBackgroundTask: true) {
|
||||||
MXLog.info("Marking Background app refresh task as complete.")
|
MXLog.info("Marking Background app refresh task as complete.")
|
||||||
task.setTaskCompleted(success: true)
|
task.setTaskCompleted(success: true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2451,56 +2451,39 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
|||||||
return hasDevicesToVerifyAgainstReturnValue
|
return hasDevicesToVerifyAgainstReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//MARK: - startSync
|
//MARK: - resumeServices
|
||||||
|
|
||||||
private let startSyncCallsCountLock = NSLock()
|
private let resumeServicesCallsCountLock = NSLock()
|
||||||
private var startSyncUnderlyingCallsCount = 0
|
private var resumeServicesUnderlyingCallsCount = 0
|
||||||
var startSyncCallsCount: Int {
|
var resumeServicesCallsCount: Int {
|
||||||
get { startSyncCallsCountLock.withLock { startSyncUnderlyingCallsCount } }
|
get { resumeServicesCallsCountLock.withLock { resumeServicesUnderlyingCallsCount } }
|
||||||
set { startSyncCallsCountLock.withLock { startSyncUnderlyingCallsCount = newValue } }
|
set { resumeServicesCallsCountLock.withLock { resumeServicesUnderlyingCallsCount = newValue } }
|
||||||
}
|
}
|
||||||
var startSyncCalled: Bool {
|
var resumeServicesCalled: Bool {
|
||||||
return startSyncCallsCount > 0
|
return resumeServicesCallsCount > 0
|
||||||
}
|
}
|
||||||
var startSyncClosure: (() async -> Void)?
|
var resumeServicesClosure: (() async -> Void)?
|
||||||
|
|
||||||
func startSync() async {
|
func resumeServices() async {
|
||||||
startSyncCallsCountLock.withLock { startSyncUnderlyingCallsCount += 1 }
|
resumeServicesCallsCountLock.withLock { resumeServicesUnderlyingCallsCount += 1 }
|
||||||
await startSyncClosure?()
|
await resumeServicesClosure?()
|
||||||
}
|
}
|
||||||
//MARK: - stopSync
|
//MARK: - pauseServices
|
||||||
|
|
||||||
private let stopSyncCallsCountLock = NSLock()
|
private let pauseServicesCompletionCallsCountLock = NSLock()
|
||||||
private var stopSyncUnderlyingCallsCount = 0
|
private var pauseServicesCompletionUnderlyingCallsCount = 0
|
||||||
var stopSyncCallsCount: Int {
|
var pauseServicesCompletionCallsCount: Int {
|
||||||
get { stopSyncCallsCountLock.withLock { stopSyncUnderlyingCallsCount } }
|
get { pauseServicesCompletionCallsCountLock.withLock { pauseServicesCompletionUnderlyingCallsCount } }
|
||||||
set { stopSyncCallsCountLock.withLock { stopSyncUnderlyingCallsCount = newValue } }
|
set { pauseServicesCompletionCallsCountLock.withLock { pauseServicesCompletionUnderlyingCallsCount = newValue } }
|
||||||
}
|
}
|
||||||
var stopSyncCalled: Bool {
|
var pauseServicesCompletionCalled: Bool {
|
||||||
return stopSyncCallsCount > 0
|
return pauseServicesCompletionCallsCount > 0
|
||||||
}
|
}
|
||||||
var stopSyncClosure: (() -> Void)?
|
var pauseServicesCompletionClosure: (((() -> Void)?) -> Void)?
|
||||||
|
|
||||||
func stopSync() {
|
func pauseServices(completion: (() -> Void)?) {
|
||||||
stopSyncCallsCountLock.withLock { stopSyncUnderlyingCallsCount += 1 }
|
pauseServicesCompletionCallsCountLock.withLock { pauseServicesCompletionUnderlyingCallsCount += 1 }
|
||||||
stopSyncClosure?()
|
pauseServicesCompletionClosure?(completion)
|
||||||
}
|
|
||||||
//MARK: - stopSync
|
|
||||||
|
|
||||||
private let stopSyncCompletionCallsCountLock = NSLock()
|
|
||||||
private var stopSyncCompletionUnderlyingCallsCount = 0
|
|
||||||
var stopSyncCompletionCallsCount: Int {
|
|
||||||
get { stopSyncCompletionCallsCountLock.withLock { stopSyncCompletionUnderlyingCallsCount } }
|
|
||||||
set { stopSyncCompletionCallsCountLock.withLock { stopSyncCompletionUnderlyingCallsCount = newValue } }
|
|
||||||
}
|
|
||||||
var stopSyncCompletionCalled: Bool {
|
|
||||||
return stopSyncCompletionCallsCount > 0
|
|
||||||
}
|
|
||||||
var stopSyncCompletionClosure: (((() -> Void)?) -> Void)?
|
|
||||||
|
|
||||||
func stopSync(completion: (() -> Void)?) {
|
|
||||||
stopSyncCompletionCallsCountLock.withLock { stopSyncCompletionUnderlyingCallsCount += 1 }
|
|
||||||
stopSyncCompletionClosure?(completion)
|
|
||||||
}
|
}
|
||||||
//MARK: - expireSyncSessions
|
//MARK: - expireSyncSessions
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
private var hasEncounteredAuthError = false
|
private var hasEncounteredAuthError = false
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
stopSync { [delegateHandle] in
|
pauseServices { [delegateHandle] in
|
||||||
// The delegate handle needs to be cancelled always after the sync stops
|
// The delegate handle needs to be cancelled always after the sync stops
|
||||||
delegateHandle?.cancel()
|
delegateHandle?.cancel()
|
||||||
}
|
}
|
||||||
@@ -389,7 +389,9 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startSync() async {
|
func resumeServices() async {
|
||||||
|
MXLog.info("Resuming services")
|
||||||
|
|
||||||
guard !hasEncounteredAuthError else {
|
guard !hasEncounteredAuthError else {
|
||||||
MXLog.warning("Ignoring request, this client has an unknown token.")
|
MXLog.warning("Ignoring request, this client has an unknown token.")
|
||||||
return
|
return
|
||||||
@@ -418,11 +420,11 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A stored task for restarting the sync after a failure. This is stored so that we can cancel
|
/// A stored task for restarting the sync after a failure. This is stored so that we can cancel
|
||||||
/// it when `stopSync` is called (e.g. when signing out) to prevent an otherwise infinite
|
/// it when `pauseServices` is called (e.g. when signing out) to prevent an otherwise infinite
|
||||||
/// loop that was triggered by trying to sync a signed out session.
|
/// loop that was triggered by trying to sync a signed out session.
|
||||||
@CancellableTask private var restartTask: Task<Void, Never>?
|
@CancellableTask private var restartTask: Task<Void, Never>?
|
||||||
|
|
||||||
func restartSync() {
|
private func restartServices() {
|
||||||
guard restartTask == nil else { return }
|
guard restartTask == nil else { return }
|
||||||
|
|
||||||
restartTask = Task { [weak self] in
|
restartTask = Task { [weak self] in
|
||||||
@@ -430,7 +432,7 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
// Until the SDK can tell us the failure, we add a small
|
// Until the SDK can tell us the failure, we add a small
|
||||||
// delay to avoid generating multi-gigabyte log files.
|
// delay to avoid generating multi-gigabyte log files.
|
||||||
try await Task.sleep(for: .milliseconds(250))
|
try await Task.sleep(for: .milliseconds(250))
|
||||||
await self?.startSync()
|
await self?.resumeServices()
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Restart cancelled.")
|
MXLog.error("Restart cancelled.")
|
||||||
}
|
}
|
||||||
@@ -438,13 +440,10 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopSync() {
|
func pauseServices(completion: (() -> Void)?) {
|
||||||
stopSync(completion: nil)
|
MXLog.info("Pausing services")
|
||||||
}
|
|
||||||
|
|
||||||
func stopSync(completion: (() -> Void)?) {
|
|
||||||
if restartTask != nil {
|
if restartTask != nil {
|
||||||
MXLog.warning("Removing the sync service restart task.")
|
|
||||||
restartTask = nil
|
restartTask = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1030,7 +1029,7 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
.sink { [weak self] reachability in
|
.sink { [weak self] reachability in
|
||||||
if reachability == .reachable {
|
if reachability == .reachable {
|
||||||
Task {
|
Task {
|
||||||
await self?.startSync()
|
await self?.resumeServices()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1147,7 +1146,7 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
case .offline:
|
case .offline:
|
||||||
homeserverReachabilitySubject.send(.unreachable)
|
homeserverReachabilitySubject.send(.unreachable)
|
||||||
case .error:
|
case .error:
|
||||||
restartSync()
|
restartServices()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,11 +169,9 @@ protocol ClientProxyProtocol: AnyObject {
|
|||||||
|
|
||||||
func hasDevicesToVerifyAgainst() async -> Result<Bool, ClientProxyError>
|
func hasDevicesToVerifyAgainst() async -> Result<Bool, ClientProxyError>
|
||||||
|
|
||||||
func startSync() async
|
func resumeServices() async
|
||||||
|
|
||||||
func stopSync()
|
func pauseServices(completion: (() -> Void)?) // Hopefully this will become async once we get SE-0371.
|
||||||
|
|
||||||
func stopSync(completion: (() -> Void)?) // Hopefully this will become async once we get SE-0371.
|
|
||||||
|
|
||||||
func expireSyncSessions() async
|
func expireSyncSessions() async
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user