jetstream-api 0.20.3__tar.gz → 0.21.3__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.21.3}/PKG-INFO +80 -1
  2. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/README.md +79 -0
  3. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/__init__.py +1 -1
  4. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/_consumer.py +63 -8
  5. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/pattern.py +69 -2
  6. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/producer.py +703 -105
  7. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/qmgr.py +90 -24
  8. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/topic.py +69 -2
  9. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/jetstream_api.egg-info/PKG-INFO +80 -1
  10. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/pyproject.toml +1 -1
  11. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/LICENSE.txt +0 -0
  12. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/_codec.py +0 -0
  13. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/_groupwire.py +0 -0
  14. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/_qleader.py +0 -0
  15. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/admin.py +0 -0
  16. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/admintopic.py +0 -0
  17. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/common.py +0 -0
  18. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/connectivity.py +0 -0
  19. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/exception.py +0 -0
  20. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/msg.py +0 -0
  21. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/partitioner.py +0 -0
  22. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/properties.py +0 -0
  23. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/py.typed +0 -0
  24. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/ilink/util.py +0 -0
  25. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/jetstream_api.egg-info/SOURCES.txt +0 -0
  26. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/jetstream_api.egg-info/dependency_links.txt +0 -0
  27. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/jetstream_api.egg-info/requires.txt +0 -0
  28. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/jetstream_api.egg-info/top_level.txt +0 -0
  29. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/setup.cfg +0 -0
  30. {jetstream_api-0.20.3 → jetstream_api-0.21.3}/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.21.3
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,85 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.21.3 변경 - 메타데이터 재질의 '같은 응답' 지수 대기 (자바 v2.17.3 과 같음, 짝 엔진 7.0.1.3512 이상 - 리더 적재만은 7.0.1.3566 이상)
64
+
65
+ > 0.21.0 ~ 0.21.2 는 test.pypi 시험 판으로만 냈다. 아래 0.21.2 · 0.21.1 · 0.21.0 절도 이 판으로 처음 PyPI 에 나간다.
66
+
67
+ - **메타데이터 응답이 성공인데 진전이 없으면 재질의 간격이 는다**(사용자 결정 2026-10-05, Kafka 3.7 Metadata 와 같게): 리더 epoch 가
68
+ 그대로인 응답이 이어지면 횟수를 세어 `retryBackoffMs` 부터 두 배씩 `retryBackoffMaxMs` 까지 늘린다(±20%). 처음 보는 / 더 큰 리더
69
+ epoch, epoch 없는 응답, 실패, 5분 넘은 메타데이터는 횟수를 지운다. 성공 뒤에도 다음 재질의는 `retryBackoffMs` 뒤다.
70
+ - producer: 리더 표(파티션 리더 epoch)와 링크별 토픽 메타데이터(토픽 epoch). 크기 초과 뒤 세그먼트 크기 재조회는 이제 봉투를 막지
71
+ 않는다(거절된 배치는 쪼개 보낸다).
72
+ - 구독: 메타데이터 재조회(리더 바뀜 · 노드 끊김 뒤). 간격 안의 재조회는 건너뛰고 지금 메타데이터로 계속한다. 패턴 구독에 새
73
+ 토픽이 맞으면 곧바로 묻는다.
74
+
75
+ ## 0.21.2 변경 - 크기 초과 배치 쪼개기 · 재접속 지수 대기 (자바 v2.17.2 와 같음, 짝 엔진 7.0.1.3512 이상 - 리더 적재만은 7.0.1.3566 이상)
76
+
77
+ - **크기 초과로 거절된 배치는 쪼개 다시 보낸다**(사용자 지시 2026-10-05, Kafka 와 같게): 2건 이상 배치가
78
+ `MIMQE_TOPIC_MESSAGE_TOO_LARGE` 로 거절되면 레코드 경계에서 나눠 다시 보낸다. 쪼개기는 재시도 횟수를 쓰지 않고, 조각은 원래
79
+ 배치의 전달 시한을 이어받는다. 1건짜리 배치는 종전처럼 실패한다. 조각은 레코드 바이트를 원래의 절반 이하로 잡아 늘 둘 이상으로
80
+ 나뉜다 - 운영 중 토픽 세그먼트가 줄어 큰 배치가 거절될 때 맞을 때까지 쪼갠다(전에는 그 배치를 실패로 올렸다).
81
+ - 멱등이면 쪼갠 조각에 보낼 때 번호를 새로 매긴다(원래 배치의 번호는 돌려받는다). 결과를 모르는 시도가 있었던 멱등 배치는
82
+ 쪼개지 않는다 - 앞 시도가 적재됐을 수 있어 종전처럼 `commit in doubt` 로 올린다.
83
+ - **재접속 대기가 지수로 는다**: 새 설정 `reconnectBackoffMs`(기본 50) / `reconnectBackoffMaxMs`(기본 1000), snake
84
+ `reconnect_backoff_ms` / `reconnect_backoff_max_ms`. 주소마다 잇달아 실패한 횟수 n 으로 첫 값 x 2^n 을 쉬고 상한에서 멈춘다(±20%).
85
+ 연결에 성공하면 처음 값으로 돌아간다. 상한을 정하지 않고 첫 값만 정하면 상한도 그 값이다(고정 대기).
86
+ - producer: 부트스트랩 재접속, 파티션 리더 연결, 생성자의 첫 연결이 후보를 다시 도는 간격(전에는 100ms -> 1s 고정 배수).
87
+ - 구독(`ILSubscribeOptions` / `ILPatternOptions` 의 같은 이름 옵션): 끊긴 노드(파티션 리더 · 코디네이터) 연결. 쉬는 동안 그
88
+ 노드의 파티션은 건너뛰고 다른 노드의 파티션을 읽는다. 전에는 다음 read 마다 곧바로 다시 열었다.
89
+ - **메타데이터 재질의 간격도 지수로**: 토픽 메타데이터(FLUSH 중)와 리더 표 재질의가 잇달아 실패하면 `retryBackoffMs` 부터 두 배씩
90
+ `retryBackoffMaxMs` 까지 늘린다. 성공하면 처음 값으로.
91
+ - 지수 대기의 지터: 지수를 상한에서 먼저 멈추고 흔든다 - 상한에 닿은 뒤의 대기는 상한의 0.8 ~ 1배다(0.21.1 은 상한으로 고정됐다).
92
+
93
+ ## 0.21.1 변경 - acks=0 완결 시점 · retries 를 Kafka 와 같게 (자바 v2.17.1 과 같음, 짝 엔진 7.0.1.3512 이상 - 리더 적재만은 7.0.1.3566 이상)
94
+
95
+ - **acks=0 은 소켓에 다 쓴 뒤 완결한다**(사용자 지시 2026-10-05): 봉투의 마지막 바이트를 커널에 넘긴 뒤 future 가 성공(오프셋 -1)
96
+ 으로 끝난다. 전에는 송신 큐에 넣는 순간 성공으로 끝나, 쓰기 전에 연결이 끊긴 배치도 성공으로 보였다. 다 쓰기 전에 끊긴 배치는
97
+ 엔진이 온전히 받지 못한 것이라 **retries 안에서 다시 붙은 연결로 다시 보내고**, 예산을 다 쓰면
98
+ `MIMQE_BIO_ERROR (acks=0 batch not written ...)` 로 실패한다. `flush()` 는 그 배치가 다 쓰일 때까지 기다린다.
99
+ - **retries 를 Kafka 와 같게**:
100
+ - 멱등을 지정하지 않았으면 `retries(0)` 일 때도 멱등이 꺼진다(acks `"all"` · `maxInFlight <= 5` 에 더해). `enableIdempotence(True)`
101
+ 를 명시하고 `retries(0)` 이면 생성이 `ValueError("enableIdempotence requires retries > 0")` 로 실패한다.
102
+ - **재시도 대기가 지수로 는다**(Kafka 3.7 KIP-580): 같은 배치의 재시도 대기가 `retryBackoffMs`(기본 100)에서 시작해 실패마다 두
103
+ 배로 늘고 새 설정 `retryBackoffMaxMs`(기본 1000, snake `retry_backoff_max_ms`)에서 멈춘다. 매번 ±20% 를 흔든다.
104
+ `retryBackoffMs` 가 상한보다 크면 상한을 고정 대기로 쓴다. 전처럼 늘 같은 대기를 원하면 두 값을 같게 준다. 연결을 다시 잇는
105
+ 간격 · 메타데이터를 다시 묻는 간격은 그대로 `retryBackoffMs` 다.
106
+
107
+ ## 0.21.0 변경 - acks 를 Kafka 와 같은 뜻으로 (자바 v2.17.0 과 같음, 짝 엔진 7.0.1.3512 이상 - 리더 적재만은 7.0.1.3566 이상)
108
+
109
+ - ☠ **acks 를 Kafka 와 같은 뜻으로**(사용자 지시 2026-10-05, 엔진 7.0.1.3566 ACKSLEADER-1): `acks("all")` / `-1` = ISR 팔로워
110
+ 확인(**기본**), `acks(1)` = **리더 적재만**, `acks(0)` = 무응답. 문자열 `"all"` · `"-1"` · `"1"` · `"0"` 도 받는다.
111
+ - **행동 변경**: 0.20.x 까지의 `acks(1)` 은 클러스터에서 팔로워 확인까지(지금의 `"all"`)였다. 이 판부터 `acks(1)` 을 **명시한**
112
+ 코드는 리더 적재만으로 약해진다 - 예전과 같게 하려면 `"all"` 을 주거나 지정하지 않는다(기본 `"all"` 이라 지정하지 않은 코드는
113
+ 그대로다). `get_acks()` 는 all 을 `-1` 로 돌려준다.
114
+ - 멱등 규칙도 Kafka 3.x 와 같다: 지정하지 않았으면 acks 가 `"all"` 일 때만 켜지고(`0` / `1` 이면 꺼짐), `enableIdempotence(True)`
115
+ 를 명시하고 `"all"` 이 아니면 생성이 `ValueError("enableIdempotence requires acks=all")` 로 실패한다.
116
+ - 엔진: 리더만은 봉투 acks 바이트 2 로 싣는다. 7.0.1.3566 전 엔진은 2 를 all 로 읽는다(더 엄격). 리더만 · 0 은 토픽 MINSYNC 를
117
+ 보지 않는다. 소비 가시성은 그대로 HW 뒤다.
118
+
119
+ ## 0.20.6 변경 - 얼어붙은 노드를 건너뛰는 핸드셰이크 시한 · 승격 대기 (자바 v2.16.6 과 같음, 짝 엔진 7.0.1.3512 이상)
120
+
121
+ > 0.20.4 · 0.20.5 는 test.pypi 시험 판으로만 냈다(회귀 BOOTFROZEN-1). 0.20.4 는 얼린 리더를 건너뛴 뒤 팔로워가 승격 전이면 생성이
122
+ > 5초 만에 실패했고, 0.20.5 는 생성자가 승격 전에 끝나면 첫 send 가 얼린 옛 리더로 리더 링크를 열며 30초를 썼다.
123
+
124
+ - **얼어붙은 노드에서 부트스트랩이 30초를 통째로 기다리지 않는다**: 멈춘(SIGSTOP) · 얼어붙은 브로커는 커널이 TCP 연결을 받아
125
+ 주므로 접속은 되고 첫 응답이 오지 않는다. 후보를 도는 접속(프로듀서 부트스트랩 · 재접속, `ILQmgr` 주소 목록 접속 ·
126
+ `reconnect` · 구독 연결 · 팔로워가 준 리더 힌트로 옮겨 붙기)에서 **다른 후보가 남아 있으면** TCP 접속과 첫 `NEW_CONNECTION`
127
+ 응답을 핸드셰이크 시한(기본 5초)만 기다리고 다음 후보로 간다. 그 시한을 넘긴 노드는 그 순회 동안 응답 없음으로 쳐서 리더
128
+ 힌트로도 다시 가지 않는다. 응답 없음이 아닌 다른 후보가 없으면(주소 하나로 붙는 접속 등) 종전처럼 요청 · 세션 시한을 다
129
+ 기다린다 - 바쁜 브로커를 거짓 실패시키지 않는다. PID 발급 · 메타데이터 같은 그 뒤 요청은 종전 시한이다.
130
+ - 설정: `ILProducerConfig.handshakeTimeoutMs(ms)`(snake `handshake_timeout_ms`), `ILQmgr.set_handshake_timeout(ms)` /
131
+ `setHandshakeTimeout`. 0 이하면 끈다.
132
+ - 팔로워 하나로 접속했는데 그 팔로워가 얼어붙은 옛 리더를 힌트로 주는 경우(장애 순간), 힌트 노드에서 짧은 시한 뒤 원래
133
+ 주소로 돌아간다(그사이 리더가 됐으면 붙는다). `ILQmgr` 의 단일 주소 접속도 힌트로 옮겨 붙을 때 원래 주소를 뒤 후보로 둔다.
134
+ - **승격 대기**: 리더가 얼어 있고 팔로워가 아직 승격 전이면(팔로워가 얼린 옛 리더를 힌트로 준다) 후보가 바닥난다. 프로듀서
135
+ 생성은 `requestTimeoutMs` 안에서 100ms → 1초로 쉬며 다시 돌아 승격이 끝나면 새 리더에 붙는다(끝내 안 되면 종전과 같은
136
+ 시간에 마지막 거절로 실패). `ILQmgr` 후보 순회는 2회전 고정이던 것을 세션 타임아웃 안에서 1초 간격으로 되풀이한다(3회전부터는
137
+ 리더 힌트 · 연결 계열 실패일 때만). 응답 없음 표시는 회차마다 새로 시작하고, 확정 거절(권한 · 토픽 없음 등)은 다시 돌지 않는다.
138
+ - **파티션 리더 링크**: 리더 표가 얼린 옛 리더를 가리키면(승격 전) 링크 열기도 핸드셰이크 시한 뒤 그 주소만 쉬고 리더 표를
139
+ 다시 받는다 - 승격 뒤 새 리더로 간다. 같은 주소에서 잇달아 3번 넘기면 다음 한 번은 `requestTimeoutMs` 로 열고(늘 느린 리더도
140
+ 끝내 붙게), 그렇게 오래 여는 동안에도 리더 표를 1초 간격으로 다시 받아 표가 바뀌면 배치는 기다리지 않고 새 리더로 간다.
141
+
63
142
  ## 0.20.3 변경 - 토픽 파티션 격리본 재주입 (자바 v2.16.3 과 같음, 짝 엔진 7.0.1.3512 이상 - 토픽 격리본은 7.0.1.3553 이상)
