mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-08 02:52:15 +00:00
fix(macos): unblock node transport reconnects (#105091)
* fix(macos): unblock node transport reconnects * chore(macos): leave release notes to release flow
This commit is contained in:
committed by
GitHub
parent
5839c2e04b
commit
ba9a04580b
@@ -807,7 +807,9 @@ extension GatewayNodeSession {
|
||||
// The underlying channel can auto-reconnect; resetting state here ensures we surface a fresh
|
||||
// onConnected callback once a new snapshot arrives after reconnect.
|
||||
self.resetConnectionState()
|
||||
let lifecycleCallback = self.enqueueLifecycleCallback(
|
||||
// Transport reconnect must not wait on owner callbacks that can suspend
|
||||
// indefinitely. The lifecycle barrier still gates readiness and invokes.
|
||||
_ = self.enqueueLifecycleCallback(
|
||||
immediate: {
|
||||
// Release held input before waiting for a connected callback that
|
||||
// may already be suspended in owner code.
|
||||
@@ -817,11 +819,8 @@ extension GatewayNodeSession {
|
||||
// This cleanup runs after all older lifecycle callbacks, so they
|
||||
// cannot resume later and restore a disconnected route.
|
||||
await onDisconnected?(reason)
|
||||
await self.awaitActiveInvokes(activeInvokes)
|
||||
})
|
||||
if !self.isExecutingLifecycleCallback() {
|
||||
await lifecycleCallback.task.value
|
||||
}
|
||||
await self.awaitActiveInvokes(activeInvokes)
|
||||
}
|
||||
|
||||
private func markSnapshotReceived() {
|
||||
|
||||
@@ -1087,10 +1087,64 @@ struct GatewayNodeSessionTests {
|
||||
await gateway._test_notifyConnectedIfNeeded(
|
||||
admissionGeneration: staleAdmissionGeneration)
|
||||
|
||||
try await waitUntil("disconnect callback completed") {
|
||||
await lifecycle.values().contains("disconnected")
|
||||
}
|
||||
#expect(await lifecycle.values() == ["connected", "disconnected"])
|
||||
await gateway.disconnect()
|
||||
}
|
||||
|
||||
@Test
|
||||
func `transport reconnect does not wait for blocked disconnect lifecycle`() async throws {
|
||||
let session = FakeGatewayWebSocketSession()
|
||||
let gateway = GatewayNodeSession()
|
||||
let invalidationGate = AsyncGate()
|
||||
let lifecycle = DisconnectProbe()
|
||||
let options = GatewayConnectOptions(
|
||||
role: "node",
|
||||
scopes: [],
|
||||
caps: ["computer"],
|
||||
commands: ["computer.act"],
|
||||
permissions: [:],
|
||||
clientId: "openclaw-macos",
|
||||
clientMode: "node",
|
||||
clientDisplayName: "macOS Test",
|
||||
includeDeviceIdentity: false)
|
||||
|
||||
try await gateway.connect(
|
||||
url: #require(URL(string: "ws://first.example.invalid")),
|
||||
token: nil,
|
||||
bootstrapToken: nil,
|
||||
password: nil,
|
||||
connectOptions: options,
|
||||
sessionBox: WebSocketSessionBox(session: session),
|
||||
onConnected: { await lifecycle.record("connected") },
|
||||
onDisconnected: { _ in await lifecycle.record("disconnected") },
|
||||
onInvoke: { req in
|
||||
BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil)
|
||||
},
|
||||
onRouteInvalidated: { await invalidationGate.wait() })
|
||||
let firstTask = try #require(session.latestTask())
|
||||
try await waitUntil("receive loop armed before disconnect") {
|
||||
firstTask.hasPendingReceiveHandler()
|
||||
}
|
||||
|
||||
firstTask.emitReceiveFailure()
|
||||
try await waitUntil("disconnect lifecycle blocked") {
|
||||
await invalidationGate.hasStarted()
|
||||
}
|
||||
try await waitUntil("replacement transport connected") {
|
||||
session.snapshotMakeCount() == 2
|
||||
}
|
||||
#expect(await lifecycle.values() == ["connected"])
|
||||
|
||||
await invalidationGate.release()
|
||||
try await waitUntil("replacement lifecycle completed") {
|
||||
await lifecycle.values() == ["connected", "disconnected", "connected"]
|
||||
}
|
||||
await gateway.disconnect()
|
||||
}
|
||||
|
||||
@Test
|
||||
func `disconnect cleanup finishes after an in flight connected callback`() async throws {
|
||||
let session = FakeGatewayWebSocketSession()
|
||||
|
||||
Reference in New Issue
Block a user