Topic 01: Single-Threaded Reactor Event Loop & Non-Blocking I/O

Target Duration: 2–4 minutes (~300–450 spoken words)
Focus: Pointwise verbal delivery covering the reactor pattern, poll() multiplexing, non-blocking socket flags, state-driven connection readiness, and graceful connection lifecycle management.


🎙️ Pointwise Spoken Speech (Word-for-Word Delivery)


📋 Step-by-Step Summary (What, How & Why)

Step What Was Done How It Works Why This Mechanism / Order Code Reference
1. Non-Blocking Sockets Configured O_NONBLOCK via fcntl Read/write syscalls return EAGAIN immediately if buffers are full/empty. Prevents a slow or malicious client from blocking the single server thread. server.cpp:48-55
2. State Representation Created struct Conn state container Encapsulates buffers, timer nodes, and want_read/want_write intent flags. Decouples network I/O events from high-level application protocol parsing. server.cpp:78-98
3. Event Multiplexing Assembled pollfd list for poll() Populates event masks dynamically based on Conn state flags; checks EINTR. Minimizes kernel wakeups by polling only for events the application actually needs. server.cpp:846-869
4. Opportunistic I/O Attempted immediate handle_write after read If client response is generated, invokes write() without re-polling. Drastically cuts median round-trip latency by avoiding an unnecessary poll() cycle. server.cpp:680-741
5. Connection Teardown Centralized conn_destroy lifecycle Closes FD, clears fd2conn index, detaches idle node, and frees heap memory. Eliminates file descriptor leaks, dangling pointers, and memory growth. server.cpp:140-147

❓ Anticipated Interview Questions & Crisp Answers

Q1: Why build a single-threaded reactor with poll() instead of spawning a thread per client or using a thread pool?

Answer: In high-concurrency in-memory databases, memory throughput and CPU cache coherence are the primary performance bottlenecks, not CPU cores. Spawning a thread per client incurs prohibitive memory overhead (stack allocations of 2–8 MB per thread) and degrades throughput through OS preemption and thread context switches. Furthermore, multi-threading requires locking or latching across the entire hash table and trees, degrading latency under contention. A single-threaded reactor guarantees that all table lookups, insertions, and progressive rehashing steps execute locklessly and deterministically in single-digit microseconds.

Q2: What problems or CPU-spinning bugs occurred with non-blocking reads/writes, and how did state flags fix them?

Answer: In non-blocking TCP sockets, send buffers are almost always empty and ready to accept bytes. If an event loop subscribes to POLLOUT on all sockets unconditionally, poll() returns immediately on every iteration, driving CPU utilization to 100% in a busy-wait spin. We solved this with explicit state intent flags (want_read, want_write): a connection only registers POLLOUT when outgoing data is actually buffered. For reads, when a slow or fragmented packet arrives with fewer than 4 bytes, handle_read keeps the partial slice in incoming and yields to poll(), avoiding thread blocking.

Q3: How did you detect client disconnections (FIN vs EAGAIN), avoid file descriptor exhaustion (EMFILE), and verify under load?

Answer: When read() returns -1, the server checks errno: EAGAIN or EWOULDBLOCK indicates transient socket buffer exhaustion and yields safely, whereas any other error (such as ECONNRESET) or read() == 0 signifies that the remote peer sent a TCP FIN packet and closed the connection. When accept() fails with EMFILE or ENFILE (process descriptor table full), we log the failure and allow the event loop to continue without crashing. To verify stability, our Python stress harness (test_cmds.py) spawned concurrent connections executing thousands of pipelined queries, verifying zero FD leaks via lsof.