jetstream-api 0.7.1__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.
Files changed (26) hide show
  1. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/PKG-INFO +12 -1
  2. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/README.md +11 -0
  3. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/__init__.py +1 -1
  4. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/producer.py +211 -98
  5. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/qmgr.py +1 -1
  6. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/jetstream_api.egg-info/PKG-INFO +12 -1
  7. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/pyproject.toml +1 -1
  8. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/LICENSE.txt +0 -0
  9. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/admin.py +0 -0
  10. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/admintopic.py +0 -0
  11. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/common.py +0 -0
  12. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/connectivity.py +0 -0
  13. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/exception.py +0 -0
  14. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/msg.py +0 -0
  15. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/partitioner.py +0 -0
  16. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/pattern.py +0 -0
  17. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/properties.py +0 -0
  18. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/py.typed +0 -0
  19. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/topic.py +0 -0
  20. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/ilink/util.py +0 -0
  21. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/jetstream_api.egg-info/SOURCES.txt +0 -0
  22. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/jetstream_api.egg-info/dependency_links.txt +0 -0
  23. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/jetstream_api.egg-info/requires.txt +0 -0
  24. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/jetstream_api.egg-info/top_level.txt +0 -0
  25. {jetstream_api-0.7.1 → jetstream_api-0.7.2}/setup.cfg +0 -0
  26. {jetstream_api-0.7.1 → 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.1
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,17 @@ 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
+
57
68
  ## 0.7.1 변경 - producer 기본값: 멱등 켜짐, batchSize 16384 (자바 v2.3.1 과 같음)
58
69
 
59
70
  - `enableIdempotence` 기본값이 **켜짐**입니다. 멱등을 직접 정하지 않았으면 `acks(0)` 이나 `maxInFlight` 가 5 를 넘을 때
@@ -8,6 +8,17 @@ 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
+
11
22
  ## 0.7.1 변경 - producer 기본값: 멱등 켜짐, batchSize 16384 (자바 v2.3.1 과 같음)
12
23
 
13
24
  - `enableIdempotence` 기본값이 **켜짐**입니다. 멱등을 직접 정하지 않았으면 `acks(0)` 이나 `maxInFlight` 가 5 를 넘을 때
@@ -1,6 +1,6 @@
1
1
  """iLink - Python client API for iLink M.O.M. (Message Oriented Middleware)."""
2
2
 
3
- __version__ = "0.7.1"
3
+ __version__ = "0.7.2"
4
4
 
5
5
  # --- Admin / Service ---
6
6
  from .admin import ILAdminService, ILAdminQmgr, ILLogBrowser, ILQueueBrowser
@@ -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,9 +63,9 @@ class ILProducerConfig:
58
63
  self._acks = 1
59
64
  #: 배치를 모으는 시간(ms). 0 이면 모이는 대로 즉시 보낸다
60
65
  self._lingerMs = 0
61
- # 0.7.1: Kafka batch.size 와 같은 16384(사용자 결정 2026-09-13). 0.7.0 은 262144 였다
62
- #: 파티션 배치 하나의 크기 상한(바이트). 기본 16384
63
- self._batchSize = 16384
66
+ # 0.7.2: 262144 로 되돌렸다(사용자 결정 2026-09-13). 0.7.1 의 16384 에서 24~35% 느렸다(GIL 천장)
67
+ #: 파티션 배치 하나의 크기 상한(바이트). 기본 262144
68
+ self._batchSize = 262144
64
69
  #: 동시에 ACK 를 기다릴 봉투 수 상한
65
70
  self._maxInFlight = 5
66
71
  #: 연결 실패 시 재시도 횟수
@@ -135,7 +140,7 @@ class ILProducerConfig:
135
140
  self._lingerMs = max(0, n); return self
136
141
 
137
142
  def batchSize(self, n: int) -> "ILProducerConfig":
138
- """파티션 배치 하나의 크기 상한(바이트)을 정한다. (기본 16384 = 16KB)
143
+ """파티션 배치 하나의 크기 상한(바이트)을 정한다. (기본 262144 = 256KB)
139
144
 
140
145
  Args:
141
146
  n (int): 상한(**바이트**). 배치는 파티션마다 따로 모인다. 레코드를 붙이면 이 값을
@@ -145,7 +150,8 @@ class ILProducerConfig:
145
150
  ILProducerConfig: 체이닝을 위해 자기 자신.
146
151
 
147
152
  Note:
148
- 뜻과 기본값이 Java 클라이언트와 같다(0.7.1 부터 16384. 0.7.0 은 262144 였다 - 처리량이 모자라면 늘린다).
153
+ 뜻과 기본값이 Java 클라이언트와 같다(0.7.2 에서 262144 로 되돌렸다. 0.7.1 은 16384 였고 파이썬은
154
+ 그 값에서 24~35% 느렸다).
149
155
  ``lingerMs`` 가 0 이면 자르는 크기로만 쓰이고(배치는 보낼 수 있게 되는 즉시 나간다),
150
156
  ``lingerMs`` 가 0 보다 크면 전송 조건도 된다(이만큼 차거나 lingerMs 가 지나면 보낸다).
151
157
  레코드 하나가 이 값보다 크면 그 레코드 혼자 배치 하나가 된다. 보낼 때는 준비된
@@ -194,8 +200,10 @@ class ILProducerConfig:
194
200
  보낸다(순서 유지). 재시도 총 시간은 :meth:`deliveryTimeoutMs` 로도 함께 제한된다.
195
201
 
196
202
  Warning:
197
- 재시도는 **중복 적재를 만들 수 있다.** ACK 를 못 받은 것과 서버가 적재를
198
- 못 한 것은 구분되지 않는다. 중복이 곤란하면 :meth:`enableIdempotence`.
203
+ 비멱등(``enableIdempotence(False)``)이면 재시도는 **중복 적재를 만들 수 있다.** ACK 를
204
+ 못 받은 것과 서버가 적재를 못 한 것은 구분되지 않는다. 멱등(기본 켜짐)이면 producer 안의
205
+ 재시도는 엔진이 걸러 한 번만 적재된다. 앱이 ``send`` 를 다시 부르는 재발행은 새 레코드라
206
+ 멱등이어도 걸러지지 않는다.
199
207
  """
200
208
  self._retries = max(0, n); return self
201
209
 
@@ -321,7 +329,8 @@ class ILProducerConfig:
321
329
  """멱등 발행(중복 제거)을 켜거나 끈다. (기본 켜짐 - 0.7.1)
322
330
 
323
331
  켜면 접속 시 서버에서 PID 를 발급받아 파티션 배치마다 (PID, baseSeq) 를 실어 보낸다.
324
- 재시도로 같은 배치가 두 번 도착해도 서버가 시퀀스로 걸러 **한 번만 적재한다.**
332
+ 재시도로 같은 배치가 두 번 도착해도 서버가 시퀀스로 걸러 **한 번만 적재한다.** 걸러지는 것은
333
+ producer 안의 재시도뿐이다 - 앱이 ``send`` 를 다시 부르면 새 레코드로 적재된다.
325
334
  ``maxInFlight`` 는 5 이하여야 한다.
326
335
 
327
336
  이 메서드로 정하지 않으면 ``acks=0`` 이거나 ``maxInFlight`` 가 5 를 넘을 때 멱등이 저절로 꺼진다.
@@ -422,10 +431,10 @@ class ILProducerConfig:
422
431
  return self._lingerMs
423
432
 
424
433
  def get_batch_size(self) -> int:
425
- """파티션 배치 하나의 크기 상한(바이트). 기본 16384.
434
+ """파티션 배치 하나의 크기 상한(바이트). 기본 262144.
426
435
 
427
436
  Returns:
428
- int: 파티션 배치 하나의 크기 상한(바이트). 기본 16384.
437
+ int: 파티션 배치 하나의 크기 상한(바이트). 기본 262144.
429
438
  """
430
439
  return self._batchSize
431
440
 
@@ -742,31 +751,75 @@ class ILRecordMetadata:
742
751
  return "ILRecordMetadata(%s[%d]@%d)" % (self.topic, self.partition, self.offset)
743
752
 
744
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
+
745
794
  class ILPublishFuture:
746
795
  """발행 완결을 기다리는 핸들. :meth:`get` 이 :class:`ILRecordMetadata` 를 돌려준다.
747
796
 
748
797
  **동기 발행은 ``send(rec).get()``** 으로 쓴다.
749
798
  """
750
799
 
751
- def __init__(self, timeout_ms: int) -> None:
800
+ def __init__(self, timeout_ms: int, _result: "_BatchResult | None" = None, _index: int = 0) -> None:
752
801
  """**응용이 직접 만들지 않는다** - :meth:`ILTopicProducer.send` 가 돌려준다.
753
802
 
754
803
  Args:
755
804
  timeout_ms (int): :meth:`get` 에 인자를 주지 않았을 때 쓸 기본 대기
756
805
  시간(밀리초). producer 의 ``deliveryTimeoutMs`` 가 들어온다.
806
+ _result (_BatchResult | None): 내부용 - 이 레코드가 든 파티션 배치의 결과(배치의 레코드들이 함께 가리킨다).
807
+ _index (int): 내부용 - 배치 안 순번. 좌표는 배치 첫 오프셋 + 순번이다.
757
808
  """
758
- self._event = threading.Event()
809
+ # 0.7.2: 완결은 배치 결과 하나로 한다(레코드마다 Event 를 만들지 않는다). 좌표는 get() 때 만든다
810
+ self._result = _result if _result is not None else _BatchResult(None, -1)
811
+ self._index = _index
759
812
  self._meta: ILRecordMetadata | None = None
760
- self._error: Exception | None = None
761
813
  self._timeout_ms = timeout_ms
762
814
 
763
815
  def _complete(self, meta: ILRecordMetadata) -> None:
816
+ # 배치 없이 만든 future 용(호환)
764
817
  self._meta = meta
765
- self._event.set()
818
+ self._result.event.set()
766
819
 
767
820
  def _fail(self, error: Exception) -> None:
768
- self._error = error
769
- self._event.set()
821
+ self._result.error = error
822
+ self._result.event.set()
770
823
 
771
824
  def get(self, timeout_ms: int | None = None) -> ILRecordMetadata:
772
825
  """발행이 끝날 때까지 기다렸다가 좌표를 돌려준다.
@@ -788,12 +841,18 @@ class ILPublishFuture:
788
841
  완결은 producer 의 I/O 스레드가 한다(0.7.0). ``lingerMs`` 가 0 이어도 ``send()``
789
842
  가 돌아온 시점에 완결되어 있다는 보장은 없다 - 결과가 필요하면 이 메서드로 기다린다.
790
843
  """
844
+ r = self._result
791
845
  wait = (timeout_ms if timeout_ms is not None else self._timeout_ms) / 1000.0
792
- if not self._event.wait(wait if wait > 0 else None):
846
+ if not r.event.wait(wait if wait > 0 else None):
793
847
  raise ILException(TimeoutError("publish timed out"))
794
- if self._error is not None:
795
- raise self._error
796
- return self._meta
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
797
856
 
798
857
  def is_done(self) -> bool:
799
858
  """완결(성공이든 실패든)되었는지.
@@ -801,7 +860,7 @@ class ILPublishFuture:
801
860
  Returns:
802
861
  bool: 결과가 정해졌으면 True. True 여도 :meth:`get` 이 예외를 올릴 수 있다.
803
862
  """
804
- return self._event.is_set()
863
+ return self._result.event.is_set()
805
864
 
806
865
  def cancel(self, may_interrupt_if_running: bool = False) -> bool:
807
866
  """발행은 취소할 수 없다. **항상 False**.
@@ -958,18 +1017,19 @@ def _as_il(exc: BaseException) -> Exception:
958
1017
  class _Batch:
959
1018
  """파티션 배치 1개 = 와이어의 PartitionBatch 1개. ``buf`` 의 앞 52바이트가 배치 헤더다."""
960
1019
 
961
- __slots__ = ("partition", "created", "batch_no", "buf", "count", "futures", "callbacks",
1020
+ __slots__ = ("partition", "created", "batch_no", "buf", "count", "result", "callbacks",
962
1021
  "closed", "done", "base_seq", "header_done", "header_pid", "retry_count",
963
1022
  "retry_at", "unknown", "size_drops", "solo")
964
1023
 
965
- 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:
966
1025
  self.partition = partition
967
1026
  self.created = created
968
1027
  #: 만든 순서. 파티션 안에서 보내야 하는 순서 = 이 순서
969
1028
  self.batch_no = batch_no
970
1029
  self.buf = bytearray(_BATCH_HEADER)
971
1030
  self.count = 0
972
- self.futures: list = []
1031
+ #: 이 배치 레코드들의 결과(0.7.2, 배치당 하나). 레코드 future 는 이것과 배치 안 순번만 든다
1032
+ self.result = _BatchResult(topic, partition)
973
1033
  self.callbacks: list | None = None
974
1034
  #: 더 붙이지 않는다(batchSize 에 닿음, drain 이 가져감, 되돌림)
975
1035
  self.closed = False
@@ -1032,8 +1092,9 @@ class ILTopicProducer:
1032
1092
  - **순서**: 한 파티션 안에서는 ``send`` 순서대로 적재된다. 파티션 사이의 순서는 보장하지 않는다.
1033
1093
  비멱등에서 FLUSH TOPIC 과 겹친 배치를 다시 보낼 때는 이미 나가 있던 같은 파티션의 뒤 배치가
1034
1094
  먼저 적재될 수 있다.
1035
- - **전달**: ``acks=1`` + 재시도 = at-least-once. ``enableIdempotence(True)`` 면 재전송이 엔진에서 중복
1036
- 제거되어 유실/중복이 없다. ``acks=0`` = at-most-once.
1095
+ - **전달**: ``acks=1`` + 재시도 = at-least-once. 멱등(기본 켜짐)이면 producer 안의 재전송이 엔진에서
1096
+ 중복 제거되어 중복 적재가 없다(적재하지 못한 배치는 future 가 실패로 알린다). 앱이 ``send`` 를 다시
1097
+ 부르면 새 레코드라 멱등이어도 걸러지지 않는다. ``acks=0`` = at-most-once.
1037
1098
  - **재시도**: 연결 단절, 비멱등의 FLUSHED / epoch 불일치(적재되지 않은 것이 확정된 거절), 멱등의
1038
1099
  순서 오류다. 그 밖의 거절(토픽 없음, 너무 큼 등)은 재시도 없이 그 배치의 future 가 실패한다.
1039
1100
  PID 가 무효가 되면 PID 를 다시 받고 seq 를 0 부터 다시 매긴다.
@@ -1059,7 +1120,8 @@ class ILTopicProducer:
1059
1120
  port (int | None): 포트. ``host`` 에 ``:포트`` 가 붙어 있으면 생략한다.
1060
1121
  client_name (str): 큐 관리자에 등록될 클라이언트 이름. 기본 ``"PRODUCER"``.
1061
1122
  topic (str): 발행할 토픽 이름. **필수.** 이 producer 는 이 토픽에만 발행한다.
1062
- config (ILProducerConfig | None): 발행 설정. 기본 None 이면 전 항목 기본값.
1123
+ config (ILProducerConfig | None): 발행 설정. 기본 None 이면 전 항목 기본값. 생성 때 복사한다 -
1124
+ 이후 넘긴 객체를 바꿔도 이 producer 에는 반영되지 않는다.
1063
1125
 
1064
1126
  Raises:
1065
1127
  TypeError: ``topic`` 이 토픽 이름(str)이 아닐 때 - 0.6.x 의
@@ -1081,7 +1143,8 @@ class ILTopicProducer:
1081
1143
  " (got %s) - since 0.7.0 a producer publishes to one topic" % type(topic).__name__)
1082
1144
  if not topic:
1083
1145
  raise ValueError("topic must not be empty - a producer publishes to one topic")
1084
- self.config = config or ILProducerConfig()
1146
+ # 0.7.2: 설정을 생성 때 복사한다(Kafka·자바와 같다) - 넘긴 객체를 나중에 바꿔도 이 producer 에는 반영되지 않는다
1147
+ self.config = copy.copy(config) if config is not None else ILProducerConfig()
1085
1148
  self.config.validate()
1086
1149
  cfg = self.config
1087
1150
  self.topic = topic
@@ -1098,6 +1161,13 @@ class ILTopicProducer:
1098
1161
  self._backoff = cfg.get_retry_backoff_ms() / 1000.0
1099
1162
  self._request_timeout = cfg.get_request_timeout_ms() / 1000.0
1100
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()
1101
1171
  #: 봉투 본문(IL 헤더 뒤) 상한 = min(maxRequestSize, maxFrameLength - 25)
1102
1172
  self._max_env_content = min(cfg.get_max_request_size(),
1103
1173
  cfg.get_max_frame_length() - ILC.SZ_IL_HEADER)
@@ -1134,6 +1204,10 @@ class ILTopicProducer:
1134
1204
  self._used = 0
1135
1205
  self._sticky = -1
1136
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
1137
1211
  self._pid = -1
1138
1212
  #: drain 이 큐에서 꺼낸 배치를 아직 in-flight 에 올리지(acks=0 이면 완결하지) 않은 동안 1 - flush 가 먼저 돌아가지 않게
1139
1213
  self._draining = 0
@@ -1288,6 +1362,8 @@ class ILTopicProducer:
1288
1362
  self._parts = parts
1289
1363
  if self._sticky >= n:
1290
1364
  self._sticky = -1
1365
+ if n != self._partition_count:
1366
+ self._key_parts.clear()
1291
1367
  self._partition_count = n
1292
1368
  self._cv.notify_all()
1293
1369
  if len(self._next_seq) != n:
@@ -1378,22 +1454,21 @@ class ILTopicProducer:
1378
1454
  rec_size = _RECORD_OVERHEAD + kl + pl + len(value)
1379
1455
  # 이 레코드 혼자 실린 봉투의 본문 크기
1380
1456
  alone = _ENVELOPE_FIXED + len(self._topic_bytes) + _BATCH_HEADER + rec_size
1381
- if alone > cfg.get_max_request_size():
1457
+ if alone > self._max_request_size:
1382
1458
  raise ILOperationException("MIMQE_MSG_SIZE_OVER (record too large: %d > maxRequestSize %d - envelope carrying"
1383
- " this record alone)" % (alone, cfg.get_max_request_size()))
1459
+ " this record alone)" % (alone, self._max_request_size))
1384
1460
  # 엔진은 Runtime@MaxFrameLength(기본 128MiB) 를 넘는 프레임을 사유 없이 끊는다 - 보내기 전에 막는다
