Skip to content

Commit 98d557b

Browse files
authored
Merge pull request #337 from dims/add-ring-fixed
Add fixed-size circular buffer with io.Writer support
2 parents 61b37f7 + 803ecd2 commit 98d557b

File tree

2 files changed

+377
-0
lines changed

2 files changed

+377
-0
lines changed

buffer/ring_fixed.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package buffer
18+
19+
import (
20+
"errors"
21+
"io"
22+
)
23+
24+
// Compile-time check that *TypedRingFixed[byte] implements io.Writer.
25+
var _ io.Writer = (*TypedRingFixed[byte])(nil)
26+
27+
// ErrInvalidSize indicates size must be > 0
28+
var ErrInvalidSize = errors.New("size must be positive")
29+
30+
// TypedRingFixed is a fixed-size circular buffer for elements of type T.
31+
// Writes overwrite older data, keeping only the last N elements.
32+
// Not thread safe.
33+
type TypedRingFixed[T any] struct {
34+
data []T
35+
size int
36+
writeCursor int
37+
written int64
38+
}
39+
40+
// NewTypedRingFixed creates a circular buffer with the given capacity (must be > 0).
41+
func NewTypedRingFixed[T any](size int) (*TypedRingFixed[T], error) {
42+
if size <= 0 {
43+
return nil, ErrInvalidSize
44+
}
45+
return &TypedRingFixed[T]{
46+
data: make([]T, size),
47+
size: size,
48+
}, nil
49+
}
50+
51+
// Write writes p to the buffer, overwriting old data if needed.
52+
func (r *TypedRingFixed[T]) Write(p []T) (int, error) {
53+
originalLen := len(p)
54+
r.written += int64(originalLen)
55+
56+
// If the input is larger than our buffer, only keep the last 'size' elements
57+
if originalLen > r.size {
58+
p = p[originalLen-r.size:]
59+
}
60+
61+
// Copy data, handling wrap-around
62+
n := len(p)
63+
remain := r.size - r.writeCursor
64+
if n <= remain {
65+
copy(r.data[r.writeCursor:], p)
66+
} else {
67+
copy(r.data[r.writeCursor:], p[:remain])
68+
copy(r.data, p[remain:])
69+
}
70+
71+
r.writeCursor = (r.writeCursor + n) % r.size
72+
return originalLen, nil
73+
}
74+
75+
// Slice returns buffer contents in write order. Don't modify the returned slice.
76+
func (r *TypedRingFixed[T]) Slice() []T {
77+
if r.written == 0 {
78+
return nil
79+
}
80+
81+
// Buffer hasn't wrapped yet
82+
if r.written < int64(r.size) {
83+
return r.data[:r.writeCursor]
84+
}
85+
86+
// Buffer has wrapped - need to return data in correct order
87+
// Data from writeCursor to end is oldest, data from 0 to writeCursor is newest
88+
if r.writeCursor == 0 {
89+
return r.data
90+
}
91+
92+
out := make([]T, r.size)
93+
copy(out, r.data[r.writeCursor:])
94+
copy(out[r.size-r.writeCursor:], r.data[:r.writeCursor])
95+
return out
96+
}
97+
98+
// Size returns the buffer capacity.
99+
func (r *TypedRingFixed[T]) Size() int {
100+
return r.size
101+
}
102+
103+
// Len returns how many elements are currently in the buffer.
104+
func (r *TypedRingFixed[T]) Len() int {
105+
if r.written < int64(r.size) {
106+
return int(r.written)
107+
}
108+
return r.size
109+
}
110+
111+
// TotalWritten returns total elements ever written (including overwritten ones).
112+
func (r *TypedRingFixed[T]) TotalWritten() int64 {
113+
return r.written
114+
}
115+
116+
// Reset clears the buffer.
117+
func (r *TypedRingFixed[T]) Reset() {
118+
r.writeCursor = 0
119+
r.written = 0
120+
}

