How DNS Resolution Works in Coroutines Without Blocking the Scheduler

libfiber intercepts standard getaddrinfo calls and replaces them with a fiber-aware implementation that uses non-blocking UDP sockets and explicit yielding to resolve hostnames without stalling the scheduler.

The iqiyi/libfiber library enables thousands of concurrent coroutines to perform network operations without blocking the scheduler. One critical capability is DNS resolution in coroutines, which traditionally blocks the calling thread while waiting for network responses. This article explains how libfiber transparently hooks POSIX DNS functions to provide asynchronous, cooperative name resolution.

Hooking the Standard getaddrinfo API

In c/src/hook/getaddrinfo.c, the library provides acl_fiber_getaddrinfo, which intercepts standard getaddrinfo calls. When user code calls the POSIX API, the request routes through this fiber-aware implementation rather than the blocking system library. The hook forwards the request to an internal resolver that understands the fiber scheduler's event loop.

Resolver Initialization and Local Caching

Before issuing network requests, the system optimizes for local entries. The resolver_init_once() function in c/src/dns/resolver.c, guarded by pthread_once, parses /etc/resolv.conf, /etc/hosts, and /etc/services into global structures only on first use.

When a lookup occurs, check_local() in c/src/hook/getaddrinfo.c queries the in-memory /etc/hosts cache via find_from_localhost. If the name exists locally, the function builds a fully populated struct addrinfo chain synchronously, returning immediately without any network I/O or fiber yielding.

Asynchronous UDP Query Construction

For external hostnames, resolver_getaddrinfo() in c/src/dns/resolver.c constructs an RFC 1035 compliant query using rfc1035_build_query. It transmits this query via acl_fiber_socket, a non-blocking UDP socket that returns control immediately without blocking the calling fiber.

Yielding During Network Waits

After sending the UDP packet, the critical non-blocking mechanism activates. The code calls read_wait() from c/src/common/read_wait.c. This function invokes acl_fiber_poll, which registers the socket file descriptor with the kernel (using poll, epoll, or kqueue) and yields the current fiber.

While the fiber remains suspended, the scheduler continues executing other ready fibers. When the socket becomes readable or the __wait_timeout (default 5000ms) expires, the scheduler resumes the DNS fiber at the exact point it yielded.

Response Parsing and CNAME Resolution

Upon resumption, udp_request receives the DNS response. The resolver calls rfc1035_response_unpack and rfc1035_to_addrinfo in c/src/dns/resolver.c to parse the answer section into a linked list of struct addrinfo. If the response contains CNAME records, the resolver follows the canonical name recursively (up to five iterations) until resolving the final A or AAAA records. The complete result returns to the original caller through the same POSIX API surface.

Why the Scheduler Remains Responsive

Fiber-aware polling: Unlike blocking system calls that park kernel threads, acl_fiber_poll suspends only the current coroutine. The scheduler's run queue stays populated with other fibers ready to execute, maintaining full CPU utilization across all cores.

Cooperative design: DNS operations yield only at well-defined wait points (specifically read_wait). No kernel thread blocks on the UDP socket, keeping the entire process in user-space and cooperative.

Timeout handling: If a DNS server fails to respond within __wait_timeout (default 5000 milliseconds), read_wait returns -1. The resolver either retries the next nameserver from /etc/resolv.conf or returns EAI_AGAIN, all without ever blocking the scheduler or consuming a thread per query.

Practical Implementation Examples

Transparent DNS with Standard API

The following code requires no modification to use libfiber's non-blocking resolver. Simply link with -lfiber:

#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void)
{
    struct addrinfo *res, hints;
    int rc;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family   = AF_UNSPEC;      // IPv4 or IPv6
    hints.ai_socktype = SOCK_STREAM;    // TCP (default)

    rc = getaddrinfo("www.qiyi.com", "http", &hints, &res);
    if (rc != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
        return 1;
    }

    for (struct addrinfo *p = res; p; p = p->ai_next) {
        char host[NI_MAXHOST];
        getnameinfo(p->ai_addr, p->ai_addrlen,
                    host, sizeof(host), NULL, 0, NI_NUMERICHOST);
        printf(" resolved: %s\n", host);
    }

    freeaddrinfo(res);
    return 0;
}

