jetstream-api 0.13.0__tar.gz → 0.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/PKG-INFO +31 -1
  2. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/README.md +30 -0
  3. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/__init__.py +1 -1
  4. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/connectivity.py +82 -13
  5. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/jetstream_api.egg-info/PKG-INFO +31 -1
  6. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/pyproject.toml +1 -1
  7. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/LICENSE.txt +0 -0
  8. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/_codec.py +0 -0
  9. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/admin.py +0 -0
  10. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/admintopic.py +0 -0
  11. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/common.py +0 -0
  12. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/exception.py +0 -0
  13. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/msg.py +0 -0
  14. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/partitioner.py +0 -0
  15. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/pattern.py +0 -0
  16. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/producer.py +0 -0
  17. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/properties.py +0 -0
  18. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/py.typed +0 -0
  19. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/qmgr.py +0 -0
  20. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/topic.py +0 -0
  21. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/ilink/util.py +0 -0
  22. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/jetstream_api.egg-info/SOURCES.txt +0 -0
  23. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/jetstream_api.egg-info/dependency_links.txt +0 -0
  24. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/jetstream_api.egg-info/requires.txt +0 -0
  25. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/jetstream_api.egg-info/top_level.txt +0 -0
  26. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/setup.cfg +0 -0
  27. {jetstream_api-0.13.0 → jetstream_api-0.14.0}/tests/test_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Python client API for iLink M.O.M. (Message Oriented Middleware)
5
5
  Author: snowjeans
6
6
  License: Boost Software License - Version 1.0 - August 17th, 2003
@@ -60,6 +60,36 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.14.0 변경 - 소켓 keepalive 탐침 타이머, NIO 커넥터 수신 기한 정합 (자바 v2.10.0 과 같음)
64
+
65
+ - **연결 소켓에 keepalive 탐침 타이머를 겁니다(유휴 60초 + 10초 간격 x 6회).** 엔진이 쓰는 값과 같습니다.
66
+ 지금까지는 `SO_KEEPALIVE` 만 켜고 타이머는 운영체제 기본에 맡겼습니다(리눅스 약 2시간 11분).
67
+ - **얻는 것은 주로 NAT · 방화벽 유휴 만료 방지입니다.** 60초마다 탐침이 흐르므로 중간 장비가 연결 상태를 지우지
68
+ 않습니다. 조용한 발행자가 밤새 붙어 있는 배치에서 제일 자주 걸립니다.
69
+ - 상대가 사라진 연결을 커널이 약 2분에 끊어 주는 것은 덤입니다. 다만 **라이브러리가 그것을 감지해 스스로 다시
70
+ 붙지는 않습니다** - 실패는 다음 요청에서 드러나고, 그건 이미 세션 타임아웃(기본 30초)이 덮습니다.
71
+ - **응용 요청이 아니라 커널 탐침입니다.** 엔진의 수신 타임아웃(`SOCKTIMEOUTRCV`)은 요청을 읽는 시계라
72
+ 탐침으로는 갱신되지 않습니다. 조용한 세션은 그 기한을 향해 그대로 늙습니다.
73
+ - **타이머를 걸지 못해도 연결은 그대로 씁니다.** 그때는 운영체제 기본 타이머로 동작합니다.
74
+ 결과는 `ilink.connectivity.keepalive_timer_state` 로 볼 수 있습니다(진단용).
75
+ - **NIO 커넥터(`TCPConnectorNIO`)의 수신 기한을 나머지와 같은 규칙으로 맞췄습니다.** 기한이 프레임 전체가 아니라
76
+ **소켓 읽기 하나마다**이고, `0` 이하면 끝없이 기다립니다. 0.13.0 까지는 헤더와 본문이 한 기한을 나눠 썼습니다.
77
+ - 송신 쪽은 전처럼 기한 하나를 씁니다. 조금씩만 나아가는 송신이 끝나지 않던 것을 0.7.4 에서 막은 것입니다.
78
+ - 이 커넥터는 `set_conn` 으로 직접 갈아 끼울 때만 쓰입니다. 기본 경로는 달라진 것이 없습니다.
79
+
80
+ ### keepalive 타이머가 실제로 걸리는 환경
81
+
82
+ 자바 클라이언트는 걸 수 있는 자리가 좁습니다. `TCP_KEEPIDLE` 계열이 `jdk.net.ExtendedSocketOptions`(자바 11 이상)에만
83
+ 있고, 윈도우는 그 판에서도 받지 않기 때문입니다.
84
+
85
+ | | 자바 8 | 자바 11 이상 |
86
+ |---|---|---|
87
+ | 윈도우 | 걸리지 않음 | **걸리지 않음** |
88
+ | 리눅스 · macOS | 걸리지 않음 | 걸림 |
89
+
90
+ **파이썬 클라이언트는 전 플랫폼에서 걸립니다.** 윈도우는 탐침 횟수가 운영체제 고정값(10회)이라 약 160초,
91
+ 그 밖은 60초 + 10초 x 6 = 약 2분입니다.
92
+
63
93
  ## 0.13.0 변경 - 세션 타임아웃 기본값 30초, acks 값 검사, 수신 기한 수정 (자바 v2.9.0 과 같음)
