Add WebSocket Connection Testing Scripts
- Introduced two new test scripts: `test-relay-connection.js` and `test-websocket-close.js` to verify WebSocket connection stability and closure behavior. - `test-relay-connection.js` tests multiple connections, monitors their open/close events, and analyzes premature closures. - `test-websocket-close.js` focuses on connection closure issues with concurrent connections and logs results for connected, closed, and error states. - Both scripts utilize the NostrWebSocket from the @nostr-dev-kit/ndk package for testing purposes.
This commit is contained in:
@@ -56,6 +56,8 @@ func (s *Server) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
whitelist:
|
||||
// Create an independent context for this connection
|
||||
// This context will be cancelled when the connection closes or server shuts down
|
||||
ctx, cancel := context.WithCancel(s.Ctx)
|
||||
defer cancel()
|
||||
var err error
|
||||
@@ -107,7 +109,8 @@ whitelist:
|
||||
log.D.F("AUTH challenge sent successfully to %s", remote)
|
||||
}
|
||||
ticker := time.NewTicker(DefaultPingWait)
|
||||
go s.Pinger(ctx, conn, ticker, cancel)
|
||||
// Don't pass cancel to Pinger - it should not be able to cancel the connection context
|
||||
go s.Pinger(ctx, conn, ticker)
|
||||
defer func() {
|
||||
log.D.F("closing websocket connection from %s", remote)
|
||||
|
||||
@@ -117,7 +120,11 @@ whitelist:
|
||||
|
||||
// Cancel all subscriptions for this connection
|
||||
log.D.F("cancelling subscriptions for %s", remote)
|
||||
listener.publishers.Receive(&W{Cancel: true})
|
||||
listener.publishers.Receive(&W{
|
||||
Cancel: true,
|
||||
Conn: listener.conn,
|
||||
remote: listener.remote,
|
||||
})
|
||||
|
||||
// Log detailed connection statistics
|
||||
dur := time.Since(listener.startTime)
|
||||
@@ -155,6 +162,11 @@ whitelist:
|
||||
typ, msg, err = conn.Read(ctx)
|
||||
|
||||
if err != nil {
|
||||
// Check if the error is due to context cancellation
|
||||
if err == context.Canceled || strings.Contains(err.Error(), "context canceled") {
|
||||
log.T.F("connection from %s cancelled (context done): %v", remote, err)
|
||||
return
|
||||
}
|
||||
if strings.Contains(
|
||||
err.Error(), "use of closed network connection",
|
||||
) {
|
||||
@@ -233,12 +245,12 @@ whitelist:
|
||||
|
||||
func (s *Server) Pinger(
|
||||
ctx context.Context, conn *websocket.Conn, ticker *time.Ticker,
|
||||
cancel context.CancelFunc,
|
||||
) {
|
||||
defer func() {
|
||||
log.D.F("pinger shutting down")
|
||||
cancel()
|
||||
ticker.Stop()
|
||||
// DO NOT call cancel here - the pinger should not be able to cancel the connection context
|
||||
// The connection handler will cancel the context when the connection is actually closing
|
||||
}()
|
||||
var err error
|
||||
pingCount := 0
|
||||
|
||||
167
test-relay-connection.js
Executable file
167
test-relay-connection.js
Executable file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Test script to verify websocket connections are not closed prematurely
|
||||
// This is a Node.js test script that can be run with: node test-relay-connection.js
|
||||
|
||||
import { NostrWebSocket } from '@nostr-dev-kit/ndk';
|
||||
|
||||
const RELAY = process.env.RELAY || 'ws://localhost:8080';
|
||||
const MAX_CONNECTIONS = 10;
|
||||
const TEST_DURATION = 30000; // 30 seconds
|
||||
|
||||
let connectionsClosed = 0;
|
||||
let connectionsOpened = 0;
|
||||
let messagesReceived = 0;
|
||||
let errors = 0;
|
||||
|
||||
const stats = {
|
||||
premature: 0,
|
||||
normal: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
class TestConnection {
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
this.ws = null;
|
||||
this.closed = false;
|
||||
this.openTime = null;
|
||||
this.closeTime = null;
|
||||
this.lastError = null;
|
||||
}
|
||||
|
||||
connect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ws = new NostrWebSocket(RELAY);
|
||||
|
||||
this.ws.addEventListener('open', () => {
|
||||
this.openTime = Date.now();
|
||||
connectionsOpened++;
|
||||
console.log(`[Connection ${this.id}] Opened`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.ws.addEventListener('close', (event) => {
|
||||
this.closeTime = Date.now();
|
||||
this.closed = true;
|
||||
connectionsClosed++;
|
||||
const duration = this.closeTime - this.openTime;
|
||||
console.log(`[Connection ${this.id}] Closed: code=${event.code}, reason="${event.reason || ''}", duration=${duration}ms`);
|
||||
|
||||
if (duration < 5000 && event.code !== 1000) {
|
||||
stats.premature++;
|
||||
console.log(`[Connection ${this.id}] PREMATURE CLOSE DETECTED: duration=${duration}ms < 5s`);
|
||||
} else {
|
||||
stats.normal++;
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.addEventListener('error', (error) => {
|
||||
this.lastError = error;
|
||||
stats.errors++;
|
||||
console.error(`[Connection ${this.id}] Error:`, error);
|
||||
});
|
||||
|
||||
this.ws.addEventListener('message', (event) => {
|
||||
messagesReceived++;
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log(`[Connection ${this.id}] Message:`, data[0]);
|
||||
} catch (e) {
|
||||
console.log(`[Connection ${this.id}] Message (non-JSON):`, event.data);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(reject, 5000); // Timeout after 5 seconds if not opened
|
||||
});
|
||||
}
|
||||
|
||||
sendReq() {
|
||||
if (this.ws && !this.closed) {
|
||||
this.ws.send(JSON.stringify(['REQ', `test-sub-${this.id}`, { kinds: [1], limit: 10 }]));
|
||||
console.log(`[Connection ${this.id}] Sent REQ`);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.ws && !this.closed) {
|
||||
this.ws.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runTest() {
|
||||
console.log('='.repeat(60));
|
||||
console.log('Testing Relay Connection Stability');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Relay: ${RELAY}`);
|
||||
console.log(`Duration: ${TEST_DURATION}ms`);
|
||||
console.log(`Connections: ${MAX_CONNECTIONS}`);
|
||||
console.log('='.repeat(60));
|
||||
console.log();
|
||||
|
||||
const connections = [];
|
||||
|
||||
// Open connections
|
||||
console.log('Opening connections...');
|
||||
for (let i = 0; i < MAX_CONNECTIONS; i++) {
|
||||
const conn = new TestConnection(i);
|
||||
try {
|
||||
await conn.connect();
|
||||
connections.push(conn);
|
||||
} catch (error) {
|
||||
console.error(`Failed to open connection ${i}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Opened ${connections.length} connections`);
|
||||
console.log();
|
||||
|
||||
// Send requests from each connection
|
||||
console.log('Sending REQ messages...');
|
||||
for (const conn of connections) {
|
||||
conn.sendReq();
|
||||
}
|
||||
|
||||
// Wait and let connections run
|
||||
console.log(`Waiting ${TEST_DURATION / 1000}s...`);
|
||||
await new Promise(resolve => setTimeout(resolve, TEST_DURATION));
|
||||
|
||||
// Close all connections
|
||||
console.log('Closing all connections...');
|
||||
for (const conn of connections) {
|
||||
conn.close();
|
||||
}
|
||||
|
||||
// Wait for close events
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Print results
|
||||
console.log();
|
||||
console.log('='.repeat(60));
|
||||
console.log('Test Results:');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Connections Opened: ${connectionsOpened}`);
|
||||
console.log(`Connections Closed: ${connectionsClosed}`);
|
||||
console.log(`Messages Received: ${messagesReceived}`);
|
||||
console.log();
|
||||
console.log('Closure Analysis:');
|
||||
console.log(`- Premature Closes: ${stats.premature}`);
|
||||
console.log(`- Normal Closes: ${stats.normal}`);
|
||||
console.log(`- Errors: ${stats.errors}`);
|
||||
console.log('='.repeat(60));
|
||||
|
||||
if (stats.premature > 0) {
|
||||
console.error('FAILED: Detected premature connection closures!');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('PASSED: No premature connection closures detected.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
runTest().catch(error => {
|
||||
console.error('Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
57
test-websocket-close.js
Executable file
57
test-websocket-close.js
Executable file
@@ -0,0 +1,57 @@
|
||||
import { NostrWebSocket } from '@nostr-dev-kit/ndk';
|
||||
|
||||
const RELAY = process.env.RELAY || 'ws://localhost:8080';
|
||||
|
||||
async function testConnectionClosure() {
|
||||
console.log('Testing websocket connection closure issues...');
|
||||
console.log('Connecting to:', RELAY);
|
||||
|
||||
// Create multiple connections to test concurrency
|
||||
const connections = [];
|
||||
const results = { connected: 0, closed: 0, errors: 0 };
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const ws = new NostrWebSocket(RELAY);
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
console.log(`Connection ${i} opened`);
|
||||
results.connected++;
|
||||
});
|
||||
|
||||
ws.addEventListener('close', (event) => {
|
||||
console.log(`Connection ${i} closed:`, event.code, event.reason);
|
||||
results.closed++;
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (error) => {
|
||||
console.error(`Connection ${i} error:`, error);
|
||||
results.errors++;
|
||||
});
|
||||
|
||||
connections.push(ws);
|
||||
}
|
||||
|
||||
// Wait a bit then send REQs
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Send some REQ messages
|
||||
for (const ws of connections) {
|
||||
ws.send(JSON.stringify(['REQ', 'test-sub', { kinds: [1] }]));
|
||||
}
|
||||
|
||||
// Wait and observe behavior
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
console.log('\nTest Results:');
|
||||
console.log(`- Connected: ${results.connected}`);
|
||||
console.log(`- Closed prematurely: ${results.closed}`);
|
||||
console.log(`- Errors: ${results.errors}`);
|
||||
|
||||
// Close all connections
|
||||
for (const ws of connections) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
testConnectionClosure().catch(console.error);
|
||||
|
||||
Reference in New Issue
Block a user