-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathring_buffer.go
More file actions
80 lines (63 loc) · 2.8 KB
/
Copy pathring_buffer.go
File metadata and controls
80 lines (63 loc) · 2.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package thread_safe_queue
import (
"errors"
"sync/atomic"
)
// RingBuffer is a highly performant, lock-free (or low-lock) circular queue.
// In Traffic Engineering (Envoy/eBPF/XDP), allocating new slice memory dynamically
// causes Garbage Collection (GC) pauses which spike P99 latency.
// A Ring Buffer allocates memory exactly ONCE on boot, and simply overwrites
// old memory mathematically. This is the cornerstone of packet-processing queues!
type RingBuffer struct {
buffer []any // The fixed-size array holding our network packets/requests
size uint64 // Mathematical size constraint (Must be a power of 2 for bitwise optimization)
head uint64 // Atomic index where the Consumer reads (reads pull from Head)
tail uint64 // Atomic index where the Producer writes (writes push to Tail)
}
var ErrBufferFull = errors.New("ring buffer is at maximum capacity (Producer must drop packet)")
var ErrBufferEmpty = errors.New("ring buffer is completely empty (Consumer must wait)")
// NewRingBuffer allocates the queue exactly once.
func NewRingBuffer(size uint64) *RingBuffer {
return &RingBuffer{
buffer: make([]any, size),
size: size,
}
}
// Push adds a new item to the queue. If it's full, we reject it (Backpressure).
func (rb *RingBuffer) Push(item any) error {
// 1. Read the current atomic indices
currentTail := atomic.LoadUint64(&rb.tail)
currentHead := atomic.LoadUint64(&rb.head)
// 2. Is it full? (If the distance between Head and Tail equals our Size constraints)
if currentTail-currentHead >= rb.size {
return ErrBufferFull // The producer is firing too fast!
}
// 3. Mathematical Modulo to wrap around the slice!
// (Example: if size is 10, and tail is 11, index is 1).
idx := currentTail % rb.size
// 4. Write data to the pre-allocated index (Zero new memory allocations overhead!)
rb.buffer[idx] = item
// 5. Commit the new tracking index to memory atomically so Consumers can see it.
atomic.AddUint64(&rb.tail, 1)
return nil
}
// Pop retrieves the oldest item from the queue without deleting it from RAM.
func (rb *RingBuffer) Pop() (any, error) {
// 1. Where are we currently?
currentHead := atomic.LoadUint64(&rb.head)
currentTail := atomic.LoadUint64(&rb.tail)
// 2. Are we empty? (If Head caught up to Tail, there is no unread data!)
if currentHead == currentTail {
return nil, ErrBufferEmpty // Consumer is too fast!
}
// 3. Modulo math to locate the item
idx := currentHead % rb.size
// 4. Retrieve data (Notice we don't 'delete' it. The next time the Tail loops
// around, it will simply overwrite this RAM sector).
item := rb.buffer[idx]
// 5. Erase reference to allow garbage collection if 'item' holds complex pointers
rb.buffer[idx] = nil
// 6. Commit the updated read pointer so the Producer knows it has free space.
atomic.AddUint64(&rb.head, 1)
return item, nil
}