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

fd is a non-blocking readable file descriptor; buf has cap bytes.

Starter code

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

Common mistakes

Spinning indefinitely on EAGAIN instead of terminating the drain loop.

Edge cases to handle

EOF on first read, EAGAIN on first read, partial reads.

Complexity

O(total bytes).

Background lessons

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