English | 한국어
Core 스펙 목차 | 이전: Events | 다음: Monitoring
Polling¶
이 장이 정의하는 것 —
zlink_poll과zlink_poller_*API로 여러 socket과 source의 readiness를 기다리는 공개 계약.
1. Polling 개요¶
이 문서는 ZLink Core의 readiness 공개 계약을 정의한다. source가 receive 또는 send를
진행할 가치가 있는 상태를 readiness라 하며, application은 raw socket, OS file
descriptor와 generic timer 세 종류의 source를 하나의 event loop에서 함께 기다릴 수 있다.
대상 독자는 이 계약을 C API와 각 언어 binding으로 옮기는 개발자다. 이 문서는 “각
source의 POLLIN과 POLLOUT, single-consumer receive mode와 lifetime은 무엇인가?”에
답한다.
관련 계약의 소유 문서는 다음과 같다.
| 관련 계약 | 정의하는 문서 |
|---|---|
| event family 구분과 readiness 의미의 경계 | Events |
socket type별 ZLINK_POLLIN·ZLINK_POLLOUT의 구체 의미 |
Socket — DEALER, Socket — ROUTER |
| result와 errno 대응 | Errors |
generic timer의 생성·수신 (zlink_timer_*) |
유틸리티 |
2. 일회성 poll과 재사용 poller¶
readiness를 기다리는 방법은 두 가지다.
- 일회성 poll —
zlink_poll은 매 호출마다 item 배열로 대상을 전달받아 한 번 기다린다. - 재사용 poller —
zlink_poller_*함수는 poller 객체에 source를 등록해 두고zlink_poller_wait로 반복해서 기다린다.
두 방법 모두 signal이 대기를 중단하면 Core가 남은 timeout으로 대기를 이어 가므로, zlink_poll과
zlink_poller_wait는 EINTR로 실패하지 않는다.
3. Source 종류와 readiness¶
각 source 종류가 ZLINK_POLLIN과 ZLINK_POLLOUT으로 알리는 readiness는 다음과 같다.
| Source | POLLIN |
POLLOUT |
추가 readiness·규칙 |
|---|---|---|---|
| raw socket | complete record를 수신할 수 있음 | socket 전체 송신 readiness 또는 읽지 않은 ZLINK_COMPLETION_WRITABLE record가 있음. 그 record가 있는 동안 level로 유지 |
socket별 receive mode 적용. close된 등록 socket은 ZLINK_POLLERR 1회 |
| socket monitor | monitor event를 받을 수 있음 | 미지원 | zlink_socket_monitor_recv()로 drain. monitor handle은 raw socket과 같은 등록 함수로 poller에 넣는다(socket/README §"application에 알리는 경로") |
| timer | fire count를 받을 수 있음 | 미지원 | zlink_timer_recv()로 drain |
| FD | platform readable | platform writable | platform POLLPRI는 ZLINK_POLLPRI, 그 밖의 platform 오류 bit는 ZLINK_POLLERR로 변환 |
여러 peer를 가진 raw socket의 ZLINK_POLLOUT은 socket 전체의 집계 readiness다.
이 event는 writable해진 routing ID나 transport pair를 식별하지 않으며, 다른 peer의
여유 때문에 설 수 있다. 따라서 특정 target의 nonblocking submit이
backpressure(downstream이 처리 속도를 따라오지 못할 때 sender의
추가 제출을 제한하는 동작)를 반환한 뒤 ZLINK_POLLOUT을 관측해도 그 target의 다음 submit
성공은 보장되지 않는다.
대기 토큰의 target별 결과는 ZLINK_POLLOUT bit가 아니라
ZLINK_COMPLETION_WRITABLE record로 전달한다. ZLINK_SEND_FLAGS_DONTWAIT submit이
ZLINK_SUBMIT_BACKPRESSURED를 반환하면 completion_id_out의 0이 아닌 값이 wait token이고,
그 제출을 거절한 자원이 회복되면(wake 조건은 socket README가
소유) Core는 같은 token, 같은 user_context, 그리고
ROUTER·STREAM이면 제출한 RID를 담은 WRITABLE record 하나를 socket-local completion queue에
넣는다. Application은 이 record를 zlink_completion_recv()로 꺼내 token·context·RID로 원래 제출을
식별하고, send_result에 따른 재제출 여부는 소켓 공통을 따른다. 이 record가 읽히지 않은 동안 ZLINK_POLLOUT과
ZLINK_POLLCOMPLETION은 모두 참으로 유지된다.
ZLINK_POLLITEMS_DFLT는 내부·application stack buffer의 권장 초기 item 수이며
readiness bit가 아니다. ZLINK_HAVE_POLLER == 1은 이 public poller API가 build에
포함되었음을 뜻한다.
Readiness는 level-trigger이므로 wake도 level에 따른다. 등록한 source의 readiness가 거짓에서
참으로 바뀌면, 그 source로 zlink_poller_wait() 또는 zlink_poll()에서 대기 중인 caller는
timeout이 남아 있어도 그 시점에 깨어난다. 그 전이를 만든 command를 caller가 아닌
thread(I/O thread, async command owner, 임시 transport owner, 같은 socket에 send·recv를 부른
다른 application thread)가 처리했더라도 이 보장은 같다.
Readiness가 참인데 caller가 timeout까지 잠드는 것(lost wake)은 계약 위반이다. 이를 위해 public
poller의 notification descriptor는 그 poller만 소비한다. Poller가 아닌 곳에서 적용한 command가
readiness를 바꾸면 descriptor에 알리는 시점은
동기화 모델의 깨어남 규칙을 따른다.
Wait token에도 lost wake는 없다. DONTWAIT submit이 거절된 뒤 token을 등록하는 사이에 그 target의 credit 회복이나 pipe attach가 동시에 일어났다면, 구현은 token을 등록한 뒤 target 상태를 다시 확인해(register → recheck) 그 edge에 대한 WRITABLE record를 게시한다. 따라서 거절 이후에 생긴 credit·attach edge로 WRITABLE record가 유실되지 않는다.
4. Completion polling¶
ZLINK_POLLCOMPLETION은 PAIR·DEALER·ROUTER·STREAM의 socket-local completion queue에
record가 하나 이상 있어 다음 zlink_completion_recv()가 성공할 수 있음을 알리는
level-triggered readiness다. Queue에 들어오는 record 종류는 ZLINK_COMPLETION_REQUEST와
ZLINK_COMPLETION_WRITABLE 두 가지다. 성공한 SEND는 record를 만들지 않으며,
ZLINK_COMPLETION_SEND는 ABI 호환을 위해 enum에 남아 있을 뿐 게시되지 않는다. 단독으로
등록하거나 ZLINK_POLLIN, ZLINK_POLLOUT과 OR할 수 있다. Queue에 record가 남아 있는 동안
readiness도 유지된다.
DEALER-ROUTER single connection에서 앞선 DATA record를 dequeue하기 전에는 뒤의
REPLY가 physical head가 아니다. 이때 ZLINK_POLLIN만 준비되고 ZLINK_POLLCOMPLETION은
준비되지 않을 수 있다. REPLY가 physical head에 도달해 socket-local completion queue로 이동한
뒤에는 위 level-trigger와 ZLINK_RECV_NO_DATA까지 drain하는 규칙을 적용한다.
ROUTER는 zlink_poll()과 poller 모두에서 ZLINK_POLLROUTE를 쓸 수 있다. RID별 선택 route가 바뀌면
준비되는 level readiness이며, 준비·해제 규칙은 ROUTER §10.1이
정한다. ROUTER가 아닌 source와 close된 source는 지원하지 않는 event와 close된 source의 기존 규칙을 따른다.
zlink_poller_wait()는 completion을 제거하거나 callback을 호출하지 않는다. Event array에는
operation payload를 넣지 않으며 event array 용량과 completion 개수는 관계가 없다. Caller는
준비된 socket마다 zlink_completion_recv(..., ZLINK_RECV_FLAGS_DONTWAIT)를
ZLINK_RECV_NO_DATA까지 반복해 queue를 비운다. Add·modify·remove도 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: zlink_poller_wait() 호출
P-->>App: POLLCOMPLETION readiness 반환
loop NO_DATA가 나올 때까지
App->>S: zlink_completion_recv(DONTWAIT)
S-->>App: REQUEST 또는 WRITABLE record 한 건
end
zlink_poller_add()와 zlink_poller_modify()는 지원 socket에 completion bit를 추가하거나
제거할 수 있다. 다른 source나 zlink_poll() item에서 이 bit를 사용하면
ZLINK_CONFIG_INVALID_ARGUMENT, errno == EINVAL이다.
같은 socket은 서로 다른 poller에 동시에 등록할 수 있다 — 등록마다 독립된 readiness wake 경로와
lifetime pin을 가진다. 예외는 completion bit 하나다: 한 socket의 completion bit를 소유하는
poller registration은 최대 하나다. 다른 poller가 이미
소유한 socket에 bit를 add하거나 modify로 추가하면 ZLINK_CONFIG_INVALID_STATE,
errno == EBUSY로 실패하고 기존 registration은 변하지 않는다. 기존 owner가 modify로 bit를
제거하거나 registration을 remove하면 다른 poller가 소유할 수 있으며, 전환 중 queue record와
readiness는 유실되지 않는다. Application은 socket마다 completion drain owner를 하나만 둔다.
한 socket의 transport completion 진행은 직렬화된 drain 경로 하나를 사용한다. Completion poller를 등록해도 같은 consumer가 zlink_completion_recv(NONE)을 직접 호출할 수 있다. 이 호출은 별도 zlink_poller_wait() 없이 RCVTIMEO 안에서 completion 진행과 대기를 수행한다. DONTWAIT receive는 이미 게시된 public queue만 소비하며 새 transport drain turn을 시작하지 않는다.
5. Source 수명과 직렬화¶
poller에 socket source를 등록하면 Core가 그 socket의 lifetime pin을 획득한다.
따라서 application이 poller에서 remove하기 전에 등록된 socket을 close해도
안전하다. close된 socket source는 POLLERR를 한 번 반환하고, 해당 등록과
lifetime pin은 remove할 때까지 유지된다.
zlink_ctx_shutdown이 호출되면, 그 context에 속한 socket source(monitor handle 포함)를 close하지 않은 채
등록한 poller의 wait는 진행 중인 wait를 포함해 timeout을 기다리지 않고 event 없이
ZLINK_CONFIG_INTERNAL_ERROR(ETERM)로 끝난다. 그 source를
remove하거나 close할 때까지 이후 wait도 같다. 그 socket의 읽지 않은 completion은
completion 종결 규칙을 따른다. poller는 context에 속하지
않고 fd·timer source는 shutdown의 영향을 받지 않으므로, 그런 socket source가 없는 poller의 wait는 event나
timeout까지 계속된다.
poller 하나의 size, add, modify, remove와 wait는 Core가 직렬화한다. wait가 active인 poller에 대한 size,
add·modify·remove(_fd·_timer 변형 포함)와 wait는 registration을 바꾸지 않고 ZLINK_CONFIG_BUSY/EBUSY로
실패한다. 서로 다른 poller는 동시에 사용할 수 있다. wait가 반환한 event array는 caller-owned이며 Core 내부
pointer를 포함하지 않는다.
6. 공개 타입¶
#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 진행 가능 (source별 의미는 §3)
ZLINK_POLLOUT = 2, // send/submit 재시도 가치 (source별 의미는 §3)
ZLINK_POLLERR = 4, // socket close 또는 FD platform 오류 (§3, §5)
ZLINK_POLLPRI = 8, // FD의 platform POLLPRI (§3)
ZLINK_POLLITEMS_DFLT = 16, // 권장 초기 item 수. readiness bit가 아니다 (§3)
ZLINK_POLLCOMPLETION = 32, // socket completion queue readiness (§4)
ZLINK_POLLROUTE = 64 // ROUTER 선택 route 변경 (ROUTER §10.1)
} zlink_poller_event_flag_e;
#define ZLINK_HAVE_POLLER 1 // public poller API가 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; // SOCKET source일 때만 유효
zlink_fd_t fd; // FD source일 때만 유효
short events; // 기다릴 event bit
short revents; // 반환된 readiness. zlink_poll이 진입 시 0으로 지운다 (§7 zlink_poll)
} zlink_pollitem_t;
typedef struct zlink_poller_event_t {
zlink_poller_source_kind_t source_kind; // 이 event의 source 종류
void *socket; // SOCKET source일 때만 유효
zlink_fd_t fd; // FD source일 때만 유효
void *timer; // TIMER source일 때만 유효
void *user_data; // 등록 시 받은 pointer를 그대로 돌려주는 borrowed value
short events; // 관측된 readiness bit
} zlink_poller_event_t;
7. 함수¶
zlink_poll¶
item 배열의 readiness를 한 번 기다린다.
ZLINK_EXPORT int zlink_poll(
zlink_pollitem_t *items,
int item_count,
long timeout_ms,
zlink_config_result_t *error_out);
return은 readiness가 있는 item 수, timeout은 0, 실패는 -1이다. 실패하면 error_out과
errno를 함께 설정한다. timeout_ms < 0은 무기한, 0은 즉시 반환한다. item_count == 0이거나
모든 item의 events가 0이면 timeout 값과 관계없이 즉시 0/ZLINK_CONFIG_OK를 반환한다. 함수는 기다리기 전에 모든 item의
revents를 0으로 지우므로 호출자가 미리 초기화할 필요가 없고, 반환 뒤의 snapshot만 유효하다.
error_out은 NULL을 허용하는 선택 output이다.
Poller 함수¶
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);
zlink_poller_new()는 성공 시 새 poller를 반환하고, allocation이 실패하면
NULL을 반환하고 errno를 ENOMEM으로 설정한다. zlink_poller_destroy()가
성공하면 caller가 제공한 pointer를 NULL로 설정한다.
zlink_poller_size()는 성공 시 현재 등록 count를, 실패 시 -1을 반환한다.
zlink_poller_wait()는 성공 시 기록한 event 수를, timeout이면 0, 실패하면
-1을 반환한다. timeout_ms < 0은 모두 무기한 대기로 정규화한다. events == NULL이거나
event_capacity <= 0이면 EINVAL로 실패한다. events가 0이 아닌 등록이 없거나 그런 등록이 모두 close된 socket source이면 준비될 source가
없으므로 timeout_ms와 관계없이 기다리지 않는다. 이때 아직 반환하지 않은 POLLERR(§5)가
있으면 그 event를, 없으면 0을 반환한다(zlink_poll의 item_count == 0과 같다). zlink_poller_size()와 zlink_poller_wait()의 error_out은 NULL을
허용하는 선택 output이다.
같은 source를 두 번 add하면 ZLINK_CONFIG_CONFLICT/EEXIST다. timer는 한 번에 poller 하나에만
등록할 수 있다 — 다른 poller에 이미 등록된 timer를 add하면 ZLINK_CONFIG_INVALID_STATE/EBUSY로
실패하고, 등록된 timer를 zlink_timer_destroy()로 파괴하면 ZLINK_CLOSE_BUSY/EBUSY다. 없는 source의
modify·remove는 ZLINK_CONFIG_NOT_FOUND/ENOENT다. 잘못된 event bit는
ZLINK_CONFIG_INVALID_ARGUMENT/EINVAL, source가 지원하지 않는 event는
ZLINK_CONFIG_NOT_SUPPORTED/ENOTSUP이다. poller destroy 중 wait가 active이면
ZLINK_CLOSE_BUSY/EBUSY다. result와 errno의 전체 대응은
errno map을 따른다.
8. 내부 구조¶
이 절의 계약 소유 — completion polling의 공개 계약은 이 문서의 Completion polling 절과 검증 요구 절이 소유한다. 이 절은 그 계약을 내부에서 어떻게 달성하는지 설명한다.
REQUEST resolver와 wait token의 WRITABLE publish는 같은 socket-local ready queue에 결과를 append한다. 이 append의 linearization 순서가 public receive 순서이며 submit 순서나 target별 wire 순서를 뜻하지 않는다.
9. 구현 및 contract test 검증 요구¶
공개 표면(zlink_poll·zlink_poller_*·zlink_completion_recv 함수, 반환값·errno와 event
array)만으로 다음을 확인한다. 각 항목은 unit test 하나로 이어진다.
zlink_poll
- readiness가 있는 item 수를 반환하고, timeout이면 0, 실패하면 -1을 반환하며 error_out과 errno를 함께 설정한다.
- timeout_ms == -1은 무기한 대기하고, 0은 즉시 반환한다.
- 호출 전 0으로 초기화한 revents는 함수 반환 뒤의 snapshot만 유효하다.
poller 등록
- 같은 source를 두 번 add하면 ZLINK_CONFIG_CONFLICT/EEXIST다.
- 다른 poller에 이미 등록된 timer를 add하면 ZLINK_CONFIG_INVALID_STATE/EBUSY이고, 등록된 timer를 zlink_timer_destroy()로 파괴하면 ZLINK_CLOSE_BUSY/EBUSY다.
- 같은 socket을 두 poller에 readiness bit(completion 제외)로 등록하면 둘 다 성공하고 각각 독립적으로 readiness를 보고한다.
- 없는 source를 modify·remove하면 ZLINK_CONFIG_NOT_FOUND/ENOENT다.
- 잘못된 event bit는 ZLINK_CONFIG_INVALID_ARGUMENT/EINVAL이고, source가 지원하지 않는 event는 ZLINK_CONFIG_NOT_SUPPORTED/ENOTSUP이다.
- ZLINK_POLLCOMPLETION은 PAIR·DEALER·ROUTER·STREAM의 add·modify에서 단독 또는 다른 socket
readiness와 OR해 추가·제거할 수 있다. 다른 source와 zlink_poll() item은
ZLINK_CONFIG_INVALID_ARGUMENT/EINVAL이다. 이 bit가 알리는 record는 REQUEST와 WRITABLE
두 종류다.
- 두 poller가 같은 socket의 completion bit를 소유하려 하면 두 번째 add·modify가
ZLINK_CONFIG_INVALID_STATE/EBUSY로 실패하고 기존 registration은 변하지 않는다. 기존
owner가 bit를 제거하거나 source를 remove한 뒤 다른 poller로 이전하면 queue record와
readiness가 유실되지 않는다.
wait와 event
- socket source 등록은 lifetime pin을 획득하므로 socket을 remove 전에 close해도 안전하다. close 후 POLLERR를 한 번 반환하고 등록은 remove할 때까지 유지된다.
- FD의 platform POLLPRI는 ZLINK_POLLPRI로, 그 밖의 platform 오류 bit는 ZLINK_POLLERR로 변환된다.
- event의 socket·fd·timer field는 각각 SOCKET·FD·TIMER source에서만 유효하고, user_data는 등록 시 받은 pointer를 그대로 돌려준다.
- wait가 반환한 event array는 caller-owned이며 Core 내부 pointer를 포함하지 않는다.
- POSIX에서 SA_RESTART 없이 설치한 signal이 zlink_poll·zlink_poller_wait의 대기를 중단해도 호출은 EINTR로 실패하지 않는다. 남은 timeout 안에 readiness가 생기면 그 event를 반환하고, 생기지 않으면 원래 timeout이 지난 뒤 0을 반환한다.
completion polling
- Completion record가 하나 이상 있으면 wait가 ZLINK_POLLCOMPLETION을 반환하며, wait·add·modify·
remove만 호출해서는 queue가 줄지 않는다.
- DONTWAIT submit이 ZLINK_SUBMIT_BACKPRESSURED로 wait token을 반환한 뒤 그 target에 credit이
생기면 WRITABLE record 하나가 queue에 들어오고, 그 record를 읽기 전까지 ZLINK_POLLOUT과
ZLINK_POLLCOMPLETION이 함께 참으로 유지된다. 거절과 동시에 일어난 credit·attach edge도
WRITABLE record로 관측된다.
- DONTWAIT completion receive로 마지막 record를 꺼내면 readiness가 해제되고, record가 남아 있으면
readiness가 유지된다.
- Event array 용량보다 completion이 많아도 record가 유실·병합되지 않으며, caller는 준비된 socket을
ZLINK_RECV_NO_DATA까지 drain한다.
- DEALER-ROUTER의 physical head가 multipart DATA이면 ZLINK_POLLIN이 준비될 수 있지만 그 뒤
REPLY의 ZLINK_POLLCOMPLETION은 준비되지 않는다. DATA record를 dequeue한 뒤 REPLY가
socket-local completion queue로 이동하면 completion readiness가 발생한다.
수명
- poller destroy 중 wait가 active이면 ZLINK_CLOSE_BUSY/EBUSY다.
- wait가 active인 poller에 대한 size, add·modify·remove(_fd·_timer 변형 포함)와 wait는 ZLINK_CONFIG_BUSY/EBUSY로 실패하고 registration은 변하지 않는다.
poller 함수 반환과 output
- zlink_poller_new의 allocation 실패는 NULL/ENOMEM이고, zlink_poller_destroy가 성공하면 caller pointer가 NULL이 된다.
- zlink_poller_size는 등록 count 또는 실패 -1을 반환한다.
- zlink_poller_wait는 event count, timeout 0, 실패 -1을 반환하며 events == NULL 또는 event_capacity <= 0은 EINVAL이다.
- events가 0이 아닌 등록이 없거나 그런 등록이 모두 close된 socket source인 poller의 zlink_poller_wait는 timeout_ms와 관계없이 기다리지 않고, 아직 반환하지 않은 POLLERR가 있으면 그 event를, 없으면 0을 반환한다.
- zlink_poll·zlink_poller_size·zlink_poller_wait의 error_out은 NULL을 허용하는 선택 output이다.