1385
- if alone + ILC.SZ_IL_HEADER > cfg.get_max_frame_length():
1461
+ if alone + ILC.SZ_IL_HEADER > self._max_frame_length:
1386
1462
  raise ILOperationException(
1387
1463
  "MIMQE_MSG_SIZE_OVER (record too large: frame %d > maxFrameLength %d - the engine drops the connection"
1388
1464
  " over its Runtime@MaxFrameLength; raise both if the server allows more)"
1389
- % (alone + ILC.SZ_IL_HEADER, cfg.get_max_frame_length()))
1465
+ % (alone + ILC.SZ_IL_HEADER, self._max_frame_length))
1390
1466
  rec = b"".join((_I32.pack(kl if key else -1), key or b"",
1391
1467
  _I32.pack(pl if props else -1), props or b"",
1392
1468
  _I32.pack(len(value)), value))
1393
- future = ILPublishFuture(cfg.get_delivery_timeout_ms())
1394
1469
  with self._cv:
1395
- # maxBlockMs 는 버퍼 대기의 상한이다(Kafka max.block.ms)
1396
- deadline = time.monotonic() + cfg.get_max_block_ms() / 1000.0
1470
+ # maxBlockMs 는 버퍼 대기의 상한이다(Kafka max.block.ms). 기한은 기다려야 할 때만 잡는다
1471
+ deadline = None
1397
1472
  partition = self._choose_partition(record.partition, key)
