Circular log buffer
Problem statement
Build a fixed-size buffer that keeps only the most recent log lines. When a service crashes you want its last few hundred lines in the crash report, but you cannot let an in-memory log grow forever. dmesg, flight recorders and many agents' "recent events" views work this way.
API (Java: String lines, List<String> last(int n), long dropped())
RingBuffer(capacity: int)append(line: str) -> Nonelast(n: int) -> list[str] # up to n most recent lines, oldest firstsize() -> int # lines currently helddropped() -> int # lines overwritten since creationRules
- When the buffer is full,
appendoverwrites the oldest line and incrementsdropped. last(n)returns at mostmin(n, size())lines in the order they were appended.n <= 0returns an empty list.appendmust be O(1) and must not allocate a growing structure. Memory is fixed at construction.capacity < 1raisesValueError.
Outputs are the return values of the calls after the appends, in order.
Examples
Example 1
Input: rb = RingBuffer(3)
append "boot", "listen :8080", "conn 1", "conn 2", "timeout"
rb.last(2)
rb.last(10)
rb.size()
rb.dropped()
Output: [conn 2, timeout]
[conn 1, conn 2, timeout]
3
2
Explanation: "boot" and "listen :8080" were overwritten. Asking for more lines than exist returns what is there.
Example 2
Input: rb = RingBuffer(4)
rb.append("a")
rb.append("b")
rb.last(5)
rb.last(0)
rb.size()
rb.dropped()
Output: [a, b]
[]
2
0
Explanation: A buffer that has not wrapped yet behaves like a short list.
Hints
Approach
A ring buffer: a fixed array plus two integers, head (next slot to write) and count (lines stored).
append: write atheadand advancehead = (head + 1) % capacity. If the buffer was already full, the slot you overwrote held the oldest line, so incrementdropped. Otherwise incrementcount.last(n): letk = min(n, count). The newest line is athead - 1, so the k most recent start athead - k. Readkslots from there, wrapping with modulo. Python's%is always non-negative. Java's%is not, henceMath.floorMod.
Nothing is ever shifted or reallocated, so append is O(1) and memory is fixed.
O(1) append, O(k) last(k)Space O(capacity)class RingBuffer: def __init__(self, capacity): if capacity < 1: raise ValueError("capacity must be at least 1") self.buf = [None] * capacity # allocated once, never grows self.head = 0 # next slot to write self.count = 0 self._dropped = 0 def append(self, line): cap = len(self.buf) if self.count == cap: self._dropped += 1 # the slot at head holds the oldest line else: self.count += 1 self.buf[self.head] = line self.head = (self.head + 1) % cap def last(self, n): k = max(0, min(n, self.count)) cap = len(self.buf) return [self.buf[(self.head - k + i) % cap] for i in range(k)] def size(self): return self.count def dropped(self): return self._droppedFollow-up questions
- Make it safe for one writer thread and several reader threads.
- Bound by total bytes instead of number of lines.
- Dump the buffer to a file when the process receives SIGUSR1.
Frequently asked questions
Yes. deque(maxlen=n) drops from the left automatically in O(1), and in production Python you would use it. Implementing the array version shows you understand the index arithmetic, which is also how kernel ring buffers, disruptor queues and fixed-size metric windows work.
In Go: a struct with buf []string made once with make([]string, capacity), head and count ints, and a sync.Mutex if several goroutines log. container/ring exists but is rarely used. With a single writer and reader, a lock-free version uses atomic head and tail counters. Production crash reporters often bound the buffer by bytes rather than lines, since one huge line can otherwise dominate memory.