한국어 | 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_pollandzlink_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_pollreceives 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 throughzlink_poller_wait.
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 | A submit retry is worthwhile (socket-wide aggregate). Level-held while an unread ZLINK_COMPLETION_WRITABLE 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 retry signal is the wait token's 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 uses the
token, context, and RID to decide which target to resubmit. 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 Core-internal thread (an I/O thread, the
async command owner, a temporary transport owner) instead of the caller. A caller that sleeps
until its timeout while readiness is true (a lost wake) is a contract violation; the
implementation keeps the guarantee by re-arming the public poller's notification descriptor
whenever an internal owner detaches or consumes commands on the socket's behalf. The same rule
applies to wait tokens. 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.
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.
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.
The caller serializes add, modify, remove, and wait on one poller. 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_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¶
zlink_poll¶
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 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. 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,
0on timeout, and-1on failure, setting botherror_outand errno on failure. timeout_ms == -1waits indefinitely, and0returns immediately.- A
reventsfield initialized to0before 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 withzlink_timer_destroy()returnsZLINK_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 returnsZLINK_CONFIG_NOT_SUPPORTED/ENOTSUP. ZLINK_POLLCOMPLETIONcan be added or removed by add or modify for PAIR, DEALER, ROUTER, and STREAM, alone or OR-ed with other socket readiness. Other sources andzlink_poll()items returnZLINK_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
POLLERRonce and remains registered until removal. - Platform
POLLPRIfor an FD maps toZLINK_POLLPRI; all other platform error bits map toZLINK_POLLERR. - The event fields
socket,fd, andtimerare valid only for SOCKET, FD, and TIMER sources, respectively, anduser_datareturns the pointer supplied at registration unchanged. - An event array returned by wait is caller-owned and contains no pointer to Core storage.
Completion polling
- Wait returns
ZLINK_POLLCOMPLETIONwhile 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, andZLINK_POLLOUTandZLINK_POLLCOMPLETIONboth 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_POLLINcan be ready whileZLINK_POLLCOMPLETIONfor 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.
Poller function returns and outputs
- Allocation failure in
zlink_poller_newreturnsNULL/ENOMEM, and successfulzlink_poller_destroysets the caller pointer to NULL. zlink_poller_sizereturns the registration count or-1on failure.zlink_poller_waitreturns an event count,0on timeout, and-1on failure;events == NULLorevent_capacity <= 0returnsEINVAL.- The
error_outparameters ofzlink_poll,zlink_poller_size, andzlink_poller_waitare optional outputs that may be NULL.
Caller serialization of add, modify, remove, and wait on one poller is a usage precondition (§5); concurrent use of different pollers is allowed.