When linked with libfiber, the getaddrinfo call automatically routes through acl_fiber_getaddrinfo. The DNS lookup runs inside a fiber, and the scheduler stays responsive to other coroutines.

Concurrent DNS in Explicit Fibers

For explicit fiber creation, spawn multiple coroutines that resolve different hosts simultaneously:

#include "fiber/fiber.h"
#include "fiber/fiber_hook.h"
#include <netdb.h>
#include <stdio.h>

static void dns_task(void *arg)
{
    const char *name = (const char *)arg;
    struct addrinfo *res, hints;
    int rc;

    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    rc = getaddrinfo(name, NULL, &hints, &res);
    if (rc) {
        printf("[fiber] %s: %s\n", name, gai_strerror(rc));
        return;
    }

    for (struct addrinfo *p = res; p; p = p->ai_next) {
        char ip[NI_MAXHOST];
        getnameinfo(p->ai_addr, p->ai_addrlen,
                    ip, sizeof(ip), NULL, 0, NI_NUMERICHOST);
        printf("[fiber] %s -> %s\n", name, ip);
    }
    freeaddrinfo(res);
}

int main(void)
{
    fiber_create(dns_task, "www.qiyi.com");
    fiber_create(dns_task, "www.baidu.com");
    /* Run the scheduler – it will interleave the two DNS queries without blocking */
    fiber_schedule();
    return 0;
}

Each fiber_create spawns a coroutine that performs a DNS query. While one fiber waits on read_wait, the other continues execution or performs unrelated work, demonstrating true non-blocking behavior.

Summary

  • libfiber hooks getaddrinfo in c/src/hook/getaddrinfo.c to intercept DNS requests transparently via acl_fiber_getaddrinfo
  • Local lookups short-circuit via the in-memory /etc/hosts cache using check_local(), avoiding network I/O entirely
  • External queries use non-blocking UDP sockets constructed in c/src/dns/resolver.c with rfc1035_build_query
  • read_wait() in c/src/common/read_wait.c yields fibers during network waits via acl_fiber_poll, keeping the scheduler unblocked
  • The implementation follows RFC 1035 standards with CNAME recursion support up to five levels, returning fully populated struct addrinfo chains

Frequently Asked Questions

Does libfiber require modifying existing code that uses getaddrinfo?

No. When linked with -lfiber, existing applications using the standard POSIX getaddrinfo automatically route through acl_fiber_getaddrinfo. The hook mechanism in c/src/hook/getaddrinfo.c intercepts calls transparently, so legacy code gains non-blocking behavior without source modification or API changes.

What happens if the DNS server is slow or unresponsive?

The resolver uses read_wait() with a default __wait_timeout of 5000 milliseconds as defined in the fiber configuration. If the timeout expires, the fiber resumes and the resolver either attempts the next configured nameserver from /etc/resolv.conf or returns an error code. The scheduler never blocks waiting for slow DNS responses, and no kernel thread is consumed during the wait.

How does libfiber handle IPv6 and CNAME records?

The resolver fully supports both IPv6 and IPv4 through AF_UNSPEC address family hints. When parsing responses in rfc1035_to_addrinfo within c/src/dns/resolver.c, the code handles CNAME records by following the canonical name recursively (up to five iterations) until reaching the final A or AAAA records. The function returns a complete struct addrinfo chain representing the final resolved addresses.

Is the DNS resolver thread-safe for multiple concurrent fibers?

Yes. The initialization phase uses pthread_once in resolver_init_once() to safely load configuration files from /etc/resolv.conf and /etc/hosts. Subsequent queries use independent UDP sockets per request, and the fiber scheduler handles concurrency through cooperative multitasking. Thousands of fibers can perform DNS resolution simultaneously without blocking each other or the central scheduler.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →