Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
a735bd3d5e
|
|||
|
0a32cc3125
|
|||
|
7906bb2295
|
|||
|
50a8b39ea3
|
@@ -37,7 +37,6 @@ type C struct {
|
||||
Pprof string `env:"ORLY_PPROF" usage:"enable pprof in modes: cpu,memory,allocation,heap,block,goroutine,threadcreate,mutex"`
|
||||
PprofPath string `env:"ORLY_PPROF_PATH" usage:"optional directory to write pprof profiles into (inside container); default is temporary dir"`
|
||||
PprofHTTP bool `env:"ORLY_PPROF_HTTP" default:"false" usage:"if true, expose net/http/pprof on port 6060"`
|
||||
OpenPprofWeb bool `env:"ORLY_OPEN_PPROF_WEB" default:"false" usage:"if true, automatically open the pprof web viewer when profiling is enabled"`
|
||||
IPWhitelist []string `env:"ORLY_IP_WHITELIST" usage:"comma-separated list of IP addresses to allow access from, matches on prefixes to allow private subnets, eg 10.0.0 = 10.0.0.0/8"`
|
||||
IPBlacklist []string `env:"ORLY_IP_BLACKLIST" usage:"comma-separated list of IP addresses to block; matches on prefixes to allow subnets, e.g. 192.168 = 192.168.0.0/16"`
|
||||
Admins []string `env:"ORLY_ADMINS" usage:"comma-separated list of admin npubs"`
|
||||
|
||||
@@ -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
|
||||
|
||||
43
main.go
43
main.go
@@ -6,9 +6,7 @@ import (
|
||||
"net/http"
|
||||
pp "net/http/pprof"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -26,33 +24,7 @@ import (
|
||||
"next.orly.dev/pkg/version"
|
||||
)
|
||||
|
||||
// openBrowser attempts to open the specified URL in the default browser.
|
||||
// It supports multiple platforms including Linux, macOS, and Windows.
|
||||
func openBrowser(url string) {
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
err = exec.Command(
|
||||
"rundll32", "url.dll,FileProtocolHandler", url,
|
||||
).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", url).Start()
|
||||
default:
|
||||
log.W.F("unsupported platform for opening browser: %s", runtime.GOOS)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.E.F("failed to open browser: %v", err)
|
||||
} else {
|
||||
log.I.F("opened browser to %s", url)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU() * 4)
|
||||
var err error
|
||||
var cfg *config.C
|
||||
if cfg, err = config.New(); chk.T(err) {
|
||||
@@ -80,11 +52,6 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// If OpenPprofWeb is true and profiling is enabled, we need to ensure HTTP profiling is also enabled
|
||||
if cfg.OpenPprofWeb && cfg.Pprof != "" && !cfg.PprofHTTP {
|
||||
log.I.F("enabling HTTP pprof server to support web viewer")
|
||||
cfg.PprofHTTP = true
|
||||
}
|
||||
// Ensure profiling is stopped on interrupts (SIGINT/SIGTERM) as well as on normal exit
|
||||
var profileStopOnce sync.Once
|
||||
profileStop := func() {}
|
||||
@@ -318,16 +285,6 @@ func main() {
|
||||
defer cancelShutdown()
|
||||
_ = ppSrv.Shutdown(shutdownCtx)
|
||||
}()
|
||||
|
||||
// Open the pprof web viewer if enabled
|
||||
if cfg.OpenPprofWeb && cfg.Pprof != "" {
|
||||
pprofURL := "http://localhost:6060/debug/pprof/"
|
||||
go func() {
|
||||
// Wait a moment for the server to start
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
openBrowser(pprofURL)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Start health check HTTP server if configured
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.19.5
|
||||
v0.19.7
|
||||
1
scripts/secp256k1
Submodule
1
scripts/secp256k1
Submodule
Submodule scripts/secp256k1 added at 0cdc758a56
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