jetstream-api 0.20.3__tar.gz → 0.20.6__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 (30) hide show
  1. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/PKG-INFO +24 -1
  2. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/README.md +23 -0
  3. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/__init__.py +1 -1
  4. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/producer.py +147 -17
  5. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/qmgr.py +90 -24
  6. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/PKG-INFO +24 -1
  7. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/pyproject.toml +1 -1
  8. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/LICENSE.txt +0 -0
  9. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/_codec.py +0 -0
  10. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/_consumer.py +0 -0
  11. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/_groupwire.py +0 -0
  12. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/_qleader.py +0 -0
  13. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/admin.py +0 -0
  14. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/admintopic.py +0 -0
  15. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/common.py +0 -0
  16. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/connectivity.py +0 -0
  17. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/exception.py +0 -0
  18. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/msg.py +0 -0
  19. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/partitioner.py +0 -0
  20. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/pattern.py +0 -0
  21. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/properties.py +0 -0
  22. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/py.typed +0 -0
  23. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/topic.py +0 -0
  24. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/util.py +0 -0
  25. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/SOURCES.txt +0 -0
  26. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/dependency_links.txt +0 -0
  27. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/requires.txt +0 -0
  28. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/top_level.txt +0 -0
  29. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/setup.cfg +0 -0
  30. {jetstream_api-0.20.3 → jetstream_api-0.20.6}/tests/test_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.20.3
3
+ Version: 0.20.6
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,29 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.20.6 변경 - 얼어붙은 노드를 건너뛰는 핸드셰이크 시한 · 승격 대기 (자바 v2.16.6 과 같음, 짝 엔진 7.0.1.3512 이상)
64
+
65
+ > 0.20.4 · 0.20.5 는 test.pypi 시험 판으로만 냈다(회귀 BOOTFROZEN-1). 0.20.4 는 얼린 리더를 건너뛴 뒤 팔로워가 승격 전이면 생성이
66
+ > 5초 만에 실패했고, 0.20.5 는 생성자가 승격 전에 끝나면 첫 send 가 얼린 옛 리더로 리더 링크를 열며 30초를 썼다.
67
+
68
+ - **얼어붙은 노드에서 부트스트랩이 30초를 통째로 기다리지 않는다**: 멈춘(SIGSTOP) · 얼어붙은 브로커는 커널이 TCP 연결을 받아
69
+ 주므로 접속은 되고 첫 응답이 오지 않는다. 후보를 도는 접속(프로듀서 부트스트랩 · 재접속, `ILQmgr` 주소 목록 접속 ·
70
+ `reconnect` · 구독 연결 · 팔로워가 준 리더 힌트로 옮겨 붙기)에서 **다른 후보가 남아 있으면** TCP 접속과 첫 `NEW_CONNECTION`
71
+ 응답을 핸드셰이크 시한(기본 5초)만 기다리고 다음 후보로 간다. 그 시한을 넘긴 노드는 그 순회 동안 응답 없음으로 쳐서 리더
72
+ 힌트로도 다시 가지 않는다. 응답 없음이 아닌 다른 후보가 없으면(주소 하나로 붙는 접속 등) 종전처럼 요청 · 세션 시한을 다
73
+ 기다린다 - 바쁜 브로커를 거짓 실패시키지 않는다. PID 발급 · 메타데이터 같은 그 뒤 요청은 종전 시한이다.
74
+ - 설정: `ILProducerConfig.handshakeTimeoutMs(ms)`(snake `handshake_timeout_ms`), `ILQmgr.set_handshake_timeout(ms)` /
75
+ `setHandshakeTimeout`. 0 이하면 끈다.
76
+ - 팔로워 하나로 접속했는데 그 팔로워가 얼어붙은 옛 리더를 힌트로 주는 경우(장애 순간), 힌트 노드에서 짧은 시한 뒤 원래
77
+ 주소로 돌아간다(그사이 리더가 됐으면 붙는다). `ILQmgr` 의 단일 주소 접속도 힌트로 옮겨 붙을 때 원래 주소를 뒤 후보로 둔다.
78
+ - **승격 대기**: 리더가 얼어 있고 팔로워가 아직 승격 전이면(팔로워가 얼린 옛 리더를 힌트로 준다) 후보가 바닥난다. 프로듀서
79
+ 생성은 `requestTimeoutMs` 안에서 100ms → 1초로 쉬며 다시 돌아 승격이 끝나면 새 리더에 붙는다(끝내 안 되면 종전과 같은
80
+ 시간에 마지막 거절로 실패). `ILQmgr` 후보 순회는 2회전 고정이던 것을 세션 타임아웃 안에서 1초 간격으로 되풀이한다(3회전부터는
81
+ 리더 힌트 · 연결 계열 실패일 때만). 응답 없음 표시는 회차마다 새로 시작하고, 확정 거절(권한 · 토픽 없음 등)은 다시 돌지 않는다.
82
+ - **파티션 리더 링크**: 리더 표가 얼린 옛 리더를 가리키면(승격 전) 링크 열기도 핸드셰이크 시한 뒤 그 주소만 쉬고 리더 표를
83
+ 다시 받는다 - 승격 뒤 새 리더로 간다. 같은 주소에서 잇달아 3번 넘기면 다음 한 번은 `requestTimeoutMs` 로 열고(늘 느린 리더도
84
+ 끝내 붙게), 그렇게 오래 여는 동안에도 리더 표를 1초 간격으로 다시 받아 표가 바뀌면 배치는 기다리지 않고 새 리더로 간다.
85
+
63
86
  ## 0.20.3 변경 - 토픽 파티션 격리본 재주입 (자바 v2.16.3 과 같음, 짝 엔진 7.0.1.3512 이상 - 토픽 격리본은 7.0.1.3553 이상)
64
87
 
65
88
  - 격리본 토픽 파티션(엔진 7.0.1.3553 QUARTOOL-2): `reinjectQuarantine` 의 큐 인자를 선택으로 바꿨다 - 토픽 파티션 격리본은 큐를 비우고
@@ -10,6 +10,29 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
10
10
  ## 설치 방법
11
11
  pip install jetstream-api
12
12
 
