Build it yourself

Circular log buffer

easyScheduling and queues

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())

◈ DIAGRAM
RingBuffer(capacity: int)
append(line: str) -> None
last(n: int) -> list[str] # up to n most recent lines, oldest first
size() -> int # lines currently held
dropped() -> int # lines overwritten since creation

Rules

  • When the buffer is full, append overwrites the oldest line and increments dropped.
  • last(n) returns at most min(n, size()) lines in the order they were appended. n <= 0 returns an empty list.
  • append must be O(1) and must not allocate a growing structure. Memory is fixed at construction.
  • capacity < 1 raises ValueError.

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).

  1. append: write at head and advance head = (head + 1) % capacity. If the buffer was already full, the slot you overwrote held the oldest line, so increment dropped. Otherwise increment count.
  2. last(n): let k = min(n, count). The newest line is at head - 1, so the k most recent start at head - k. Read k slots from there, wrapping with modulo. Python's % is always non-negative. Java's % is not, hence Math.floorMod.

Nothing is ever shifted or reallocated, so append is O(1) and memory is fixed.

ComplexityTime O(1) append, O(k) last(k)Space O(capacity)
Python
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._dropped

Follow-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.