Skip to content

Commit 57c85b6

Browse files
committed
Add fixed-size circular buffer
This adds TypedRingFixed, a generic circular buffer with a fixed capacity. When the buffer is full, new writes overwrite the oldest entries, keeping only the most recent N elements. This is handy for scenarios like capturing the tail of command output or log streams where memory needs to stay bounded. Signed-off-by: Davanum Srinivas <davanum@gmail.com>
1 parent bc988d5 commit 57c85b6

2 files changed

Lines changed: 399 additions & 0 deletions

File tree

buffer/ring_fixed.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
)
22+
23+
// ErrInvalidSize indicates size must be > 0
24+
var ErrInvalidSize = errors.New("size must be positive")
25+
26+
// TypedRingFixed is a fixed-size circular buffer for elements of type T.
27+
// Writes overwrite older data, keeping only the last N elements.
28+
// Not thread safe.
29+
type TypedRingFixed[T any] struct {
30+
data []T
31+
size int
32+
writeCursor int
33+
written int64
34+
}
35+
36+
// NewTypedRingFixed creates a circular buffer with the given capacity (must be > 0).
37+
func NewTypedRingFixed[T any](size int) (*TypedRingFixed[T], error) {
38+
if size <= 0 {
39+
return nil, ErrInvalidSize
40+
}
41+
return &TypedRingFixed[T]{
42+
data: make([]T, size),
43+
size: size,
44+
}, nil
45+
}
46+
47+
// Write writes p to the buffer, overwriting old data if needed. Returns len(p).
48+
func (r *TypedRingFixed[T]) Write(p []T) int {
49+
originalLen := len(p)
50+
r.written += int64(originalLen)
51+
52+
// If the input is larger than our buffer, only keep the last 'size' elements
53+
if originalLen > r.size {
54+
p = p[originalLen-r.size:]
55+
}
56+
57+
// Copy data, handling wrap-around
58+
n := len(p)
59+
remain := r.size - r.writeCursor
60+
if n <= remain {
61+
copy(r.data[r.writeCursor:], p)
62+
} else {
63+
copy(r.data[r.writeCursor:], p[:remain])
64+
copy(r.data, p[remain:])
65+
}
66+
67+
r.writeCursor = (r.writeCursor + n) % r.size
68+
return originalLen
69+
}
70+
71+
// Slice returns buffer contents in write order. Don't modify the returned slice.
72+
func (r *TypedRingFixed[T]) Slice() []T {
73+
if r.written == 0 {
74+
return nil
75+
}
76+
77+
// Buffer hasn't wrapped yet
78+
if r.written < int64(r.size) {
79+
return r.data[:r.writeCursor]
80+
}
81+
82+
// Buffer has wrapped - need to return data in correct order
83+
// Data from writeCursor to end is oldest, data from 0 to writeCursor is newest
84+
if r.writeCursor == 0 {
85+
return r.data
86+
}
87+
88+
out := make([]T, r.size)
89+
copy(out, r.data[r.writeCursor:])
90+
copy(out[r.size-r.writeCursor:], r.data[:r.writeCursor])
91+
return out
92+
}
93+
94+
// Size returns the buffer capacity.
95+
func (r *TypedRingFixed[T]) Size() int {
96+
return r.size
97+
}
98+
99+
// Len returns how many elements are currently in the buffer.
100+
func (r *TypedRingFixed[T]) Len() int {
101+
if r.written < int64(r.size) {
102+
return int(r.written)
103+
}
104+
return r.size
105+
}
106+
107+
// TotalWritten returns total elements ever written (including overwritten ones).
108+
func (r *TypedRingFixed[T]) TotalWritten() int64 {
109+
return r.written
110+
}
111+
112+
// Reset clears the buffer.
113+
func (r *TypedRingFixed[T]) Reset() {
114+
r.writeCursor = 0
115+
r.written = 0
116+
}

0 commit comments

Comments
 (0)