ringbuffer

package
v0.1.0-alpha.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 30, 2025 License: Apache-2.0 Imports: 1 Imported by: 0

README

pkg/events/ringbuffer

Generic thread-safe ring buffer implementation using Go generics.

Overview

A ring buffer (circular buffer) is a fixed-size data structure that overwrites the oldest items when full. This implementation is generic (works with any type) and thread-safe for concurrent access.

Use cases:

  • Event history/buffering
  • Sliding window metrics
  • Recent log storage
  • Any bounded collection with automatic old-item eviction

Installation

import "haptic/pkg/events/ringbuffer"

Quick Start

// Create buffer with capacity of 1000
buffer := ringbuffer.New[int](1000)

// Add items
buffer.Add(1)
buffer.Add(2)
buffer.Add(3)

// Retrieve items
all := buffer.GetAll()           // All items (up to 1000)
recent := buffer.GetLast(10)     // Last 10 items
count := buffer.Len()            // Current count

API Reference

Creating a Buffer
func New[T any](size int) *RingBuffer[T]

Creates a new ring buffer with the specified capacity.

Parameters:

  • size - Maximum number of items to store

Example:

// Buffer for last 1000 events
eventBuffer := ringbuffer.New[Event](1000)

// Buffer for last 60 metrics
metricsBuffer := ringbuffer.New[Metric](60)
Adding Items
func (rb *RingBuffer[T]) Add(item T)

Adds an item to the buffer. If buffer is full, overwrites the oldest item.

Thread-safe: Yes

Example:

buffer := ringbuffer.New[string](100)
buffer.Add("event1")
buffer.Add("event2")
// When full, oldest items are automatically removed
Retrieving Items
func (rb *RingBuffer[T]) GetLast(n int) []T

Returns the last N items in chronological order (oldest first).

If N exceeds the current count, returns all available items.

Thread-safe: Yes Returns: New slice (does not expose internal buffer)

Example:

// Get last 100 events
recent := buffer.GetLast(100)

// Get last 10
last10 := buffer.GetLast(10)
func (rb *RingBuffer[T]) GetAll() []T

Returns all items currently in the buffer in chronological order.

Thread-safe: Yes Returns: New slice

Example:

allItems := buffer.GetAll()
for _, item := range allItems {
    // Process items in chronological order
}
Getting Count
func (rb *RingBuffer[T]) Len() int

Returns the current number of items in the buffer (never exceeds capacity).

Thread-safe: Yes

Example:

count := buffer.Len()
capacity := 1000
utilization := float64(count) / float64(capacity)

Behavior

Circular Overwriting

When the buffer is full, new items overwrite the oldest items:

buffer := ringbuffer.New[int](3)

buffer.Add(1)  // [1, _, _]
buffer.Add(2)  // [1, 2, _]
buffer.Add(3)  // [1, 2, 3] - full!
buffer.Add(4)  // [4, 2, 3] - overwrites 1
buffer.Add(5)  // [4, 5, 3] - overwrites 2

all := buffer.GetAll()  // [3, 4, 5] in chronological order
Chronological Order

Items are always returned in chronological order (oldest first):

buffer := ringbuffer.New[string](5)
buffer.Add("first")
buffer.Add("second")
buffer.Add("third")

all := buffer.GetAll()
// Returns: ["first", "second", "third"]
// NOT: ["third", "second", "first"]
Thread Safety

All operations are thread-safe:

buffer := ringbuffer.New[int](1000)

// Safe to use from multiple goroutines
go func() {
    for i := 0; i < 100; i++ {
        buffer.Add(i)
    }
}()

go func() {
    for i := 0; i < 100; i++ {
        recent := buffer.GetLast(10)
        fmt.Println(len(recent))
    }
}()

Common Use Cases

Event History
type Event struct {
    Timestamp time.Time
    Type      string
    Data      map[string]interface{}
}

eventBuffer := ringbuffer.New[Event](1000)

// Store events
eventBuffer.Add(Event{
    Timestamp: time.Now(),
    Type:      "user.login",
    Data:      map[string]interface{}{"user_id": 123},
})

// Query recent events
recentEvents := eventBuffer.GetLast(100)
Sliding Window Metrics
type Metric struct {
    Timestamp time.Time
    Value     float64
}

// Keep last 60 seconds of metrics
metricsWindow := ringbuffer.New[Metric](60)

// Record every second
ticker := time.NewTicker(1 * time.Second)
for range ticker.C {
    metricsWindow.Add(Metric{
        Timestamp: time.Now(),
        Value:     getCurrentMetric(),
    })

    // Calculate rolling average
    metrics := metricsWindow.GetAll()
    sum := 0.0
    for _, m := range metrics {
        sum += m.Value
    }
    avg := sum / float64(len(metrics))
    fmt.Printf("Rolling avg: %.2f\n", avg)
}
Debug Log Buffer
type LogEntry struct {
    Level     string
    Message   string
    Timestamp time.Time
}