64
143
 
65
144
  - 격리본 토픽 파티션(엔진 7.0.1.3553 QUARTOOL-2): `reinjectQuarantine` 의 큐 인자를 선택으로 바꿨다 - 토픽 파티션 격리본은 큐를 비우고
@@ -10,6 +10,85 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
10
10
  ## 설치 방법
11
11
  pip install jetstream-api
12
12
 
13
+ ## 0.21.3 변경 - 메타데이터 재질의 '같은 응답' 지수 대기 (자바 v2.17.3 과 같음, 짝 엔진 7.0.1.3512 이상 - 리더 적재만은 7.0.1.3566 이상)
14
+
15
+ > 0.21.0 ~ 0.21.2 는 test.pypi 시험 판으로만 냈다. 아래 0.21.2 · 0.21.1 · 0.21.0 절도 이 판으로 처음 PyPI 에 나간다.
16
+
17
+ - **메타데이터 응답이 성공인데 진전이 없으면 재질의 간격이 는다**(사용자 결정 2026-10-05, Kafka 3.7 Metadata 와 같게): 리더 epoch 가
18
+ 그대로인 응답이 이어지면 횟수를 세어 `retryBackoffMs` 부터 두 배씩 `retryBackoffMaxMs` 까지 늘린다(±20%). 처음 보는 / 더 큰 리더
19
+ epoch, epoch 없는 응답, 실패, 5분 넘은 메타데이터는 횟수를 지운다. 성공 뒤에도 다음 재질의는 `retryBackoffMs` 뒤다.
20
+ - producer: 리더 표(파티션 리더 epoch)와 링크별 토픽 메타데이터(토픽 epoch). 크기 초과 뒤 세그먼트 크기 재조회는 이제 봉투를 막지
21
+ 않는다(거절된 배치는 쪼개 보낸다).
22
+ - 구독: 메타데이터 재조회(리더 바뀜 · 노드 끊김 뒤). 간격 안의 재조회는 건너뛰고 지금 메타데이터로 계속한다. 패턴 구독에 새
23
+ 토픽이 맞으면 곧바로 묻는다.
24
+
25
+ ## 0.21.2 변경 - 크기 초과 배치 쪼개기 · 재접속 지수 대기 (자바 v2.17.2 와 같음, 짝 엔진 7.0.1.3512 이상 - 리더 적재만은 7.0.1.3566 이상)
26
+
27
+ - **크기 초과로 거절된 배치는 쪼개 다시 보낸다**(사용자 지시 2026-10-05, Kafka 와 같게): 2건 이상 배치가
28
+ `MIMQE_TOPIC_MESSAGE_TOO_LARGE` 로 거절되면 레코드 경계에서 나눠 다시 보낸다. 쪼개기는 재시도 횟수를 쓰지 않고, 조각은 원래
29
+ 배치의 전달 시한을 이어받는다. 1건짜리 배치는 종전처럼 실패한다. 조각은 레코드 바이트를 원래의 절반 이하로 잡아 늘 둘 이상으로
30
+ 나뉜다 - 운영 중 토픽 세그먼트가 줄어 큰 배치가 거절될 때 맞을 때까지 쪼갠다(전에는 그 배치를 실패로 올렸다).
31
+ - 멱등이면 쪼갠 조각에 보낼 때 번호를 새로 매긴다(원래 배치의 번호는 돌려받는다). 결과를 모르는 시도가 있었던 멱등 배치는
32
+ 쪼개지 않는다 - 앞 시도가 적재됐을 수 있어 종전처럼 `commit in doubt` 로 올린다.
33
+ - **재접속 대기가 지수로 는다**: 새 설정 `reconnectBackoffMs`(기본 50) / `reconnectBackoffMaxMs`(기본 1000), snake
34
+ `reconnect_backoff_ms` / `reconnect_backoff_max_ms`. 주소마다 잇달아 실패한 횟수 n 으로 첫 값 x 2^n 을 쉬고 상한에서 멈춘다(±20%).
35
+ 연결에 성공하면 처음 값으로 돌아간다. 상한을 정하지 않고 첫 값만 정하면 상한도 그 값이다(고정 대기).
36
+ - producer: 부트스트랩 재접속, 파티션 리더 연결, 생성자의 첫 연결이 후보를 다시 도는 간격(전에는 100ms -> 1s 고정 배수).
37
+ - 구독(`ILSubscribeOptions` / `ILPatternOptions` 의 같은 이름 옵션): 끊긴 노드(파티션 리더 · 코디네이터) 연결. 쉬는 동안 그
38
+ 노드의 파티션은 건너뛰고 다른 노드의 파티션을 읽는다. 전에는 다음 read 마다 곧바로 다시 열었다.
39
+ - **메타데이터 재질의 간격도 지수로**: 토픽 메타데이터(FLUSH 중)와 리더 표 재질의가 잇달아 실패하면 `retryBackoffMs` 부터 두 배씩
40
+ `retryBackoffMaxMs` 까지 늘린다. 성공하면 처음 값으로.
41
+ - 지수 대기의 지터: 지수를 상한에서 먼저 멈추고 흔든다 - 상한에 닿은 뒤의 대기는 상한의 0.8 ~ 1배다(0.21.1 은 상한으로 고정됐다).
42
+
43
+ ## 0.21.1 변경 - acks=0 완결 시점 · retries 를 Kafka 와 같게 (자바 v2.17.1 과 같음, 짝 엔진 7.0.1.3512 이상 - 리더 적재만은 7.0.1.3566 이상)
44
+
45
+ - **acks=0 은 소켓에 다 쓴 뒤 완결한다**(사용자 지시 2026-10-05): 봉투의 마지막 바이트를 커널에 넘긴 뒤 future 가 성공(오프셋 -1)
46
+ 으로 끝난다. 전에는 송신 큐에 넣는 순간 성공으로 끝나, 쓰기 전에 연결이 끊긴 배치도 성공으로 보였다. 다 쓰기 전에 끊긴 배치는
47
+ 엔진이 온전히 받지 못한 것이라 **retries 안에서 다시 붙은 연결로 다시 보내고**, 예산을 다 쓰면
48
+ `MIMQE_BIO_ERROR (acks=0 batch not written ...)` 로 실패한다. `flush()` 는 그 배치가 다 쓰일 때까지 기다린다.
49
+ - **retries 를 Kafka 와 같게**:
50
+ - 멱등을 지정하지 않았으면 `retries(0)` 일 때도 멱등이 꺼진다(acks `"all"` · `maxInFlight <= 5` 에 더해). `enableIdempotence(True)`
51
+ 를 명시하고 `retries(0)` 이면 생성이 `ValueError("enableIdempotence requires retries > 0")` 로 실패한다.
52
+ - **재시도 대기가 지수로 는다**(Kafka 3.7 KIP-580): 같은 배치의 재시도 대기가 `retryBackoffMs`(기본 100)에서 시작해 실패마다 두
53
+ 배로 늘고 새 설정 `retryBackoffMaxMs`(기본 1000, snake `retry_backoff_max_ms`)에서 멈춘다. 매번 ±20% 를 흔든다.
54
+ `retryBackoffMs` 가 상한보다 크면 상한을 고정 대기로 쓴다. 전처럼 늘 같은 대기를 원하면 두 값을 같게 준다. 연결을 다시 잇는
55
+ 간격 · 메타데이터를 다시 묻는 간격은 그대로 `retryBackoffMs` 다.
56
+
57
+ ## 0.21.0 변경 - acks 를 Kafka 와 같은 뜻으로 (자바 v2.17.0 과 같음, 짝 엔진 7.0.1.3512 이상 - 리더 적재만은 7.0.1.3566 이상)
58
+
59
+ - ☠ **acks 를 Kafka 와 같은 뜻으로**(사용자 지시 2026-10-05, 엔진 7.0.1.3566 ACKSLEADER-1): `acks("all")` / `-1` = ISR 팔로워
60
+ 확인(**기본**), `acks(1)` = **리더 적재만**, `acks(0)` = 무응답. 문자열 `"all"` · `"-1"` · `"1"` · `"0"` 도 받는다.
61
+ - **행동 변경**: 0.20.x 까지의 `acks(1)` 은 클러스터에서 팔로워 확인까지(지금의 `"all"`)였다. 이 판부터 `acks(1)` 을 **명시한**
62
+ 코드는 리더 적재만으로 약해진다 - 예전과 같게 하려면 `"all"` 을 주거나 지정하지 않는다(기본 `"all"` 이라 지정하지 않은 코드는
63
+ 그대로다). `get_acks()` 는 all 을 `-1` 로 돌려준다.
64
+ - 멱등 규칙도 Kafka 3.x 와 같다: 지정하지 않았으면 acks 가 `"all"` 일 때만 켜지고(`0` / `1` 이면 꺼짐), `enableIdempotence(True)`
65
+ 를 명시하고 `"all"` 이 아니면 생성이 `ValueError("enableIdempotence requires acks=all")` 로 실패한다.
66
+ - 엔진: 리더만은 봉투 acks 바이트 2 로 싣는다. 7.0.1.3566 전 엔진은 2 를 all 로 읽는다(더 엄격). 리더만 · 0 은 토픽 MINSYNC 를
67
+ 보지 않는다. 소비 가시성은 그대로 HW 뒤다.
68
+
69
+ ## 0.20.6 변경 - 얼어붙은 노드를 건너뛰는 핸드셰이크 시한 · 승격 대기 (자바 v2.16.6 과 같음, 짝 엔진 7.0.1.3512 이상)
70
+
71
+ > 0.20.4 · 0.20.5 는 test.pypi 시험 판으로만 냈다(회귀 BOOTFROZEN-1). 0.20.4 는 얼린 리더를 건너뛴 뒤 팔로워가 승격 전이면 생성이
72
+ > 5초 만에 실패했고, 0.20.5 는 생성자가 승격 전에 끝나면 첫 send 가 얼린 옛 리더로 리더 링크를 열며 30초를 썼다.
73
+
74
+ - **얼어붙은 노드에서 부트스트랩이 30초를 통째로 기다리지 않는다**: 멈춘(SIGSTOP) · 얼어붙은 브로커는 커널이 TCP 연결을 받아
75
+ 주므로 접속은 되고 첫 응답이 오지 않는다. 후보를 도는 접속(프로듀서 부트스트랩 · 재접속, `ILQmgr` 주소 목록 접속 ·
76
+ `reconnect` · 구독 연결 · 팔로워가 준 리더 힌트로 옮겨 붙기)에서 **다른 후보가 남아 있으면** TCP 접속과 첫 `NEW_CONNECTION`
77
+ 응답을 핸드셰이크 시한(기본 5초)만 기다리고 다음 후보로 간다. 그 시한을 넘긴 노드는 그 순회 동안 응답 없음으로 쳐서 리더
78
+ 힌트로도 다시 가지 않는다. 응답 없음이 아닌 다른 후보가 없으면(주소 하나로 붙는 접속 등) 종전처럼 요청 · 세션 시한을 다
79
+ 기다린다 - 바쁜 브로커를 거짓 실패시키지 않는다. PID 발급 · 메타데이터 같은 그 뒤 요청은 종전 시한이다.
80
+ - 설정: `ILProducerConfig.handshakeTimeoutMs(ms)`(snake `handshake_timeout_ms`), `ILQmgr.set_handshake_timeout(ms)` /
81
+ `setHandshakeTimeout`. 0 이하면 끈다.
82
+ - 팔로워 하나로 접속했는데 그 팔로워가 얼어붙은 옛 리더를 힌트로 주는 경우(장애 순간), 힌트 노드에서 짧은 시한 뒤 원래
83
+ 주소로 돌아간다(그사이 리더가 됐으면 붙는다). `ILQmgr` 의 단일 주소 접속도 힌트로 옮겨 붙을 때 원래 주소를 뒤 후보로 둔다.
84
+ - **승격 대기**: 리더가 얼어 있고 팔로워가 아직 승격 전이면(팔로워가 얼린 옛 리더를 힌트로 준다) 후보가 바닥난다. 프로듀서
85
+ 생성은 `requestTimeoutMs` 안에서 100ms → 1초로 쉬며 다시 돌아 승격이 끝나면 새 리더에 붙는다(끝내 안 되면 종전과 같은
86
+ 시간에 마지막 거절로 실패). `ILQmgr` 후보 순회는 2회전 고정이던 것을 세션 타임아웃 안에서 1초 간격으로 되풀이한다(3회전부터는
87
+ 리더 힌트 · 연결 계열 실패일 때만). 응답 없음 표시는 회차마다 새로 시작하고, 확정 거절(권한 · 토픽 없음 등)은 다시 돌지 않는다.
88
+ - **파티션 리더 링크**: 리더 표가 얼린 옛 리더를 가리키면(승격 전) 링크 열기도 핸드셰이크 시한 뒤 그 주소만 쉬고 리더 표를
89
+ 다시 받는다 - 승격 뒤 새 리더로 간다. 같은 주소에서 잇달아 3번 넘기면 다음 한 번은 `requestTimeoutMs` 로 열고(늘 느린 리더도
90
+ 끝내 붙게), 그렇게 오래 여는 동안에도 리더 표를 1초 간격으로 다시 받아 표가 바뀌면 배치는 기다리지 않고 새 리더로 간다.
91
+
13
92
  ## 0.20.3 변경 - 토픽 파티션 격리본 재주입 (자바 v2.16.3 과 같음, 짝 엔진 7.0.1.3512 이상 - 토픽 격리본은 7.0.1.3553 이상)
14
93
 
15
94
  - 격리본 토픽 파티션(엔진 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.21.3"
4
4
 
5
5
  # --- Admin / Service ---
6
6
  from .admin import ILAdminService, ILAdminQmgr, ILLogBrowser, ILQueueBrowser
@@ -69,7 +69,8 @@ class GroupConsumer:
69
69
 
70
70
  def __init__(self, qmgr, group: str, topics, *, commit_mode: int = COMMIT_AUTO, durable: bool = True,
71
71
  start_mode: str = START_LATEST, poll_interval_ms: int = 0, auto_commit_ms: int = 0,
72
- manual=None, on_assign=None, make_msg=None, on_topic_error=None) -> None:
72
+ manual=None, on_assign=None, make_msg=None, on_topic_error=None, reconnect_backoff_ms: int = 50,
73
+ reconnect_backoff_max_ms: int = 1000) -> None:
73
74
  """멤버를 만든다. 연결은 :meth:`start` 가 연다.
74
75
 
75
76
  Args:
@@ -86,6 +87,8 @@ class GroupConsumer:
86
87
  make_msg (Callable): 레코드 객체 생성자(ILTopicMsg).
87
88
  on_topic_error (Callable | None): 주면 FETCH 의 토픽 오류(상태 9)를 올리지 않고 ``(토픽, 사유)`` 로 부른다
88
89
  (패턴 구독이 그 토픽을 떼어 낸다).
90
+ reconnect_backoff_ms (int): 끊긴 노드 연결을 다시 열기 전 첫 대기(밀리초, 0.21.2). 노드마다 두 배씩 는다.
91
+ reconnect_backoff_max_ms (int): 그 상한(밀리초).
89
92
  """
90
93
  self.qmgr = qmgr
91
94
  self.group = group
@@ -125,6 +128,13 @@ class GroupConsumer:
125
128
  self._hb_thread = None
126
129
  self._hb_stop = threading.Event()
127
130
  self._hb_err = None # 하트비트 스레드가 본 치명 오류(다음 read 가 올린다)
131
+ # 0.21.2: 노드 재연결 대기 - "host:port" -> [잇달아 실패한 횟수, 다시 열 수 있는 시각(monotonic)]. 열리면 지운다
132
+ self._rc_base = max(0, reconnect_backoff_ms) / 1000.0
133
+ self._rc_max = max(0, reconnect_backoff_max_ms) / 1000.0
134
+ self._rc = {}
135
+ # 0.21.3: 메타데이터 재질의 간격(실패 + 진전 없는 같은 응답 - 파티션 리더 epoch 로 가른다)
136
+ from .producer import _MetaPace
137
+ self._meta_pace = _MetaPace(_BACKOFF_S, _BACKOFF_MAX_S)
128
138
 
129
139
  # ── 연결 ─────────────────────────────────────────────────────────────
130
140
  def start(self) -> None:
@@ -165,16 +175,40 @@ class GroupConsumer:
165
175
  return self._boot
166
176
  s = self._sessions.get(addr)
167
177
  if s is None:
168
- s = self.qmgr._open_sub_session(addr)
178
+ # 0.21.2: 그 노드가 재연결 대기 중이면 남은 시간을 쉬고 연다(FETCH 는 _rc_left 로 먼저 건너뛴다)
179
+ left = self._rc_left(addr)
180
+ if left > 0:
181
+ time.sleep(left)
182
+ try:
183
+ s = self.qmgr._open_sub_session(addr)
184
+ except Exception:
185
+ self._rc_down(addr)
186
+ raise
187
+ self._rc.pop(addr, None)
169
188
  self._sessions[addr] = s
170
189
  return s
171
190
 
191
+ def _rc_left(self, addr: str | None) -> float:
192
+ st = self._rc.get(addr) if addr is not None else None
193
+ return max(0.0, st[1] - time.monotonic()) if st is not None else 0.0
194
+
195
+ def _rc_down(self, addr: str | None) -> None:
196
+ # 0.21.2: 노드 연결이 끊겼거나 열리지 않았다 - 다음 열기를 첫 대기 x 2^(잇달아 실패한 횟수)(상한, ±20%) 뒤로 미룬다
197
+ if addr is None:
198
+ return
199
+ from .producer import _exp_backoff
200
+ st = self._rc.get(addr) or [0, 0.0]
201
+ st[1] = time.monotonic() + _exp_backoff(self._rc_base, self._rc_max, st[0])
202
+ st[0] += 1
203
+ self._rc[addr] = st
204
+
172
205
  def _drop_session(self, addr: str | None) -> None:
173
206
  if addr is None:
174
207
  return
175
208
  s = self._sessions.pop(addr, None)
176
209
  if s is not None and s is not self._boot:
177
210
  _disconnect_quietly(s)
211
+ self._rc_down(addr)
178
212
 
179
213
  def _close_sessions(self) -> None:
180
214
  for s in list(self._sessions.values()):
@@ -196,23 +230,35 @@ class GroupConsumer:
196
230
  return self._call(sess, ILMsgUtil.create_request_msg_with_ext(code, ext, a1, a2, a3))
197
231
 
198
232
  # ── 메타데이터 ───────────────────────────────────────────────────────
199
- def _refresh_meta(self) -> None:
233
+ def _refresh_meta(self, new_topics: bool = False) -> None:
200
234
  # 3397 + "P" + 토픽 목록. 엔진이 모르면(REPORT 거절) 메타데이터 없이 모든 파티션을 boot 가 맡는다고 본다
235
+ # 0.21.3: 재질의 간격 - 마지막 응답 뒤 retryBackoffMs 규칙의 대기가 지나기 전이면 건너뛴다(지금 메타데이터로 계속한다).
236
+ # 실패와 진전 없는 같은 응답(리더 epoch 그대로)이 이어지면 간격이 는다. 새 토픽(패턴)이면 곧바로 묻는다
201
237
  if not self.topics:
202
238
  self._meta = {}
203
239
  return
240
+ pace = self._meta_pace
241
+ if new_topics:
242
+ pace.new_topics()
243
+ now = time.monotonic()
244
+ if self._meta is not None and not pace.allowed(now):
245
+ return
204
246
  try:
205
247
  res = self._request(self._boot, ILC.MIMQ_TOPIC_METADATA, G.enc_topics(self.topics), self.topics[0], "P", "")
206
- except ILSessionException:
248
+ except Exception:
249
+ pace.failed(time.monotonic())
207
250
  raise
208
251
  if res.getType() == ILC.MIMQ_LONG_REPORT_MESSAGE:
209
252
  topics, nodes = G.parse_metadata(_text(res))
210
253
  with self._lock:
211
254
  self._meta = topics
212
255
  self._nodes = nodes
256
+ pace.ok(time.monotonic(), [((t, p), pm.leader_epoch if pm.leader >= 0 else -1)
257
+ for t, tm in topics.items() for p, pm in tm.partitions.items()])
213
258
  else:
214
259
  with self._lock:
215
260
  self._meta = None
261
+ pace.ok(time.monotonic(), [("-", -1)])
216
262
 
217
263
  def _leader(self, topic: str, p: int):
218
264
  # (주소 또는 None=boot, leaderEpoch)
@@ -266,7 +312,7 @@ class GroupConsumer:
266
312
  # 둘 다 쉬었다가(_BACKOFF_S 부터 두 배씩, _BACKOFF_MAX_S 까지) _COORD_WAIT_S 안에서만 다시 한다. 한도를 넘기면 None - 부른 쪽이
267
313
  # MIMQE_NOT_AVAILABLE (group coordinator for <g>) 로 올린다(4175 / 4176 은 앱에 보이지 않는다)
268
314
  deadline = time.monotonic() + _COORD_WAIT_S
269
- pause = _BACKOFF_S
315
+ tries = 0
270
316
  while True:
271
317
  try:
272
318
  sess = self._coord_session()
@@ -289,8 +335,10 @@ class GroupConsumer:
289
335
  remain = deadline - time.monotonic()
290
336
  if remain <= 0:
291
337
  return None
292
- time.sleep(min(pause, remain))
293
- pause = min(pause * 2, _BACKOFF_MAX_S)
338
+ # 0.21.2: retryBackoffMs 규칙과 같은 지수 대기(두 배씩, 상한, ±20%)
339
+ from .producer import _exp_backoff
340
+ time.sleep(min(_exp_backoff(_BACKOFF_S, _BACKOFF_MAX_S, tries), remain))
341
+ tries += 1
294
342
 
295
343
  def _join(self) -> None:
296
344
  # 가입(재가입). AUTO 면 먼저 건넨 좌표를 동기 커밋한다(재배정으로 넘어갈 파티션의 진행을 잃지 않게)
@@ -527,6 +575,13 @@ class GroupConsumer:
527
575
  return False
528
576
  any_data = False
529
577
  items = list(groups.items())
578
+ # 0.21.2: 재연결 대기 중인 노드는 이번 바퀴에 건너뛴다 - 다른 노드의 파티션을 읽고, 모두 대기 중이면 남은 시간만 쉰다
579
+ waiting = [self._rc_left(a) for a, _ in items if a is not None and a not in self._sessions]
580
+ items = [(a, t) for a, t in items if a is None or a in self._sessions or self._rc_left(a) <= 0]
581
+ if not items:
582
+ if wait_ms > 0:
583
+ time.sleep(min(wait_ms / 1000.0, min(x for x in waiting if x > 0)))
584
+ return False
530
585
  for i, (addr, topics) in enumerate(items):
531
586
  w = wait_ms if i == len(items) - 1 else 0
