Skip to content

한국어 | English

Core Spec Index | Previous: Events | Next: Monitoring

Polling

What this chapter defines — The public contract for waiting on the readiness of multiple sockets and sources through the zlink_poll and zlink_poller_* APIs.

1. Polling overview

This document defines the ZLink Core public readiness contract. Readiness is the state in which it is worthwhile for a source to proceed with receive or send. An application can wait for three types of sources—raw sockets, OS file descriptors, and generic timers—together in one event loop. The intended audience is developers who carry this contract into the C API and each language binding. This document answers: “What do POLLIN and POLLOUT, single-consumer receive mode, and lifetime mean for each source?”

The following documents own the related contracts.

Related contract Defining document
Event-family classification and the boundary of readiness semantics Events
Specific meanings of ZLINK_POLLIN and ZLINK_POLLOUT for each socket type Socket — DEALER, Socket — ROUTER
Result and errno mapping Errors
Generic timer creation and receive (zlink_timer_*) Utilities

2. One-shot poll and reusable poller

There are two ways to wait for readiness.

  • One-shot poll — zlink_poll receives its targets as an item array on each call and waits once.
  • Reusable poller — The zlink_poller_* functions keep sources registered with a poller object and repeatedly wait through zlink_poller_wait.

In both forms, when a signal interrupts the wait, Core resumes it with the remaining timeout, so zlink_poll and zlink_poller_wait never fail with EINTR.

3. Source types and readiness

The readiness reported through ZLINK_POLLIN and ZLINK_POLLOUT for each source type is as follows.

Source POLLIN POLLOUT Additional readiness and rules
raw socket A complete record can be received Socket-wide send readiness or an unread ZLINK_COMPLETION_WRITABLE record. Level-held while that record exists Per-socket receive mode applies. A closed registered socket reports ZLINK_POLLERR once
socket monitor A monitor event can be received Unsupported Drain with zlink_socket_monitor_recv(). A monitor handle is registered with the poller through the same functions as a raw socket (socket/README, "paths that notify the application")
timer A fire count can be received Unsupported Drain with zlink_timer_recv()
FD Platform-readable Platform-writable Platform POLLPRI maps to ZLINK_POLLPRI; all other platform error bits map to ZLINK_POLLERR

For a raw socket with multiple peers, ZLINK_POLLOUT is aggregate readiness for the socket. The event does not identify which routing ID or transport pair became writable and can be raised because another peer has capacity. Therefore, after a nonblocking submit to one target reports backpressure (behavior that limits additional submissions from a sender when the downstream cannot keep pace with processing), observing ZLINK_POLLOUT does not guarantee that the next submit to that target succeeds. The per-target result of a wait token is delivered in a ZLINK_COMPLETION_WRITABLE record, not the ZLINK_POLLOUT bit. When a ZLINK_SEND_FLAGS_DONTWAIT submit returns ZLINK_SUBMIT_BACKPRESSURED, the nonzero value in completion_id_out is the wait token. When the resource that refused that submit recovers (the wake condition is owned by the socket README), Core enqueues one WRITABLE record carrying the same token, the same user_context, and, for ROUTER and STREAM, the submitted RID into the socket-local completion queue. The application pulls that record with zlink_completion_recv() and identifies the original submit by token, context, and RID; whether to resubmit follows the record's send_result under Socket Common. While the record is unread, both ZLINK_POLLOUT and ZLINK_POLLCOMPLETION remain true.

ZLINK_POLLITEMS_DFLT is the recommended initial item count for internal and application stack buffers; it is not a readiness bit. ZLINK_HAVE_POLLER == 1 means that this public poller API is included in the build.

Readiness is level-triggered, and so is the wake-up. When the readiness of a registered source changes from false to true, a caller waiting on that source in zlink_poller_wait() or zlink_poll() wakes at that point even if timeout remains. The guarantee is the same when the command that produced the transition was processed by a thread other than the caller (an I/O thread, the async command owner, a temporary transport owner, or another application thread that called send or recv on the same socket). A caller that sleeps until its timeout while readiness is true (a lost wake) is a contract violation. To keep the guarantee, only the public poller consumes its notification descriptor. When a command applied anywhere other than the poller changes readiness, the descriptor is signalled at the time set by the wake-up rule of the synchronization model.

Wait tokens have no lost wake either. If the target's credit recovery or pipe attach happens concurrently with the token registration that follows a refused DONTWAIT submit, the implementation rechecks the target state after registering the token (register → recheck) and publishes the WRITABLE record for that edge. A credit or attach edge that occurs after the refusal therefore never loses its WRITABLE record.

4. Completion polling

ZLINK_POLLCOMPLETION is level-triggered readiness indicating that a PAIR, DEALER, ROUTER, or STREAM socket-local completion queue contains at least one record and the next zlink_completion_recv() can succeed. The record kinds that enter the queue are ZLINK_COMPLETION_REQUEST and ZLINK_COMPLETION_WRITABLE. A successful SEND produces no record, and ZLINK_COMPLETION_SEND remains in the enum for ABI compatibility only and is never published. The bit can be registered alone or OR-ed with ZLINK_POLLIN and ZLINK_POLLOUT. Readiness remains set while records remain in the queue.

On a DEALER-ROUTER single connection, a REPLY behind a preceding DATA record is not the physical head until that DATA record is dequeued. At that point only ZLINK_POLLIN may be ready and ZLINK_POLLCOMPLETION may not be ready. After the REPLY reaches the physical head and moves to the socket-local completion queue, the level-triggering and drain-through-ZLINK_RECV_NO_DATA rules above apply.

A ROUTER can use ZLINK_POLLROUTE with both zlink_poll() and a poller. It is a level readiness that becomes ready when the selected route of an RID changes; ROUTER §10.1 defines when it becomes ready and when it is cleared. A source that is not a ROUTER and a closed source follow the existing rules for an unsupported event and a closed source.

zlink_poller_wait() does not remove completions or invoke callbacks. The event array does not contain operation payloads, and its capacity is unrelated to the number of completions. For each ready socket, the caller repeatedly invokes zlink_completion_recv(..., ZLINK_RECV_FLAGS_DONTWAIT) until it receives ZLINK_RECV_NO_DATA. Add, modify, and remove do not consume the queue.

%%{init: {'sequence': {'actorFontSize': '18px', 'messageFontSize': '18px', 'noteFontSize': '18px', 'boxMargin': 8, 'width': 140}, 'themeVariables': {'fontSize': '18px'}}}%%
sequenceDiagram
    participant App as Application
    participant P as Poller
    participant S as Socket completion queue
    App->>P: Call zlink_poller_wait()
    P-->>App: Return POLLCOMPLETION readiness
    loop Until NO_DATA
        App->>S: zlink_completion_recv(DONTWAIT)
        S-->>App: One REQUEST or WRITABLE record
    end

zlink_poller_add() and zlink_poller_modify() can add or remove the completion bit for a supported socket. Using the bit with another source or in a zlink_poll() item returns ZLINK_CONFIG_INVALID_ARGUMENT with errno == EINVAL.

The same socket may be registered with several pollers at once — each registration has its own readiness wake path and lifetime pin. The one exception is the completion bit: at most one poller registration owns a socket's completion bit. If another poller already owns it, adding the socket or adding the bit through modify fails with ZLINK_CONFIG_INVALID_STATE and errno == EBUSY, and the existing registration remains unchanged. Another poller may take ownership after the current owner removes the bit with modify or removes the registration. Queue records and readiness are not lost during the transfer. The application maintains one completion-drain owner per socket.

Transport completion progress on a socket uses one serialized drain path. Registering a completion poller does not prevent the same consumer from calling zlink_completion_recv(NONE) directly. That call performs completion progress and waiting within RCVTIMEO without a separate zlink_poller_wait(). A DONTWAIT receive consumes only the published public queue and starts no new transport drain turn.

5. Source lifetime and serialization

