Topic 06: Active Min-Heap TTL Eviction & Asynchronous Thread Pool

Target Duration: 2–4 minutes (~300–450 spoken words)
Focus: Pointwise verbal delivery covering multi-timer scheduling in the event loop, active TTL eviction via a binary min-heap with bidirectional tracking, idle connection pruning, and asynchronous deallocation via a background worker thread pool.


🎙️ 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. Dynamic Poll Timeout Computed next_timer_ms() for poll() Takes min(oldest_idle_conn, min_heap_root) - now_ms. Prevents CPU busy spinning while ensuring zero timer drift for expirations. server.cpp:745
2. Active Min-Heap Stored expire_at in binary heap Heap property ensures smallest timestamp is at root index 0. Eliminates memory leaks from unaccessed keys without scanning the entire database. heap.h:7, heap.cpp:16
3. Bidirectional Index Maintained size_t *ref = &ent->heap_idx Swaps update *ref = pos during heap_up and heap_down. Enables $O(\log N)$ in-place TTL updates and deletions without $O(N)$ scans. heap.cpp:55, server.cpp:280
4. Bounded Eviction Enforced 2,000-work cap per tick Breaks process_timers loop if work exceeds k_max_works. Protects request latency from degradation during mass simultaneous expirations. server.cpp:771
5. Async Deallocation Offloaded large containers to ThreadPool Containers $>1,000$ items dispatched to 4 worker threads via mutex/condvar. Eliminates main-thread deallocation freezing when dropping large sets. server.cpp:296, thread_pool.cpp:40

❓ Anticipated Interview Questions & Crisp Answers

Q1: Why combine an active min-heap and circular idle list instead of simple passive expiration or thread-per-timer?

Answer: Passive expiration (checking TTL only when a client queries a key) leaks memory if keys are written once and never touched again. Periodic whole-table scanning wastes massive CPU ($O(N)$ overhead). Active min-heap provides $O(1)$ earliest-expiration inspection and $O(\log N)$ updates. For idle socket timeouts, all connections share an identical 5-second deadline, meaning the intrusive circular doubly-linked list (idle_list) is naturally sorted by recency: bumping a connection on activity is $O(1)$ (dlist_detach + dlist_insert_before to tail), and checking expirations at head is $O(1)$. Combining both into next_timer_ms() calculates the exact millisecond poll() timeout, eliminating busy-wait CPU spinning entirely.

Q2: What concurrency bugs or race conditions can occur when offloading memory reclamation to background worker threads in an otherwise single-threaded engine?

Answer: Memory reclamation races happen if a background worker attempts to deallocate memory while the reactor still holds references, or if worker threads deallocate nodes back to the OS while the main thread's memory allocator experiences lock contention. To guarantee complete safety: (1) In entry_del(), the node is completely severed from the database hash table (h_del()), sorted set AVL tree, and TTL min-heap synchronously on the reactor thread before submitting to thread_pool_queue(). (2) The dispatched pointer is unreachable by any client lookup, giving the worker exclusive ownership. (3) Deletion is only offloaded for large structures (zset_del() with $>1000$ members) where recursive tree destruction would block the event loop for dozens of milliseconds. Smaller entries are deleted synchronously to avoid queue lock contention overhead.

Q3: How does the bidirectional HeapItem.ref pointer work during rebalances, and how did you verify it doesn't cause invalid memory writes?

Answer: In standard std::priority_queue, updating or deleting an element requires an $O(N)$ scan because items move during heapify without notifying their owners. In Radis, HeapItem embeds a pointer size_t *ref = &ent->heap_idx. Whenever heap_up() or heap_down() swaps array elements, it immediately executes *a[pos].ref = pos. When an entry is deleted or its TTL is extended via PEXPIRE, the code directly passes ent->heap_idx into heap_update(), performing an in-place reheapify in $O(\log N)$ without searching. To prevent invalid memory writes, whenever a key is removed from the heap, ent->heap_idx is set to (size_t)-1 (sentinel), and heap_update() asserts bounds before dereferencing ref. Validated under high-concurrency client mutations with AddressSanitizer.