Skip to content

Commit 7848d61

Browse files
wallyqsneilalexander
authored andcommitted
Add WebSocket-specific ping interval configuration option
This commit adds a new PingInterval field to WebsocketOpts that allows setting a custom ping interval specifically for WebSocket connections, independent from the default NATS client ping interval. ``` websocket { port: 8080 ping_interval: "30s" } ``` Signed-off-by: Waldemar Quevedo <wally@nats.io>
1 parent 3a5cdc3 commit 7848d61

File tree

4 files changed

+107
-2
lines changed

4 files changed

+107
-2
lines changed

server/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5503,6 +5503,9 @@ func (c *client) processPingTimer() {
55035503
if c.kind == ROUTER && opts.Cluster.PingInterval > 0 {
55045504
pingInterval = opts.Cluster.PingInterval
55055505
}
5506+
if c.isWebsocket() && opts.Websocket.PingInterval > 0 {
5507+
pingInterval = opts.Websocket.PingInterval
5508+
}
55065509
pingInterval = adjustPingInterval(c.kind, pingInterval)
55075510
now := time.Now()
55085511
needRTT := c.rtt == 0 || now.Sub(c.rttStart) > DEFAULT_RTT_MEASUREMENT_INTERVAL
@@ -5585,6 +5588,9 @@ func (c *client) setPingTimer() {
55855588
if c.kind == ROUTER && opts.Cluster.PingInterval > 0 {
55865589
d = opts.Cluster.PingInterval
55875590
}
5591+
if c.isWebsocket() && opts.Websocket.PingInterval > 0 {
5592+
d = opts.Websocket.PingInterval
5593+
}
55885594
d = adjustPingInterval(c.kind, d)
55895595
c.ping.tmr = time.AfterFunc(d, c.processPingTimer)
55905596
}
@@ -6619,6 +6625,9 @@ func (c *client) setFirstPingTimer() {
66196625
if c.kind == ROUTER && opts.Cluster.PingInterval > 0 {
66206626
d = opts.Cluster.PingInterval
66216627
}
6628+
if c.isWebsocket() && opts.Websocket.PingInterval > 0 {
6629+
d = opts.Websocket.PingInterval
6630+
}
66226631
if !opts.DisableShortFirstPing {
66236632
if c.kind != CLIENT {
66246633
if d > firstPingInterval {

server/opts.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,11 @@ type WebsocketOpts struct {
595595
// time needed for the TLS Handshake.
596596
HandshakeTimeout time.Duration
597597

598+
// How often to send pings to WebSocket clients. When set to a non-zero
599+
// duration, this overrides the default PingInterval for WebSocket connections.
600+
// If not set or zero, the server's default PingInterval will be used.
601+
PingInterval time.Duration
602+
598603
// Headers to be added to the upgrade response.
599604
// Useful for adding custom headers like Strict-Transport-Security.
600605
Headers map[string]string
@@ -1685,7 +1690,7 @@ func (o *Options) processConfigFileLine(k string, v any, errors *[]error, warnin
16851690
case "reconnect_error_reports":
16861691
o.ReconnectErrorReports = int(v.(int64))
16871692
case "websocket", "ws":
1688-
if err := parseWebsocket(tk, o, errors); err != nil {
1693+
if err := parseWebsocket(tk, o, errors, warnings); err != nil {
16891694
*errors = append(*errors, err)
16901695
return
16911696
}
@@ -5313,7 +5318,7 @@ func parseStringArray(fieldName string, tk token, lt *token, mv any, errors *[]e
53135318
}
53145319
}
53155320

5316-
func parseWebsocket(v any, o *Options, errors *[]error) error {
5321+
func parseWebsocket(v any, o *Options, errors *[]error, warnings *[]error) error {
53175322
var lt token
53185323
defer convertPanicToErrorList(&lt, errors)
53195324

@@ -5414,6 +5419,8 @@ func parseWebsocket(v any, o *Options, errors *[]error) error {
54145419
o.Websocket.Headers[key] = headerValue
54155420
}
54165421
}
5422+
case "ping_interval":
5423+
o.Websocket.PingInterval = parseDuration("ping_interval", tk, mv, errors, warnings)
54175424
default:
54185425
if !tk.IsUsedVariable() {
54195426
err := &unknownConfigFieldErr{

server/opts_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4119,3 +4119,64 @@ func TestWriteTimeoutConfigParsing(t *testing.T) {
41194119
}
41204120
}
41214121
}
4122+
4123+
func TestWebsocketPingIntervalConfig(t *testing.T) {
4124+
// Test with string format (duration string)
4125+
confFile := createConfFile(t, []byte(`
4126+
websocket {
4127+
port: 8080
4128+
ping_interval: "30s"
4129+
}
4130+
`))
4131+
opts, err := ProcessConfigFile(confFile)
4132+
if err != nil {
4133+
t.Fatalf("Unexpected error: %v", err)
4134+
}
4135+
if opts.Websocket.PingInterval != 30*time.Second {
4136+
t.Fatalf("Expected websocket ping_interval to be 30s, got %v", opts.Websocket.PingInterval)
4137+
}
4138+
4139+
// Test with integer format (seconds)
4140+
confFile = createConfFile(t, []byte(`
4141+
websocket {
4142+
port: 8080
4143+
ping_interval: 45
4144+
}
4145+
`))
4146+
opts, err = ProcessConfigFile(confFile)
4147+
if err != nil {
4148+
t.Fatalf("Unexpected error: %v", err)
4149+
}
4150+
if opts.Websocket.PingInterval != 45*time.Second {
4151+
t.Fatalf("Expected websocket ping_interval to be 45s, got %v", opts.Websocket.PingInterval)
4152+
}
4153+
4154+
// Test with different duration format
4155+
confFile = createConfFile(t, []byte(`
4156+
websocket {
4157+
port: 8080
4158+
ping_interval: "2m"
4159+
}
4160+
`))
4161+
opts, err = ProcessConfigFile(confFile)
4162+
if err != nil {
4163+
t.Fatalf("Unexpected error: %v", err)
4164+
}
4165+
if opts.Websocket.PingInterval != 2*time.Minute {
4166+
t.Fatalf("Expected websocket ping_interval to be 2m, got %v", opts.Websocket.PingInterval)
4167+
}
4168+
4169+
// Test without ping_interval (should be zero/unset)
4170+
confFile = createConfFile(t, []byte(`
4171+
websocket {
4172+
port: 8080
4173+
}
4174+
`))
4175+
opts, err = ProcessConfigFile(confFile)
4176+
if err != nil {
4177+
t.Fatalf("Unexpected error: %v", err)
4178+
}
4179+
if opts.Websocket.PingInterval != 0 {
4180+
t.Fatalf("Expected websocket ping_interval to be 0 (unset), got %v", opts.Websocket.PingInterval)
4181+
}
4182+
}

server/websocket_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4841,6 +4841,34 @@ func Benchmark_WS_Subx5_CY__4096b(b *testing.B) {
48414841
wsBenchSub(b, 5, true, s)
48424842
}
48434843

4844+
func TestWebsocketPingInterval(t *testing.T) {
4845+
opts := testWSOptions()
4846+
opts.Websocket.PingInterval = 200 * time.Millisecond
4847+
4848+
s := RunServer(opts)
4849+
defer s.Shutdown()
4850+
4851+
wsc, br := testWSCreateClient(t, false, false, opts.Websocket.Host, opts.Websocket.Port)
4852+
defer wsc.Close()
4853+
4854+
pingCount := 0
4855+
deadline := time.Now().Add(1 * time.Second)
4856+
4857+
for time.Now().Before(deadline) {
4858+
wsc.SetReadDeadline(time.Now().Add(1 * time.Second))
4859+
4860+
msg := testWSReadFrame(t, br)
4861+
if bytes.Contains(msg, []byte("PING\r\n")) {
4862+
pingCount++
4863+
pongMsg := testWSCreateClientMsg(wsBinaryMessage, 1, true, false, []byte("PONG\r\n"))
4864+
wsc.Write(pongMsg)
4865+
}
4866+
}
4867+
if pingCount < 2 {
4868+
t.Fatalf("Expected at least 2 PINGs, got %d", pingCount)
4869+
}
4870+
}
4871+
48444872
func Benchmark_WS_Subx5_CN__8192b(b *testing.B) {
48454873
s := sizedString(8192)
48464874
wsBenchSub(b, 5, false, s)

0 commit comments

Comments
 (0)