1398
1473
  while True:
1399
1474
  if self._fatal is not None:
@@ -1415,16 +1490,18 @@ class ILTopicProducer:
1415
1490
  fresh = (b is None or b.closed or len(b.buf) + rec_size > self._batch_limit
1416
1491
  or (seg > 0 and len(b.buf) + rec_size + (b.count + 1) * self._v3_per_record > seg))
1417
1492
  need = rec_size + (_BATCH_HEADER if fresh else 0)
1418
- if need > cfg.get_buffer_memory():
1493
+ if need > self._buffer_memory:
1419
1494
  raise ILOperationException("MIMQE_MSG_SIZE_OVER (record too large: needs %d bytes of buffer"
1420
- " > bufferMemory %d)" % (need, cfg.get_buffer_memory()))
1421
- if self._used + need > cfg.get_buffer_memory():
1495
+ " > bufferMemory %d)" % (need, self._buffer_memory))
1496
+ if self._used + need > self._buffer_memory:
1422
1497
  # 버퍼 확보(가득이면 maxBlockMs 대기). 기다리는 사이 배치가 나가거나 파티션 수가 바뀔 수 있어 처음부터 다시 본다
1498
+ if deadline is None:
1499
+ deadline = time.monotonic() + self._max_block
1423
1500
  remain = deadline - time.monotonic()
