reject sub-10ns ping intervals and clamp pinger jitter

This commit is contained in:
Jay
2026-08-11 16:09:54 -04:00
parent 54ac311051
commit 296872faee
3 changed files with 42 additions and 3 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ All configuration uses option functions applied at construction time. Validation
| Session | `ReconnectDelay` | 2s | `0` | 0 means reconnect immediately | | Session | `ReconnectDelay` | 2s | `0` | 0 means reconnect immediately |
| Session | Retry | enabled | `WithRetryDisabled()` | Skips RetryConfig validation when disabled | | Session | Retry | enabled | `WithRetryDisabled()` | Skips RetryConfig validation when disabled |
| Connection | `WriteTimeout` | 30s | `0` | Per-message write deadline | | Connection | `WriteTimeout` | 30s | `0` | Per-message write deadline |
| Connection | `PingInterval` | 20s | `0` | ±10% jitter applied per interval | | Connection | `PingInterval` | 20s | `0` | ±10% jitter applied per interval; positive minimum 10ns |
| Connection | `IncomingBufferSize` | 100 | — | Must be positive | | Connection | `IncomingBufferSize` | 100 | — | Must be positive |
| Connection | `ErrorsBufferSize` | 10 | — | Must be positive | | Connection | `ErrorsBufferSize` | 10 | — | Must be positive |
| Connection | `ReadLimit` | nil | 0 | Does nothing when unset, 0 = unlimited | | Connection | `ReadLimit` | nil | 0 | Does nothing when unset, 0 = unlimited |
@@ -56,7 +56,7 @@ These are passed to `transport.NewConnectionConfig`. Import `git.wisehodl.dev/ja
**`WithWriteTimeout(duration)`** — sets a per-message write deadline. Applied before every `WriteMessage` call. Set to zero to disable. Must not be negative. **`WithWriteTimeout(duration)`** — sets a per-message write deadline. Applied before every `WriteMessage` call. Set to zero to disable. Must not be negative.
**`WithPingInterval(duration)`** — sets the interval between WebSocket ping frames. A ±10% jitter is applied to each interval. Set to zero to disable pings entirely. Must not be negative. **`WithPingInterval(duration)`** — sets the interval between WebSocket ping frames. A ±10% jitter is applied to each interval. Set to zero to disable pings entirely. A positive interval must be at least 10ns; smaller positive values leave no room for the jitter window and are rejected.
**`WithIncomingBufferSize(int)`** — sets the capacity of the channel buffering inbound messages. Must be at least 1. **`WithIncomingBufferSize(int)`** — sets the capacity of the channel buffering inbound messages. Must be at least 1.
+9 -1
View File
@@ -115,6 +115,10 @@ func ValidateConnectionConfig(c ConnectionConfig) error {
if c.PingInterval < 0 { if c.PingInterval < 0 {
return fmt.Errorf("invalid ping interval: %v", c.PingInterval) return fmt.Errorf("invalid ping interval: %v", c.PingInterval)
} }
if c.PingInterval > 0 && c.PingInterval < 10*time.Nanosecond {
return fmt.Errorf("invalid ping interval: %v "+
"(positive minimum is 10ns)", c.PingInterval)
}
if c.IncomingBufferSize < 1 { if c.IncomingBufferSize < 1 {
return fmt.Errorf("invalid incoming buffer size: %d", return fmt.Errorf("invalid incoming buffer size: %d",
c.IncomingBufferSize) c.IncomingBufferSize)
@@ -380,8 +384,12 @@ func (c *Connection) setupPongHandler() {
func (c *Connection) startPinger() { func (c *Connection) startPinger() {
defer c.shutdownInternal() defer c.shutdownInternal()
// Calculate 10% jitter window // Calculate 10% jitter window, clamped so Int63n can never panic
// even if config validation is bypassed
jitter := c.config.PingInterval / 10 jitter := c.config.PingInterval / 10
if jitter < 1 {
jitter = 1
}
for { for {
offset := time.Duration(rand.Int63n(int64(jitter*2))) - jitter offset := time.Duration(rand.Int63n(int64(jitter*2))) - jitter
+31
View File
@@ -322,6 +322,37 @@ func TestConnection_Heartbeat(t *testing.T) {
}) })
} }
func TestStartPingerSubMinimumInterval(t *testing.T) {
// Construct directly to bypass NewConnectionConfig validation; the
// jitter clamp alone must prevent the Int63n(0) panic.
conn := &Connection{
socket: honeybeetest.NewMockSocket(),
config: ConnectionConfig{PingInterval: 5 * time.Nanosecond},
incoming: make(chan []byte, 1),
errors: make(chan error, 1),
done: make(chan struct{}),
}
finished := make(chan struct{})
go func() {
defer close(finished)
conn.startPinger()
}()
// let several jittered intervals elapse
time.Sleep(50 * time.Millisecond)
conn.Close()
honeybeetest.Eventually(t, func() bool {
select {
case <-finished:
return true
default:
return false
}
}, "pinger should exit cleanly after Close")
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Send // Send
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------