13
+ ## 0.20.6 변경 - 얼어붙은 노드를 건너뛰는 핸드셰이크 시한 · 승격 대기 (자바 v2.16.6 과 같음, 짝 엔진 7.0.1.3512 이상)
14
+
15
+ > 0.20.4 · 0.20.5 는 test.pypi 시험 판으로만 냈다(회귀 BOOTFROZEN-1). 0.20.4 는 얼린 리더를 건너뛴 뒤 팔로워가 승격 전이면 생성이
16
+ > 5초 만에 실패했고, 0.20.5 는 생성자가 승격 전에 끝나면 첫 send 가 얼린 옛 리더로 리더 링크를 열며 30초를 썼다.
17
+
18
+ - **얼어붙은 노드에서 부트스트랩이 30초를 통째로 기다리지 않는다**: 멈춘(SIGSTOP) · 얼어붙은 브로커는 커널이 TCP 연결을 받아
19
+ 주므로 접속은 되고 첫 응답이 오지 않는다. 후보를 도는 접속(프로듀서 부트스트랩 · 재접속, `ILQmgr` 주소 목록 접속 ·
20
+ `reconnect` · 구독 연결 · 팔로워가 준 리더 힌트로 옮겨 붙기)에서 **다른 후보가 남아 있으면** TCP 접속과 첫 `NEW_CONNECTION`
21
+ 응답을 핸드셰이크 시한(기본 5초)만 기다리고 다음 후보로 간다. 그 시한을 넘긴 노드는 그 순회 동안 응답 없음으로 쳐서 리더
22
+ 힌트로도 다시 가지 않는다. 응답 없음이 아닌 다른 후보가 없으면(주소 하나로 붙는 접속 등) 종전처럼 요청 · 세션 시한을 다
23
+ 기다린다 - 바쁜 브로커를 거짓 실패시키지 않는다. PID 발급 · 메타데이터 같은 그 뒤 요청은 종전 시한이다.
24
+ - 설정: `ILProducerConfig.handshakeTimeoutMs(ms)`(snake `handshake_timeout_ms`), `ILQmgr.set_handshake_timeout(ms)` /
25
+ `setHandshakeTimeout`. 0 이하면 끈다.
26
+ - 팔로워 하나로 접속했는데 그 팔로워가 얼어붙은 옛 리더를 힌트로 주는 경우(장애 순간), 힌트 노드에서 짧은 시한 뒤 원래
27
+ 주소로 돌아간다(그사이 리더가 됐으면 붙는다). `ILQmgr` 의 단일 주소 접속도 힌트로 옮겨 붙을 때 원래 주소를 뒤 후보로 둔다.
28
+ - **승격 대기**: 리더가 얼어 있고 팔로워가 아직 승격 전이면(팔로워가 얼린 옛 리더를 힌트로 준다) 후보가 바닥난다. 프로듀서
29
+ 생성은 `requestTimeoutMs` 안에서 100ms → 1초로 쉬며 다시 돌아 승격이 끝나면 새 리더에 붙는다(끝내 안 되면 종전과 같은
30
+ 시간에 마지막 거절로 실패). `ILQmgr` 후보 순회는 2회전 고정이던 것을 세션 타임아웃 안에서 1초 간격으로 되풀이한다(3회전부터는
31
+ 리더 힌트 · 연결 계열 실패일 때만). 응답 없음 표시는 회차마다 새로 시작하고, 확정 거절(권한 · 토픽 없음 등)은 다시 돌지 않는다.
32
+ - **파티션 리더 링크**: 리더 표가 얼린 옛 리더를 가리키면(승격 전) 링크 열기도 핸드셰이크 시한 뒤 그 주소만 쉬고 리더 표를
33
+ 다시 받는다 - 승격 뒤 새 리더로 간다. 같은 주소에서 잇달아 3번 넘기면 다음 한 번은 `requestTimeoutMs` 로 열고(늘 느린 리더도
34
+ 끝내 붙게), 그렇게 오래 여는 동안에도 리더 표를 1초 간격으로 다시 받아 표가 바뀌면 배치는 기다리지 않고 새 리더로 간다.
35
+
13
36
  ## 0.20.3 변경 - 토픽 파티션 격리본 재주입 (자바 v2.16.3 과 같음, 짝 엔진 7.0.1.3512 이상 - 토픽 격리본은 7.0.1.3553 이상)
14
37
 
15
38
  - 격리본 토픽 파티션(엔진 7.0.1.3553 QUARTOOL-2): `reinjectQuarantine` 의 큐 인자를 선택으로 바꿨다 - 토픽 파티션 격리본은 큐를 비우고
@@ -1,6 +1,6 @@
1
1
  """iLink - Python client API for iLink M.O.M. (Message Oriented Middleware)."""
2
2
 
3
- __version__ = "0.20.3"
3
+ __version__ = "0.20.6"
4
4
 
5
5
  # --- Admin / Service ---
6
6
  from .admin import ILAdminService, ILAdminQmgr, ILLogBrowser, ILQueueBrowser
@@ -27,7 +27,7 @@ from . import _codec
27
27
  from . import _groupwire as _GW
28
28
  from .common import ILC
29
29
  from .connectivity import TCPConnector, _io_lock_of
30
- from .exception import ILException, ILOperationException, ILTimeoutException
30
+ from .exception import ILException, ILOperationException, ILSessionException, ILTimeoutException
31
31
  from .msg import ILMsg, ILMsgUtil, encode_property
32
32
  from .partitioner import partition_for_key
33
33
 
@@ -58,7 +58,7 @@ class ILProducerConfig:
58
58
  "acks", "lingerMs", "batchSize", "maxInFlight", "retries",
59
59
  "deliveryTimeoutMs", "requestTimeoutMs", "retryBackoffMs",
60
60
  "bufferMemory", "maxBlockMs", "maxRequestSize", "maxFrameLength", "enableIdempotence",
61
- "compressionType", "compressionLz4Level", "compressionZstdLevel",
61
+ "compressionType", "compressionLz4Level", "compressionZstdLevel", "handshakeTimeoutMs",
62
62
  ))
63
63
 
64
64
  def __init__(self) -> None:
@@ -103,6 +103,9 @@ class ILProducerConfig:
103
103
  self._compressionLz4Level = _codec.LZ4_LEVEL_DEFAULT
104
104
  #: zstd 레벨 -131072~22. 기본 3
105
105
  self._compressionZstdLevel = _codec.ZSTD_LEVEL_DEFAULT
