Implement a Concurrent Buffered File Logger
DatabricksSoftware Engineer · OnsiteLast updated
Design and implement a thread-safe, buffered logger that aggregates concurrent writes across multiple threads and flushes them to disk efficiently with documented ordering guarantees.
Problem Statement
High-throughput distributed systems frequently emit millions of log events per second across dozens of worker threads. Writing synchronously to a file on every log call incurs unacceptable I/O overhead and lock contention.
Design and implement a thread-safe, buffered file logger class, ConcurrentFileLogger, that buffers log events in memory and flushes them to a single file in batches.
Requirements
- Concurrent Ingestion: Multiple threads can call
log(message)concurrently without deadlocking or corrupting output. - Ordering Guarantee: Log messages submitted by the same thread must appear in the file in their relative submission order. Across different threads, logs should be ordered by their insertion timestamp or relative ingestion order.
- Flushing Conditions: The logger must flush buffered records to disk when:
- The buffer reaches
max_buffer_sizemessages. - A maximum time of
flush_interval_mshas passed since the last flush. - An explicit call to
flush()is made by the caller.
- The buffer reaches
- Clean Shutdown: Calling
close()must flush all remaining in-memory records and safely stop any background threads.
Interface
Implement the following class:
class ConcurrentFileLogger:
def __init__(self, file_path: str, max_buffer_size: int = 1000, flush_interval_ms: int = 200):
"""
Initializes the logger with a destination file, buffer limit, and flush interval.
"""
pass
def log(self, message: str) -> None:
"""
Appends a message to the buffer safely from any thread.
"""
pass
def flush(self) -> None:
"""
Synchronously flushes all currently buffered records to disk.
"""
pass
def close(self) -> None:
"""
Flushes remaining records, closes the underlying file, and terminates background workers.
"""
pass
Example Usage
import threading
logger = ConcurrentFileLogger("app.log", max_buffer_size=10, flush_interval_ms=500)
def worker(thread_id):
for i in range(5):
logger.log(f"Thread-{thread_id}: message {i}")
threads = [threading.Thread(target=worker, args=(t,)) for t in range(3)]
for t in threads: t.start()
for t in threads: t.join()
logger.close()
# app.log now contains all 15 formatted records, with Thread-X messages properly ordered relative to each other.
Edge Cases & Invariants
- Backpressure: Define your policy (blocking vs. dropping vs. unbounded memory) when producers outpace consumer disk writes.
- Error Handling: An I/O error during a flush should not corrupt the in-memory state or silently swallow future logs.
- Double Close: Calling
close()multiple times or callinglog()afterclose()must be handled gracefully.
Hints
Hint 1
Decouple the producer threads from disk I/O using a thread-safe blocking queue or a dual-buffer swap pattern.
Hint 2
Run a dedicated background worker thread that waits on the queue with a timeout equivalent to `flush_interval_ms`.
Hint 3
Ensure `close()` uses a sentinel or flag so the flusher thread completely drains the queue before joining.
Follow-up questions
- How would you handle backpressure if disk I/O stalls and the queue reaches capacity (e.g., block callers, drop messages, or write to a fallback dead-letter store)?
- How can you implement log rotation (e.g., max file size 100MB) without interrupting active concurrent writers?
- What guarantees can this architecture make against data loss during an abrupt OS crash or power failure versus a graceful shutdown?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.