532
587
  req = ILMsg(ILC.MIMQ_TOPIC_FETCH_V2_MESSAGE,
@@ -811,7 +866,7 @@ class GroupConsumer:
811
866
  self.topics = new
812
867
  self._need_join = self.manual is None
813
868
  if self._boot is not None:
814
- self._refresh_meta()
869
+ self._refresh_meta(new_topics=True)
815
870
  return True
816
871
 
817
872
  # ── 종료 ─────────────────────────────────────────────────────────────
@@ -38,7 +38,7 @@ class ILPatternOptions:
38
38
  _OPTIONS = frozenset((
39
39
  "startMode", "newTopicStartMode", "durable", "commitMode", "refreshMs",
40
40
  "maxTopics", "bufferPerTopic", "prefetchCount",
41
- "onTopicsChanged", "onTopicFailed",
41
+ "onTopicsChanged", "onTopicFailed", "reconnectBackoffMs", "reconnectBackoffMaxMs",
42
42
  ))
43
43
 
44
44
  def __init__(self) -> None:
@@ -55,6 +55,11 @@ class ILPatternOptions:
55
55
  self._prefetchCount = 0
56
56
  self._onTopicsChanged = None # callable(added, removed, current)
57
57
  self._onTopicFailed = None # callable(topic, exception)
58
+ #: 0.21.2: 노드 재연결 대기(첫 값 / 상한, 밀리초)
59
+ self._reconnectBackoffMs = 50
60
+ self._reconnectBackoffMaxMs = 1000
61
+ self._reconnectBackoffSet = False
62
+ self._reconnectBackoffMaxSet = False
58
63
 
59
64
  def __setattr__(self, name: str, value) -> None:
60
65
  # 구버전 호환: opt.maxTopics = 8 같은 직접 대입을 내부 슬롯으로 돌린다.
@@ -210,6 +215,67 @@ class ILPatternOptions:
210
215
  self._onTopicsChanged = fn; return self
211
216
 
212
217
  # ── 빌더 (snake 별칭) ───────────────────────────────────────────────
218
+ def reconnectBackoffMs(self, ms: int) -> "ILPatternOptions":
219
+ """끊긴 노드 연결(파티션 리더 · 코디네이터)을 다시 열기 전 첫 대기(밀리초). (기본 50, 0.21.2)
220
+
221
+ 노드마다 잇달아 실패한 횟수 n 을 세어 이 값 x 2^n 을 쉬고 :meth:`reconnectBackoffMaxMs` 에서 멈춘다(±20% 흔듦).
222
+ 연결에 성공하면 처음 값으로 돌아간다. 쉬는 동안 그 노드의 파티션은 건너뛰고 다른 노드의 파티션을 읽는다.
223
+ 전에는 끊긴 노드를 다음 read 마다 곧바로 다시 열었다.
224
+
225
+ Args:
226
+ ms (int): 첫 대기(밀리초). **음수는 0 으로 보정된다.**
227
+
228
+ Returns:
229
+ ILPatternOptions: 체이닝을 위해 자기 자신.
230
+ """
231
+ self._reconnectBackoffMs = 0 if ms < 0 else ms
232
+ self._reconnectBackoffSet = True
233
+ return self
234
+
235
+ def reconnectBackoffMaxMs(self, ms: int) -> "ILPatternOptions":
236
+ """노드 재연결 대기의 상한(밀리초). (기본 1000, 0.21.2)
237
+
238
+ 정하지 않고 :meth:`reconnectBackoffMs` 만 정하면 이 값도 그 값이 된다(늘리지 않는 고정 대기).
239
+
240
+ Args:
241
+ ms (int): 상한(밀리초). **음수는 0 으로 보정된다.**
242
+
243
+ Returns:
244
+ ILPatternOptions: 체이닝을 위해 자기 자신.
245
+ """
246
+ self._reconnectBackoffMaxMs = 0 if ms < 0 else ms
247
+ self._reconnectBackoffMaxSet = True
248
+ return self
249
+
250
+ def reconnect_backoff_ms(self, ms: int) -> "ILPatternOptions":
251
+ """:meth:`reconnectBackoffMs` 의 snake 별칭."""
252
+ return self.reconnectBackoffMs(ms)
253
+
254
+ def reconnect_backoff_max_ms(self, ms: int) -> "ILPatternOptions":
255
+ """:meth:`reconnectBackoffMaxMs` 의 snake 별칭."""
256
+ return self.reconnectBackoffMaxMs(ms)
257
+
258
+ def get_reconnect_backoff_ms(self) -> int:
259
+ """노드 재연결 첫 대기(밀리초). 기본 50.
260
+
261
+ Returns:
262
+ int: 첫 대기(밀리초).
263
+ """
264
+ return self._reconnectBackoffMs
265
+
266
+ def get_reconnect_backoff_max_ms(self) -> int:
267
+ """노드 재연결 대기의 상한(밀리초). 기본 1000. 정하지 않고 :meth:`reconnectBackoffMs` 만 정했으면 그 값이다.
268
+
269
+ Returns:
270
+ int: 상한(밀리초).
271
+ """
272
+ if not self._reconnectBackoffMaxSet and self._reconnectBackoffSet:
273
+ return self._reconnectBackoffMs
274
+ return self._reconnectBackoffMaxMs
275
+
276
+ getReconnectBackoffMs = get_reconnect_backoff_ms
277
+ getReconnectBackoffMaxMs = get_reconnect_backoff_max_ms
278
+
213
279
  def start_mode(self, mode: str) -> "ILPatternOptions":
214
280
  """:meth:`startMode` 의 snake 별칭."""
215
281
  return self.startMode(mode)
@@ -547,7 +613,8 @@ class ILPatternSubscription:
547
613
  self._c = GroupConsumer(
548
614
  qmgr, subscription_name, [], commit_mode=o.get_commit_mode(), durable=o.is_durable(),
549
615
  start_mode=o.get_start_mode() or ILTopic.START_LATEST, make_msg=ILTopicMsg,
550
- on_topic_error=self._topic_error)
616
+ on_topic_error=self._topic_error, reconnect_backoff_ms=o.get_reconnect_backoff_ms(),
617
+ reconnect_backoff_max_ms=o.get_reconnect_backoff_max_ms())
551
618
  try:
552
619
  self._c.open()
553
620
  self._refresh(initial=True)