debugLogs := ringbuffer.New[LogEntry](500)

// Log function
func Log(level, message string) {
    debugLogs.Add(LogEntry{
        Level:     level,
        Message:   message,
        Timestamp: time.Now(),
    })
}

// Dump recent logs
func DumpLogs() {
    logs := debugLogs.GetAll()
    for _, entry := range logs {
        fmt.Printf("[%s] %s: %s\n",
            entry.Timestamp.Format(time.RFC3339),
            entry.Level,
            entry.Message)
    }
}

Choosing Buffer Size

Buffer size depends on:

  1. Event rate - How many items per second?
  2. Retention time - How much history do you need?
  3. Memory constraints - How much memory can you use?

Formula:

buffer_size = event_rate × retention_seconds

Examples:

// 10 events/second, keep 5 minutes (300s)
buffer := ringbuffer.New[Event](10 * 300)  // 3000

// 100 events/second, keep 1 minute (60s)
buffer := ringbuffer.New[Event](100 * 60)  // 6000

// 1 event/second, keep 1 hour (3600s)
buffer := ringbuffer.New[Event](1 * 3600)  // 3600

Memory Usage

Memory usage is fixed based on capacity:

buffer := ringbuffer.New[Event](1000)
// Uses: 1000 × sizeof(Event) bytes
// Plus small overhead for: head, count, size fields

For large types, consider storing pointers:

// Instead of
buffer := ringbuffer.New[LargeStruct](1000)  // 1000 × large size

// Use pointers
buffer := ringbuffer.New[*LargeStruct](1000)  // 1000 × 8 bytes (pointer size)

Performance

  • Add: O(1) - constant time
  • GetLast(n): O(n) - linear in items requested
  • GetAll: O(size) - linear in buffer capacity
  • Len: O(1) - constant time

No allocations during Add (except initial buffer creation).

Examples

See:

  • Event buffering: pkg/controller/commentator/commentator.go
  • Debug events: pkg/controller/debug/events.go
  • Tests: pkg/events/ringbuffer/ringbuffer_test.go

License

See main repository for license information.

Documentation

Overview

Package ringbuffer provides a thread-safe generic ring buffer implementation.

A ring buffer (circular buffer) is a fixed-size buffer that wraps around when full. This implementation uses Go generics to support any type and provides thread-safe operations through mutex locking.

Example usage:

type Event struct {
    Timestamp time.Time
    Message   string
}

buffer := ringbuffer.New[Event](100)
buffer.Add(Event{Timestamp: time.Now(), Message: "Event 1"})
recent := buffer.GetLast(10)  // Get last 10 events

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type RingBuffer

type RingBuffer[T any] struct {
	// contains filtered or unexported fields
}

RingBuffer is a thread-safe circular buffer that stores a fixed number of items. When the buffer is full, new items overwrite the oldest items.

The buffer uses Go generics to support any type T.

func New

func New[T any](size int) *RingBuffer[T]

New creates a new ring buffer with the specified capacity.

The size parameter determines the maximum number of items the buffer can hold. Once full, adding new items will overwrite the oldest items.

Example:

buffer := ringbuffer.New[string](100)  // Can hold up to 100 strings

func (*RingBuffer[T]) Add

func (rb *RingBuffer[T]) Add(item T)

Add inserts an item into the buffer.

If the buffer is full, the oldest item is overwritten. This operation is thread-safe.

Example:

buffer.Add("event 1")
buffer.Add("event 2")

func (*RingBuffer[T]) Cap

func (rb *RingBuffer[T]) Cap() int

Cap returns the maximum capacity of the buffer.

This is the size specified when creating the buffer with New(). This operation is thread-safe.

Example:

capacity := buffer.Cap()  // Returns the buffer's maximum size

func (*RingBuffer[T]) Clear

func (rb *RingBuffer[T]) Clear()

Clear removes all items from the buffer, resetting it to empty state.

This operation is thread-safe.

Example:

buffer.Clear()  // Buffer is now empty

func (*RingBuffer[T]) GetAll

func (rb *RingBuffer[T]) GetAll() []T

GetAll returns all items currently in the buffer, in chronological order.

The returned slice is ordered from oldest to newest. This operation is thread-safe.

Example:

all := buffer.GetAll()

func (*RingBuffer[T]) GetLast

func (rb *RingBuffer[T]) GetLast(n int) []T

GetLast returns the n most recently added items, in chronological order.

If n is greater than the number of items in the buffer, all items are returned. The returned slice is ordered from oldest to newest. This operation is thread-safe.

Example:

buffer.Add("event 1")
buffer.Add("event 2")
buffer.Add("event 3")
recent := buffer.GetLast(2)  // Returns ["event 2", "event 3"]

func (*RingBuffer[T]) Len

func (rb *RingBuffer[T]) Len() int

Len returns the current number of items in the buffer.

This operation is thread-safe.

Example:

count := buffer.Len()  // Returns number of items currently stored

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL