콘텐츠로 이동

English | 한국어

Core 스펙 목차 | 이전: Events | 다음: Monitoring

Polling

이 장이 정의하는 것zlink_pollzlink_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의 POLLINPOLLOUT, 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를 기다리는 방법은 두 가지다.

  • 일회성 pollzlink_poll은 매 호출마다 item 배열로 대상을 전달받아 한 번 기다린다.
  • 재사용 pollerzlink_poller_* 함수는 poller 객체에 source를 등록해 두고 zlink_poller_wait로 반복해서 기다린다.

3. Source 종류와 readiness

각 source 종류가 ZLINK_POLLINZLINK_POLLOUT으로 알리는 readiness는 다음과 같다.

Source POLLIN POLLOUT 추가 readiness·규칙
raw socket complete record를 수신할 수 있음 submit 재시도 가치가 있음(socket 전체 집계). 읽지 않은 ZLINK_COMPLETION_WRITABLE 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 POLLPRIZLINK_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가 아니라 wait token의 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로 어느 target을 다시 submit할지 결정한다. 이 record가 읽히지 않은 동안 ZLINK_POLLOUTZLINK_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 대신 Core 내부 thread(I/O thread, async command owner, 임시 transport owner)가 처리했더라도 이 보장은 같다. Readiness가 참인데 caller가 timeout까지 잠드는 것(lost wake)은 계약 위반이며, 구현은 내부 owner가 detach하거나 command를 소비한 뒤 public poller의 notification descriptor를 다시 무장해 이를 지킨다. 같은 규칙이 wait token에도 적용된다. 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_REQUESTZLINK_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하는 규칙을 적용한다.

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를 하나만 둔다.

5. Source 수명과 직렬화

poller에 socket source를 등록하면 Core가 그 socket의 lifetime pin을 획득한다. 따라서 application이 poller에서 remove하기 전에 등록된 socket을 close해도 안전하다. close된 socket source는 POLLERR를 한 번 반환하고, 해당 등록과 lifetime pin은 remove할 때까지 유지된다.

poller 하나의 add, modify, remove와 wait는 caller가 직렬화한다. 서로 다른 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_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. 함수

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이면 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을 반환하고 errnoENOMEM으로 설정한다. 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로 실패한다. 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 POLLPRIZLINK_POLLPRI로, 그 밖의 platform 오류 bit는 ZLINK_POLLERR로 변환된다. - event의 socket·fd·timer field는 각각 SOCKET·FD·TIMER source에서만 유효하고, user_data는 등록 시 받은 pointer를 그대로 돌려준다. - wait가 반환한 event array는 caller-owned이며 Core 내부 pointer를 포함하지 않는다.

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_POLLOUTZLINK_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다.

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 <= 0EINVAL이다. - zlink_poll·zlink_poller_size·zlink_poller_waiterror_out은 NULL을 허용하는 선택 output이다.

poller 하나의 add·modify·remove와 wait를 caller가 직렬화하는 것은 caller의 사용 전제이며(§5), 서로 다른 poller의 동시 사용은 허용된다.

Core 스펙 목차 | 이전: Events | 다음: Monitoring