1424
1501
  if remain <= 0:
1425
1502
  # 부하 문제다(Kafka BufferExhaustedException) - 큐 가득과 같은 분류(재시도 가능)로 올린다
1426
1503
  raise ILOperationException("MIMQE_QUEUE_FULL (buffer memory full %d/%d - blocked over maxBlockMs)"
1427
- % (self._used, cfg.get_buffer_memory()))
1504
+ % (self._used, self._buffer_memory))
1428
1505
  self._cv.wait(remain)
1429
1506
  continue
1430
1507
  wake = False
@@ -1432,14 +1509,14 @@ class ILTopicProducer:
1432
1509
  if b is not None:
1433
1510
  b.closed = True
1434
1511
  self._batch_counter += 1
1435
- b = _Batch(partition, time.monotonic(), self._batch_counter)
1512
+ b = _Batch(partition, time.monotonic(), self._batch_counter, self.topic)
1436
1513
  q.append(b)
1437
1514
  self._queued += 1
1438
1515
  self._used += _BATCH_HEADER
1439
1516
  wake = True
1440
1517
  b.buf += rec
1441
1518
  self._used += rec_size
1442
- b.futures.append(future)
1519
+ future = ILPublishFuture(self._delivery_timeout_ms, b.result, b.count)
1443
1520
  if callback is not None:
1444
1521
  if b.callbacks is None:
1445
1522
  b.callbacks = [None] * b.count
@@ -1468,7 +1545,15 @@ class ILTopicProducer:
1468
1545
  % (explicit, n - 1))
1469
1546
  return explicit
1470
1547
  if key:
1471
- return partition_for_key(key, n)
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
1472
1557
  if self._sticky < 0 or self._sticky >= n:
1473
1558
  self._sticky = random.randrange(n)
1474
1559
  self._sticky_bytes = 0
@@ -1477,7 +1562,7 @@ class ILTopicProducer:
1477
1562
  def _sticky_advance(self, nbytes: int) -> None:
1478
1563
  """키 없는 레코드를 붙인 만큼 센다. ``batchSize`` 를 채우면 다음 파티션으로 넘긴다(Kafka 3.3+ 균일 스티키)."""
1479
1564
  self._sticky_bytes += nbytes
1480
- if self._sticky_bytes >= self.config.get_batch_size():
1565
+ if self._sticky_bytes >= self._batch_size:
1481
1566
  self._sticky = (self._sticky + 1) % self._partition_count
1482
1567
  self._sticky_bytes = 0
1483
1568
 
@@ -1596,10 +1681,15 @@ class ILTopicProducer:
1596
1681
 
1597
1682
  # ── I/O 스레드 ───────────────────────────────────────────────────────