64
94
 
65
95
  - **세션 타임아웃 기본값이 경로마다 달라집니다.** 리더가 얼어붙어 응답이 오지 않으면 0.12.0 까지는 최대 10분을 매달렸습니다.
@@ -10,6 +10,36 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
10
10
  ## 설치 방법
11
11
  pip install jetstream-api
12
12
 
13
+ ## 0.14.0 변경 - 소켓 keepalive 탐침 타이머, NIO 커넥터 수신 기한 정합 (자바 v2.10.0 과 같음)
14
+
15
+ - **연결 소켓에 keepalive 탐침 타이머를 겁니다(유휴 60초 + 10초 간격 x 6회).** 엔진이 쓰는 값과 같습니다.
16
+ 지금까지는 `SO_KEEPALIVE` 만 켜고 타이머는 운영체제 기본에 맡겼습니다(리눅스 약 2시간 11분).
17
+ - **얻는 것은 주로 NAT · 방화벽 유휴 만료 방지입니다.** 60초마다 탐침이 흐르므로 중간 장비가 연결 상태를 지우지
18
+ 않습니다. 조용한 발행자가 밤새 붙어 있는 배치에서 제일 자주 걸립니다.
19
+ - 상대가 사라진 연결을 커널이 약 2분에 끊어 주는 것은 덤입니다. 다만 **라이브러리가 그것을 감지해 스스로 다시
20
+ 붙지는 않습니다** - 실패는 다음 요청에서 드러나고, 그건 이미 세션 타임아웃(기본 30초)이 덮습니다.
21
+ - **응용 요청이 아니라 커널 탐침입니다.** 엔진의 수신 타임아웃(`SOCKTIMEOUTRCV`)은 요청을 읽는 시계라
22
+ 탐침으로는 갱신되지 않습니다. 조용한 세션은 그 기한을 향해 그대로 늙습니다.
23
+ - **타이머를 걸지 못해도 연결은 그대로 씁니다.** 그때는 운영체제 기본 타이머로 동작합니다.
24
+ 결과는 `ilink.connectivity.keepalive_timer_state` 로 볼 수 있습니다(진단용).
25
+ - **NIO 커넥터(`TCPConnectorNIO`)의 수신 기한을 나머지와 같은 규칙으로 맞췄습니다.** 기한이 프레임 전체가 아니라
26
+ **소켓 읽기 하나마다**이고, `0` 이하면 끝없이 기다립니다. 0.13.0 까지는 헤더와 본문이 한 기한을 나눠 썼습니다.
27
+ - 송신 쪽은 전처럼 기한 하나를 씁니다. 조금씩만 나아가는 송신이 끝나지 않던 것을 0.7.4 에서 막은 것입니다.
28
+ - 이 커넥터는 `set_conn` 으로 직접 갈아 끼울 때만 쓰입니다. 기본 경로는 달라진 것이 없습니다.
29
+
30
+ ### keepalive 타이머가 실제로 걸리는 환경
31
+
32
+ 자바 클라이언트는 걸 수 있는 자리가 좁습니다. `TCP_KEEPIDLE` 계열이 `jdk.net.ExtendedSocketOptions`(자바 11 이상)에만
33
+ 있고, 윈도우는 그 판에서도 받지 않기 때문입니다.
34
+
35
+ | | 자바 8 | 자바 11 이상 |
36
+ |---|---|---|
37
+ | 윈도우 | 걸리지 않음 | **걸리지 않음** |
38
+ | 리눅스 · macOS | 걸리지 않음 | 걸림 |
39
+
40
+ **파이썬 클라이언트는 전 플랫폼에서 걸립니다.** 윈도우는 탐침 횟수가 운영체제 고정값(10회)이라 약 160초,
41
+ 그 밖은 60초 + 10초 x 6 = 약 2분입니다.
42
+
13
43
  ## 0.13.0 변경 - 세션 타임아웃 기본값 30초, acks 값 검사, 수신 기한 수정 (자바 v2.9.0 과 같음)