106
+ # 0.20.4: 응답 없는(얼어붙은) 노드에서 부트스트랩이 requestTimeoutMs 를 통째로 쓰지 않게
107
+ #: 다른 후보가 있을 때 TCP 접속 + 첫 NEW_CONNECTION 응답을 기다리는 시간(ms). 기본 5000, 0 이하면 끔
108
+ self._handshakeTimeoutMs = 5000
106
109
 
107
110
  def __setattr__(self, name: str, value) -> None:
108
111
  # 구버전 호환: cfg.acks = 0 / cfg.lingerMs = 5 같은 직접 대입을 내부 슬롯으로 돌린다.
@@ -292,6 +295,31 @@ class ILProducerConfig:
292
295
  """
293
296
  self._requestTimeoutMs = max(1, n); return self
294
297
 
298
+ def handshakeTimeoutMs(self, n: int) -> "ILProducerConfig":
299
+ """부트스트랩 접속 핸드셰이크 시한(밀리초) - **다른 후보가 있을 때만** 쓴다. (기본 5000 = 5초, 0.20.4)
300
+
301
+ 부트스트랩(처음 접속 · 재접속)이 후보를 돌 때, 다른 후보가 남아 있으면 TCP 접속과 첫 ``NEW_CONNECTION`` 응답을 이
302
+ 시간만 기다리고 다음 후보로 간다. 멈춘(SIGSTOP) · 얼어붙은 브로커는 커널이 TCP 연결을 받아 주므로 접속은 곧바로 되고
303
+ 첫 응답이 오지 않는다 - 전에는 그 노드에서 :meth:`requestTimeoutMs` 를 통째로 기다렸다(장애 순간 리더 힌트가 얼어붙은
304
+ 옛 리더를 가리킬 때 특히). 엔진은 ``NEW_CONNECTION`` 에 곧바로 답하므로 정상 노드가 이 시간을 넘길 일은 없다.
305
+
306
+ Args:
307
+ n (int): 시한. **0 이하면 끈다**(늘 :meth:`requestTimeoutMs`). ``requestTimeoutMs`` 보다 크면 그 값을 쓴다.
308
+
309
+ Returns:
310
+ ILProducerConfig: 체이닝을 위해 자기 자신.
311
+
312
+ Note:
313
+ 짧은 시한을 넘긴 노드는 그 부트스트랩 동안 응답 없음으로 치고(리더 힌트가 가리켜도 다시 가지 않는다),
314
+ **응답 없음이 아닌 다른 후보가 하나도 남지 않으면** 종전처럼 ``requestTimeoutMs`` 를 다 기다린다 - 후보가 하나뿐인
315
+ 접속이나 바쁜 브로커를 거짓 실패시키지 않는다. PID 발급 · 메타데이터 같은 그 뒤 요청은 늘 ``requestTimeoutMs`` 다.
316
+
317
+ 0.20.6: **파티션 리더 링크**를 열 때도 쓴다 - 리더 표가 얼어붙은 옛 리더를 가리키면(승격 전) 이 시간 뒤 그 주소만 쉬고
318
+ 리더 표를 다시 받아 승격 뒤 새 리더로 간다. 같은 주소에서 잇달아 3번 넘기면 다음 한 번은 ``requestTimeoutMs`` 로 열고,
319
+ 그동안에도 리더 표를 주기적으로 다시 받는다(늘 느린 리더도 끝내 붙는다).
320
+ """
321
+ self._handshakeTimeoutMs = int(n); return self
322
+
295
323
  def retryBackoffMs(self, n: int) -> "ILProducerConfig":
296
324
  """재시도 사이에 쉬는 시간(밀리초). (기본 100)
297
325
 
@@ -515,6 +543,10 @@ class ILProducerConfig:
515
543
  """:meth:`requestTimeoutMs` 의 snake 별칭."""
516
544
  return self.requestTimeoutMs(n)
517
545
 
546
+ def handshake_timeout_ms(self, n: int) -> "ILProducerConfig":
547
+ """:meth:`handshakeTimeoutMs` 의 snake 별칭."""
548
+ return self.handshakeTimeoutMs(n)
549
+
518
550
  def retry_backoff_ms(self, n: int) -> "ILProducerConfig":
519
551
  """:meth:`retryBackoffMs` 의 snake 별칭."""
520
552
  return self.retryBackoffMs(n)
@@ -615,6 +647,14 @@ class ILProducerConfig:
615
647
  """
616
648
  return self._requestTimeoutMs
617
649
 
650
+ def get_handshake_timeout_ms(self) -> int:
651
+ """부트스트랩 접속 핸드셰이크 시한(밀리초). 기본 5000. 0 이하면 꺼짐.
652
+
653
+ Returns:
654
+ int: 시한(밀리초).
655
+ """
656
+ return self._handshakeTimeoutMs
657
+
618
658
  def get_retry_backoff_ms(self) -> int:
619
659
  """재시도 간 대기(밀리초). 기본 100.
620
660
 
@@ -698,6 +738,7 @@ class ILProducerConfig:
698
738
  getDeliveryTimeoutMs = get_delivery_timeout_ms
699
739
  getRequestTimeoutMs = get_request_timeout_ms
700
740
  getRetryBackoffMs = get_retry_backoff_ms
741
+ getHandshakeTimeoutMs = get_handshake_timeout_ms
701
742
  getBufferMemory = get_buffer_memory
702
743
  getMaxBlockMs = get_max_block_ms
703
744
  getMaxRequestSize = get_max_request_size
@@ -1623,8 +1664,14 @@ class ILTopicProducer:
1623
1664
  self._pmeta_retry_at = 0.0
1624
1665
  #: 리더 주소 -> 다시 열어 볼 시각(열기에 실패한 뒤 백오프)
1625
1666
  self._open_retry_at: dict = {}
1667
+ #: 0.20.5: 리더 링크 주소 -> 짧은 핸드셰이크 시한을 잇달아 넘긴 횟수. 3번이면 다음 한 번은 requestTimeoutMs 로 연다
1668
+ self._hs_strikes: dict = {}
1626
1669
  #: 여는 스레드가 여는 중인 리더 주소 / 끝낸 결과 (주소, 커넥터, (파티션 수, epoch, 세그먼트), 예외)
1627
1670
  self._opening: set = set()
