Files
tailscale/util/ringlog/ringlog.go
Aman Jain 08a9050b18 util/ringlog: make Clear a no-op on a nil RingLog
Add, GetAll and Len all check for a nil receiver and document that they
do nothing, because a nil *RingLog is how callers represent a disabled
log: magicsock leaves endpoint.debugUpdates nil on iOS and Android to
save memory, and calls Add on it unconditionally.

Clear was the one method without that check, so it would panic on those
platforms. Nothing calls it on a nil log today, so this is a latent
footgun rather than an observed crash, but the inconsistency is easy to
fall into. Add the check and a test covering all four methods.

Updates #cleanup

Change-Id: Ic13693a27ad404f0e27906f4010980c9dafb7cc2
Signed-off-by: Aman Jain <jn_aman@yahoo.com>
2026-09-14 21:06:33 -07:00

84 lines
1.7 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package ringlog contains a limited-size concurrency-safe generic ring log.
package ringlog
import "tailscale.com/syncs"
// New creates a new [RingLog] containing at most max items.
func New[T any](max int) *RingLog[T] {
return &RingLog[T]{
max: max,
}
}
// RingLog is a concurrency-safe fixed size log window containing entries of [T].
type RingLog[T any] struct {
mu syncs.Mutex
pos int
buf []T
max int
}
// Add appends a new item to the [RingLog], possibly overwriting the oldest
// item in the log if it is already full.
//
// It does nothing if rb is nil.
func (rb *RingLog[T]) Add(t T) {
if rb == nil {
return
}
rb.mu.Lock()
defer rb.mu.Unlock()
if len(rb.buf) < rb.max {
rb.buf = append(rb.buf, t)
} else {
rb.buf[rb.pos] = t
rb.pos = (rb.pos + 1) % rb.max
}
}
// GetAll returns a copy of all the entries in the ring log in the order they
// were added.
//
// It returns nil if rb is nil.
func (rb *RingLog[T]) GetAll() []T {
if rb == nil {
return nil
}
rb.mu.Lock()
defer rb.mu.Unlock()
out := make([]T, len(rb.buf))
for i := range len(rb.buf) {
x := (rb.pos + i) % rb.max
out[i] = rb.buf[x]
}
return out
}
// Len returns the number of elements in the ring log. Note that this value
// could change immediately after being returned if a concurrent caller
// modifies the log.
func (rb *RingLog[T]) Len() int {
if rb == nil {
return 0
}
rb.mu.Lock()
defer rb.mu.Unlock()
return len(rb.buf)
}
// Clear will empty the ring log.
//
// It does nothing if rb is nil.
func (rb *RingLog[T]) Clear() {
if rb == nil {
return
}
rb.mu.Lock()
defer rb.mu.Unlock()
rb.pos = 0
rb.buf = nil
}