jetstream-api 0.7.0__tar.gz → 0.7.2__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.7.0 → jetstream_api-0.7.2}/PKG-INFO +25 -2
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/README.md +24 -1
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/__init__.py +1 -1
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/producer.py +252 -116
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/qmgr.py +6 -3
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/jetstream_api.egg-info/PKG-INFO +25 -2
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/pyproject.toml +1 -1
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/LICENSE.txt +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/admin.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/admintopic.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/common.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/connectivity.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/exception.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/msg.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/partitioner.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/pattern.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/properties.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/py.typed +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/topic.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/ilink/util.py +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/jetstream_api.egg-info/SOURCES.txt +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/jetstream_api.egg-info/dependency_links.txt +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/jetstream_api.egg-info/requires.txt +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/jetstream_api.egg-info/top_level.txt +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/setup.cfg +0 -0
- {jetstream_api-0.7.0 → jetstream_api-0.7.2}/tests/test_example.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jetstream-api
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
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
|
|
@@ -54,6 +54,28 @@ Python으로 작성된 ILink 클라이언트 API입니다.
|
|
|
54
54
|
## 설치 방법
|
|
55
55
|
pip install jetstream-api
|
|
56
56
|
|
|
57
|
+
## 0.7.2 변경 - batchSize 기본값 262144 로 되돌림, 발행 경로 CPU 절감 (자바 v2.3.2 와 같음)
|
|
58
|
+
|
|
59
|
+
- `batchSize` 기본값을 **262144** 로 되돌렸습니다. 0.7.1 의 16384 에서 파이썬 클라는 처리량이 24~35% 낮았습니다(GIL 이
|
|
60
|
+
천장이라 배치마다 드는 비용이 그대로 처리량에서 빠집니다). 멱등 기본 켜짐은 그대로입니다.
|
|
61
|
+
- 발행 경로 CPU 를 줄였습니다(API 는 그대로입니다). 레코드 future 가 배치 결과 하나를 함께 가리키고(완결은 배치당 한 번),
|
|
62
|
+
키 -> 파티션 결과를 캐시하고, `send()` 가 설정값을 레코드마다 다시 읽지 않고, I/O 스레드를 깨우는 신호를 합치고,
|
|
63
|
+
ACK 하나의 배치 완결을 잠금 한 번에 묶습니다.
|
|
64
|
+
- producer 는 만들 때 설정 객체를 **복사**합니다(자바 v2.3.2 도 같고, Kafka 와 같습니다). 만든 뒤 넘긴 객체를 바꿔도 그
|
|
65
|
+
producer 에는 반영되지 않습니다 - 설정을 바꾸려면 producer 를 새로 만드십시오.
|
|
66
|
+
- 문서: 멱등이 거르는 것은 producer 안의 재전송뿐입니다 - 앱이 `send` 를 다시 부르면 새 레코드로 적재됩니다.
|
|
67
|
+
|
|
68
|
+
## 0.7.1 변경 - producer 기본값: 멱등 켜짐, batchSize 16384 (자바 v2.3.1 과 같음)
|
|
69
|
+
|
|
70
|
+
- `enableIdempotence` 기본값이 **켜짐**입니다. 멱등을 직접 정하지 않았으면 `acks(0)` 이나 `maxInFlight` 가 5 를 넘을 때
|
|
71
|
+
멱등이 저절로 꺼집니다. `enableIdempotence(True)` 를 명시하고 그 둘과 함께 쓰면 전처럼 생성 때 `ValueError` 입니다.
|
|
72
|
+
- 멱등이면 토픽을 비우거나(FLUSH) epoch 가 바뀐 순간 걸려 있던 배치를 다시 보내지 않고 실패로 올립니다
|
|
73
|
+
(`MIMQE_TOPIC_FLUSHED` / `MIMQE_TOPIC_EPOCH_MISMATCH`). 0.7.0 기본처럼 조용히 다시 보내려면 `enableIdempotence(False)` 를
|
|
74
|
+
명시하십시오.
|
|
75
|
+
- `batchSize` 기본값이 **16384** 입니다(파티션 배치 하나의 상한). 0.7.0 은 262144 였습니다 - 처리량이 모자라면
|
|
76
|
+
`batchSize(262144)` 처럼 늘리십시오.
|
|
77
|
+
- producer 수 안내: 한 토픽의 producer(연결)는 1~4개면 충분합니다(엔진 7.0.1.3407 실측).
|
|
78
|
+
|
|
57
79
|
## 0.7.0 변경 - 발행 v2: producer 하나 = 토픽 하나, 파티션별 배치, 봉투 겹쳐 보내기 (엔진 v2 필요)
|
|
58
80
|
|
|
59
81
|
발행 경로를 엔진의 새 발행 형식(v2)으로 바꿨습니다. **엔진도 v2 판이어야 합니다** - 옛 엔진에는 발행이 되지 않고
|
|
@@ -61,7 +83,8 @@ pip install jetstream-api
|
|
|
61
83
|
|
|
62
84
|
- **producer 하나 = 토픽 하나 = 연결 하나.** `qmgr.create_producer("ORDER.EVENT", config)` 로 만듭니다. 생성할 때 토픽의
|
|
63
85
|
파티션 수를 받아 두므로 없는 토픽이면 생성 단계에서 `MIMQE_OBJECT_NOT_FOUND` 로 실패합니다. 토픽이 여러 개면 토픽마다
|
|
64
|
-
producer 를
|
|
86
|
+
producer 를 만듭니다. 한 토픽의 producer(연결)는 1~4개면 충분합니다 - 엔진 7.0.1.3407 실측에서 8개로 늘리면 어느 크기든
|
|
87
|
+
오히려 줄었습니다. 큰 레코드(1KB 급)는 1개로도 디스크가 먼저 한계에 닿고, 작은 레코드(100B 급)는 4개 근처가 최대입니다.
|
|
65
88
|
- 레코드 토픽이 producer 토픽과 다르면 `send()` 가 `MIMQE_INVALID_ARGUMENT` 로 거부합니다.
|
|
66
89
|
- 옛 `create_producer(config)` / `ILTopicProducer(host, port, name, config)` 모양으로 부르면 `TypeError` 가 납니다.
|
|
67
90
|
- 배치는 **파티션마다** 모입니다. `batchSize`(기본 **262144**, 자바와 같음)는 파티션 배치 하나의 상한입니다. 보낼 때는
|
|
@@ -8,6 +8,28 @@ Python으로 작성된 ILink 클라이언트 API입니다.
|
|
|
8
8
|
## 설치 방법
|
|
9
9
|
pip install jetstream-api
|
|
10
10
|
|
|
11
|
+
## 0.7.2 변경 - batchSize 기본값 262144 로 되돌림, 발행 경로 CPU 절감 (자바 v2.3.2 와 같음)
|
|
12
|
+
|
|
13
|
+
- `batchSize` 기본값을 **262144** 로 되돌렸습니다. 0.7.1 의 16384 에서 파이썬 클라는 처리량이 24~35% 낮았습니다(GIL 이
|
|
14
|
+
천장이라 배치마다 드는 비용이 그대로 처리량에서 빠집니다). 멱등 기본 켜짐은 그대로입니다.
|
|
15
|
+
- 발행 경로 CPU 를 줄였습니다(API 는 그대로입니다). 레코드 future 가 배치 결과 하나를 함께 가리키고(완결은 배치당 한 번),
|
|
16
|
+
키 -> 파티션 결과를 캐시하고, `send()` 가 설정값을 레코드마다 다시 읽지 않고, I/O 스레드를 깨우는 신호를 합치고,
|
|
17
|
+
ACK 하나의 배치 완결을 잠금 한 번에 묶습니다.
|
|
18
|
+
- producer 는 만들 때 설정 객체를 **복사**합니다(자바 v2.3.2 도 같고, Kafka 와 같습니다). 만든 뒤 넘긴 객체를 바꿔도 그
|
|
19
|
+
producer 에는 반영되지 않습니다 - 설정을 바꾸려면 producer 를 새로 만드십시오.
|
|
20
|
+
- 문서: 멱등이 거르는 것은 producer 안의 재전송뿐입니다 - 앱이 `send` 를 다시 부르면 새 레코드로 적재됩니다.
|
|
21
|
+
|
|
22
|
+
## 0.7.1 변경 - producer 기본값: 멱등 켜짐, batchSize 16384 (자바 v2.3.1 과 같음)
|
|
23
|
+
|
|
24
|
+
- `enableIdempotence` 기본값이 **켜짐**입니다. 멱등을 직접 정하지 않았으면 `acks(0)` 이나 `maxInFlight` 가 5 를 넘을 때
|
|
25
|
+
멱등이 저절로 꺼집니다. `enableIdempotence(True)` 를 명시하고 그 둘과 함께 쓰면 전처럼 생성 때 `ValueError` 입니다.
|
|
26
|
+
- 멱등이면 토픽을 비우거나(FLUSH) epoch 가 바뀐 순간 걸려 있던 배치를 다시 보내지 않고 실패로 올립니다
|
|
27
|
+
(`MIMQE_TOPIC_FLUSHED` / `MIMQE_TOPIC_EPOCH_MISMATCH`). 0.7.0 기본처럼 조용히 다시 보내려면 `enableIdempotence(False)` 를
|
|
28
|
+
명시하십시오.
|
|
29
|
+
- `batchSize` 기본값이 **16384** 입니다(파티션 배치 하나의 상한). 0.7.0 은 262144 였습니다 - 처리량이 모자라면
|
|
30
|
+
`batchSize(262144)` 처럼 늘리십시오.
|
|
31
|
+
- producer 수 안내: 한 토픽의 producer(연결)는 1~4개면 충분합니다(엔진 7.0.1.3407 실측).
|
|
32
|
+
|
|
11
33
|
## 0.7.0 변경 - 발행 v2: producer 하나 = 토픽 하나, 파티션별 배치, 봉투 겹쳐 보내기 (엔진 v2 필요)
|
|
12
34
|
|
|
13
35
|
발행 경로를 엔진의 새 발행 형식(v2)으로 바꿨습니다. **엔진도 v2 판이어야 합니다** - 옛 엔진에는 발행이 되지 않고
|
|
@@ -15,7 +37,8 @@ pip install jetstream-api
|
|
|
15
37
|
|
|
16
38
|
- **producer 하나 = 토픽 하나 = 연결 하나.** `qmgr.create_producer("ORDER.EVENT", config)` 로 만듭니다. 생성할 때 토픽의
|
|
17
39
|
파티션 수를 받아 두므로 없는 토픽이면 생성 단계에서 `MIMQE_OBJECT_NOT_FOUND` 로 실패합니다. 토픽이 여러 개면 토픽마다
|
|
18
|
-
producer 를
|
|
40
|
+
producer 를 만듭니다. 한 토픽의 producer(연결)는 1~4개면 충분합니다 - 엔진 7.0.1.3407 실측에서 8개로 늘리면 어느 크기든
|
|
41
|
+
오히려 줄었습니다. 큰 레코드(1KB 급)는 1개로도 디스크가 먼저 한계에 닿고, 작은 레코드(100B 급)는 4개 근처가 최대입니다.
|
|
19
42
|
- 레코드 토픽이 producer 토픽과 다르면 `send()` 가 `MIMQE_INVALID_ARGUMENT` 로 거부합니다.
|
|
20
43
|
- 옛 `create_producer(config)` / `ILTopicProducer(host, port, name, config)` 모양으로 부르면 `TypeError` 가 납니다.
|
|
21
44
|
- 배치는 **파티션마다** 모입니다. `batchSize`(기본 **262144**, 자바와 같음)는 파티션 배치 하나의 상한입니다. 보낼 때는
|
|
@@ -12,6 +12,7 @@ producer 하나 = 토픽 하나 = 전용 연결 하나 + I/O 스레드 하나다
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import collections
|
|
15
|
+
import copy
|
|
15
16
|
import itertools
|
|
16
17
|
import os
|
|
17
18
|
import random
|
|
@@ -31,6 +32,10 @@ from .partitioner import partition_for_key
|
|
|
31
32
|
|
|
32
33
|
#: retries 기본값. 사실상 무제한 재시도를 뜻한다.
|
|
33
34
|
_INT_MAX = 2147483647
|
|
35
|
+
#: 키 -> 파티션 캐시에 둘 키 수 상한(producer 당). 차면 비우고 다시 채운다
|
|
36
|
+
_KEY_CACHE_MAX = 16384
|
|
37
|
+
#: 키 -> 파티션 캐시에 담을 키 길이 상한(바이트). 긴 키는 매번 해시한다(캐시 메모리를 묶어 둔다)
|
|
38
|
+
_KEY_CACHE_MAX_KEY = 256
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
class ILProducerConfig:
|
|
@@ -58,7 +63,8 @@ class ILProducerConfig:
|
|
|
58
63
|
self._acks = 1
|
|
59
64
|
#: 배치를 모으는 시간(ms). 0 이면 모이는 대로 즉시 보낸다
|
|
60
65
|
self._lingerMs = 0
|
|
61
|
-
|
|
66
|
+
# 0.7.2: 262144 로 되돌렸다(사용자 결정 2026-09-13). 0.7.1 의 16384 에서 24~35% 느렸다(GIL 천장)
|
|
67
|
+
#: 파티션 배치 하나의 크기 상한(바이트). 기본 262144
|
|
62
68
|
self._batchSize = 262144
|
|
63
69
|
#: 동시에 ACK 를 기다릴 봉투 수 상한
|
|
64
70
|
self._maxInFlight = 5
|
|
@@ -78,14 +84,20 @@ class ILProducerConfig:
|
|
|
78
84
|
self._maxRequestSize = 1048576
|
|
79
85
|
#: 접속한 큐관리자가 받는 프레임 길이 상한(바이트) - 엔진 Runtime@MaxFrameLength 기본 128MiB
|
|
80
86
|
self._maxFrameLength = ILC.MIMQ_DEFAULT_MAX_FRAME_LENGTH
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
# 0.7.1: Kafka 3.x 와 같은 기본 켜짐(사용자 결정 2026-09-13)
|
|
88
|
+
#: 멱등 발행(PID 발급 + 시퀀스). 재전송 시 중복 적재를 막는다. 기본 켜짐
|
|
89
|
+
self._enableIdempotence = True
|
|
90
|
+
#: 멱등을 사용자가 정했는지. 정하지 않았으면 acks=0 이나 maxInFlight > 5 에서 저절로 꺼진다
|
|
91
|
+
self._idempotenceSet = False
|
|
83
92
|
|
|
84
93
|
def __setattr__(self, name: str, value) -> None:
|
|
85
94
|
# 구버전 호환: cfg.acks = 0 / cfg.lingerMs = 5 같은 직접 대입을 내부 슬롯으로 돌린다.
|
|
86
95
|
# (예전 이름 idempotent 도 함께 받아 준다)
|
|
87
96
|
if name == "idempotent":
|
|
88
97
|
name = "enableIdempotence"
|
|
98
|
+
if name == "enableIdempotence":
|
|
99
|
+
# 직접 대입도 사용자가 정한 것이다 - 기본 멱등의 저절로 끄기를 적용하지 않는다
|
|
100
|
+
object.__setattr__(self, "_idempotenceSet", True)
|
|
89
101
|
if name in self._OPTIONS:
|
|
90
102
|
name = "_" + name
|
|
91
103
|
object.__setattr__(self, name, value)
|
|
@@ -104,7 +116,8 @@ class ILProducerConfig:
|
|
|
104
116
|
ILProducerConfig: 체이닝을 위해 자기 자신.
|
|
105
117
|
|
|
106
118
|
Note:
|
|
107
|
-
``acks=0`` 은
|
|
119
|
+
멱등(기본 켜짐)을 정하지 않았으면 ``acks=0`` 은 멱등을 끈다. ``enableIdempotence(True)`` 를
|
|
120
|
+
명시하고 ``acks=0`` 이면 :meth:`validate` 가 거부한다.
|
|
108
121
|
"""
|
|
109
122
|
self._acks = 0 if n == 0 else 1; return self
|
|
110
123
|
|
|
@@ -137,7 +150,8 @@ class ILProducerConfig:
|
|
|
137
150
|
ILProducerConfig: 체이닝을 위해 자기 자신.
|
|
138
151
|
|
|
139
152
|
Note:
|
|
140
|
-
뜻과 기본값이 Java 클라이언트와 같다(0.7.0
|
|
153
|
+
뜻과 기본값이 Java 클라이언트와 같다(0.7.2 에서 262144 로 되돌렸다. 0.7.1 은 16384 였고 파이썬은
|
|
154
|
+
그 값에서 24~35% 느렸다).
|
|
141
155
|
``lingerMs`` 가 0 이면 자르는 크기로만 쓰이고(배치는 보낼 수 있게 되는 즉시 나간다),
|
|
142
156
|
``lingerMs`` 가 0 보다 크면 전송 조건도 된다(이만큼 차거나 lingerMs 가 지나면 보낸다).
|
|
143
157
|
레코드 하나가 이 값보다 크면 그 레코드 혼자 배치 하나가 된다. 보낼 때는 준비된
|
|
@@ -156,9 +170,9 @@ class ILProducerConfig:
|
|
|
156
170
|
|
|
157
171
|
Note:
|
|
158
172
|
I/O 스레드가 봉투를 이만큼 겹쳐 보낸다(0.7.0). 엔진은 한 연결의 봉투를 겹쳐
|
|
159
|
-
처리하고 응답은 요청 순서대로 온다.
|
|
160
|
-
엔진이 파티션마다 최근 배치 5개로 중복을 가리기
|
|
161
|
-
``ValueError
|
|
173
|
+
처리하고 응답은 요청 순서대로 온다. **멱등(기본 켜짐)은 5 이하에서만 쓰인다** -
|
|
174
|
+
엔진이 파티션마다 최근 배치 5개로 중복을 가리기 때문이다. 멱등을 정하지 않았으면 5 를 넘길 때
|
|
175
|
+
멱등이 꺼지고, ``enableIdempotence(True)`` 를 명시했으면 :meth:`validate` 가 ``ValueError``.
|
|
162
176
|
"""
|
|
163
177
|
self._maxInFlight = max(1, n); return self
|
|
164
178
|
|
|
@@ -186,8 +200,10 @@ class ILProducerConfig:
|
|
|
186
200
|
보낸다(순서 유지). 재시도 총 시간은 :meth:`deliveryTimeoutMs` 로도 함께 제한된다.
|
|
187
201
|
|
|
188
202
|
Warning:
|
|
189
|
-
재시도는 **중복 적재를 만들 수 있다.** ACK 를
|
|
190
|
-
못 한 것은 구분되지 않는다.
|
|
203
|
+
비멱등(``enableIdempotence(False)``)이면 재시도는 **중복 적재를 만들 수 있다.** ACK 를
|
|
204
|
+
못 받은 것과 서버가 적재를 못 한 것은 구분되지 않는다. 멱등(기본 켜짐)이면 producer 안의
|
|
205
|
+
재시도는 엔진이 걸러 한 번만 적재된다. 앱이 ``send`` 를 다시 부르는 재발행은 새 레코드라
|
|
206
|
+
멱등이어도 걸러지지 않는다.
|
|
191
207
|
"""
|
|
192
208
|
self._retries = max(0, n); return self
|
|
193
209
|
|
|
@@ -310,23 +326,34 @@ class ILProducerConfig:
|
|
|
310
326
|
return self
|
|
311
327
|
|
|
312
328
|
def enableIdempotence(self, b: bool) -> "ILProducerConfig":
|
|
313
|
-
"""멱등 발행(중복 제거)을
|
|
329
|
+
"""멱등 발행(중복 제거)을 켜거나 끈다. (기본 켜짐 - 0.7.1)
|
|
314
330
|
|
|
315
331
|
켜면 접속 시 서버에서 PID 를 발급받아 파티션 배치마다 (PID, baseSeq) 를 실어 보낸다.
|
|
316
|
-
재시도로 같은 배치가 두 번 도착해도 서버가 시퀀스로 걸러 **한 번만 적재한다.**
|
|
332
|
+
재시도로 같은 배치가 두 번 도착해도 서버가 시퀀스로 걸러 **한 번만 적재한다.** 걸러지는 것은
|
|
333
|
+
producer 안의 재시도뿐이다 - 앱이 ``send`` 를 다시 부르면 새 레코드로 적재된다.
|
|
317
334
|
``maxInFlight`` 는 5 이하여야 한다.
|
|
318
335
|
|
|
336
|
+
이 메서드로 정하지 않으면 ``acks=0`` 이거나 ``maxInFlight`` 가 5 를 넘을 때 멱등이 저절로 꺼진다.
|
|
337
|
+
명시적으로 켜고 그 둘과 함께 쓰면 :meth:`validate` 가 거부한다.
|
|
338
|
+
|
|
339
|
+
멱등이면 토픽 FLUSH / epoch 변경으로 거절된 배치(``MIMQE_TOPIC_FLUSHED`` /
|
|
340
|
+
``MIMQE_TOPIC_EPOCH_MISMATCH``)를 다시 보내지 않고 실패로 올린다 - 엔진이 그 토픽의 번호 상태를
|
|
341
|
+
새로 잡으므로 다시 보낸 배치가 중복으로 가려져 사라질 수 있기 때문이다. 비멱등(False)은 메타데이터를
|
|
342
|
+
다시 받고 조용히 다시 보낸다.
|
|
343
|
+
|
|
319
344
|
Args:
|
|
320
|
-
b (bool): True 면 멱등
|
|
345
|
+
b (bool): True 면 멱등 발행, False 면 비멱등.
|
|
321
346
|
|
|
322
347
|
Returns:
|
|
323
348
|
ILProducerConfig: 체이닝을 위해 자기 자신.
|
|
324
349
|
|
|
325
350
|
Raises:
|
|
326
|
-
ValueError: (:meth:`validate` 시점) ``acks=0``
|
|
351
|
+
ValueError: (:meth:`validate` 시점) 명시적으로 켜고 ``acks=0`` 이나 ``maxInFlight`` > 5 와 함께 썼을 때.
|
|
327
352
|
응답을 받지 않는 모드에서는 중복 제거가 성립하지 않는다.
|
|
328
353
|
"""
|
|
329
|
-
self._enableIdempotence = bool(b)
|
|
354
|
+
self._enableIdempotence = bool(b)
|
|
355
|
+
self._idempotenceSet = True
|
|
356
|
+
return self
|
|
330
357
|
|
|
331
358
|
# ── 빌더 (snake 별칭) ───────────────────────────────────────────────
|
|
332
359
|
# Java 이름 빌더와 동작이 같다. 상세 설명은 각 원본 메서드를 참조.
|
|
@@ -404,18 +431,18 @@ class ILProducerConfig:
|
|
|
404
431
|
return self._lingerMs
|
|
405
432
|
|
|
406
433
|
def get_batch_size(self) -> int:
|
|
407
|
-
"""
|
|
434
|
+
"""파티션 배치 하나의 크기 상한(바이트). 기본 262144.
|
|
408
435
|
|
|
409
436
|
Returns:
|
|
410
|
-
int:
|
|
437
|
+
int: 파티션 배치 하나의 크기 상한(바이트). 기본 262144.
|
|
411
438
|
"""
|
|
412
439
|
return self._batchSize
|
|
413
440
|
|
|
414
441
|
def get_max_in_flight(self) -> int:
|
|
415
|
-
"""
|
|
442
|
+
"""응답을 기다리는 봉투 수의 상한. 기본 5.
|
|
416
443
|
|
|
417
444
|
Returns:
|
|
418
|
-
int:
|
|
445
|
+
int: 응답을 기다리는 봉투 수의 상한. 기본 5.
|
|
419
446
|
"""
|
|
420
447
|
return self._maxInFlight
|
|
421
448
|
|
|
@@ -484,12 +511,15 @@ class ILProducerConfig:
|
|
|
484
511
|
return self._maxFrameLength
|
|
485
512
|
|
|
486
513
|
def is_idempotence_enabled(self) -> bool:
|
|
487
|
-
"""멱등
|
|
514
|
+
"""실제로 쓰일 멱등 발행 여부. 기본 True.
|
|
488
515
|
|
|
489
516
|
Returns:
|
|
490
|
-
bool: 멱등
|
|
517
|
+
bool: 멱등 발행 여부. 정하지 않았으면 기본(True)이되, ``acks=0`` 이거나 ``maxInFlight`` 가
|
|
518
|
+
5 를 넘으면 False 다.
|
|
491
519
|
"""
|
|
492
|
-
|
|
520
|
+
if self._idempotenceSet:
|
|
521
|
+
return self._enableIdempotence
|
|
522
|
+
return self._acks != 0 and self._maxInFlight <= 5
|
|
493
523
|
|
|
494
524
|
getAcks = get_acks
|
|
495
525
|
getLingerMs = get_linger_ms
|
|
@@ -507,12 +537,13 @@ class ILProducerConfig:
|
|
|
507
537
|
|
|
508
538
|
def validate(self) -> None:
|
|
509
539
|
"""조합 검증. producer 생성 시 호출된다."""
|
|
510
|
-
|
|
540
|
+
# 0.7.1: 명시적으로 켰을 때만 막는다. 정하지 않은 기본 멱등은 is_idempotence_enabled() 에서 저절로 꺼진다
|
|
541
|
+
if self._idempotenceSet and self._enableIdempotence and self._acks == 0:
|
|
511
542
|
raise ValueError(
|
|
512
543
|
"enableIdempotence requires acks=1"
|
|
513
544
|
)
|
|
514
545
|
# 0.7.0: 엔진은 (pid, 파티션)마다 최근 배치 5개로 중복을 가린다 - 그보다 많이 겹쳐 보내면 재전송이 중복으로 가려지지 않는다
|
|
515
|
-
if self._enableIdempotence and self._maxInFlight > 5:
|
|
546
|
+
if self._idempotenceSet and self._enableIdempotence and self._maxInFlight > 5:
|
|
516
547
|
raise ValueError(
|
|
517
548
|
"enableIdempotence requires maxInFlight <= 5 (the engine remembers the last 5 batches"
|
|
518
549
|
" per partition; Kafka has the same limit)"
|
|
@@ -720,31 +751,75 @@ class ILRecordMetadata:
|
|
|
720
751
|
return "ILRecordMetadata(%s[%d]@%d)" % (self.topic, self.partition, self.offset)
|
|
721
752
|
|
|
722
753
|
|
|
754
|
+
class _BatchResult:
|
|
755
|
+
"""파티션 배치 하나의 결과(0.7.2). 같은 배치의 레코드 future 들이 함께 가리킨다 - 완결은 배치당 한 번이다."""
|
|
756
|
+
|
|
757
|
+
__slots__ = ("topic", "partition", "event", "base_offset", "timestamp", "error")
|
|
758
|
+
|
|
759
|
+
def __init__(self, topic: str | None, partition: int) -> None:
|
|
760
|
+
"""배치 결과를 만든다.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
topic (str | None): 토픽 이름.
|
|
764
|
+
partition (int): 파티션 번호.
|
|
765
|
+
"""
|
|
766
|
+
self.topic = topic
|
|
767
|
+
self.partition = partition
|
|
768
|
+
self.event = threading.Event()
|
|
769
|
+
self.base_offset = -1
|
|
770
|
+
self.timestamp = 0
|
|
771
|
+
self.error: Exception | None = None
|
|
772
|
+
|
|
773
|
+
def complete(self, base_offset: int, timestamp: int) -> None:
|
|
774
|
+
"""성공으로 정한다.
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
base_offset (int): 배치 첫 레코드의 오프셋(-1 = 모름).
|
|
778
|
+
timestamp (int): 적재 시각.
|
|
779
|
+
"""
|
|
780
|
+
self.base_offset = base_offset
|
|
781
|
+
self.timestamp = timestamp
|
|
782
|
+
self.event.set()
|
|
783
|
+
|
|
784
|
+
def fail(self, error: Exception) -> None:
|
|
785
|
+
"""실패로 정한다.
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
error (Exception): 실패 원인.
|
|
789
|
+
"""
|
|
790
|
+
self.error = error
|
|
791
|
+
self.event.set()
|
|
792
|
+
|
|
793
|
+
|
|
723
794
|
class ILPublishFuture:
|
|
724
795
|
"""발행 완결을 기다리는 핸들. :meth:`get` 이 :class:`ILRecordMetadata` 를 돌려준다.
|
|
725
796
|
|
|
726
797
|
**동기 발행은 ``send(rec).get()``** 으로 쓴다.
|
|
727
798
|
"""
|
|
728
799
|
|
|
729
|
-
def __init__(self, timeout_ms: int) -> None:
|
|
800
|
+
def __init__(self, timeout_ms: int, _result: "_BatchResult | None" = None, _index: int = 0) -> None:
|
|
730
801
|
"""**응용이 직접 만들지 않는다** - :meth:`ILTopicProducer.send` 가 돌려준다.
|
|
731
802
|
|
|
732
803
|
Args:
|
|
733
804
|
timeout_ms (int): :meth:`get` 에 인자를 주지 않았을 때 쓸 기본 대기
|
|
734
805
|
시간(밀리초). producer 의 ``deliveryTimeoutMs`` 가 들어온다.
|
|
806
|
+
_result (_BatchResult | None): 내부용 - 이 레코드가 든 파티션 배치의 결과(배치의 레코드들이 함께 가리킨다).
|
|
807
|
+
_index (int): 내부용 - 배치 안 순번. 좌표는 배치 첫 오프셋 + 순번이다.
|
|
735
808
|
"""
|
|
736
|
-
|
|
809
|
+
# 0.7.2: 완결은 배치 결과 하나로 한다(레코드마다 Event 를 만들지 않는다). 좌표는 get() 때 만든다
|
|
810
|
+
self._result = _result if _result is not None else _BatchResult(None, -1)
|
|
811
|
+
self._index = _index
|
|
737
812
|
self._meta: ILRecordMetadata | None = None
|
|
738
|
-
self._error: Exception | None = None
|
|
739
813
|
self._timeout_ms = timeout_ms
|
|
740
814
|
|
|
741
815
|
def _complete(self, meta: ILRecordMetadata) -> None:
|
|
816
|
+
# 배치 없이 만든 future 용(호환)
|
|
742
817
|
self._meta = meta
|
|
743
|
-
self.
|
|
818
|
+
self._result.event.set()
|
|
744
819
|
|
|
745
820
|
def _fail(self, error: Exception) -> None:
|
|
746
|
-
self.
|
|
747
|
-
self.
|
|
821
|
+
self._result.error = error
|
|
822
|
+
self._result.event.set()
|
|
748
823
|
|
|
749
824
|
def get(self, timeout_ms: int | None = None) -> ILRecordMetadata:
|
|
750
825
|
"""발행이 끝날 때까지 기다렸다가 좌표를 돌려준다.
|
|
@@ -766,12 +841,18 @@ class ILPublishFuture:
|
|
|
766
841
|
완결은 producer 의 I/O 스레드가 한다(0.7.0). ``lingerMs`` 가 0 이어도 ``send()``
|
|
767
842
|
가 돌아온 시점에 완결되어 있다는 보장은 없다 - 결과가 필요하면 이 메서드로 기다린다.
|
|
768
843
|
"""
|
|
844
|
+
r = self._result
|
|
769
845
|
wait = (timeout_ms if timeout_ms is not None else self._timeout_ms) / 1000.0
|
|
770
|
-
if not
|
|
846
|
+
if not r.event.wait(wait if wait > 0 else None):
|
|
771
847
|
raise ILException(TimeoutError("publish timed out"))
|
|
772
|
-
if
|
|
773
|
-
raise
|
|
774
|
-
|
|
848
|
+
if r.error is not None:
|
|
849
|
+
raise r.error
|
|
850
|
+
m = self._meta
|
|
851
|
+
if m is None:
|
|
852
|
+
m = ILRecordMetadata(r.topic, r.partition, r.base_offset + self._index if r.base_offset >= 0 else -1,
|
|
853
|
+
r.timestamp)
|
|
854
|
+
self._meta = m
|
|
855
|
+
return m
|
|
775
856
|
|
|
776
857
|
def is_done(self) -> bool:
|
|
777
858
|
"""완결(성공이든 실패든)되었는지.
|
|
@@ -779,7 +860,7 @@ class ILPublishFuture:
|
|
|
779
860
|
Returns:
|
|
780
861
|
bool: 결과가 정해졌으면 True. True 여도 :meth:`get` 이 예외를 올릴 수 있다.
|
|
781
862
|
"""
|
|
782
|
-
return self.
|
|
863
|
+
return self._result.event.is_set()
|
|
783
864
|
|
|
784
865
|
def cancel(self, may_interrupt_if_running: bool = False) -> bool:
|
|
785
866
|
"""발행은 취소할 수 없다. **항상 False**.
|
|
@@ -936,18 +1017,19 @@ def _as_il(exc: BaseException) -> Exception:
|
|
|
936
1017
|
class _Batch:
|
|
937
1018
|
"""파티션 배치 1개 = 와이어의 PartitionBatch 1개. ``buf`` 의 앞 52바이트가 배치 헤더다."""
|
|
938
1019
|
|
|
939
|
-
__slots__ = ("partition", "created", "batch_no", "buf", "count", "
|
|
1020
|
+
__slots__ = ("partition", "created", "batch_no", "buf", "count", "result", "callbacks",
|
|
940
1021
|
"closed", "done", "base_seq", "header_done", "header_pid", "retry_count",
|
|
941
1022
|
"retry_at", "unknown", "size_drops", "solo")
|
|
942
1023
|
|
|
943
|
-
def __init__(self, partition: int, created: float, batch_no: int) -> None:
|
|
1024
|
+
def __init__(self, partition: int, created: float, batch_no: int, topic: str = "") -> None:
|
|
944
1025
|
self.partition = partition
|
|
945
1026
|
self.created = created
|
|
946
1027
|
#: 만든 순서. 파티션 안에서 보내야 하는 순서 = 이 순서
|
|
947
1028
|
self.batch_no = batch_no
|
|
948
1029
|
self.buf = bytearray(_BATCH_HEADER)
|
|
949
1030
|
self.count = 0
|
|
950
|
-
|
|
1031
|
+
#: 이 배치 레코드들의 결과(0.7.2, 배치당 하나). 레코드 future 는 이것과 배치 안 순번만 든다
|
|
1032
|
+
self.result = _BatchResult(topic, partition)
|
|
951
1033
|
self.callbacks: list | None = None
|
|
952
1034
|
#: 더 붙이지 않는다(batchSize 에 닿음, drain 이 가져감, 되돌림)
|
|
953
1035
|
self.closed = False
|
|
@@ -1001,16 +1083,18 @@ class ILTopicProducer:
|
|
|
1001
1083
|
"""토픽 하나에 묶인 발행자. ``ILQmgr.create_producer(topic, config)`` 로 얻는다. (0.7.0 발행 v2)
|
|
1002
1084
|
|
|
1003
1085
|
producer 하나 = 토픽 하나 = 전용 연결 하나(clientName + ``"-PRD"``) + I/O 스레드 하나다. 생성할 때
|
|
1004
|
-
연결을 열고 토픽의 파티션 수·epoch·세그먼트 크기를 받아 둔다.
|
|
1005
|
-
|
|
1086
|
+
연결을 열고 토픽의 파티션 수·epoch·세그먼트 크기를 받아 둔다. 한 토픽의 producer(연결)는 1~4개면
|
|
1087
|
+
충분하다 - 엔진 7.0.1.3407 실측에서 8개로 늘리면 어느 크기든 오히려 줄었다. 큰 레코드(1KB 급)는 1개로도
|
|
1088
|
+
디스크가 먼저 한계에 닿고, 작은 레코드(100B 급)는 4개 근처가 최대다.
|
|
1006
1089
|
|
|
1007
1090
|
보장:
|
|
1008
1091
|
|
|
1009
1092
|
- **순서**: 한 파티션 안에서는 ``send`` 순서대로 적재된다. 파티션 사이의 순서는 보장하지 않는다.
|
|
1010
1093
|
비멱등에서 FLUSH TOPIC 과 겹친 배치를 다시 보낼 때는 이미 나가 있던 같은 파티션의 뒤 배치가
|
|
1011
1094
|
먼저 적재될 수 있다.
|
|
1012
|
-
- **전달**: ``acks=1`` + 재시도 = at-least-once.
|
|
1013
|
-
제거되어
|
|
1095
|
+
- **전달**: ``acks=1`` + 재시도 = at-least-once. 멱등(기본 켜짐)이면 producer 안의 재전송이 엔진에서
|
|
1096
|
+
중복 제거되어 중복 적재가 없다(적재하지 못한 배치는 future 가 실패로 알린다). 앱이 ``send`` 를 다시
|
|
1097
|
+
부르면 새 레코드라 멱등이어도 걸러지지 않는다. ``acks=0`` = at-most-once.
|
|
1014
1098
|
- **재시도**: 연결 단절, 비멱등의 FLUSHED / epoch 불일치(적재되지 않은 것이 확정된 거절), 멱등의
|
|
1015
1099
|
순서 오류다. 그 밖의 거절(토픽 없음, 너무 큼 등)은 재시도 없이 그 배치의 future 가 실패한다.
|
|
1016
1100
|
PID 가 무효가 되면 PID 를 다시 받고 seq 를 0 부터 다시 매긴다.
|
|
@@ -1036,13 +1120,14 @@ class ILTopicProducer:
|
|
|
1036
1120
|
port (int | None): 포트. ``host`` 에 ``:포트`` 가 붙어 있으면 생략한다.
|
|
1037
1121
|
client_name (str): 큐 관리자에 등록될 클라이언트 이름. 기본 ``"PRODUCER"``.
|
|
1038
1122
|
topic (str): 발행할 토픽 이름. **필수.** 이 producer 는 이 토픽에만 발행한다.
|
|
1039
|
-
config (ILProducerConfig | None): 발행 설정. 기본 None 이면 전 항목 기본값.
|
|
1123
|
+
config (ILProducerConfig | None): 발행 설정. 기본 None 이면 전 항목 기본값. 생성 때 복사한다 -
|
|
1124
|
+
이후 넘긴 객체를 바꿔도 이 producer 에는 반영되지 않는다.
|
|
1040
1125
|
|
|
1041
1126
|
Raises:
|
|
1042
1127
|
TypeError: ``topic`` 이 토픽 이름(str)이 아닐 때 - 0.6.x 의
|
|
1043
1128
|
``ILTopicProducer(host, port, name, config)`` 모양으로 부르면 여기서 멈춘다.
|
|
1044
|
-
ValueError: ``topic`` 이 비었거나 ``config`` 조합이 잘못됐을 때(멱등 + ``acks=0``,
|
|
1045
|
-
멱등 + ``maxInFlight`` > 5).
|
|
1129
|
+
ValueError: ``topic`` 이 비었거나 ``config`` 조합이 잘못됐을 때(명시적으로 켠 멱등 + ``acks=0``,
|
|
1130
|
+
명시적으로 켠 멱등 + ``maxInFlight`` > 5).
|
|
1046
1131
|
ILOperationException: 주소 목록이 비었거나, 접속·토픽 조회를 거절당했을 때
|
|
1047
1132
|
(없는 토픽이면 ``MIMQE_OBJECT_NOT_FOUND``).
|
|
1048
1133
|
ILException: 후보 주소 어디에도 접속하지 못했을 때.
|
|
@@ -1058,7 +1143,8 @@ class ILTopicProducer:
|
|
|
1058
1143
|
" (got %s) - since 0.7.0 a producer publishes to one topic" % type(topic).__name__)
|
|
1059
1144
|
if not topic:
|
|
1060
1145
|
raise ValueError("topic must not be empty - a producer publishes to one topic")
|
|
1061
|
-
|
|
1146
|
+
# 0.7.2: 설정을 생성 때 복사한다(Kafka·자바와 같다) - 넘긴 객체를 나중에 바꿔도 이 producer 에는 반영되지 않는다
|
|
1147
|
+
self.config = copy.copy(config) if config is not None else ILProducerConfig()
|
|
1062
1148
|
self.config.validate()
|
|
1063
1149
|
cfg = self.config
|
|
1064
1150
|
self.topic = topic
|
|
@@ -1075,6 +1161,13 @@ class ILTopicProducer:
|
|
|
1075
1161
|
self._backoff = cfg.get_retry_backoff_ms() / 1000.0
|
|
1076
1162
|
self._request_timeout = cfg.get_request_timeout_ms() / 1000.0
|
|
1077
1163
|
self._delivery_timeout = cfg.get_delivery_timeout_ms() / 1000.0
|
|
1164
|
+
# 0.7.2: send() 가 레코드마다 부르던 설정 게터를 생성 때 한 번 읽는다
|
|
1165
|
+
self._delivery_timeout_ms = cfg.get_delivery_timeout_ms()
|
|
1166
|
+
self._max_request_size = cfg.get_max_request_size()
|
|
1167
|
+
self._max_frame_length = cfg.get_max_frame_length()
|
|
1168
|
+
self._buffer_memory = cfg.get_buffer_memory()
|
|
1169
|
+
self._max_block = cfg.get_max_block_ms() / 1000.0
|
|
1170
|
+
self._batch_size = cfg.get_batch_size()
|
|
1078
1171
|
#: 봉투 본문(IL 헤더 뒤) 상한 = min(maxRequestSize, maxFrameLength - 25)
|
|
1079
1172
|
self._max_env_content = min(cfg.get_max_request_size(),
|
|
1080
1173
|
cfg.get_max_frame_length() - ILC.SZ_IL_HEADER)
|
|
@@ -1111,6 +1204,10 @@ class ILTopicProducer:
|
|
|
1111
1204
|
self._used = 0
|
|
1112
1205
|
self._sticky = -1
|
|
1113
1206
|
self._sticky_bytes = 0
|
|
1207
|
+
#: 키 -> 파티션 캐시(0.7.2). 파티션 수가 바뀌면 비운다
|
|
1208
|
+
self._key_parts: dict = {}
|
|
1209
|
+
#: 깨울 신호를 이미 보냈고 I/O 스레드가 아직 비우지 않았다(0.7.2 - 신호를 합친다)
|
|
1210
|
+
self._wake_pending = False
|
|
1114
1211
|
self._pid = -1
|
|
1115
1212
|
#: drain 이 큐에서 꺼낸 배치를 아직 in-flight 에 올리지(acks=0 이면 완결하지) 않은 동안 1 - flush 가 먼저 돌아가지 않게
|
|
1116
1213
|
self._draining = 0
|
|
@@ -1265,6 +1362,8 @@ class ILTopicProducer:
|
|
|
1265
1362
|
self._parts = parts
|
|
1266
1363
|
if self._sticky >= n:
|
|
1267
1364
|
self._sticky = -1
|
|
1365
|
+
if n != self._partition_count:
|
|
1366
|
+
self._key_parts.clear()
|
|
1268
1367
|
self._partition_count = n
|
|
1269
1368
|
self._cv.notify_all()
|
|
1270
1369
|
if len(self._next_seq) != n:
|
|
@@ -1355,22 +1454,21 @@ class ILTopicProducer:
|
|
|
1355
1454
|
rec_size = _RECORD_OVERHEAD + kl + pl + len(value)
|
|
1356
1455
|
# 이 레코드 혼자 실린 봉투의 본문 크기
|
|
1357
1456
|
alone = _ENVELOPE_FIXED + len(self._topic_bytes) + _BATCH_HEADER + rec_size
|
|
1358
|
-
if alone >
|
|
1457
|
+
if alone > self._max_request_size:
|
|
1359
1458
|
raise ILOperationException("MIMQE_MSG_SIZE_OVER (record too large: %d > maxRequestSize %d - envelope carrying"
|
|
1360
|
-
" this record alone)" % (alone,
|
|
1459
|
+
" this record alone)" % (alone, self._max_request_size))
|
|
1361
1460
|
# 엔진은 Runtime@MaxFrameLength(기본 128MiB) 를 넘는 프레임을 사유 없이 끊는다 - 보내기 전에 막는다
|
|
1362
|
-
if alone + ILC.SZ_IL_HEADER >
|
|
1461
|
+
if alone + ILC.SZ_IL_HEADER > self._max_frame_length:
|
|
1363
1462
|
raise ILOperationException(
|
|
1364
1463
|
"MIMQE_MSG_SIZE_OVER (record too large: frame %d > maxFrameLength %d - the engine drops the connection"
|
|
1365
1464
|
" over its Runtime@MaxFrameLength; raise both if the server allows more)"
|
|
1366
|
-
% (alone + ILC.SZ_IL_HEADER,
|
|
1465
|
+
% (alone + ILC.SZ_IL_HEADER, self._max_frame_length))
|
|
1367
1466
|
rec = b"".join((_I32.pack(kl if key else -1), key or b"",
|
|
1368
1467
|
_I32.pack(pl if props else -1), props or b"",
|
|
1369
1468
|
_I32.pack(len(value)), value))
|
|
1370
|
-
future = ILPublishFuture(cfg.get_delivery_timeout_ms())
|
|
1371
1469
|
with self._cv:
|
|
1372
|
-
# maxBlockMs 는 버퍼 대기의 상한이다(Kafka max.block.ms)
|
|
1373
|
-
deadline =
|
|
1470
|
+
# maxBlockMs 는 버퍼 대기의 상한이다(Kafka max.block.ms). 기한은 기다려야 할 때만 잡는다
|
|
1471
|
+
deadline = None
|
|
1374
1472
|
partition = self._choose_partition(record.partition, key)
|
|
1375
1473
|
while True:
|
|
1376
1474
|
if self._fatal is not None:
|
|
@@ -1392,16 +1490,18 @@ class ILTopicProducer:
|
|
|
1392
1490
|
fresh = (b is None or b.closed or len(b.buf) + rec_size > self._batch_limit
|
|
1393
1491
|
or (seg > 0 and len(b.buf) + rec_size + (b.count + 1) * self._v3_per_record > seg))
|
|
1394
1492
|
need = rec_size + (_BATCH_HEADER if fresh else 0)
|
|
1395
|
-
if need >
|
|
1493
|
+
if need > self._buffer_memory:
|
|
1396
1494
|
raise ILOperationException("MIMQE_MSG_SIZE_OVER (record too large: needs %d bytes of buffer"
|
|
1397
|
-
" > bufferMemory %d)" % (need,
|
|
1398
|
-
if self._used + need >
|
|
1495
|
+
" > bufferMemory %d)" % (need, self._buffer_memory))
|
|
1496
|
+
if self._used + need > self._buffer_memory:
|
|
1399
1497
|
# 버퍼 확보(가득이면 maxBlockMs 대기). 기다리는 사이 배치가 나가거나 파티션 수가 바뀔 수 있어 처음부터 다시 본다
|
|
1498
|
+
if deadline is None:
|
|
1499
|
+
deadline = time.monotonic() + self._max_block
|
|
1400
1500
|
remain = deadline - time.monotonic()
|
|
1401
1501
|
if remain <= 0:
|
|
1402
1502
|
# 부하 문제다(Kafka BufferExhaustedException) - 큐 가득과 같은 분류(재시도 가능)로 올린다
|
|
1403
1503
|
raise ILOperationException("MIMQE_QUEUE_FULL (buffer memory full %d/%d - blocked over maxBlockMs)"
|
|
1404
|
-
% (self._used,
|
|
1504
|
+
% (self._used, self._buffer_memory))
|
|
1405
1505
|
self._cv.wait(remain)
|
|
1406
1506
|
continue
|
|
1407
1507
|
wake = False
|
|
@@ -1409,14 +1509,14 @@ class ILTopicProducer:
|
|
|
1409
1509
|
if b is not None:
|
|
1410
1510
|
b.closed = True
|
|
1411
1511
|
self._batch_counter += 1
|
|
1412
|
-
b = _Batch(partition, time.monotonic(), self._batch_counter)
|
|
1512
|
+
b = _Batch(partition, time.monotonic(), self._batch_counter, self.topic)
|
|
1413
1513
|
q.append(b)
|
|
1414
1514
|
self._queued += 1
|
|
1415
1515
|
self._used += _BATCH_HEADER
|
|
1416
1516
|
wake = True
|
|
1417
1517
|
b.buf += rec
|
|
1418
1518
|
self._used += rec_size
|
|
1419
|
-
b.
|
|
1519
|
+
future = ILPublishFuture(self._delivery_timeout_ms, b.result, b.count)
|
|
1420
1520
|
if callback is not None:
|
|
1421
1521
|
if b.callbacks is None:
|
|
1422
1522
|
b.callbacks = [None] * b.count
|
|
@@ -1445,7 +1545,15 @@ class ILTopicProducer:
|
|
|
1445
1545
|
% (explicit, n - 1))
|
|
1446
1546
|
return explicit
|
|
1447
1547
|
if key:
|
|
1448
|
-
|
|
1548
|
+
# 0.7.2: 같은 키는 같은 파티션이다 - 파티션 수가 그대로인 동안 결과를 캐시한다(순수 파이썬 murmur2 가 레코드당 수 us)
|
|
1549
|
+
p = self._key_parts.get(key)
|
|
1550
|
+
if p is None:
|
|
1551
|
+
p = partition_for_key(key, n)
|
|
1552
|
+
if len(key) <= _KEY_CACHE_MAX_KEY:
|
|
1553
|
+
if len(self._key_parts) >= _KEY_CACHE_MAX:
|
|
1554
|
+
self._key_parts.clear()
|
|
1555
|
+
self._key_parts[key] = p
|
|
1556
|
+
return p
|
|
1449
1557
|
if self._sticky < 0 or self._sticky >= n:
|
|
1450
1558
|
self._sticky = random.randrange(n)
|
|
1451
1559
|
self._sticky_bytes = 0
|
|
@@ -1454,7 +1562,7 @@ class ILTopicProducer:
|
|
|
1454
1562
|
def _sticky_advance(self, nbytes: int) -> None:
|
|
1455
1563
|
"""키 없는 레코드를 붙인 만큼 센다. ``batchSize`` 를 채우면 다음 파티션으로 넘긴다(Kafka 3.3+ 균일 스티키)."""
|
|
1456
1564
|
self._sticky_bytes += nbytes
|
|
1457
|
-
if self._sticky_bytes >= self.
|
|
1565
|
+
if self._sticky_bytes >= self._batch_size:
|
|
1458
1566
|
self._sticky = (self._sticky + 1) % self._partition_count
|
|
1459
1567
|
self._sticky_bytes = 0
|
|
1460
1568
|
|
|
@@ -1573,10 +1681,15 @@ class ILTopicProducer:
|
|
|
1573
1681
|
|
|
1574
1682
|
# ── I/O 스레드 ───────────────────────────────────────────────────────
|
|
1575
1683
|
def _wakeup(self) -> None:
|
|
1684
|
+
# 0.7.2: 깨울 신호가 이미 가 있으면(I/O 스레드가 아직 비우지 않았으면) 다시 쓰지 않는다 - 배치를 열고 닫을 때마다 쓰던
|
|
1685
|
+
# 시스템 호출을 합친다. I/O 스레드는 신호를 비운 뒤에 이 표시를 내리고, 그다음에 큐를 본다
|
|
1686
|
+
if self._wake_pending:
|
|
1687
|
+
return
|
|
1688
|
+
self._wake_pending = True
|
|
1576
1689
|
try:
|
|
1577
1690
|
self._wake_w.send(b"\x00")
|
|
1578
1691
|
except OSError:
|
|
1579
|
-
|
|
1692
|
+
self._wake_pending = False
|
|
1580
1693
|
|
|
1581
1694
|
def _drain_wake(self) -> None:
|
|
1582
1695
|
try:
|
|
@@ -1584,6 +1697,8 @@ class ILTopicProducer:
|
|
|
1584
1697
|
pass
|
|
1585
1698
|
except OSError:
|
|
1586
1699
|
pass
|
|
1700
|
+
# 비운 뒤에 내린다. 먼저 내리면 비우는 사이 온 신호를 먹고도 표시가 올라간 채 남아 다음 깨우기가 막힐 수 있다
|
|
1701
|
+
self._wake_pending = False
|
|
1587
1702
|
|
|
1588
1703
|
def _wake_waiters(self) -> None:
|
|
1589
1704
|
with self._cv:
|
|
@@ -1942,45 +2057,56 @@ class ILTopicProducer:
|
|
|
1942
2057
|
raise ConnectionError(results[0][4] or "MIMQE_INVALID_PID")
|
|
1943
2058
|
now = time.monotonic()
|
|
1944
2059
|
again = []
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
if
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2060
|
+
# 0.7.2: 이 ACK 에서 완결한 배치들의 버퍼는 아래 잠금 한 번에 반납한다(배치마다 잠금을 잡지 않는다)
|
|
2061
|
+
freed = 0
|
|
2062
|
+
try:
|
|
2063
|
+
for b, (code, off, ts, exp, reason) in zip(env.batches, results):
|
|
2064
|
+
# seq 틈: 이 배치의 어느 시도도 적재됐을 수 없는데(결과를 모른 적이 없다) 엔진이 중복이라 한다 = 같은 파티션의
|
|
2065
|
+
# 뒤 배치가 먼저 기준선으로 적재돼(두 봉투 사이 리더 교체 등) 이 번호가 틈으로 남았다. 성공으로 올리면 조용한 유실이다.
|
|
2066
|
+
if code == ILC.MIMQ_TOPIC_PUBLISH_DUPLICATE and self._idem and b.base_seq >= 0 and not b.unknown:
|
|
2067
|
+
freed += self._complete_batch(b, -1, 0, ILOperationException(
|
|
2068
|
+
"MIMQE_OUT_OF_ORDER_SEQUENCE (sequence gap: partition %d seq %d..%d was never stored but the engine"
|
|
2069
|
+
" answered duplicate - a later batch of this partition was stored first)"
|
|
2070
|
+
% (b.partition, b.base_seq, b.base_seq + b.count - 1)), release=False)
|
|
2071
|
+
continue
|
|
2072
|
+
if code == ILC.MIMQ_SUCCESSFUL_TOPIC_PUBLISH or code == ILC.MIMQ_TOPIC_PUBLISH_DUPLICATE:
|
|
2073
|
+
# 중복 응답은 원 좌표로 성공한다. 엔진 링 밖이면 baseOffset 이 -1 이다.
|
|
2074
|
+
self._pid_resets = 0
|
|
2075
|
+
if self._idem and b.base_seq >= 0 and b.partition < len(self._acked_seq):
|
|
2076
|
+
last = b.base_seq + b.count - 1
|
|
2077
|
+
if last > self._acked_seq[b.partition]:
|
|
2078
|
+
self._acked_seq[b.partition] = last
|
|
2079
|
+
freed += self._complete_batch(b, off, ts, None, release=False)
|
|
2080
|
+
continue
|
|
2081
|
+
# epoch 가 바뀌었거나(FLUSH / 재생성), 토픽이 비워지는 중이거나, 세그먼트가 줄었다 - 메타데이터를 다시 받는다
|
|
2082
|
+
if code in (ILC.MIMQ_TOPIC_PUBLISH_EPOCH_MISMATCH, ILC.MIMQ_TOPIC_PUBLISH_FLUSHED,
|
|
2083
|
+
ILC.MIMQ_TOPIC_PUBLISH_TOO_LARGE):
|
|
2084
|
+
self._meta_stale = True
|
|
2085
|
+
expected = exp if code == ILC.MIMQ_OUT_OF_ORDER_SEQUENCE else -1
|
|
2086
|
+
if self._idem and b.base_seq >= 0:
|
|
2087
|
+
# 배치별 거절 = 이번 시도는 적재되지 않았다(엔진 계약). 번호를 돌려받고 재번호를 건다.
|
|
2088
|
+
# 단 앞선 시도가 답 없이 끝난 배치(unknown)는 그때 적재됐을 수 있다 - 순서 오류가 아니면 번호를
|
|
2089
|
+
# 돌려받지 않는다. 돌려받아 다음 새 배치에 주면 그 배치가 엔진에서 중복 응답으로 사라진다(유실).
|
|
2090
|
+
if expected >= 0 or not b.unknown:
|
|
2091
|
+
self._note_rejected(b.partition, b.base_seq, expected)
|
|
2092
|
+
b.base_seq = -1
|
|
2093
|
+
b.header_done = False
|
|
2094
|
+
if self._can_retry(b, code, expected >= 0, now):
|
|
2095
|
+
b.retry_count += 1
|
|
2096
|
+
b.retry_at = now + self._backoff
|
|
2097
|
+
again.append(b)
|
|
2098
|
+
else:
|
|
2099
|
+
freed += self._complete_batch(b, -1, 0, _batch_error(code, reason), release=False)
|
|
2100
|
+
except BaseException:
|
|
2101
|
+
# 도중에 끊겨도 이미 완결한 배치의 버퍼는 돌려준다
|
|
2102
|
+
if freed:
|
|
2103
|
+
with self._cv:
|
|
2104
|
+
self._used -= freed
|
|
2105
|
+
self._cv.notify_all()
|
|
2106
|
+
raise
|
|
1982
2107
|
gone: list = []
|
|
1983
2108
|
with self._cv:
|
|
2109
|
+
self._used -= freed
|
|
1984
2110
|
# 배치는 되돌린 것을 큐에 넣은 **뒤에** in-flight 에서 뺀다. 먼저 빼면 그 사이 flush() 가 비었다고 보고 돌아간다.
|
|
1985
2111
|
if again:
|
|
1986
2112
|
gone = self._requeue_batches(again)
|
|
@@ -2059,30 +2185,40 @@ class ILTopicProducer:
|
|
|
2059
2185
|
self._queued += 1
|
|
2060
2186
|
return gone
|
|
2061
2187
|
|
|
2062
|
-
def _complete_batch(self, b: _Batch, base_offset: int, timestamp: int, err: Exception | None
|
|
2188
|
+
def _complete_batch(self, b: _Batch, base_offset: int, timestamp: int, err: Exception | None,
|
|
2189
|
+
release: bool = True) -> int:
|
|
2190
|
+
"""배치 하나를 완결한다. 레코드 future 들은 배치 결과 하나를 함께 가리키므로 결과를 한 번 정하면 모두 풀린다(0.7.2).
|
|
2191
|
+
|
|
2192
|
+
``release`` 가 False 면 버퍼 반납(``self._used``)과 대기자 깨우기를 부른 쪽이 한꺼번에 한다 -
|
|
2193
|
+
ACK 하나에 든 배치들을 잠금 한 번에 반납하려는 것이다. 반납할 바이트 수를 돌려준다.
|
|
2194
|
+
"""
|
|
2063
2195
|
if b.done:
|
|
2064
|
-
return
|
|
2196
|
+
return 0
|
|
2065
2197
|
b.done = True
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
self._cv
|
|
2069
|
-
|
|
2070
|
-
|
|
2198
|
+
freed = len(b.buf)
|
|
2199
|
+
if release:
|
|
2200
|
+
with self._cv:
|
|
2201
|
+
self._used -= freed
|
|
2202
|
+
self._cv.notify_all()
|
|
2203
|
+
r = b.result
|
|
2204
|
+
if err is None:
|
|
2205
|
+
r.complete(base_offset, timestamp)
|
|
2206
|
+
else:
|
|
2207
|
+
r.fail(err)
|
|
2208
|
+
callbacks = b.callbacks
|
|
2071
2209
|
b.callbacks = None
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
fut._fail(err)
|
|
2080
|
-
cb = callbacks[i] if callbacks is not None else None
|
|
2081
|
-
if cb is not None:
|
|
2210
|
+
if callbacks:
|
|
2211
|
+
topic = self.topic
|
|
2212
|
+
for i, cb in enumerate(callbacks):
|
|
2213
|
+
if cb is None:
|
|
2214
|
+
continue
|
|
2215
|
+
meta = (ILRecordMetadata(topic, b.partition, base_offset + i if base_offset >= 0 else -1, timestamp)
|
|
2216
|
+
if err is None else None)
|
|
2082
2217
|
try:
|
|
2083
2218
|
cb(meta, err)
|
|
2084
2219
|
except Exception: # noqa: BLE001 - 콜백 예외는 무시한다(계약)
|
|
2085
2220
|
pass
|
|
2221
|
+
return freed
|
|
2086
2222
|
|
|
2087
2223
|
def _fail_batches(self, batches, err: Exception | None) -> None:
|
|
2088
2224
|
"""파티션이 없어진 배치 등을 실패시킨다(self._cv 밖에서 부른다)."""
|
|
@@ -1139,7 +1139,7 @@ class ILQmgr:
|
|
|
1139
1139
|
Args:
|
|
1140
1140
|
topic (str): 발행할 토픽 이름. 이 producer 는 이 토픽에만 발행한다.
|
|
1141
1141
|
config (ILProducerConfig | None): 발행 설정. 기본 None 이면 전 항목
|
|
1142
|
-
기본값(acks=1 / linger 0 / batch 256KB / maxInFlight 5 / 멱등
|
|
1142
|
+
기본값(acks=1 / linger 0 / batch 256KB / maxInFlight 5 / 멱등 켜짐).
|
|
1143
1143
|
|
|
1144
1144
|
Returns:
|
|
1145
1145
|
ILTopicProducer: 발행자. 다 쓰면 ``close()`` 로 닫는다.
|
|
@@ -1149,13 +1149,16 @@ class ILQmgr:
|
|
|
1149
1149
|
부르면 여기서 멈춘다.
|
|
1150
1150
|
ILOperationException: 아직 :meth:`connect` 하지 않았을 때, 없는 토픽일 때
|
|
1151
1151
|
(``MIMQE_OBJECT_NOT_FOUND``).
|
|
1152
|
-
ValueError: ``config`` 조합이 잘못됐을 때(멱등 + ``acks=0``, 멱등 +
|
|
1152
|
+
ValueError: ``config`` 조합이 잘못됐을 때(명시적으로 켠 멱등 + ``acks=0``, 명시적으로 켠 멱등 +
|
|
1153
|
+
``maxInFlight`` > 5).
|
|
1153
1154
|
ILException: 발행 전용 연결을 맺지 못했을 때.
|
|
1154
1155
|
|
|
1155
1156
|
Note:
|
|
1156
1157
|
발행자는 **전용 연결**을 하나 연다(클라이언트 이름 뒤에 ``"-PRD"`` 가 붙는다). 생성할 때 토픽의
|
|
1157
1158
|
파티션 수를 받아 두고, I/O 스레드가 봉투를 겹쳐 보낸다. 토픽이 여러 개면 토픽마다 producer 를
|
|
1158
|
-
|
|
1159
|
+
만든다. 한 토픽의 producer 는 1~4개면 충분하다 - 엔진 7.0.1.3407 실측에서 8개로 늘리면 어느 크기든
|
|
1160
|
+
오히려 줄었다. 큰 레코드(1KB 급)는 1개로도 디스크가 먼저 한계에 닿고, 작은 레코드(100B 급)는 4개
|
|
1161
|
+
근처가 최대다.
|
|
1159
1162
|
|
|
1160
1163
|
Example::
|
|
1161
1164
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jetstream-api
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
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
|
|
@@ -54,6 +54,28 @@ Python으로 작성된 ILink 클라이언트 API입니다.
|
|
|
54
54
|
## 설치 방법
|
|
55
55
|
pip install jetstream-api
|
|
56
56
|
|
|
57
|
+
## 0.7.2 변경 - batchSize 기본값 262144 로 되돌림, 발행 경로 CPU 절감 (자바 v2.3.2 와 같음)
|
|
58
|
+
|
|
59
|
+
- `batchSize` 기본값을 **262144** 로 되돌렸습니다. 0.7.1 의 16384 에서 파이썬 클라는 처리량이 24~35% 낮았습니다(GIL 이
|
|
60
|
+
천장이라 배치마다 드는 비용이 그대로 처리량에서 빠집니다). 멱등 기본 켜짐은 그대로입니다.
|
|
61
|
+
- 발행 경로 CPU 를 줄였습니다(API 는 그대로입니다). 레코드 future 가 배치 결과 하나를 함께 가리키고(완결은 배치당 한 번),
|
|
62
|
+
키 -> 파티션 결과를 캐시하고, `send()` 가 설정값을 레코드마다 다시 읽지 않고, I/O 스레드를 깨우는 신호를 합치고,
|
|
63
|
+
ACK 하나의 배치 완결을 잠금 한 번에 묶습니다.
|
|
64
|
+
- producer 는 만들 때 설정 객체를 **복사**합니다(자바 v2.3.2 도 같고, Kafka 와 같습니다). 만든 뒤 넘긴 객체를 바꿔도 그
|
|
65
|
+
producer 에는 반영되지 않습니다 - 설정을 바꾸려면 producer 를 새로 만드십시오.
|
|
66
|
+
- 문서: 멱등이 거르는 것은 producer 안의 재전송뿐입니다 - 앱이 `send` 를 다시 부르면 새 레코드로 적재됩니다.
|
|
67
|
+
|
|
68
|
+
## 0.7.1 변경 - producer 기본값: 멱등 켜짐, batchSize 16384 (자바 v2.3.1 과 같음)
|
|
69
|
+
|
|
70
|
+
- `enableIdempotence` 기본값이 **켜짐**입니다. 멱등을 직접 정하지 않았으면 `acks(0)` 이나 `maxInFlight` 가 5 를 넘을 때
|
|
71
|
+
멱등이 저절로 꺼집니다. `enableIdempotence(True)` 를 명시하고 그 둘과 함께 쓰면 전처럼 생성 때 `ValueError` 입니다.
|
|
72
|
+
- 멱등이면 토픽을 비우거나(FLUSH) epoch 가 바뀐 순간 걸려 있던 배치를 다시 보내지 않고 실패로 올립니다
|
|
73
|
+
(`MIMQE_TOPIC_FLUSHED` / `MIMQE_TOPIC_EPOCH_MISMATCH`). 0.7.0 기본처럼 조용히 다시 보내려면 `enableIdempotence(False)` 를
|
|
74
|
+
명시하십시오.
|
|
75
|
+
- `batchSize` 기본값이 **16384** 입니다(파티션 배치 하나의 상한). 0.7.0 은 262144 였습니다 - 처리량이 모자라면
|
|
76
|
+
`batchSize(262144)` 처럼 늘리십시오.
|
|
77
|
+
- producer 수 안내: 한 토픽의 producer(연결)는 1~4개면 충분합니다(엔진 7.0.1.3407 실측).
|
|
78
|
+
|
|
57
79
|
## 0.7.0 변경 - 발행 v2: producer 하나 = 토픽 하나, 파티션별 배치, 봉투 겹쳐 보내기 (엔진 v2 필요)
|
|
58
80
|
|
|
59
81
|
발행 경로를 엔진의 새 발행 형식(v2)으로 바꿨습니다. **엔진도 v2 판이어야 합니다** - 옛 엔진에는 발행이 되지 않고
|
|
@@ -61,7 +83,8 @@ pip install jetstream-api
|
|
|
61
83
|
|
|
62
84
|
- **producer 하나 = 토픽 하나 = 연결 하나.** `qmgr.create_producer("ORDER.EVENT", config)` 로 만듭니다. 생성할 때 토픽의
|
|
63
85
|
파티션 수를 받아 두므로 없는 토픽이면 생성 단계에서 `MIMQE_OBJECT_NOT_FOUND` 로 실패합니다. 토픽이 여러 개면 토픽마다
|
|
64
|
-
producer 를
|
|
86
|
+
producer 를 만듭니다. 한 토픽의 producer(연결)는 1~4개면 충분합니다 - 엔진 7.0.1.3407 실측에서 8개로 늘리면 어느 크기든
|
|
87
|
+
오히려 줄었습니다. 큰 레코드(1KB 급)는 1개로도 디스크가 먼저 한계에 닿고, 작은 레코드(100B 급)는 4개 근처가 최대입니다.
|
|
65
88
|
- 레코드 토픽이 producer 토픽과 다르면 `send()` 가 `MIMQE_INVALID_ARGUMENT` 로 거부합니다.
|
|
66
89
|
- 옛 `create_producer(config)` / `ILTopicProducer(host, port, name, config)` 모양으로 부르면 `TypeError` 가 납니다.
|
|
67
90
|
- 배치는 **파티션마다** 모입니다. `batchSize`(기본 **262144**, 자바와 같음)는 파티션 배치 하나의 상한입니다. 보낼 때는
|
|
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
|