14
44
 
15
45
  - **세션 타임아웃 기본값이 경로마다 달라집니다.** 리더가 얼어붙어 응답이 오지 않으면 0.12.0 까지는 최대 10분을 매달렸습니다.
@@ -1,6 +1,6 @@
1
1
  """iLink - Python client API for iLink M.O.M. (Message Oriented Middleware)."""
2
2
 
3
- __version__ = "0.13.0"
3
+ __version__ = "0.14.0"
4
4
 
5
5
  # --- Admin / Service ---
6
6
  from .admin import ILAdminService, ILAdminQmgr, ILLogBrowser, ILQueueBrowser
@@ -167,6 +167,10 @@ _TAG_BODY = struct.Struct(">Hq")
167
167
  _TAG_FRAME_HEAD = bytes(ILMsg(ILC.MIMQ_REQ_TAG_MESSAGE, ILReqTagMsg(0)).to_byte_array(False)[:ILC.SZ_IL_HEADER])
168
168
  _RX_CHUNK = 65536
169
169
 
170
+ #: NIO 커넥터가 "끝없이 기다린다"(timeout 0 이하)를 나눠 기다리는 한 조각(밀리초). 셀렉터 대기에 무한을 그대로 줄 수
171
+ #: 없어 이 길이로 끊어 다시 기다린다 - 판정에는 영향이 없다
172
+ _NIO_WAIT_SLICE_MS = 30000
173
+
170
174
  #: 데이터 경로(큐 관리자 / 큐 / 토픽)의 응답 수신 · 접속 대기 기본값(밀리초). 리더가 얼어붙어 응답이 오지 않을 때
171
175
  #: 매달리는 시간이다. 0.12.0 까지는 600000(10분)이었다. 관리 경로는 엔진이 오래 붙드는 요청이 있어
172
176
  #: :class:`~ilink.admin.ILAdminService` 가 제 값을 따로 건다
@@ -181,6 +185,55 @@ def _tmo_sec(timeout_ms: int) -> float | None:
181
185
  return None if timeout_ms <= 0 else timeout_ms / 1000.0
182
186
 
183
187
 