When a socket source is registered with a poller, Core acquires a lifetime pin on that socket. It is therefore safe for the application to close a registered socket before removing it from the poller. A closed socket source reports POLLERR once, and its registration and lifetime pin remain in place until it is removed.

After zlink_ctx_shutdown, every wait of a poller that has an unclosed socket source (including a monitor handle) of that context, including a wait already in progress, ends without waiting for its timeout and without events with ZLINK_CONFIG_INTERNAL_ERROR (ETERM). Later waits end the same way until that source is removed or closed. Unread completions of that socket follow Completion pull and ownership. A poller does not belong to a context and fd and timer sources are not affected by shutdown, so the wait of a poller with no such socket source continues until an event or its timeout.

Core serializes size, add, modify, remove and wait on one poller. While a wait is active on a poller, size, add, modify and remove (including the _fd and _timer variants) and wait on that poller fail with ZLINK_CONFIG_BUSY/EBUSY and leave its registrations unchanged. Different pollers can be used concurrently. An event array returned by wait is caller-owned and contains no pointer to Core storage.

6. Public types

#if defined _WIN32
typedef uintptr_t zlink_fd_t;
#else
typedef int zlink_fd_t;
#endif

typedef short zlink_poller_event_mask_t;

typedef enum zlink_poller_event_flag_e {
  ZLINK_POLLIN         = 1,   // receive can proceed (source-specific meaning in §3)
  ZLINK_POLLOUT        = 2,   // send/submit retry is worthwhile (source-specific meaning in §3)
  ZLINK_POLLERR        = 4,   // socket close or FD platform error (§3, §5)
  ZLINK_POLLPRI        = 8,   // platform POLLPRI for an FD (§3)
  ZLINK_POLLITEMS_DFLT = 16,  // recommended initial item count; not a readiness bit (§3)
  ZLINK_POLLCOMPLETION = 32,  // socket completion-queue readiness (§4)
  ZLINK_POLLROUTE      = 64   // ROUTER selected-route change (ROUTER §10.1)
} zlink_poller_event_flag_e;

#define ZLINK_HAVE_POLLER 1   // public poller API is included in the build

typedef enum zlink_poller_source_kind_t {
  ZLINK_POLLER_SOURCE_SOCKET    = 1,  // raw socket
  ZLINK_POLLER_SOURCE_FD        = 2,  // OS file descriptor
  ZLINK_POLLER_SOURCE_TIMER     = 3   // generic timer
} zlink_poller_source_kind_t;

typedef struct zlink_pollitem_t {
  void *socket;    // valid only for a SOCKET source
  zlink_fd_t fd;   // valid only for an FD source
  short events;    // event bits to wait for
  short revents;   // returned readiness; zlink_poll clears it to 0 on entry (§7 zlink_poll)
} zlink_pollitem_t;

typedef struct zlink_poller_event_t {
  zlink_poller_source_kind_t source_kind;  // source type for this event
  void *socket;     // valid only for a SOCKET source
  zlink_fd_t fd;    // valid only for an FD source
  void *timer;      // valid only for a TIMER source
  void *user_data;  // borrowed value returning the pointer supplied at registration
  short events;     // observed readiness bits
} zlink_poller_event_t;

7. Functions

Waits once for the readiness of an item array.

ZLINK_EXPORT int zlink_poll(
  zlink_pollitem_t *items,
  int item_count,
  long timeout_ms,
  zlink_config_result_t *error_out);

The return value is the number of items with readiness, 0 on timeout, and -1 on failure. Failure sets both error_out and errno. timeout_ms < 0 waits indefinitely, and 0 returns immediately. With item_count == 0, or when every item's events is 0, the call returns 0/ZLINK_CONFIG_OK immediately regardless of the timeout. The function clears every item's revents to 0 before waiting, so the caller need not initialize it; only the snapshot after the function returns is valid. error_out is an optional output that may be NULL.

Poller functions

