mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 16:51:36 +00:00
fix(macos): fail fast during node lifecycle cleanup (#105282)
This commit is contained in:
committed by
GitHub
parent
4249cdb8fe
commit
7303f42917
@@ -965,10 +965,25 @@ extension GatewayNodeSession {
|
|||||||
channel: GatewayChannelActor,
|
channel: GatewayChannelActor,
|
||||||
socketGeneration: UInt64) async
|
socketGeneration: UInt64) async
|
||||||
{
|
{
|
||||||
guard await self.awaitLifecycleCallbacks(ifCurrentRoute: route) else { return }
|
|
||||||
guard self.isCurrentRoute(route),
|
guard self.isCurrentRoute(route),
|
||||||
self.channel === channel
|
self.channel === channel
|
||||||
else { return }
|
else { return }
|
||||||
|
// Lifecycle cleanup gates owner readiness. Reject while it is suspended instead of
|
||||||
|
// holding the Gateway request until timeout; the replacement route stays fail-closed.
|
||||||
|
if self.lifecycleCallbackBarrier != nil {
|
||||||
|
self.logger.info("node invoke rejected during lifecycle transition id=\(request.id, privacy: .public)")
|
||||||
|
await self.sendInvokeResult(
|
||||||
|
request: request,
|
||||||
|
response: BridgeInvokeResponse(
|
||||||
|
id: request.id,
|
||||||
|
ok: false,
|
||||||
|
error: OpenClawNodeError(
|
||||||
|
code: .unavailable,
|
||||||
|
message: "UNAVAILABLE: node lifecycle transition in progress")),
|
||||||
|
channel: channel,
|
||||||
|
socketGeneration: socketGeneration)
|
||||||
|
return
|
||||||
|
}
|
||||||
self.logger.info("node invoke executing id=\(request.id, privacy: .public)")
|
self.logger.info("node invoke executing id=\(request.id, privacy: .public)")
|
||||||
let bridgeRequest = BridgeInvokeRequest(
|
let bridgeRequest = BridgeInvokeRequest(
|
||||||
id: request.id,
|
id: request.id,
|
||||||
@@ -1004,14 +1019,6 @@ extension GatewayNodeSession {
|
|||||||
socketGeneration: socketGeneration)
|
socketGeneration: socketGeneration)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func awaitLifecycleCallbacks(ifCurrentRoute route: GatewayNodeSessionRoute) async -> Bool {
|
|
||||||
while let lifecycleCallback = self.lifecycleCallbackBarrier {
|
|
||||||
await lifecycleCallback.task.value
|
|
||||||
guard self.isCurrentRoute(route), self.channel != nil else { return false }
|
|
||||||
}
|
|
||||||
return self.isCurrentRoute(route) && self.channel != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func invokeIfCurrentRoute(
|
func invokeIfCurrentRoute(
|
||||||
_ request: BridgeInvokeRequest,
|
_ request: BridgeInvokeRequest,
|
||||||
expectedRoute: GatewayNodeSessionRoute,
|
expectedRoute: GatewayNodeSessionRoute,
|
||||||
|
|||||||
@@ -1145,6 +1145,71 @@ struct GatewayNodeSessionTests {
|
|||||||
await gateway.disconnect()
|
await gateway.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `replacement invoke fails promptly while disconnect lifecycle is blocked`() async throws {
|
||||||
|
let session = FakeGatewayWebSocketSession()
|
||||||
|
let gateway = GatewayNodeSession()
|
||||||
|
let invalidationGate = AsyncGate()
|
||||||
|
let invocations = DisconnectProbe()
|
||||||
|
let options = GatewayConnectOptions(
|
||||||
|
role: "node",
|
||||||
|
scopes: [],
|
||||||
|
caps: [],
|
||||||
|
commands: ["system.which"],
|
||||||
|
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: {},
|
||||||
|
onDisconnected: { _ in },
|
||||||
|
onInvoke: { request in
|
||||||
|
await invocations.record(request.id)
|
||||||
|
return BridgeInvokeResponse(id: request.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
|
||||||
|
}
|
||||||
|
let replacementTask = try #require(session.latestTask())
|
||||||
|
try await waitUntil("replacement socket receiving") {
|
||||||
|
replacementTask.hasPendingReceiveHandler()
|
||||||
|
}
|
||||||
|
replacementTask.emitInvokeRequest(id: "during-lifecycle", command: "system.which")
|
||||||
|
|
||||||
|
try await waitUntil("lifecycle unavailable result") {
|
||||||
|
replacementTask.sentRequestCount(method: "node.invoke.result") == 1
|
||||||
|
}
|
||||||
|
let result = try #require(replacementTask.sentRequests(method: "node.invoke.result").first)
|
||||||
|
let params = try #require(result["params"] as? [String: Any])
|
||||||
|
let error = try #require(params["error"] as? [String: Any])
|
||||||
|
#expect(params["id"] as? String == "during-lifecycle")
|
||||||
|
#expect(params["ok"] as? Bool == false)
|
||||||
|
#expect(error["code"] as? String == OpenClawNodeErrorCode.unavailable.rawValue)
|
||||||
|
#expect(error["message"] as? String == "UNAVAILABLE: node lifecycle transition in progress")
|
||||||
|
#expect(await invocations.values() == [])
|
||||||
|
|
||||||
|
await invalidationGate.release()
|
||||||
|
await gateway.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func `disconnect cleanup finishes after an in flight connected callback`() async throws {
|
func `disconnect cleanup finishes after an in flight connected callback`() async throws {
|
||||||
let session = FakeGatewayWebSocketSession()
|
let session = FakeGatewayWebSocketSession()
|
||||||
|
|||||||
Reference in New Issue
Block a user