buffer/ring_fixed_test.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package buffer
18+
19+
import (
20+
"io"
21+
"reflect"
22+
"testing"
23+
)
24+
25+
func TestTypedRingFixedNew(t *testing.T) {
26+
t.Parallel()
27+
28+
buf, err := NewTypedRingFixed[int](10)
29+
if err != nil {
30+
t.Errorf("size 10: unexpected error: %v", err)
31+
}
32+
if buf.Size() != 10 {
33+
t.Errorf("Size() = %d, want 10", buf.Size())
34+
}
35+
if _, err := NewTypedRingFixed[int](1); err != nil {
36+
t.Errorf("size 1: unexpected error: %v", err)
37+
}
38+
if _, err := NewTypedRingFixed[int](0); err != ErrInvalidSize {
39+
t.Errorf("size 0: expected ErrInvalidSize, got %v", err)
40+
}
41+
if _, err := NewTypedRingFixed[int](-1); err != ErrInvalidSize {
42+
t.Errorf("size -1: expected ErrInvalidSize, got %v", err)
43+
}
44+
}
45+
46+
func TestTypedRingFixedWrite(t *testing.T) {
47+
t.Parallel()
48+
49+
tests := []struct {
50+
name string
51+
size int
52+
writes [][]int
53+
wantSlice []int
54+
wantLen int
55+
wantWritten int64
56+
}{
57+
{"short write", 10, [][]int{{1, 2, 3}}, []int{1, 2, 3}, 3, 3},
58+
{"full write", 3, [][]int{{1, 2, 3}}, []int{1, 2, 3}, 3, 3},
59+
{"long write", 3, [][]int{{1, 2, 3, 4, 5}}, []int{3, 4, 5}, 3, 5},
60+
{"empty write", 10, [][]int{{1, 2}, {}}, []int{1, 2}, 2, 2},
61+
{"overwrite", 5, [][]int{{1, 2, 3, 4, 5}, {6, 7}}, []int{3, 4, 5, 6, 7}, 5, 7},
62+
{"multiple small", 5, [][]int{{1}, {2}, {3}, {4}, {5}, {6}}, []int{2, 3, 4, 5, 6}, 5, 6},
63+
}
64+
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
buf, _ := NewTypedRingFixed[int](tt.size)
68+
for _, w := range tt.writes {
69+
_, _ = buf.Write(w)
70+
}
71+
if got := buf.Slice(); !reflect.DeepEqual(got, tt.wantSlice) {
72+
t.Errorf("Slice() = %v, want %v", got, tt.wantSlice)
73+
}
74+
if got := buf.Len(); got != tt.wantLen {
75+
t.Errorf("Len() = %d, want %d", got, tt.wantLen)
76+
}
77+
if got := buf.TotalWritten(); got != tt.wantWritten {
78+
t.Errorf("TotalWritten() = %d, want %d", got, tt.wantWritten)
79+
}
80+
})
81+
}
82+
}
83+
84+
func TestTypedRingFixedReset(t *testing.T) {
85+
t.Parallel()
86+
87+
buf, _ := NewTypedRingFixed[int](4)
88+
_, _ = buf.Write([]int{1, 2, 3, 4, 5, 6})
89+
buf.Reset()
90+
91+
if buf.Len() != 0 || buf.TotalWritten() != 0 || buf.Slice() != nil {
92+
t.Errorf("after Reset: Len=%d, TotalWritten=%d, Slice=%v; want 0, 0, nil",
93+
buf.Len(), buf.TotalWritten(), buf.Slice())
94+
}
95+
96+
_, _ = buf.Write([]int{7, 8, 9, 10, 11})
97+
if got := buf.Slice(); !reflect.DeepEqual(got, []int{8, 9, 10, 11}) {
98+
t.Errorf("after Reset+Write: Slice() = %v, want %v", got, []int{8, 9, 10, 11})
99+
}
100+
}
101+
102+
func BenchmarkTypedRingFixed_Write(b *testing.B) {
103+
b.ReportAllocs()
104+
buf, _ := NewTypedRingFixed[int](1024)
105+
data := make([]int, 100)
106+
107+
for i := 0; i < b.N; i++ {
108+
_, _ = buf.Write(data)
109+
}
110+
}
111+
112+
func BenchmarkTypedRingFixed_Slice(b *testing.B) {
113+
b.ReportAllocs()
114+
buf, _ := NewTypedRingFixed[int](1024)
115+
_, _ = buf.Write(make([]int, 500)) // Partial fill, cursor at 500
116+
_, _ = buf.Write(make([]int, 1024)) // Wrapped, cursor at 500
117+
118+
for i := 0; i < b.N; i++ {
119+
_ = buf.Slice()
120+
}
121+
}
122+
123+
func TestTypedRingFixedByteWriterInterface(t *testing.T) {
124+
t.Parallel()
125+
var _ io.Writer = &TypedRingFixed[byte]{}
126+
}
127+
128+
func TestTypedRingFixedByteWrite(t *testing.T) {
129+
t.Parallel()
130+
131+
tests := []struct {
132+
name string
133+
size int
134+
writes []string
135+
wantString string
136+
wantLen int
137+
wantWritten int64
138+
}{
139+
{"short write", 1024, []string{"hello world"}, "hello world", 11, 11},
140+
{"full write", 11, []string{"hello world"}, "hello world", 11, 11},
141+
{"long write", 6, []string{"hello world"}, " world", 6, 11},
142+
{"huge write", 3, []string{"hello world"}, "rld", 3, 11},
143+
{"empty write", 10, []string{"hello", ""}, "hello", 5, 5},
144+
{"size one", 1, []string{"a", "b", "xyz"}, "z", 1, 5},
145+
{"overwrite", 10, []string{"0123456789", "abc"}, "3456789abc", 10, 13},
146+
{"multiple small", 10, []string{"aa", "bb", "cc", "dd", "ee", "ff"}, "bbccddeeff", 10, 12},
147+
{"many single bytes", 3, []string{"h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"}, "rld", 3, 11},
148+
{
149+
"multi part",
150+
16,
151+
[]string{"hello world\n", "this is a test\n", "my cool input\n"},
152+
"t\nmy cool input\n",
153+
16,
154+
41,
155+
},
156+
}
157+
158+
for _, tt := range tests {
159+
t.Run(tt.name, func(t *testing.T) {
160+
buf, _ := NewTypedRingFixed[byte](tt.size)
161+
for _, w := range tt.writes {
162+
_, _ = buf.Write([]byte(w))
163+
}
164+
if got := string(buf.Slice()); got != tt.wantString {
165+
t.Errorf("Slice() = %q, want %q", got, tt.wantString)
166+
}
167+
if got := buf.Len(); got != tt.wantLen {
168+
t.Errorf("Len() = %d, want %d", got, tt.wantLen)
169+
}
170+
if got := buf.TotalWritten(); got != tt.wantWritten {
171+
t.Errorf("TotalWritten() = %d, want %d", got, tt.wantWritten)
172+
}
173+
})
174+
}
175+
}
176+
177+
func TestTypedRingFixedByteWriteReturnValue(t *testing.T) {
178+
t.Parallel()
179+
180+
buf, _ := NewTypedRingFixed[byte](5)
181+
182+
// Write returns original length even when truncated
183+
if n, err := buf.Write([]byte("hello world")); n != 11 || err != nil {
184+
t.Errorf("Write() = (%d, %v), want (11, nil)", n, err)
185+
}
186+
}
187+
188+
func TestTypedRingFixedByteReset(t *testing.T) {
189+
t.Parallel()
190+
191+
buf, _ := NewTypedRingFixed[byte](4)
192+
_, _ = buf.Write([]byte("hello world\n"))
193+
_, _ = buf.Write([]byte("this is a test\n"))
194+
buf.Reset()
195+
196+
if buf.Len() != 0 || buf.TotalWritten() != 0 || buf.Slice() != nil {
197+
t.Errorf("after Reset: Len=%d, TotalWritten=%d, Slice=%v; want 0, 0, nil",
198+
buf.Len(), buf.TotalWritten(), buf.Slice())
199+
}
200+
201+
// Write after reset
202+
_, _ = buf.Write([]byte("hello"))
203+
if got := string(buf.Slice()); got != "ello" {
204+
t.Errorf("after Reset+Write: Slice() = %q, want %q", got, "ello")
205+
}
206+
}
207+
208+
func TestTypedRingFixedByteSlice(t *testing.T) {
209+
t.Parallel()
210+
211+
buf, _ := NewTypedRingFixed[byte](10)
212+
213+
// Empty
214+
if buf.Slice() != nil {
215+
t.Errorf("empty buffer: Slice() = %v, want nil", buf.Slice())
216+
}
217+
218+
// Partial fill - returns slice of internal buffer
219+
_, _ = buf.Write([]byte("hello"))
220+
if got := string(buf.Slice()); got != "hello" {
221+
t.Errorf("partial fill: Slice() = %q, want %q", got, "hello")
222+
}
223+
224+
// Exact fill at cursor 0 - returns internal buffer directly
225+
buf.Reset()
226+
_, _ = buf.Write([]byte("0123456789"))
227+
if got := string(buf.Slice()); got != "0123456789" {
228+
t.Errorf("exact fill: Slice() = %q, want %q", got, "0123456789")
229+
}
230+
231+
// Wrapped - returns new slice with reordered data
232+
_, _ = buf.Write([]byte("ab"))
233+
if got := string(buf.Slice()); got != "23456789ab" {
234+
t.Errorf("wrapped: Slice() = %q, want %q", got, "23456789ab")
235+
}
236+
}
237+
238+
func BenchmarkTypedRingFixedByte_Write(b *testing.B) {
239+
b.ReportAllocs()
240+
buf, _ := NewTypedRingFixed[byte](1024)
241+
data := make([]byte, 100)
242+
243+
for i := 0; i < b.N; i++ {
244+
_, _ = buf.Write(data)
245+
}
246+
}
247+
248+
func BenchmarkTypedRingFixedByte_Slice(b *testing.B) {
249+
b.ReportAllocs()
250+
buf, _ := NewTypedRingFixed[byte](1024)
251+
_, _ = buf.Write(make([]byte, 500)) // Partial fill, cursor at 500
252+
_, _ = buf.Write(make([]byte, 1024)) // Wrapped, cursor at 500
253+
254+
for i := 0; i < b.N; i++ {
255+
_ = buf.Slice()
256+
}
257+
}

0 commit comments

Comments
 (0)