1671
+ #: 0.20.5: 여는 중인 리더 링크 주소 -> 열기 시작 시각. 핸드셰이크 시한보다 오래 걸리면(얼어붙은 리더 · 종전 시한 회차)
1672
+ #: 리더 표를 주기적으로 다시 받는다 - 표가 새 리더를 가리키면 배치는 열기가 끝나기를 기다리지 않고 그리로 간다
1673
+ self._opening_since: dict = {}
1674
+ self._open_refresh_at = 0.0
1628
1675
  self._opened: collections.deque = collections.deque()
1629
1676
  self._corr = 0
1630
1677
  self._next_seq: list[int] = []
@@ -1650,7 +1697,7 @@ class ILTopicProducer:
1650
1697
  self._sel.register(self._wake_r, selectors.EVENT_READ, "wake")
1651
1698
  try:
1652
1699
  # 첫 연결은 동기로 한다 - 주소 / 핸드셰이크 / 토픽 오류를 생성 시점에 드러낸다
1653
- self._bootstrap()
1700
+ self._bootstrap(wait=True)
1654
1701
  except Exception:
1655
1702
  self._shutdown_quietly()
1656
1703
  raise
@@ -1659,31 +1706,78 @@ class ILTopicProducer:
1659
1706
  self._thread.start()
1660
1707
 
1661
1708
  # ── 부트스트랩(블로킹): 연결 + 핸드셰이크 + (멱등) PID + 토픽 메타데이터 -> 논블로킹 등록 ──
1662
- def _bootstrap(self) -> None:
1663
- """후보를 순회하며 접속한다. 리더 힌트를 받으면 그 주소를 맨 앞으로 올려 곧바로 다시 시도한다."""
1709
+ def _bootstrap(self, wait: bool = False) -> None:
1710
+ """후보를 순회하며 접속한다(:meth:`_bootstrap_pass`).
1711
+
1712
+ 0.20.4: ``wait`` 면(생성자의 첫 연결) 후보가 바닥나도 requestTimeoutMs 안에서 100ms -> 1s 로 쉬며 다시 돈다 - 리더가
1713
+ 얼어 있고 팔로워가 아직 승격 전이면(팔로워가 얼어붙은 옛 리더를 힌트로 준다) 승격이 끝날 때 새 리더에 붙는다. 다시
1714
+ 도는 것은 리더 힌트 · 연결 계열 실패뿐이다(토픽 없음 같은 확정 거절은 곧바로 올린다). 응답 없음 표시는 회차마다 새로
1715
+ 시작한다. I/O 스레드의 재접속은 그 루프가 백오프하며 전달 시한까지 다시 돌므로 한 회차만 한다."""
1716
+ if not wait:
1717
+ self._bootstrap_pass()
1718
+ return
1719
+ deadline = time.monotonic() + self.config.get_request_timeout_ms() / 1000.0
1720
+ pause = 0.1
1721
+ while True:
1722
+ try:
1723
+ self._bootstrap_pass()
1724
+ return
1725
+ except Exception as exc: # noqa: BLE001
1726
+ retryable = (_extract_leader_hint(exc) is not None
1727
+ or isinstance(exc, (ILSessionException, OSError))
1728
+ or getattr(exc, "_il_handshake", False))
1729
+ if not retryable or time.monotonic() + pause >= deadline:
1730
+ raise
1731
+ time.sleep(pause)
1732
+ pause = min(pause * 2, 1.0)
1733
+
1734
+ def _bootstrap_pass(self) -> None:
1735
+ """후보를 한 바퀴 돈다. 리더 힌트를 받으면 그 주소를 맨 앞으로 올려 곧바로 다시 시도한다.
1736
+
1737
+ 0.20.4: 다른 후보가 남아 있으면 TCP 접속과 첫 NEW_CONNECTION 응답을 handshakeTimeoutMs 만 기다린다. 그 시한을 넘긴
1738
+ 노드는 이번 부트스트랩 동안 응답 없음(silent)으로 치고 리더 힌트로도 다시 올리지 않는다. 응답 없음이 아닌 다른
1739
+ 후보가 없으면 requestTimeoutMs 를 다 기다린다(후보 하나 · 바쁜 브로커를 거짓 실패시키지 않는다)."""
1664
1740
  last: Exception | None = None
1665
- for _ in range(len(self._candidates) + 1):
1741
+ full = self.config.get_request_timeout_ms()
1742
+ hs = self.config.get_handshake_timeout_ms()
1743
+ silent: set = set()
1744
+ n = 0
1745
+ while n < len(self._candidates) + 1:
1746
+ n += 1
1666
1747
  addr = self._candidates[self._cand_idx % len(self._candidates)]
1667
1748
  colon = addr.rfind(":")
1749
+ short = 0 < hs < full and any(c != addr and c not in silent for c in self._candidates)
1668
1750
  try:
1669
- self._bootstrap_one(addr[:colon], int(addr[colon + 1:]))
1751
+ self._bootstrap_one(addr[:colon], int(addr[colon + 1:]), hs if short else full)
1670
1752
  return
1671
1753
  except Exception as exc: # noqa: BLE001 - 후보를 계속 돈다
1672
1754
  last = exc
1755
+ if short and getattr(exc, "_il_handshake", False):
1756
+ silent.add(addr)
1673
1757
  hint = _extract_leader_hint(exc)
1674
- if hint and self._promote(hint):
1758
+ if hint and hint not in silent and self._promote(hint):
1675
1759
  continue # 힌트부터 재시도
1676
1760
  self._cand_idx = (self._cand_idx + 1) % len(self._candidates)
1677
1761
  raise last if last is not None else ILOperationException("no reachable node")
1678
1762
 
1679
- def _bootstrap_one(self, host: str, port: int) -> None:
1763
+ def _bootstrap_one(self, host: str, port: int, handshake_ms: int = 0) -> None:
1680
1764
  conn = TCPConnector()
1681
1765
  conn.max_frame_length = self.config.get_max_frame_length()
1682
- conn.connect(host, port, self.config.get_request_timeout_ms())
1766
+ hs = handshake_ms if handshake_ms > 0 else self.config.get_request_timeout_ms()
1683
1767
  try:
1768
+ conn.connect(host, port, hs)
1684
1769
  # 0.18.0: 능력 선언. ☠ arg1("1")은 자동커밋 플래그라 그대로 둔다.