188
+ #: keepalive 탐침 타이머(초). 엔진과 같은 값이다(MIMQMainConstants.h 의 60 / 10 / 6). 설정이 아니라 안전망이라
189
+ #: 바꿀 수 있게 열어 두지 않는다 - 양쪽이 다르면 누가 먼저 끊었는지 나중에 가릴 수 없다
190
+ _KEEPALIVE_IDLE_SEC = 60
191
+ _KEEPALIVE_INTVL_SEC = 10
192
+ _KEEPALIVE_CNT = 6
193
+
194
+ #: :func:`_apply_keepalive` 가 마지막으로 한 일 - ``"posix"`` / ``"windows"`` / ``"unsupported"`` / ``"failed"``.
195
+ #: 진단용이다(라이브러리는 로그를 남기지 않는다).
196
+ keepalive_timer_state = "unset"
197
+
198
+
199
+ def _apply_keepalive(sock) -> None:
200
+ """이 소켓의 keepalive 탐침 타이머를 엔진과 같은 값으로 건다(SO_KEEPALIVE 는 부른 쪽이 이미 켰다).
201
+
202
+ ``SO_KEEPALIVE`` 만 켜면 운영체제 기본 타이머가 쓰인다(리눅스는 7200초 + 75초 x 9 = 약 2시간 11분). 그러면
203
+ 상대가 FIN 도 RST 도 남기지 못하고 사라졌을 때 그 시간까지 연결이 남고, 중간의 NAT · 방화벽이 먼저 유휴
204
+ 항목을 지워 버린다. 60초마다 탐침이 흐르면 그 두 가지가 같이 해결된다.
205
+
206
+ **실패해도 연결을 막지 않는다.** 매크로가 없는 플랫폼도 있고, 권한이나 커널 설정으로 거부될 수도 있다.
207
+ 그때는 운영체제 기본 타이머로 동작한다 - 결과는 :data:`keepalive_timer_state` 로 볼 수 있다.
208
+ """
209
+ global keepalive_timer_state
210
+ try:
211
+ if hasattr(socket, "SIO_KEEPALIVE_VALS"):
212
+ # 윈도우: 유휴 시간과 간격만 준다(횟수는 OS 고정 10회 -> 약 160초). onoff=1 이라 켜는 것도 겸한다
213
+ sock.ioctl(
214
+ socket.SIO_KEEPALIVE_VALS,
215
+ (1, _KEEPALIVE_IDLE_SEC * 1000, _KEEPALIVE_INTVL_SEC * 1000),
216
+ )
217
+ keepalive_timer_state = "windows"
218
+ return
219
+ done = False
220
+ idle_opt = getattr(socket, "TCP_KEEPIDLE", None) # 리눅스
221
+ if idle_opt is None:
222
+ idle_opt = getattr(socket, "TCP_KEEPALIVE", None) # macOS 는 이름이 다르다
223
+ if idle_opt is not None:
224
+ sock.setsockopt(socket.IPPROTO_TCP, idle_opt, _KEEPALIVE_IDLE_SEC)
225
+ done = True
226
+ if hasattr(socket, "TCP_KEEPINTVL"):
227
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, _KEEPALIVE_INTVL_SEC)
228
+ done = True
229
+ if hasattr(socket, "TCP_KEEPCNT"):
230
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, _KEEPALIVE_CNT)
231
+ done = True
232
+ keepalive_timer_state = "posix" if done else "unsupported"
233
+ except Exception: # noqa: BLE001 - 안전망이다. 못 걸어도 연결은 그대로 쓴다
234
+ keepalive_timer_state = "failed"
235
+
236
+
184
237
  # 0.11.0 K2: TCPConnector.send_deferred 가 쥐고 있을 프레임 하나 / 합의 상한(바이트). 넘으면 쥔 것과 함께 곧바로 보낸다
185
238
  _WPEND_FRAME_MAX = 65536
186
239
  _WPEND_TOTAL_MAX = 262144
@@ -563,6 +616,7 @@ class TCPConnector:
563
616
  self.socket.settimeout(_tmo_sec(timeout_ms))
564
617
  self._rx_tmo = _tmo_sec(timeout_ms)
565
618
  self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
619
+ _apply_keepalive(self.socket) # 0.14.0: OS 기본(약 2시간 11분) 대신 엔진과 같은 타이머
566
620
  self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
567
621
  self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
