From 779df19208d00a8f1760ec4c0451b1f97bf31092 Mon Sep 17 00:00:00 2001 From: javi11 Date: Wed, 8 Apr 2026 10:04:09 +0200 Subject: [PATCH] fix: replace fixed [8]int array with dynamic slice in round-robin dispatch The weighted round-robin cumulative weights array was hard-coded to size 8, causing an index-out-of-range panic when more than 8 main providers were configured. Replace with make([]int, n) sized to the actual provider count. Adds a regression test with 9 providers to prevent recurrence. --- integration_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++ nntp.go | 4 ++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/integration_test.go b/integration_test.go index f0887bd..d75f724 100644 --- a/integration_test.go +++ b/integration_test.go @@ -791,6 +791,55 @@ func TestClient_FIFO430Fallthrough(t *testing.T) { } } +// TestClient_RoundRobinMoreThan8Providers verifies that weighted round-robin +// dispatch does not panic when more than 8 providers are configured. +// Previously a fixed-size [8]int array caused an index-out-of-range panic. +func TestClient_RoundRobinMoreThan8Providers(t *testing.T) { + const numProviders = 9 // one more than the old hard-coded limit + + makeFactory := func() ConnFactory { + return func(ctx context.Context) (net.Conn, error) { + client, server := net.Pipe() + go func() { + _, _ = server.Write([]byte("200 server ready\r\n")) + buf := make([]byte, 4096) + for { + _, err := server.Read(buf) + if err != nil { + return + } + _, _ = server.Write([]byte("223 1 exists\r\n")) + } + }() + return client, nil + } + } + + providers := make([]Provider, numProviders) + for i := range providers { + providers[i] = Provider{Factory: makeFactory(), Connections: 2} + } + + c, err := NewClient(context.Background(), providers, WithDispatchStrategy(DispatchRoundRobin)) + if err != nil { + t.Fatalf("NewClient() error = %v", err) + } + defer func() { _ = c.Close() }() + + // Send several requests — any panic in the dispatch path will surface here. + for range 10 { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + resp := <-c.Send(ctx, []byte("STAT \r\n"), nil) + cancel() + if resp.Err != nil { + t.Fatalf("Send() error = %v", resp.Err) + } + if resp.StatusCode != 223 { + t.Errorf("StatusCode = %d, want 223", resp.StatusCode) + } + } +} + // --- Benchmarks --- func benchSend(b *testing.B, providers []Provider) { diff --git a/nntp.go b/nntp.go index 304c10c..c1654ab 100644 --- a/nntp.go +++ b/nntp.go @@ -1573,7 +1573,7 @@ func (c *Client) doSendWithRetry(ctx context.Context, payload []byte, bodyWriter default: // DispatchRoundRobin // Dynamic weighted round-robin: each provider's weight equals // its available capacity (allowed - held). - var cumWeights [8]int // stack-allocated; covers up to 8 providers + cumWeights := make([]int, n) totalW := 0 for i, g := range mains { avail := max(1, int(g.gate.available.Load())) @@ -1581,7 +1581,7 @@ func (c *Client) doSendWithRetry(ctx context.Context, payload []byte, bodyWriter cumWeights[i] = totalW } slot := int(c.nextIdx.Add(1) % uint64(totalW)) - start = sort.SearchInts(cumWeights[:n], slot+1) + start = sort.SearchInts(cumWeights, slot+1) } for attempt := range n {