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
| Function | What It Does | Used By |
|---|---|---|
evl_fd_add_events(fd, POLLOUT) | Register for write-ready notification | hiredis addWrite callback |
evl_defer_start(cb) | Queue callback for next iteration step 2 | db-proxy (one per write) |
evl_timer_start(ms, cb) | Fire callback after delay | Retransmission timers, batch timers |
evl_wakeup_tickle(evl) | Wake EVL from another thread | Cross-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
- EVL iteration: timers → defers (with limit) → poll/IO
- Callback limit can split db-proxy writes across iterations → multiple TCP segments
- db-mux bypasses defers → all commands in buffer before poll → one TCP write
- evl_wakeup_tickle: how threads wake each other's event loops