568
622
  self.socket.setsockopt(
@@ -957,6 +1011,7 @@ class TCPConnectorNIO:
957
1011
  self.sc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
958
1012
  self.sc.setblocking(False)
959
1013
  self.sc.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
1014
+ _apply_keepalive(self.sc) # 0.14.0: 블로킹 커넥터와 같다
960
1015
  self.sc.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
961
1016
  self.sc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
962
1017
  self.sc.setsockopt(
@@ -1070,9 +1125,13 @@ class TCPConnectorNIO:
1070
1125
  if sent == 0:
1071
1126
  try:
1072
1127
  self._wait_for_device_event("write", remain_ms)
1073
- except ILSessionException:
1074
- if time.monotonic() < deadline:
1075
- raise # 기한 전의 실패(연결 오류 등)는 그대로 올린다
1128
+ except ILSessionException as exc:
1129
+ # 0.14.0: 사유 코드로 가른다. 전에는 시각으로 갈라(monotonic() < deadline) 안쪽 대기가 기한보다
1130
+ # 아주 조금 먼저 깨면 - remain_ms 를 int 로 버리므로 1 ms 안쪽에서 그럴 수 있다 - 기한 초과를
1131
+ # 연결 오류로 잘못 보고 "written : [...]" 없는 사유를 올렸다(시험이 12회에 한 번 걸렸다)
1132
+ if exc.reason_code != ILRC.MIMQE_SESSION_TIMEOUT:
1133
+ raise # 연결 오류 등은 그대로 올린다
1134
+ # 기한 초과는 삼킨다 - 아래에서 보낸 양을 실은 사유로 올린다(_read_bytes 와 같은 규칙)
1076
1135
  if total < size:
1077
1136
  raise ILSessionException(
1078
1137
  ILRC.MIMQE_SESSION_TIMEOUT,
@@ -1095,14 +1154,16 @@ class TCPConnectorNIO:
1095
1154
  """메시지 하나를 받는다. 길이 헤더를 먼저 읽고 그만큼 채워 읽는다.
1096
1155
 
1097
1156
  Args:
1098
- timeout_ms (int): 수신 대기 시간(밀리초).
1157
+ timeout_ms (int): 수신 대기 시간(밀리초). **소켓 읽기 하나마다의 상한이고 프레임 전체의 기한이 아니다**
1158
+ - 응답이 조각으로 나뉘어 와도 다음 조각이 이 시간 안에 오면 계속 받는다.
1159
+ ``0`` 이하면 끝없이 기다린다.
1099
1160
 
1100
1161
  Returns:
1101
1162
  ILMsg: 받은 메시지.
1102
1163
 
1103
1164
  Raises:
1104
- ILSessionException: 연결이 없거나, 시간이 다했거나(``MIMQE_WAIT_TIMEOUT``),
1105
- 상대가 끊었거나, 구분자가 어긋났을 때(``MIMQE_INVALID_DELIMITER``).
1165
+ ILSessionException: 연결이 없거나, 시간이 다했거나(``MIMQE_SESSION_TIMEOUT``),
1166
+ 상대가 끊었거나(``MIMQE_NIO_ERROR``), 구분자가 어긋났을 때(``MIMQE_INVALID_DELIMITER``).
1106
1167
  **실패하면 소켓을 닫는다.**
1107
1168
  """
1108
1169
  try:
@@ -1111,10 +1172,10 @@ class TCPConnectorNIO:
1111
1172
  ILRC.MIMQE_NOT_CONNECTED,
1112
1173
  "connect() has not yet been called or disconnect() has been called.",
1113
1174
  )
1114
- deadline = time.monotonic() + timeout_ms / 1000.0 # 0.7.4: 헤더와 본문이 한 기한을 나눠 쓴다
1115
- ba_len = self._read_bytes(ILC.SZ_LEN, deadline, timeout_ms)
1175
+ # 0.14.0: 기한이 소켓 읽기 하나마다다(블로킹 커넥터와 같은 규칙). 0.13.0 까지는 헤더와 본문이 한 기한을 나눠 썼다
1176
+ ba_len = self._read_bytes(ILC.SZ_LEN, timeout_ms)
1116
1177
  data_len = _ba_pick_int(ba_len, 0, ILC.SZ_LEN) - 9
1117
- ba_data = self._read_bytes(data_len, deadline, timeout_ms)
1178
+ ba_data = self._read_bytes(data_len, timeout_ms)
1118
1179
  if ba_data[-1] != ord("\n"):
1119
1180
  raise ILSessionException(
1120
1181
  ILRC.MIMQE_INVALID_DELIMITER,
@@ -1198,10 +1259,11 @@ class TCPConnectorNIO:
1198
1259
  except Exception:
1199
1260
  pass
1200
1261
 
1201
- def _read_bytes(self, size: int, deadline: float, timeout_ms: int) -> bytes:
1202
- # 0.7.4: deadline 은 이 recv 의 기한(time.monotonic), timeout_ms 는 오류 문구에만 쓴다. 읽을 것이 있으면 기한이
1203
- # 지났어도 먼저 읽고, 기다려야 할 때만 남은 시간만큼 기다린다. 전에는 바퀴마다 int(ms) 를 빼서 1 ms 가 안
1204
- # 걸리는 바퀴가 이어지면 남은 시간이 줄지 않았다
1262
+ def _read_bytes(self, size: int, timeout_ms: int) -> bytes:
1263
+ # 0.14.0: 기한을 소켓 읽기 하나마다 준다(블로킹 커넥터 / 자바 setSoTimeout 과 같은 규칙) - 조각이 timeout_ms 안에
1264
+ # 이어지면 프레임 전체 시간이 그보다 길어도 계속 받는다. timeout_ms 가 0 이하면 끝없이 기다린다.
1265
+ # 송신 쪽(send_bytes)은 전처럼 기한 하나를 쓴다 - 조금씩만 나아가는 송신이 끝나지 않던 것을 0.7.4 에서 막은 것이다.
1266
+ # 읽을 것이 있으면 기한이 지났어도 먼저 읽고, 기다려야 할 때만 남은 시간만큼 기다린다
1205
1267
  sc = self.sc
1206
1268
  if sc is None:
1207
1269
  raise ILSessionException(
@@ -1213,6 +1275,9 @@ class TCPConnectorNIO:
1213
1275
  buf = bytearray(size)
1214
1276
  view = memoryview(buf)
1215
1277
  read_total = 0
1278
+ infinite = timeout_ms <= 0
1279
+ slice_ms = _NIO_WAIT_SLICE_MS if infinite else timeout_ms
1280
+ deadline = time.monotonic() + slice_ms / 1000.0
1216
1281
  while read_total < size:
1217
1282
  try:
1218
1283
  n = sc.recv_into(view[read_total:])
@@ -1226,9 +1291,13 @@ class TCPConnectorNIO:
1226
1291
  )
1227
1292
  if n > 0:
1228
1293
  read_total += n
1294
+ deadline = time.monotonic() + slice_ms / 1000.0 # 0.14.0: 읽기 하나마다 기한을 다시 준다
1229
1295
  continue
1230
1296
  remain_ms = (deadline - time.monotonic()) * 1000.0
1231
1297
  if remain_ms <= 0:
1298
+ if infinite:
1299
+ deadline = time.monotonic() + slice_ms / 1000.0
1300
+ continue
1232
1301
  break
1233
1302
  try:
1234
1303
  self._wait_for_device_event("read", max(int(remain_ms), 1))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Python client API for iLink M.O.M. (Message Oriented Middleware)
5
5
  Author: snowjeans
6
6
  License: Boost Software License - Version 1.0 - August 17th, 2003
@@ -60,6 +60,36 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.14.0 변경 - 소켓 keepalive 탐침 타이머, NIO 커넥터 수신 기한 정합 (자바 v2.10.0 과 같음)
64
+
65
+ - **연결 소켓에 keepalive 탐침 타이머를 겁니다(유휴 60초 + 10초 간격 x 6회).** 엔진이 쓰는 값과 같습니다.
66
+ 지금까지는 `SO_KEEPALIVE` 만 켜고 타이머는 운영체제 기본에 맡겼습니다(리눅스 약 2시간 11분).
67
+ - **얻는 것은 주로 NAT · 방화벽 유휴 만료 방지입니다.** 60초마다 탐침이 흐르므로 중간 장비가 연결 상태를 지우지
68
+ 않습니다. 조용한 발행자가 밤새 붙어 있는 배치에서 제일 자주 걸립니다.
69
+ - 상대가 사라진 연결을 커널이 약 2분에 끊어 주는 것은 덤입니다. 다만 **라이브러리가 그것을 감지해 스스로 다시
70
+ 붙지는 않습니다** - 실패는 다음 요청에서 드러나고, 그건 이미 세션 타임아웃(기본 30초)이 덮습니다.
71
+ - **응용 요청이 아니라 커널 탐침입니다.** 엔진의 수신 타임아웃(`SOCKTIMEOUTRCV`)은 요청을 읽는 시계라
72
+ 탐침으로는 갱신되지 않습니다. 조용한 세션은 그 기한을 향해 그대로 늙습니다.
73
+ - **타이머를 걸지 못해도 연결은 그대로 씁니다.** 그때는 운영체제 기본 타이머로 동작합니다.
74
+ 결과는 `ilink.connectivity.keepalive_timer_state` 로 볼 수 있습니다(진단용).
75
+ - **NIO 커넥터(`TCPConnectorNIO`)의 수신 기한을 나머지와 같은 규칙으로 맞췄습니다.** 기한이 프레임 전체가 아니라
76
+ **소켓 읽기 하나마다**이고, `0` 이하면 끝없이 기다립니다. 0.13.0 까지는 헤더와 본문이 한 기한을 나눠 썼습니다.
77
+ - 송신 쪽은 전처럼 기한 하나를 씁니다. 조금씩만 나아가는 송신이 끝나지 않던 것을 0.7.4 에서 막은 것입니다.
78
+ - 이 커넥터는 `set_conn` 으로 직접 갈아 끼울 때만 쓰입니다. 기본 경로는 달라진 것이 없습니다.
79
+
80
+ ### keepalive 타이머가 실제로 걸리는 환경
81
+
82
+ 자바 클라이언트는 걸 수 있는 자리가 좁습니다. `TCP_KEEPIDLE` 계열이 `jdk.net.ExtendedSocketOptions`(자바 11 이상)에만
83
+ 있고, 윈도우는 그 판에서도 받지 않기 때문입니다.
84
+
85
+ | | 자바 8 | 자바 11 이상 |
86
+ |---|---|---|
87
+ | 윈도우 | 걸리지 않음 | **걸리지 않음** |
88
+ | 리눅스 · macOS | 걸리지 않음 | 걸림 |
89
+
90
+ **파이썬 클라이언트는 전 플랫폼에서 걸립니다.** 윈도우는 탐침 횟수가 운영체제 고정값(10회)이라 약 160초,
91
+ 그 밖은 60초 + 10초 x 6 = 약 2분입니다.
92
+
63
93
  ## 0.13.0 변경 - 세션 타임아웃 기본값 30초, acks 값 검사, 수신 기한 수정 (자바 v2.9.0 과 같음)
64
94
 
65
95
  - **세션 타임아웃 기본값이 경로마다 달라집니다.** 리더가 얼어붙어 응답이 오지 않으면 0.12.0 까지는 최대 10분을 매달렸습니다.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jetstream-api"
7
- version = "0.13.0"
7
+ version = "0.14.0"
8
8
  description = "Python client API for iLink M.O.M. (Message Oriented Middleware)"
9
9
  authors = [{name = "snowjeans"}]
10
10
  readme = "README.md"
File without changes