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.
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/PKG-INFO +24 -1
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/README.md +23 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/__init__.py +1 -1
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/producer.py +147 -17
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/qmgr.py +90 -24
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/PKG-INFO +24 -1
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/pyproject.toml +1 -1
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/LICENSE.txt +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/_codec.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/_consumer.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/_groupwire.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/_qleader.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/admin.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/admintopic.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/common.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/connectivity.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/exception.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/msg.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/partitioner.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/pattern.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/properties.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/py.typed +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/topic.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/ilink/util.py +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/SOURCES.txt +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/dependency_links.txt +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/requires.txt +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/jetstream_api.egg-info/top_level.txt +0 -0
- {jetstream_api-0.20.3 → jetstream_api-0.20.6}/setup.cfg +0 -0
- {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
|
+
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` 의 큐 인자를 선택으로 바꿨다 - 토픽 파티션 격리본은 큐를 비우고
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
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
|
+
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` 의 큐 인자를 선택으로 바꿨다 - 토픽 파티션 격리본은 큐를 비우고
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|