ZLINK_EXPORT void *zlink_poller_new(void);
ZLINK_EXPORT zlink_close_result_t zlink_poller_destroy(void **poller_p);
ZLINK_EXPORT int zlink_poller_size(void *poller, zlink_config_result_t *error_out);
ZLINK_EXPORT zlink_config_result_t zlink_poller_add(
  void *poller,
  void *source,
  void *user_data,
  short events);
ZLINK_EXPORT zlink_config_result_t zlink_poller_modify(
  void *poller,
  void *source,
  short events);
ZLINK_EXPORT zlink_config_result_t zlink_poller_remove(void *poller, void *source);
ZLINK_EXPORT zlink_config_result_t zlink_poller_add_fd(
  void *poller,
  zlink_fd_t fd,
  void *user_data,
  short events);
ZLINK_EXPORT zlink_config_result_t zlink_poller_modify_fd(
  void *poller,
  zlink_fd_t fd,
  short events);
ZLINK_EXPORT zlink_config_result_t zlink_poller_remove_fd(void *poller, zlink_fd_t fd);
ZLINK_EXPORT zlink_config_result_t zlink_poller_add_timer(
  void *poller,
  void *timer,
  void *user_data);
ZLINK_EXPORT zlink_config_result_t zlink_poller_remove_timer(
  void *poller,
  void *timer);
ZLINK_EXPORT int zlink_poller_wait(
  void *poller,
  zlink_poller_event_t *events,
  int event_capacity,
  long timeout_ms,
  zlink_config_result_t *error_out);

On success, zlink_poller_new() returns a new poller. If allocation fails, it returns NULL and sets errno to ENOMEM. On successful completion, zlink_poller_destroy() sets the caller-provided pointer to NULL.

zlink_poller_size() returns the current registration count on success and -1 on failure. zlink_poller_wait() returns the number of events written on success, 0 on timeout, and -1 on failure. Every timeout_ms < 0 is normalized to an indefinite wait. If events == NULL or event_capacity <= 0, it fails with EINVAL. When no registration has non-zero events, or every such registration is a closed socket source, no source can become ready, so it does not wait, regardless of timeout_ms. It returns any POLLERR not yet returned (§5), otherwise 0 (as zlink_poll does for item_count == 0). The error_out parameters of zlink_poller_size() and zlink_poller_wait() are optional outputs that may be NULL.

Adding the same source twice returns ZLINK_CONFIG_CONFLICT/EEXIST. A timer can be registered with only one poller at a time — adding a timer that another poller already holds fails with ZLINK_CONFIG_INVALID_STATE/EBUSY, and destroying a registered timer with zlink_timer_destroy() returns ZLINK_CLOSE_BUSY/EBUSY. Modifying or removing a missing source returns ZLINK_CONFIG_NOT_FOUND/ENOENT. An invalid event bit returns ZLINK_CONFIG_INVALID_ARGUMENT/EINVAL, while an event unsupported by the source returns ZLINK_CONFIG_NOT_SUPPORTED/ENOTSUP. Destroying a poller while a wait is active returns ZLINK_CLOSE_BUSY/EBUSY. The errno map defines all result and errno mappings.

8. Internal structure

Contract ownership for this section — The public contract for completion polling is owned by Completion polling and Verification requirements in this document. This section describes how that contract is achieved internally.

The REQUEST resolver and the wait-token WRITABLE publish append results to the same socket-local ready queue. The linearization order of these appends is the public receive order; it does not imply submit order or per-target wire order.

9. Implementation and contract-test verification requirements

Verify the following using only the public surface: the zlink_poll, zlink_poller_*, and zlink_completion_recv functions, return values and errno, and event-array contents. Each item maps to one unit test.

zlink_poll

  • It returns the number of items with readiness, 0 on timeout, and -1 on failure, setting both error_out and errno on failure.
  • timeout_ms == -1 waits indefinitely, and 0 returns immediately.
  • A revents field initialized to 0 before the call is valid only as a snapshot after the function returns.

