Skip to content

Commit eaf4947

Browse files
authored
perf(ring): prevent resize oscillation (#179)
Shrink only at quarter utilization and add FIFO regression coverage and capacity-boundary benchmarks.
1 parent 9721be6 commit eaf4947

3 files changed

Lines changed: 83 additions & 4 deletions

File tree

‎ring.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var _ core.Worker = (*Ring)(nil)
1313
// Ring is an in-memory worker implementation using a dynamic circular buffer.
1414
// It implements the core.Worker interface and provides automatic resizing:
1515
// - Doubles capacity when full
16-
// - Halves capacity when less than 25% utilized
16+
// - Halves capacity when at most 25% utilized
1717
//
1818
// The ring buffer uses two pointers (head and tail) to track the queue boundaries:
1919
// - head: points to the next task to dequeue
@@ -95,7 +95,7 @@ func (s *Ring) Queue(task core.TaskMessage) error {
9595
}
9696

9797
// Request dequeues and returns the next task from the ring buffer.
98-
// The buffer shrinks automatically (halves in size) when less than 25% full.
98+
// The buffer shrinks automatically (halves in size) when at most 25% full.
9999
// Returns:
100100
// - (task, nil) if a task is successfully dequeued
101101
// - (nil, ErrNoTaskInQueue) if the queue is currently empty
@@ -129,9 +129,9 @@ func (s *Ring) Request() (core.TaskMessage, error) {
129129
s.head = (s.head + 1) % len(s.taskQueue) // Advance head with wraparound
130130
s.count--
131131

132-
// Shrink buffer if less than 25% full (to save memory)
132+
// Shrink buffer if at most 25% full (to save memory)
133133
// Minimum buffer size is 2 to maintain circular buffer structure
134-
if n := len(s.taskQueue) / 2; n >= 2 && s.count <= n {
134+
if n := len(s.taskQueue) / 2; n >= 2 && s.count <= len(s.taskQueue)/4 {
135135
s.resize(n)
136136
}
137137

‎ring_performance_test.go‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package queue
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/golang-queue/queue/job"
9+
)
10+
11+
// BenchmarkRingBoundary holds the backlog near a power-of-two capacity, where
12+
// growing at full and shrinking at half can otherwise copy the entire backlog
13+
// twice per operation. One operation is one successful enqueue/dequeue pair.
14+
func BenchmarkRingBoundary(b *testing.B) {
15+
for _, backlog := range []int{64, 1024} {
16+
b.Run(fmt.Sprintf("backlog=%d", backlog), func(b *testing.B) {
17+
w := NewRing()
18+
m := job.NewTask(func(context.Context) error { return nil })
19+
for range backlog {
20+
if err := w.Queue(&m); err != nil {
21+
b.Fatal(err)
22+
}
23+
}
24+
b.ReportAllocs()
25+
b.ResetTimer()
26+
for range b.N {
27+
if err := w.Queue(&m); err != nil {
28+
b.Fatal(err)
29+
}
30+
if _, err := w.Request(); err != nil {
31+
b.Fatal(err)
32+
}
33+
}
34+
})
35+
}
36+
}

‎ring_resize_test.go‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package queue
2+
3+
import (
4+
"testing"
5+
6+
"github.com/golang-queue/queue/job"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestRingShrinkHysteresisAndFIFO(t *testing.T) {
12+
w := NewRing()
13+
var expected []*job.Message
14+
for range 16 {
15+
m := new(job.Message)
16+
expected = append(expected, m)
17+
require.NoError(t, w.Queue(m))
18+
}
19+
for i := range 8 {
20+
m, err := w.Request()
21+
require.NoError(t, err)
22+
require.Same(t, expected[i], m)
23+
}
24+
require.Len(t, w.taskQueue, 16, "half-full buffers must not shrink")
25+
for i := 8; i < 12; i++ {
26+
m, err := w.Request()
27+
require.NoError(t, err)
28+
require.Same(t, expected[i], m)
29+
}
30+
require.Len(t, w.taskQueue, 8, "quarter-full buffers should halve")
31+
// Refill, wrap, grow, and drain to verify FIFO survives both resize paths.
32+
for range 20 {
33+
m := new(job.Message)
34+
expected = append(expected, m)
35+
require.NoError(t, w.Queue(m))
36+
}
37+
for _, want := range expected[12:] {
38+
m, err := w.Request()
39+
require.NoError(t, err)
40+
require.Same(t, want, m)
41+
}
42+
require.Len(t, w.taskQueue, 2)
43+
}

0 commit comments

Comments
 (0)