Edge-triggered vs level-triggered epoll
The two epoll notification modes, why edge-triggered is the tricky one, and which one I'm using.
When you build an event loop with epoll (or kqueue), you have to pick a notification mode. It changes how the loop behaves and how much code you need to get it right.
# Level-triggered: "this file descriptor IS ready"
Level-triggered is the default, and the simpler mode. You get notified every time you call epoll_wait() while data is available.
// 100 bytes arrive on socket
-> socket ready
-> socket
If you don't read all the data, the next epoll_wait() just tells you again. It's forgiving.
# Edge-triggered: "this file descriptor BECAME ready"
Edge-triggered only notifies you when the state changes from "not ready" to "ready".
// 100 bytes arrive on socket
-> socket ready
->
So if you don't read all the data, you won't hear about it again until NEW data arrives, and the 50 bytes you left behind just sit there. That's the gotcha. You have to drain the socket completely every time:
-> socket ready
while
EAGAIN means "would block", the socket is empty and there's no more data. Only then is it safe to go back to epoll_wait().
Same 100 bytes, side by side. Level-triggered:
-> socket ready
// Partial read is fine
-> socket ready // Notified again
Edge-triggered, done wrong:
-> socket ready
// Partial read
->
Edge-triggered, done right:
-> socket ready
while
// Loop exits when read() returns EAGAIN
# So why would you pick edge-triggered?
Efficiency. Level-triggered can cause spurious wakeups, where you get notified again and again for the same data, and at scale (thousands of connections) that matters. Edge-triggered means fewer calls to epoll_wait() and no redundant notifications, which is better for high-throughput servers.
With edge-triggered you have to drain the socket completely, if not you block, and it's a bit more complex on error handling. But I would guess if you're building your own event loop you do care about efficiency.
Level-triggered is simpler logically. You can read part of the data and come back later, at the cost of more syscalls and slightly worse efficiency, and for learning or low-scale stuff the difference doesn't matter.
# What I'm using
Level-triggered, for my key-value store learning project. The performance hit is ~10-20% at massive scale, which I'll never reach, so the simplicity is worth it for a toy project. If I were building a production proxy server handling 100k+ connections, edge-triggered would matter. Not that I would ever build my own event loop for production, I'm not that crazy.
All of this applies to kqueue (macOS/BSD) too, with different terminology: the EV_CLEAR flag gives you edge-triggered and the default behavior is level-triggered. Same concepts, different API.
Either way, don't cargo-cult edge-triggered just because it's "more efficient". The complexity might not be worth it for your workload.