Poller registration

  • Adding the same source twice returns ZLINK_CONFIG_CONFLICT/EEXIST.
  • Adding a timer that another poller already holds returns ZLINK_CONFIG_INVALID_STATE/EBUSY, and destroying a registered timer with zlink_timer_destroy() returns ZLINK_CLOSE_BUSY/EBUSY.
  • Registering the same socket with two pollers for readiness bits (other than completion) succeeds on both, and each reports readiness independently.
  • Modifying or removing a missing source returns ZLINK_CONFIG_NOT_FOUND/ENOENT.
  • An invalid event bit returns ZLINK_CONFIG_INVALID_ARGUMENT/EINVAL, while an event unsupported by the source returns ZLINK_CONFIG_NOT_SUPPORTED/ENOTSUP.
  • ZLINK_POLLCOMPLETION can be added or removed by add or modify for PAIR, DEALER, ROUTER, and STREAM, alone or OR-ed with other socket readiness. Other sources and zlink_poll() items return ZLINK_CONFIG_INVALID_ARGUMENT/EINVAL. The bit reports two record kinds: REQUEST and WRITABLE.
  • When two pollers try to own the same socket's completion bit, the second add or modify fails with ZLINK_CONFIG_INVALID_STATE/EBUSY, and the existing registration remains unchanged. Moving ownership after the current owner removes the bit or source loses neither queued records nor readiness.

Wait and events

  • Registering a socket source acquires a lifetime pin, so it is safe to close the socket before removal. After close, it reports POLLERR once and remains registered until removal.
  • Platform POLLPRI for an FD maps to ZLINK_POLLPRI; all other platform error bits map to ZLINK_POLLERR.
  • The event fields socket, fd, and timer are valid only for SOCKET, FD, and TIMER sources, respectively, and user_data returns the pointer supplied at registration unchanged.
  • An event array returned by wait is caller-owned and contains no pointer to Core storage.
  • On POSIX, when a signal installed without SA_RESTART interrupts the wait of zlink_poll or zlink_poller_wait, the call does not fail with EINTR: it returns the event if readiness occurs within the remaining timeout, and otherwise returns 0 after the original timeout elapses.

Completion polling

  • Wait returns ZLINK_POLLCOMPLETION while at least one completion record exists; calls to wait, add, modify, or remove alone do not reduce the queue.
  • After a DONTWAIT submit returns a wait token with ZLINK_SUBMIT_BACKPRESSURED, credit on that target enqueues one WRITABLE record, and ZLINK_POLLOUT and ZLINK_POLLCOMPLETION both remain true until that record is read. A credit or attach edge concurrent with the refusal is also observed as a WRITABLE record.
  • Receiving the last record with DONTWAIT completion receive clears readiness; readiness remains set while records remain.
  • Completions are neither lost nor merged when their count exceeds event-array capacity; the caller drains each ready socket until ZLINK_RECV_NO_DATA.
  • If the physical head on a DEALER-ROUTER connection is multipart DATA, ZLINK_POLLIN can be ready while ZLINK_POLLCOMPLETION for a following REPLY is not ready. Completion readiness is raised after the DATA record is dequeued and the REPLY moves to the socket-local completion queue.

Lifetime

  • Destroying a poller while a wait is active returns ZLINK_CLOSE_BUSY/EBUSY.
  • While a wait is active on a poller, size, add, modify and remove (including the _fd and _timer variants) and wait fail with ZLINK_CONFIG_BUSY/EBUSY, and registrations remain unchanged.

Poller function returns and outputs

  • Allocation failure in zlink_poller_new returns NULL/ENOMEM, and successful zlink_poller_destroy sets the caller pointer to NULL.
  • zlink_poller_size returns the registration count or -1 on failure.
  • zlink_poller_wait returns an event count, 0 on timeout, and -1 on failure; events == NULL or event_capacity <= 0 returns EINVAL.
  • zlink_poller_wait on a poller where no registration has non-zero events, or every such registration is a closed socket source, does not wait regardless of timeout_ms, and returns any POLLERR not yet returned, otherwise 0.
  • The error_out parameters of zlink_poll, zlink_poller_size, and zlink_poller_wait are optional outputs that may be NULL.

Core Spec Index | Previous: Events | Next: Monitoring