1685
1770
  rep = self._request(conn, ILC.MIMQ_NEW_CONNECTION, "1", self.clientName, "USR",
1686
- ext=ILC.client_caps_extension())
1771
+ ext=ILC.client_caps_extension(), timeout_ms=hs)
1772
+ except ILSessionException as exc:
1773
+ # 0.20.4: 핸드셰이크(접속 + 첫 응답)에서 닿지 않았다 - _bootstrap 이 이 노드를 응답 없음으로 친다
1774
+ conn.shutdown()
1775
+ exc._il_handshake = True
1776
+ raise
1777
+ except Exception:
1778
+ conn.shutdown()
1779
+ raise
1780
+ try:
1687
1781
  if rep is None or rep.getCode() != ILC.MIMQ_SUCCESSFUL_CONNECTION:
1688
1782
  raise ILOperationException((rep.getArg2() if rep is not None else "")
1689
1783
  or "MIMQE_INVALID_MSG_FORMAT (connection)")
@@ -1742,9 +1836,21 @@ class ILTopicProducer:
1742
1836
  colon = addr.rfind(":")
1743
1837
  conn = TCPConnector()
1744
1838
  conn.max_frame_length = self.config.get_max_frame_length()
1745
- conn.connect(addr[:colon], int(addr[colon + 1:]), self.config.get_request_timeout_ms())
1746
- rep = self._request(conn, ILC.MIMQ_NEW_CONNECTION, "1", self.clientName, "USR",
1747
- ext=ILC.client_caps_extension(True))
1839
+ # 0.20.5: 리더 표가 얼어붙은 옛 리더를 가리키면(승격 전) 접속 + 첫 응답을 handshakeTimeoutMs 만 기다리고 그 주소만
1840
+ # 백오프한다 - 리더 표를 다시 받아 승격 뒤 새 리더로 간다. 같은 주소에서 잇달아 3번 넘기면 다음 한 번은
1841
+ # requestTimeoutMs 로 연다(늘 느린 리더를 끝내 못 붙는 일이 없게). 여는 스레드라 I/O 스레드는 멈추지 않는다
1842
+ full = self.config.get_request_timeout_ms()
1843
+ hs = self.config.get_handshake_timeout_ms()
1844
+ short = 0 < hs < full and self._hs_strikes.get(addr, 0) < 3
1845
+ tmo = hs if short else full
1846
+ try:
1847
+ conn.connect(addr[:colon], int(addr[colon + 1:]), tmo)
1848
+ rep = self._request(conn, ILC.MIMQ_NEW_CONNECTION, "1", self.clientName, "USR",
1849
+ ext=ILC.client_caps_extension(True), timeout_ms=tmo)
1850
+ except ILSessionException as hx:
1851
+ if short:
1852
+ hx._il_handshake = True
1853
+ raise
1748
1854
  if rep is None or rep.getCode() != ILC.MIMQ_SUCCESSFUL_CONNECTION:
1749
1855
  raise ILOperationException((rep.getArg2() if rep is not None else "")
1750
1856
  or "MIMQE_INVALID_MSG_FORMAT (connection)")
@@ -1773,7 +1879,12 @@ class ILTopicProducer:
1773
1879
  while self._opened:
1774
1880
  addr, conn, meta, exc = self._opened.popleft()
1775
1881
  self._opening.discard(addr)
1882
+ self._opening_since.pop(addr, None)
1776
1883
  if exc is not None:
1884
+ if getattr(exc, "_il_handshake", False):
1885
+ self._hs_strikes[addr] = self._hs_strikes.get(addr, 0) + 1
1886
+ else:
1887
+ self._hs_strikes.pop(addr, None) # 종전 시한으로 연 회차(또는 다른 실패) 뒤에는 다시 짧은 시한부터
1777
1888
  self._open_retry_at[addr] = time.monotonic() + self._backoff
1778
1889
  self._pmeta_stale = True
1779
1890
  answer = "cannot reach partition leader %s: %s" % (addr, str(exc) or type(exc).__name__)
@@ -1795,15 +1906,16 @@ class ILTopicProducer:
1795
1906
  self._open_retry_at[addr] = time.monotonic() + self._backoff
1796
1907
  continue
1797
1908
  self._open_retry_at.pop(addr, None)
1909
+ self._hs_strikes.pop(addr, None)
1798
1910
  with self._cv:
1799
1911
  self._by_addr[addr] = link
1800
1912
  self._links = self._links + (link,)
1801
1913
 
1802
- def _request(self, conn: TCPConnector, code: int, *args: str, ext: bytes = None):
1914
+ def _request(self, conn: TCPConnector, code: int, *args: str, ext: bytes = None, timeout_ms: int = 0):
1803
1915
  with _io_lock_of(conn): # 0.7.3: 부트스트랩 전용 연결이지만 요청-응답 짝 규칙은 같게
1804
1916
  conn.send(ILMsgUtil.create_request_msg_with_ext(code, ext, *args)
1805
1917
  if ext is not None else ILMsgUtil.create_request_msg(code, *args))
1806
- res = conn.recv(self.config.get_request_timeout_ms())
1918
+ res = conn.recv(timeout_ms if timeout_ms > 0 else self.config.get_request_timeout_ms())
1807
1919
  if res.getType() == ILC.MIMQ_REPORT_MESSAGE:
1808
1920
  return res.getReportMsg()
1809
1921
  return None
@@ -2374,13 +2486,27 @@ class ILTopicProducer:
2374
2486
  addr = self._leaders[p]
2375
2487
  if addr is not None and addr not in self._by_addr and addr not in want:
2376
2488
  want.append(addr)
2489
+ hs_s = self._open_stall_s()
2377
2490
  for addr in want:
2378
- if addr in self._opening or now < self._open_retry_at.get(addr, 0.0):
2491
+ if addr in self._opening:
2492
+ # 0.20.5: 열기가 핸드셰이크 시한을 넘겼다 - 리더 표를 다시 받는다(승격되면 새 리더로 간다)
2493
+ if now - self._opening_since.get(addr, now) >= hs_s and now >= self._open_refresh_at:
2494
+ self._pmeta_stale = True
2495
+ self._open_refresh_at = now + max(self._backoff, 1.0)
2496
+ continue
2497
+ if now < self._open_retry_at.get(addr, 0.0):
2379
2498
  continue
2380
2499
  self._opening.add(addr)
