networking · intermediate · ~15 min

Drain a non-blocking 'read' to EAGAIN

The drain loop for edge-triggered I/O.

Challenge

Write the read-until-EAGAIN loop that edge-triggered epoll requires. Get it wrong and connections silently stall.

Task

Implement int drain(int (*read_fn)(char *, int), char *buf, int cap, int *out_total).

read_fn(dst, n) is a stub that mimics a non-blocking read into dst (up to n bytes) and returns:

  • >0: number of bytes copied into dst
  • 0: EOF — the peer closed
  • -1: EAGAIN — no more data available right now
  • -2: a real error

Call read_fn repeatedly into buf + offset, advancing offset by each positive return, until it returns 0, -1, or -2, or until you have filled cap bytes (so you never loop forever or overrun buf).

Input

  • read_fn: the stub read function.
  • buf, cap: destination buffer and its capacity.
  • out_total: receives the total bytes read.

Output

Return 0 on EAGAIN or EOF (drained as far as possible — success). Return -1 on a real error (-2 from read_fn). In both cases set *out_total to the total bytes read so far.

Example

reads 2 bytes, then 1 byte, then EAGAIN   ->  returns 0, *out_total = 3, buf = "abc"
EOF on the first call                     ->  returns 0, *out_total = 0
EAGAIN on the first call                  ->  returns 0, *out_total = 0
error (-2) from read_fn                   ->  returns -1, *out_total = bytes read so far

Edge cases

  • Immediate EAGAIN reads 0 bytes and still returns 0 (success).
  • Stop when offset reaches cap, even if more data may be available.

Rules

  • EAGAIN (-1) is NOT an error — it means "done for now". Only -2 returns -1.

Why this matters

Edge-triggered epoll requires this loop. Get the loop wrong and connections silently stall — a notorious cause of 'works in dev, fails in prod' bugs.

Input format

read_fn, a non-blocking read stub; buf/cap, the destination buffer and its size; out_total for the byte count.

Output format

0 on EAGAIN or EOF, -1 on a real error (-2); *out_total set to total bytes read in both cases.

Constraints

Loop until EOF/EAGAIN/error or cap is reached; never overrun buf; treat EAGAIN as success, not failure.

Starter code

int drain(int (*read_fn)(char *, int), char *buf, int cap, int *out_total) { /* TODO */ return 0; }

Common mistakes

Returning -1 on EAGAIN; treating EAGAIN as fatal.

Edge cases to handle

Immediate EAGAIN (0 bytes read). Cap reached mid-drain.

Complexity

O(total bytes).

Background lessons

Solve this exercise in the browser editor — compile and run against the test harness, no setup required.