1598
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
1599
1689
  try:
1600
1690
  self._wake_w.send(b"\x00")
1601
1691
  except OSError:
1602
- pass
1692
+ self._wake_pending = False
1603
1693
 
1604
1694
  def _drain_wake(self) -> None:
1605
1695
  try:
@@ -1607,6 +1697,8 @@ class ILTopicProducer:
1607
1697
  pass
1608
1698
  except OSError:
1609
1699
  pass
1700
+ # 비운 뒤에 내린다. 먼저 내리면 비우는 사이 온 신호를 먹고도 표시가 올라간 채 남아 다음 깨우기가 막힐 수 있다
1701
+ self._wake_pending = False
1610
1702
 
1611
1703
  def _wake_waiters(self) -> None:
1612
1704
  with self._cv:
@@ -1965,45 +2057,56 @@ class ILTopicProducer:
1965
2057
  raise ConnectionError(results[0][4] or "MIMQE_INVALID_PID")
1966
2058
  now = time.monotonic()
1967
2059
  again = []
1968
- for b, (code, off, ts, exp, reason) in zip(env.batches, results):
1969
- # seq 틈: 이 배치의 어느 시도도 적재됐을 수 없는데(결과를 모른 적이 없다) 엔진이 중복이라 한다 = 같은 파티션의 뒤
1970
- # 배치가 먼저 기준선으로 적재돼(두 봉투 사이 리더 교체 등) 이 번호가 틈으로 남았다. 성공으로 올리면 조용한 유실이다.
1971
- if code == ILC.MIMQ_TOPIC_PUBLISH_DUPLICATE and self._idem and b.base_seq >= 0 and not b.unknown:
1972
- self._complete_batch(b, -1, 0, ILOperationException(
1973
- "MIMQE_OUT_OF_ORDER_SEQUENCE (sequence gap: partition %d seq %d..%d was never stored but the engine"
1974
- " answered duplicate - a later batch of this partition was stored first)"
1975
- % (b.partition, b.base_seq, b.base_seq + b.count - 1)))
1976
- continue
1977
- if code == ILC.MIMQ_SUCCESSFUL_TOPIC_PUBLISH or code == ILC.MIMQ_TOPIC_PUBLISH_DUPLICATE:
1978
- # 중복 응답은 원 좌표로 성공한다. 엔진 링 밖이면 baseOffset 이 -1 이다.
1979
- self._pid_resets = 0
1980
- if self._idem and b.base_seq >= 0 and b.partition < len(self._acked_seq):
1981
- last = b.base_seq + b.count - 1
1982
- if last > self._acked_seq[b.partition]:
1983
- self._acked_seq[b.partition] = last
1984
- self._complete_batch(b, off, ts, None)
1985
- continue
1986
- # epoch 가 바뀌었거나(FLUSH / 재생성), 토픽이 비워지는 중이거나, 세그먼트가 줄었다 - 메타데이터를 다시 받는다
1987
- if code in (ILC.MIMQ_TOPIC_PUBLISH_EPOCH_MISMATCH, ILC.MIMQ_TOPIC_PUBLISH_FLUSHED,
1988
- ILC.MIMQ_TOPIC_PUBLISH_TOO_LARGE):
1989
- self._meta_stale = True
1990
- expected = exp if code == ILC.MIMQ_OUT_OF_ORDER_SEQUENCE else -1
1991
- if self._idem and b.base_seq >= 0:
1992
- # 배치별 거절 = 이번 시도는 적재되지 않았다(엔진 계약). 번호를 돌려받고 재번호를 건다.
1993
- # 단 앞선 시도가 답 없이 끝난 배치(unknown)는 그때 적재됐을 수 있다 - 순서 오류가 아니면 번호를
1994
- # 돌려받지 않는다. 돌려받아 다음 새 배치에 주면 그 배치가 엔진에서 중복 응답으로 사라진다(유실).
1995
- if expected >= 0 or not b.unknown:
1996
- self._note_rejected(b.partition, b.base_seq, expected)
1997
- b.base_seq = -1
1998
- b.header_done = False
1999
- if self._can_retry(b, code, expected >= 0, now):
2000
- b.retry_count += 1
2001
- b.retry_at = now + self._backoff
2002
- again.append(b)
2003
- else:
2004
- self._complete_batch(b, -1, 0, _batch_error(code, reason))
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
2005
2107
  gone: list = []
