mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(ios): settle expired background refreshes exactly once (#129097)
This commit is contained in:
committed by
GitHub
parent
50b3e4bc5b
commit
0528f44ce4
@@ -7797,6 +7797,7 @@ extension NodeAppModel {
|
||||
}
|
||||
|
||||
func handleBackgroundRefreshWake(trigger: String = "bg_app_refresh") async -> Bool {
|
||||
guard !Task.isCancelled else { return false }
|
||||
let wakeId = Self.makePushWakeAttemptID()
|
||||
let normalizedTrigger = BackgroundAliveBeacon.normalizeTrigger(trigger)
|
||||
let receivedMessage =
|
||||
@@ -7815,7 +7816,7 @@ extension NodeAppModel {
|
||||
+ "reason=\(result.reason) "
|
||||
+ "durationMs=\(result.durationMs)"
|
||||
self.pushWakeLogger.info("\(outcomeMessage, privacy: .public)")
|
||||
return result.handled
|
||||
return !Task.isCancelled && result.handled
|
||||
}
|
||||
|
||||
func handleSignificantLocationWakeIfNeeded() async {
|
||||
@@ -9799,6 +9800,9 @@ extension NodeAppModel {
|
||||
}
|
||||
let now = Date()
|
||||
let gatewayConnected = await isGatewayConnected()
|
||||
guard !Task.isCancelled else {
|
||||
return makeResult(false, .unhandled, "cancelled")
|
||||
}
|
||||
|
||||
var appliedReconnect = false
|
||||
if !gatewayConnected {
|
||||
@@ -9811,7 +9815,8 @@ extension NodeAppModel {
|
||||
"Wake reconnect begin wakeId=\(wakeId, privacy: .public) stableID=\(cfg.stableID, privacy: .public)")
|
||||
self.grantBackgroundReconnectLease(seconds: 30, reason: "wake_\(wakeId)")
|
||||
await self.resetGatewaySessionsForForcedReconnect()
|
||||
guard generation == self.gatewayConnectGeneration,
|
||||
guard !Task.isCancelled,
|
||||
generation == self.gatewayConnectGeneration,
|
||||
self.gatewayAutoReconnectEnabled,
|
||||
self.activeGatewayConnectConfig?.hasSameConnectionInputs(as: cfg) == true
|
||||
else {
|
||||
@@ -9826,7 +9831,7 @@ extension NodeAppModel {
|
||||
self.pushWakeLogger.info("Wake reconnect trigger applied wakeId=\(wakeId, privacy: .public)")
|
||||
|
||||
let connected = await waitForGatewayConnection(timeoutMs: 12000, pollMs: 250)
|
||||
guard connected else {
|
||||
guard !Task.isCancelled, connected else {
|
||||
return makeResult(appliedReconnect, .unhandled, "connect_timeout")
|
||||
}
|
||||
guard generation == self.gatewayConnectGeneration else {
|
||||
@@ -9841,6 +9846,9 @@ extension NodeAppModel {
|
||||
}
|
||||
|
||||
let beacon = await publishBackgroundAliveBeacon(trigger: trigger)
|
||||
guard !Task.isCancelled else {
|
||||
return makeResult(appliedReconnect, .unhandled, "cancelled")
|
||||
}
|
||||
if beacon.handled {
|
||||
let successAtMs = Date().timeIntervalSince1970 * 1000
|
||||
UserDefaults.standard.set(successAtMs, forKey: Self.backgroundAliveLastSuccessAtMsKey)
|
||||
|
||||
@@ -16,6 +16,40 @@ private struct PendingWatchPromptAction {
|
||||
|
||||
private typealias PendingExecApprovalPrompt = ApprovalNotificationPrompt
|
||||
|
||||
/// BackgroundTasks expires on a background queue; settle there before a delayed
|
||||
/// main-actor waiter can report success or complete the same delivery twice.
|
||||
final class BackgroundWakeRefreshAttempt: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var wakeTask: Task<Bool, Never>?
|
||||
private var completion: ((Bool) -> Void)?
|
||||
|
||||
init(wakeTask: Task<Bool, Never>, completion: @escaping (Bool) -> Void) {
|
||||
self.wakeTask = wakeTask
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func complete(success: Bool) {
|
||||
self.lock.lock()
|
||||
guard let completion = self.completion else {
|
||||
self.lock.unlock()
|
||||
return
|
||||
}
|
||||
self.completion = nil
|
||||
let wakeTask = self.wakeTask
|
||||
self.wakeTask = nil
|
||||
self.lock.unlock()
|
||||
|
||||
if !success {
|
||||
wakeTask?.cancel()
|
||||
}
|
||||
completion(success)
|
||||
}
|
||||
|
||||
func expire() {
|
||||
self.complete(success: false)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
enum OpenClawAppModelRegistry {
|
||||
static var appModel: NodeAppModel?
|
||||
@@ -39,7 +73,7 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
|
||||
return bundleId
|
||||
}
|
||||
|
||||
private var backgroundWakeTask: Task<Bool, Never>?
|
||||
private var backgroundWakeAttempt: BackgroundWakeRefreshAttempt?
|
||||
private var pendingAPNsDeviceToken: Data?
|
||||
private var pendingWatchPromptActions: [PendingWatchPromptAction] = []
|
||||
private var pendingExecApprovalPrompts: [PendingExecApprovalPrompt] = []
|
||||
@@ -280,19 +314,22 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc
|
||||
|
||||
private func handleBackgroundWakeRefresh(task: BGAppRefreshTask) {
|
||||
self.scheduleBackgroundWakeRefresh(afterSeconds: 15 * 60, reason: "reschedule")
|
||||
self.backgroundWakeTask?.cancel()
|
||||
self.backgroundWakeAttempt?.expire()
|
||||
|
||||
let wakeTask = Task { @MainActor [weak self] in
|
||||
guard let self, let appModel = self.resolvedAppModel() else { return false }
|
||||
return await appModel.handleBackgroundRefreshWake(trigger: "bg_app_refresh")
|
||||
}
|
||||
self.backgroundWakeTask = wakeTask
|
||||
let attempt = BackgroundWakeRefreshAttempt(wakeTask: wakeTask) { success in
|
||||
task.setTaskCompleted(success: success)
|
||||
}
|
||||
self.backgroundWakeAttempt = attempt
|
||||
task.expirationHandler = {
|
||||
wakeTask.cancel()
|
||||
attempt.expire()
|
||||
}
|
||||
Task {
|
||||
let applied = await wakeTask.value
|
||||
task.setTaskCompleted(success: applied)
|
||||
attempt.complete(success: applied)
|
||||
self.backgroundWakeLogger.info(
|
||||
"Background wake refresh finished applied=\(applied, privacy: .public)")
|
||||
}
|
||||
|
||||
@@ -1039,6 +1039,61 @@ private final class TimingOutDeviceStatusService: DeviceStatusServicing {
|
||||
|
||||
#expect(result == .noData)
|
||||
#expect(await appModel.handleBackgroundRefreshWake())
|
||||
|
||||
let expiredRefresh = Task { @MainActor in
|
||||
await appModel.handleBackgroundRefreshWake()
|
||||
}
|
||||
expiredRefresh.cancel()
|
||||
#expect(await expiredRefresh.value == false)
|
||||
}
|
||||
|
||||
@Test @MainActor func `expired background refresh settles unsuccessful exactly once`() {
|
||||
let wakeTask = Task { true }
|
||||
var completions: [Bool] = []
|
||||
let attempt = BackgroundWakeRefreshAttempt(wakeTask: wakeTask) {
|
||||
completions.append($0)
|
||||
}
|
||||
|
||||
attempt.expire()
|
||||
attempt.complete(success: true)
|
||||
attempt.expire()
|
||||
|
||||
#expect(completions == [false])
|
||||
#expect(wakeTask.isCancelled)
|
||||
}
|
||||
|
||||
@Test @MainActor func `completed background refresh ignores later expiration`() {
|
||||
let wakeTask = Task { true }
|
||||
var completions: [Bool] = []
|
||||
let attempt = BackgroundWakeRefreshAttempt(wakeTask: wakeTask) {
|
||||
completions.append($0)
|
||||
}
|
||||
|
||||
attempt.complete(success: true)
|
||||
attempt.expire()
|
||||
|
||||
#expect(completions == [true])
|
||||
#expect(!wakeTask.isCancelled)
|
||||
}
|
||||
|
||||
@Test @MainActor func `replaced background refresh settles before its successor`() {
|
||||
let replacedTask = Task { true }
|
||||
let replacementTask = Task { true }
|
||||
var completions: [Bool] = []
|
||||
let replaced = BackgroundWakeRefreshAttempt(wakeTask: replacedTask) {
|
||||
completions.append($0)
|
||||
}
|
||||
let replacement = BackgroundWakeRefreshAttempt(wakeTask: replacementTask) {
|
||||
completions.append($0)
|
||||
}
|
||||
|
||||
replaced.expire()
|
||||
replacement.complete(success: true)
|
||||
replaced.complete(success: true)
|
||||
|
||||
#expect(completions == [false, true])
|
||||
#expect(replacedTask.isCancelled)
|
||||
#expect(!replacementTask.isCancelled)
|
||||
}
|
||||
|
||||
@Test func `network status timeout never invents offline network facts`() async {
|
||||
|
||||
Reference in New Issue
Block a user