2500
+ self._opening_since[addr] = now
2381
2501
  threading.Thread(target=self._open_link_bg, args=(addr,), daemon=True,
2382
2502
  name="jetstream-producer-link-%d" % next(_THREAD_SEQ)).start()
2383
2503
 
2504
+ def _open_stall_s(self) -> float:
2505
+ """리더 링크 열기가 이보다 오래 걸리면 리더 표를 다시 받는다(초) - 핸드셰이크 시한, 꺼져 있으면 requestTimeoutMs."""
2506
+ hs = self.config.get_handshake_timeout_ms()
2507
+ full = self.config.get_request_timeout_ms()
2508
+ return (hs if 0 < hs < full else full) / 1000.0
2509
+
2384
2510
  def _compute_wait(self, now: float) -> float:
2385
2511
  wait = _IO_WAIT_CAP
2386
2512
  links = self._links
@@ -2408,6 +2534,10 @@ class ILTopicProducer:
2408
2534
  # 리더 링크를 아직 못 열었다 - 여는 중이면 여는 스레드가 깨우고, 아니면 다시 열어 볼 시각에 깬다
2409
2535
  if self._leaders[p] not in self._opening:
2410
2536
  wait = min(wait, self._open_retry_at.get(self._leaders[p], 0.0) - now)
2537
+ else:
2538
+ # 0.20.5: 오래 걸리는 열기 동안 리더 표를 다시 받을 시각에 깬다
2539
+ since = self._opening_since.get(self._leaders[p], now)
2540
+ wait = min(wait, max(since + self._open_stall_s(), self._open_refresh_at) - now)
2411
2541
  continue
2412
2542
  # 그 링크가 epoch 를 다시 받는 중이거나 가득 찼거나, 다른 링크에 이 파티션이 실려 있다 - 응답이 깨운다
2413
2543
  if link.meta_stale or link.retire or (self._acks != 0 and len(link.inflight) >= self._max_in_flight):
@@ -200,6 +200,8 @@ class ILQmgr:
200
200
  self._connGen = 0
201
201
  # 0.7.3 소비 v5: 구독 전용 세션이면 True - 접속 핸드셰이크가 끝날 때마다 연결을 요청 태그 계층으로 바꾼다
202
202
  self._subTagged = False
203
+ #: 0.20.4: 다른 후보가 있을 때 TCP 접속 + 첫 NEW_CONNECTION 응답을 기다리는 시간(ms). 0 이하면 끔
204
+ self._handshakeTimeoutMS = 5000
203
205
  self._capPlead = False # 0.19.0: True 면 접속 CAP 에 plead=1(팔로워도 받아 달라) - 구독 세션만
204
206
  # 0.20.0: True 면 접속 CAP 에 qlead=1(큐마다 묶음 리더를 찾아 간다) - 앱의 큐 세션과 큐 링크. 구독 세션은 끈다
205
207
  self._capQlead = True
@@ -371,6 +373,7 @@ class ILQmgr:
371
373
  clientName: str | None = None,
372
374
  isAutoCommit: bool | None = None,
373
375
  _allow_redirect: bool = True,
376
+ _handshake_ms: int = 0,
374
377
  ) -> None:
375
378
  """큐 관리자에 접속해 세션을 연다.
376
379
 
@@ -478,20 +481,32 @@ class ILQmgr:
478
481
  try:
479
482
  self.host = host
480
483
  self.port = port
481
- self.service.connect(host, port)
482
- if not client_name:
483
- # 0.11.0 K10: inspect.stack()[-1] 과 같은 값(가장 바깥 프레임의 모듈 이름). 프레임만 거슬러 올라가므로 소스 줄을
484
- # 읽지 않는다(전에는 첫 접속에 약 1.6 ms)
485
- client_name = caller_module_name()
486
- self.clientName = client_name
487
- conn_mode = ILC.MIMQ_SESSION_TRANSACTED
488
- if auto_commit:
489
- conn_mode = ILC.MIMQ_AUTO_ACKNOWLEDGE
490
- # 0.18.0: 능력 선언. ☠ arg1(conn_mode)은 자동커밋 플래그라 건드리지 않는다.
491
- _caps = ILC.client_caps_extension(getattr(self, "_capPlead", False), getattr(self, "_capQlead", False))
492
- _cargs = ((ILC.MIMQ_NEW_CONNECTION,) + ((_caps,) if _caps else ())
493
- + (str(conn_mode), client_name, "USR"))
494
- conn_res_msg = self.service.request(*_cargs)
484
+ # 0.20.4: 후보 순회(_connect_any)가 다른 후보를 남겨 두고 부르면 접속 + 첫 응답만 짧은 시한으로 기다린다
485
+ _saved_tmo = self.service.sessionTimeoutMS
486
+ _short = 0 < _handshake_ms < _saved_tmo
487
+ if _short:
488
+ self.service.sessionTimeoutMS = _handshake_ms
489
+ try:
490
+ self.service.connect(host, port)
491
+ if not client_name:
492
+ # 0.11.0 K10: inspect.stack()[-1] 과 같은 값(가장 바깥 프레임의 모듈 이름). 프레임만 거슬러 올라가므로 소스
493
+ # 줄을 읽지 않는다(전에는 첫 접속에 약 1.6 ms)
494
+ client_name = caller_module_name()
495
+ self.clientName = client_name
496
+ conn_mode = ILC.MIMQ_SESSION_TRANSACTED
497
+ if auto_commit:
498
+ conn_mode = ILC.MIMQ_AUTO_ACKNOWLEDGE
499
+ # 0.18.0: 능력 선언. ☠ arg1(conn_mode)은 자동커밋 플래그라 건드리지 않는다.
500
+ _caps = ILC.client_caps_extension(getattr(self, "_capPlead", False), getattr(self, "_capQlead", False))
501
+ _cargs = ((ILC.MIMQ_NEW_CONNECTION,) + ((_caps,) if _caps else ())
502
+ + (str(conn_mode), client_name, "USR"))
503
+ conn_res_msg = self.service.request(*_cargs)
504
+ except ILSessionException as _hx:
505
+ if _short:
506
+ _hx._il_handshake = True # _connect_any 가 이 노드를 응답 없음으로 친다
507
+ raise
508
+ finally:
509
+ self.service.sessionTimeoutMS = _saved_tmo
495
510
  if conn_res_msg is None:
496
511
  raise ILOperationException("MIMQE_INTERNAL_ERROR")
497
512
  if conn_res_msg.getCode() != ILC.MIMQ_SUCCESSFUL_CONNECTION:
@@ -563,18 +578,13 @@ class ILQmgr:
563
578
  if _allow_redirect:
564
579
  hint = self._remember_leader_hint(exc)
565
580
  if hint:
566
- colon = hint.rfind(":")
567
581
  try:
568
582
  self.service.shutdown()
569
583
  except Exception:
570
584
  pass
571
- self.connect(
572
- hint[:colon],
573
- int(hint[colon + 1:]),
574
- client_name,
575
- auto_commit,
576
- _allow_redirect=False,
577
- )
585
+ # 0.20.4: 힌트 노드로 옮겨 붙되 원래 주소를 뒤 후보로 둔다 - 힌트가 얼어붙은 옛 리더면 짧은 시한 뒤
586
+ # 원래 주소(그사이 리더가 됐을 수 있다)로 돌아간다. 전에는 힌트 노드에서 세션 타임아웃을 통째로 기다렸다
587
+ self._connect_any([hint, "%s:%s" % (host, port)], client_name, auto_commit)
578
588
  return
579
589
  # 힌트를 줄 상대조차 없으면(노드가 죽어 있음) 파일에 남겨 둔
580
590
  # 후보로 한 번 더 돈다. 경로를 지정했을 때만 동작한다.
@@ -803,6 +813,38 @@ class ILQmgr:
803
813
  """:meth:`is_connected` 의 camelCase 별칭."""
804
814
  return self.is_connected()
805
815
 
816
+ def set_handshake_timeout(self, timeout_ms: int) -> None:
817
+ """여러 후보로 접속할 때 **응답 없는(얼어붙은) 노드**를 건너뛰는 핸드셰이크 시한(밀리초)을 정한다(0.20.4). 기본 5000.
818
+
819
+ 주소 목록 접속 · :meth:`reconnect` · 구독 연결 · 리더 힌트 이동처럼 후보를 돌 때, 다른 후보가 남아 있으면 TCP 접속과 첫
820
+ ``NEW_CONNECTION`` 응답을 이 시간만 기다리고 다음 후보로 간다. 멈춘(SIGSTOP) · 얼어붙은 노드는 커널이 TCP 연결을 받아
821
+ 주므로 접속은 되고 응답이 오지 않는다 - 전에는 세션 타임아웃을 통째로 기다렸다.
822
+
823
+ Args:
824
+ timeout_ms (int): 시한(밀리초). **0 이하면 끈다**(늘 세션 타임아웃). 세션 타임아웃보다 크면 세션 타임아웃을 쓴다.
825
+
826
+ Note:
827
+ 짧은 시한을 넘긴 노드는 그 순회 동안 응답 없음으로 친다. **응답 없음이 아닌 다른 후보가 없으면** 종전처럼 세션
828
+ 타임아웃을 다 기다린다 - 주소 하나로 붙는 접속이나 바쁜 엔진을 거짓 실패시키지 않는다.
829
+ """
830
+ self._handshakeTimeoutMS = int(timeout_ms)
831
+
832
+ def setHandshakeTimeout(self, timeoutMS: int) -> None:
833
+ """:meth:`set_handshake_timeout` 의 camelCase 별칭."""
834
+ self.set_handshake_timeout(timeoutMS)
835
+
836
+ def get_handshake_timeout(self) -> int:
837
+ """핸드셰이크 시한(밀리초). 기본 5000. 0 이하면 꺼짐.
838
+
839
+ Returns:
840
+ int: 시한(밀리초).
841
+ """
842
+ return self._handshakeTimeoutMS
843
+
844
+ def getHandshakeTimeout(self) -> int:
845
+ """:meth:`get_handshake_timeout` 의 camelCase 별칭."""
846
+ return self.get_handshake_timeout()
847
+
806
848
  def set_session_timeout(self, timeout_ms: int) -> None:
807
849
  """응답 패킷을 기다리는 시간을 정한다.
808
850
 
@@ -1121,8 +1163,17 @@ class ILQmgr:
1121
1163
  def _connect_any(self, candidates: list[str], client_name: str, auto_commit: bool) -> None:
1122
1164
  # 후보를 순회하며 접속한다. 리더 힌트를 받으면 그 주소를 맨 앞으로 올려 재시도.
1123
1165
  # 최대 2회전(그 사이 승격이 끝날 수 있으므로 1초 쉬고 한 번 더).
1166
+ # 0.20.4: 다른 후보가 남아 있으면 접속 + 첫 NEW_CONNECTION 응답을 _handshakeTimeoutMS 만 기다린다. 그 시한을 넘긴
1167
+ # 노드는 이번 순회 동안 응답 없음(silent)으로 치고 리더 힌트로도 다시 올리지 않는다. 응답 없음이 아닌 다른 후보가
1168
+ # 없으면 세션 타임아웃을 다 기다린다(후보 하나 · 바쁜 엔진을 거짓 실패시키지 않는다)
1169
+ # 0.20.4: 2회전 고정이던 것을 세션 타임아웃 안에서 되풀이한다(최소 2회전, 사이 1초) - 리더가 얼어 있고 팔로워가 아직
1170
+ # 승격 전이면 승격이 끝날 때 새 리더에 붙는다. 응답 없음 표시는 회전마다 새로 시작한다
1124
1171
  last_error = None
1125
- for _round in range(2):
1172
+ hs = self._handshakeTimeoutMS
1173
+ deadline = time.monotonic() + max(self.service.sessionTimeoutMS, 0) / 1000.0
1174
+ _round = 0
1175
+ while True:
1176
+ silent: set = set()
1126
1177
  i = 0
1127
1178
  while i < len(candidates):
1128
1179
  addr = candidates[i]
@@ -1134,14 +1185,20 @@ class ILQmgr:
1134
1185
  port = int(addr[colon + 1:])
1135
1186
  except Exception:
1136
1187
  i += 1; continue
1188
+ short = hs > 0 and any(c != addr and c not in silent for c in candidates)
1137
1189
  try:
1138
- self.connect(host, port, client_name, auto_commit, _allow_redirect=False)
1190
+ self.connect(host, port, client_name, auto_commit, _allow_redirect=False,
1191
+ _handshake_ms=hs if short else 0)
1139
1192
  return
