Chapter 9: EVL in PCG — Timers, Defers, Poll, and the Iteration Cycle

EVL Iteration in Detail

// From up-common/src/evl/src/evl.c (simplified):
void evl_run(evl_t *evl) {
    while (!evl->stop) {
        evl_process_timers(evl);      // 1. Fire expired timers
        evl_process_defers(evl);      // 2. Run deferred callbacks (with limit!)
        evl_process_poll(evl);        // 3. poll() + handle I/O
    }
}

// Key detail: evl_process_defers has a CALLBACK LIMIT
// If more defers are queued than the limit, remaining spill to next iteration
// This is why db-proxy (which creates a defer per write) can break batching

Key EVL Functions Used in DB Path

FunctionWhat It DoesUsed By
evl_fd_add_events(fd, POLLOUT)Register for write-ready notificationhiredis addWrite callback
evl_defer_start(cb)Queue callback for next iteration step 2db-proxy (one per write)
evl_timer_start(ms, cb)Fire callback after delayRetransmission timers, batch timers
evl_wakeup_tickle(evl)Wake EVL from another threadCross-thread communication

The Callback Limit Problem

db-proxy creates 10 defers (one per DB write): Iteration 1: process 8 defers (limit=8) → 8 commands in obuf poll() → POLLOUT → write() → TCP segment 1 Iteration 2: process 2 remaining defers → 2 commands in obuf poll() → POLLOUT → write() → TCP segment 2 Result: 2 TCP segments instead of 1! Batching broken by callback limit. db-mux writes directly to obuf (no defers): All 10 commands in obuf before poll() runs poll() → POLLOUT → write() → TCP segment 1 (all 10 commands) Result: 1 TCP segment. Batching preserved.
Key Takeaways