2006
2108
  with self._cv:
2109
+ self._used -= freed
2007
2110
  # 배치는 되돌린 것을 큐에 넣은 **뒤에** in-flight 에서 뺀다. 먼저 빼면 그 사이 flush() 가 비었다고 보고 돌아간다.
2008
2111
  if again:
2009
2112
  gone = self._requeue_batches(again)
@@ -2082,30 +2185,40 @@ class ILTopicProducer:
2082
2185
  self._queued += 1
2083
2186
  return gone
2084
2187
 
2085
- def _complete_batch(self, b: _Batch, base_offset: int, timestamp: int, err: Exception | None) -> 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
+ """
2086
2195
  if b.done:
2087
- return
2196
+ return 0
2088
2197
  b.done = True
2089
- with self._cv:
2090
- self._used -= len(b.buf)
2091
- self._cv.notify_all()
2092
- futures, callbacks = b.futures, b.callbacks
2093
- b.futures = []
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
2094
2209
  b.callbacks = None
2095
- topic = self.topic
2096
- for i, fut in enumerate(futures):
2097
- meta = None
2098
- if err is None:
2099
- meta = ILRecordMetadata(topic, b.partition, base_offset + i if base_offset >= 0 else -1, timestamp)
2100
- fut._complete(meta)
2101
- else:
2102
- fut._fail(err)
2103
- cb = callbacks[i] if callbacks is not None else None
2104
- 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)
2105
2217
  try:
2106
2218
  cb(meta, err)
2107
2219
  except Exception: # noqa: BLE001 - 콜백 예외는 무시한다(계약)
2108
2220
  pass
2221
+ return freed
2109
2222
 
2110
2223
  def _fail_batches(self, batches, err: Exception | None) -> None:
2111
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 16KB / maxInFlight 5 / 멱등 켜짐).
1142
+ 기본값(acks=1 / linger 0 / batch 256KB / maxInFlight 5 / 멱등 켜짐).
1143
1143
 
1144
1144
  Returns:
1145
1145
  ILTopicProducer: 발행자. 다 쓰면 ``close()`` 로 닫는다.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.7.1
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,17 @@ 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
+
57
68
  ## 0.7.1 변경 - producer 기본값: 멱등 켜짐, batchSize 16384 (자바 v2.3.1 과 같음)
58
69
 
59
70
  - `enableIdempotence` 기본값이 **켜짐**입니다. 멱등을 직접 정하지 않았으면 `acks(0)` 이나 `maxInFlight` 가 5 를 넘을 때
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jetstream-api"
7
- version = "0.7.1"
7
+ version = "0.7.2"
8
8
  description = "Python client API for iLink M.O.M. (Message Oriented Middleware)"
9
9
  authors = [{name = "snowjeans"}]
10
10
  readme = "README.md"
File without changes
File without changes