Guard solver relay websocket close recursion
All checks were successful
deploy / deploy (push) Successful in 33s
All checks were successful
deploy / deploy (push) Successful in 33s
Proof: npm test; npm run operator-dashboard:build; node --test test/solver-relay-ws.test.mjs; PYTHONPATH=. python3 test/bootstrap_script_static_test.py; PYTHONPATH=. python3 test/render_release_manifest_test.py; PYTHONPATH=. python3 test/repo_deployments_test.py; PYTHONPATH=. python3 test/ntfy_manifest_test.py; kubectl kustomize deploy/k8s/base. Assumptions: the solver relay websocket client can receive reentrant error events while closing, matching the ingest runtime failure pattern observed in production. Still fake: Notification emissions are limited to credited deposits, completed withdrawals, and completed trades with durable inventory movement; generic alert notification policy remains disabled.
This commit is contained in:
parent
ea0a7cbb4c
commit
a7a73336a5
2 changed files with 39 additions and 2 deletions
|
|
@ -22,6 +22,7 @@ export async function startSolverRelayWs({
|
|||
let lastConnectedAt = null;
|
||||
let lastDisconnectedAt = null;
|
||||
let lastReconnectAt = null;
|
||||
const closingSockets = new WeakSet();
|
||||
|
||||
connect();
|
||||
|
||||
|
|
@ -202,9 +203,11 @@ export async function startSolverRelayWs({
|
|||
}
|
||||
|
||||
function closeSocket() {
|
||||
if (!socket || socket.readyState > WebSocket.OPEN) return;
|
||||
const activeSocket = socket;
|
||||
if (!activeSocket || activeSocket.readyState > WebSocket.OPEN || closingSockets.has(activeSocket)) return;
|
||||
closingSockets.add(activeSocket);
|
||||
try {
|
||||
socket.close();
|
||||
activeSocket.close();
|
||||
} catch (error) {
|
||||
logger?.warn('socket_close_failed', {
|
||||
venue: 'near-intents',
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ function installMockWebSocket() {
|
|||
this.readyState = MockWebSocket.CONNECTING;
|
||||
this.sent = [];
|
||||
this.listeners = new Map();
|
||||
this.closeCalls = 0;
|
||||
this.emitErrorDuringClose = false;
|
||||
instances.push(this);
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +39,11 @@ function installMockWebSocket() {
|
|||
}
|
||||
|
||||
close() {
|
||||
this.closeCalls += 1;
|
||||
if (this.readyState === MockWebSocket.CLOSED) return;
|
||||
if (this.emitErrorDuringClose) {
|
||||
this.emit('error', new Error('socket failed while closing'));
|
||||
}
|
||||
this.readyState = MockWebSocket.CLOSED;
|
||||
this.emit('close', {});
|
||||
}
|
||||
|
|
@ -112,6 +118,34 @@ test('solver relay reconnects after websocket error even when no close event is
|
|||
}
|
||||
});
|
||||
|
||||
test('solver relay websocket close is reentrant-safe when close emits an error', async () => {
|
||||
const mock = installMockWebSocket();
|
||||
const client = await startSolverRelayWs({
|
||||
apiKey: 'api-key',
|
||||
wsUrl: 'wss://relay.example/ws',
|
||||
reconnectDelayMs: 1,
|
||||
});
|
||||
|
||||
try {
|
||||
assert.equal(mock.instances.length, 1);
|
||||
mock.instances[0].open();
|
||||
mock.instances[0].emitErrorDuringClose = true;
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
mock.instances[0].error(new Error('network failed'));
|
||||
});
|
||||
await delay(10);
|
||||
|
||||
assert.equal(mock.instances[0].closeCalls, 1);
|
||||
assert.equal(client.getState().connected, false);
|
||||
assert.ok(client.getState().reconnect_count >= 2);
|
||||
assert.ok(mock.instances.length >= 2);
|
||||
} finally {
|
||||
client.close();
|
||||
mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('solver relay sends queued request after connection and resolves rpc result', async () => {
|
||||
const mock = installMockWebSocket();
|
||||
const client = await startSolverRelayWs({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue