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 lastConnectedAt = null;
|
||||||
let lastDisconnectedAt = null;
|
let lastDisconnectedAt = null;
|
||||||
let lastReconnectAt = null;
|
let lastReconnectAt = null;
|
||||||
|
const closingSockets = new WeakSet();
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
|
|
@ -202,9 +203,11 @@ export async function startSolverRelayWs({
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSocket() {
|
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 {
|
try {
|
||||||
socket.close();
|
activeSocket.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger?.warn('socket_close_failed', {
|
logger?.warn('socket_close_failed', {
|
||||||
venue: 'near-intents',
|
venue: 'near-intents',
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ function installMockWebSocket() {
|
||||||
this.readyState = MockWebSocket.CONNECTING;
|
this.readyState = MockWebSocket.CONNECTING;
|
||||||
this.sent = [];
|
this.sent = [];
|
||||||
this.listeners = new Map();
|
this.listeners = new Map();
|
||||||
|
this.closeCalls = 0;
|
||||||
|
this.emitErrorDuringClose = false;
|
||||||
instances.push(this);
|
instances.push(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +39,11 @@ function installMockWebSocket() {
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
this.closeCalls += 1;
|
||||||
if (this.readyState === MockWebSocket.CLOSED) return;
|
if (this.readyState === MockWebSocket.CLOSED) return;
|
||||||
|
if (this.emitErrorDuringClose) {
|
||||||
|
this.emit('error', new Error('socket failed while closing'));
|
||||||
|
}
|
||||||
this.readyState = MockWebSocket.CLOSED;
|
this.readyState = MockWebSocket.CLOSED;
|
||||||
this.emit('close', {});
|
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 () => {
|
test('solver relay sends queued request after connection and resolves rpc result', async () => {
|
||||||
const mock = installMockWebSocket();
|
const mock = installMockWebSocket();
|
||||||
const client = await startSolverRelayWs({
|
const client = await startSolverRelayWs({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue