mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 07:17:14 +00:00
fix(macos): wait for the current reconnect snapshot (#116550)
* fix(macos): isolate snapshot waiter timeouts * chore: leave changelog to release generation * test(macos): wait for voice ear expiry
This commit is contained in:
@@ -23,7 +23,10 @@ struct AppStateVoiceEarsTests {
|
||||
|
||||
#expect(state.earBoostActive)
|
||||
|
||||
try await Task.sleep(for: .milliseconds(60))
|
||||
let deadline = ContinuousClock.now + .seconds(1)
|
||||
while state.earBoostActive, ContinuousClock.now < deadline {
|
||||
try await Task.sleep(for: .milliseconds(10))
|
||||
}
|
||||
#expect(!state.earBoostActive)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ public actor GatewayNodeSession {
|
||||
private var serverMethods: Set<String>?
|
||||
private var serverCapabilities: Set<GatewayServerCapability>?
|
||||
private var mainSessionKey: String?
|
||||
private var snapshotWaiters: [CheckedContinuation<Bool, Never>] = []
|
||||
private var snapshotWaiters: [UUID: CheckedContinuation<Bool, Never>] = [:]
|
||||
private var snapshotReadyWaiters: [CheckedContinuation<Bool, Never>] = []
|
||||
// `computer.act` is not safe to repeat after a response is lost. Keep recent
|
||||
// in-flight/results on the long-lived node session so a channel reconnect can
|
||||
@@ -979,12 +979,13 @@ extension GatewayNodeSession {
|
||||
return true
|
||||
}
|
||||
let clamped = max(0, timeoutMs)
|
||||
let waiterID = UUID()
|
||||
return await withCheckedContinuation { cont in
|
||||
self.snapshotWaiters.append(cont)
|
||||
self.snapshotWaiters[waiterID] = cont
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
try? await Task.sleep(nanoseconds: UInt64(clamped) * 1_000_000)
|
||||
await self.timeoutSnapshotWaiters()
|
||||
await self.timeoutSnapshotWaiter(id: waiterID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -998,14 +999,16 @@ extension GatewayNodeSession {
|
||||
}
|
||||
}
|
||||
|
||||
private func timeoutSnapshotWaiters() {
|
||||
guard !self.snapshotReceived else { return }
|
||||
self.drainSnapshotWaiters(returning: false)
|
||||
private func timeoutSnapshotWaiter(id: UUID) {
|
||||
guard !self.snapshotReceived,
|
||||
let waiter = self.snapshotWaiters.removeValue(forKey: id)
|
||||
else { return }
|
||||
waiter.resume(returning: false)
|
||||
}
|
||||
|
||||
private func drainSnapshotWaiters(returning value: Bool) {
|
||||
if !self.snapshotWaiters.isEmpty {
|
||||
let waiters = self.snapshotWaiters
|
||||
let waiters = self.snapshotWaiters.values
|
||||
self.snapshotWaiters.removeAll()
|
||||
for waiter in waiters {
|
||||
waiter.resume(returning: value)
|
||||
@@ -1332,6 +1335,26 @@ extension GatewayNodeSession {
|
||||
self.broadcastServerEvent(event)
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests reproduce a stale timeout across route reset.
|
||||
func _test_waitForSnapshot(timeoutMs: Int) async -> Bool {
|
||||
await self.waitForSnapshot(timeoutMs: timeoutMs)
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests complete snapshot waits without a live socket.
|
||||
func _test_markSnapshotReceived() {
|
||||
self.markSnapshotReceived()
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests reproduce route replacement snapshot state.
|
||||
func _test_resetConnectionState() {
|
||||
self.resetConnectionState()
|
||||
}
|
||||
|
||||
// periphery:ignore - package tests wait until a snapshot continuation is registered.
|
||||
func _test_snapshotWaiterCount() -> Int {
|
||||
self.snapshotWaiters.count
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
private func cancelActiveInvokes(
|
||||
|
||||
@@ -1320,6 +1320,30 @@ struct GatewayNodeSessionTests {
|
||||
await gateway.disconnect()
|
||||
}
|
||||
|
||||
@Test
|
||||
func `completed snapshot timeout cannot release a later route waiter`() async throws {
|
||||
let gateway = GatewayNodeSession()
|
||||
let firstWait = Task {
|
||||
await gateway._test_waitForSnapshot(timeoutMs: 1000)
|
||||
}
|
||||
try await waitUntil("initial snapshot waiter registered") {
|
||||
await gateway._test_snapshotWaiterCount() == 1
|
||||
}
|
||||
await gateway._test_markSnapshotReceived()
|
||||
#expect(await firstWait.value)
|
||||
|
||||
await gateway._test_resetConnectionState()
|
||||
let replacementWait = Task {
|
||||
await gateway._test_waitForSnapshot(timeoutMs: 3000)
|
||||
}
|
||||
try await waitUntil("replacement snapshot waiter registered") {
|
||||
await gateway._test_snapshotWaiterCount() == 1
|
||||
}
|
||||
try await Task.sleep(nanoseconds: 1_200_000_000)
|
||||
await gateway._test_markSnapshotReceived()
|
||||
#expect(await replacementWait.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `concurrent replacements wait for route invalidation before installing a channel`() async throws {
|
||||
let session = FakeGatewayWebSocketSession()
|
||||
|
||||
Reference in New Issue
Block a user