1140
1193
  except Exception as exc:
1141
1194
  last_error = exc
1142
1195
  try: self.service.shutdown()
1143
1196
  except Exception: pass
1197
+ if short and any(getattr(t, "_il_handshake", False) for t in (exc, exc.__cause__)):
1198
+ silent.add(addr)
1144
1199
  hint = self._remember_leader_hint(exc)
1200
+ if hint and hint in silent:
1201
+ hint = None
1145
1202
  if hint and hint not in candidates:
1146
1203
  candidates.insert(0, hint); i = 0; continue
1147
1204
  if hint:
@@ -1149,6 +1206,15 @@ class ILQmgr:
1149
1206
  if idx > 0:
1150
1207
  candidates.pop(idx); candidates.insert(0, hint); i = 0; continue
1151
1208
  i += 1
1209
+ _round += 1
1210
+ if _round >= 2:
1211
+ # 3회전부터는 리더 힌트 · 연결 계열 실패일 때만(권한 거절 같은 확정 실패는 종전대로 2회전)
1212
+ retryable = (last_error is not None
1213
+ and (self._extract_leader_hint(last_error) is not None
1214
+ or isinstance(last_error, ILSessionException)
1215
+ or isinstance(getattr(last_error, "__cause__", None), ILSessionException)))
1216
+ if not retryable or time.monotonic() + 1.0 >= deadline:
1217
+ break
1152
1218
  time.sleep(1.0)
1153
1219
  if last_error is not None:
1154
1220
  raise last_error
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.20.3
3
+ Version: 0.20.6
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,29 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.20.6 변경 - 얼어붙은 노드를 건너뛰는 핸드셰이크 시한 · 승격 대기 (자바 v2.16.6 과 같음, 짝 엔진 7.0.1.3512 이상)
64
+
65
+ > 0.20.4 · 0.20.5 는 test.pypi 시험 판으로만 냈다(회귀 BOOTFROZEN-1). 0.20.4 는 얼린 리더를 건너뛴 뒤 팔로워가 승격 전이면 생성이
66
+ > 5초 만에 실패했고, 0.20.5 는 생성자가 승격 전에 끝나면 첫 send 가 얼린 옛 리더로 리더 링크를 열며 30초를 썼다.
67
+
68
+ - **얼어붙은 노드에서 부트스트랩이 30초를 통째로 기다리지 않는다**: 멈춘(SIGSTOP) · 얼어붙은 브로커는 커널이 TCP 연결을 받아
69
+ 주므로 접속은 되고 첫 응답이 오지 않는다. 후보를 도는 접속(프로듀서 부트스트랩 · 재접속, `ILQmgr` 주소 목록 접속 ·
70
+ `reconnect` · 구독 연결 · 팔로워가 준 리더 힌트로 옮겨 붙기)에서 **다른 후보가 남아 있으면** TCP 접속과 첫 `NEW_CONNECTION`
71
+ 응답을 핸드셰이크 시한(기본 5초)만 기다리고 다음 후보로 간다. 그 시한을 넘긴 노드는 그 순회 동안 응답 없음으로 쳐서 리더
72
+ 힌트로도 다시 가지 않는다. 응답 없음이 아닌 다른 후보가 없으면(주소 하나로 붙는 접속 등) 종전처럼 요청 · 세션 시한을 다
73
+ 기다린다 - 바쁜 브로커를 거짓 실패시키지 않는다. PID 발급 · 메타데이터 같은 그 뒤 요청은 종전 시한이다.
74
+ - 설정: `ILProducerConfig.handshakeTimeoutMs(ms)`(snake `handshake_timeout_ms`), `ILQmgr.set_handshake_timeout(ms)` /
75
+ `setHandshakeTimeout`. 0 이하면 끈다.
76
+ - 팔로워 하나로 접속했는데 그 팔로워가 얼어붙은 옛 리더를 힌트로 주는 경우(장애 순간), 힌트 노드에서 짧은 시한 뒤 원래
77
+ 주소로 돌아간다(그사이 리더가 됐으면 붙는다). `ILQmgr` 의 단일 주소 접속도 힌트로 옮겨 붙을 때 원래 주소를 뒤 후보로 둔다.
78
+ - **승격 대기**: 리더가 얼어 있고 팔로워가 아직 승격 전이면(팔로워가 얼린 옛 리더를 힌트로 준다) 후보가 바닥난다. 프로듀서
79
+ 생성은 `requestTimeoutMs` 안에서 100ms → 1초로 쉬며 다시 돌아 승격이 끝나면 새 리더에 붙는다(끝내 안 되면 종전과 같은
80
+ 시간에 마지막 거절로 실패). `ILQmgr` 후보 순회는 2회전 고정이던 것을 세션 타임아웃 안에서 1초 간격으로 되풀이한다(3회전부터는
81
+ 리더 힌트 · 연결 계열 실패일 때만). 응답 없음 표시는 회차마다 새로 시작하고, 확정 거절(권한 · 토픽 없음 등)은 다시 돌지 않는다.
82
+ - **파티션 리더 링크**: 리더 표가 얼린 옛 리더를 가리키면(승격 전) 링크 열기도 핸드셰이크 시한 뒤 그 주소만 쉬고 리더 표를
83
+ 다시 받는다 - 승격 뒤 새 리더로 간다. 같은 주소에서 잇달아 3번 넘기면 다음 한 번은 `requestTimeoutMs` 로 열고(늘 느린 리더도
84
+ 끝내 붙게), 그렇게 오래 여는 동안에도 리더 표를 1초 간격으로 다시 받아 표가 바뀌면 배치는 기다리지 않고 새 리더로 간다.
85
+
63
86
  ## 0.20.3 변경 - 토픽 파티션 격리본 재주입 (자바 v2.16.3 과 같음, 짝 엔진 7.0.1.3512 이상 - 토픽 격리본은 7.0.1.3553 이상)
64
87
 
65
88
  - 격리본 토픽 파티션(엔진 7.0.1.3553 QUARTOOL-2): `reinjectQuarantine` 의 큐 인자를 선택으로 바꿨다 - 토픽 파티션 격리본은 큐를 비우고
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jetstream-api"
7
- version = "0.20.3"
7
+ version = "0.20.6"
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