jetstream-api 0.9.0__tar.gz → 0.10.0__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 (27) hide show
  1. {jetstream_api-0.9.0/jetstream_api.egg-info → jetstream_api-0.10.0}/PKG-INFO +15 -1
  2. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/README.md +14 -0
  3. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/__init__.py +1 -1
  4. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/_codec.py +1 -1
  5. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/common.py +3 -2
  6. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/msg.py +138 -40
  7. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/producer.py +76 -24
  8. {jetstream_api-0.9.0 → jetstream_api-0.10.0/jetstream_api.egg-info}/PKG-INFO +15 -1
  9. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/pyproject.toml +1 -1
  10. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/LICENSE.txt +0 -0
  11. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/admin.py +0 -0
  12. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/admintopic.py +0 -0
  13. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/connectivity.py +0 -0
  14. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/exception.py +0 -0
  15. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/partitioner.py +0 -0
  16. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/pattern.py +0 -0
  17. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/properties.py +0 -0
  18. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/py.typed +0 -0
  19. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/qmgr.py +0 -0
  20. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/topic.py +0 -0
  21. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/util.py +0 -0
  22. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/jetstream_api.egg-info/SOURCES.txt +0 -0
  23. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/jetstream_api.egg-info/dependency_links.txt +0 -0
  24. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/jetstream_api.egg-info/requires.txt +0 -0
  25. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/jetstream_api.egg-info/top_level.txt +0 -0
  26. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/setup.cfg +0 -0
  27. {jetstream_api-0.9.0 → jetstream_api-0.10.0}/tests/test_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.9.0
3
+ Version: 0.10.0
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,20 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.10.0 변경 - 토픽 레코드 v5: 길이 칸 varint (자바 v2.6.0 과 같음)
64
+
65
+ - **레코드 v5**: producer 가 파티션 배치를 magic 5 로 씁니다. 레코드의 길이 칸 셋(key / 속성 / 값)이 4바이트 고정에서
66
+ unsigned LEB128(길이 + 1, 1~5바이트)로 바뀌었습니다 - 126바이트까지 1바이트, 16,382바이트까지 2바이트입니다. 배치 헤더
67
+ 52바이트, CRC, 압축 코덱과 압축 범위, 오프셋, 멱등 seq 는 그대로입니다.
68
+ - **레코드당 저장 바이트 감소**: 값 100B + uuid 키 36B + 속성 없음 = 148 → 139B(-6.1%), 값 1KB = 1,072 → 1,064B(-0.75%).
69
+ 압축을 켜면 차이는 거의 없습니다.
70
+ - **소비는 배치마다 magic 을 봅니다**: 4(i32 길이 칸)와 5 를 모두 풀고, 한 응답에 섞여 있어도 됩니다.
71
+ - **깨진 v5 레코드**: 길이 칸이 5바이트를 넘거나, 적힌 값이 2^31 이상이거나, 잘렸거나, 값이 없으면(적힌 값 0) 그 배치는
72
+ 0.8.0 의 "풀지 못한 배치" 규칙대로 그 자리에서 멈춥니다(`MIMQC_FATAL_ERROR` / `MIMQE_INTERNAL_ERROR`). 사유는
73
+ `invalid v5 varint (...)` / `invalid v5 record value (absent ...)` 입니다.
74
+ - **공개 API 변화 없음**: 설정 / 메서드 / 반환 모양이 그대로입니다. 크기 검사(`maxRequestSize` / `maxFrameLength` /
75
+ `bufferMemory` / `batchSize`)는 v5 부호화 길이로 셉니다 - 같은 설정에 레코드가 조금 더 들어갑니다.
76
+
63
77
  ## 0.9.0 변경 - 압축 코덱 lz4 / zstd, gzip 제거 (자바 v2.5.0 과 같음)
64
78
 
65
79
  - **코덱은 `none` / `lz4` / `zstd` 입니다**: gzip 을 빼고 lz4 를 넣었습니다(사용자 결정). 클라이언트 CPU·지연이 중요하면
@@ -10,6 +10,20 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
10
10
  ## 설치 방법
11
11
  pip install jetstream-api
12
12
 
13
+ ## 0.10.0 변경 - 토픽 레코드 v5: 길이 칸 varint (자바 v2.6.0 과 같음)
14
+
15
+ - **레코드 v5**: producer 가 파티션 배치를 magic 5 로 씁니다. 레코드의 길이 칸 셋(key / 속성 / 값)이 4바이트 고정에서
16
+ unsigned LEB128(길이 + 1, 1~5바이트)로 바뀌었습니다 - 126바이트까지 1바이트, 16,382바이트까지 2바이트입니다. 배치 헤더
17
+ 52바이트, CRC, 압축 코덱과 압축 범위, 오프셋, 멱등 seq 는 그대로입니다.
18
+ - **레코드당 저장 바이트 감소**: 값 100B + uuid 키 36B + 속성 없음 = 148 → 139B(-6.1%), 값 1KB = 1,072 → 1,064B(-0.75%).
19
+ 압축을 켜면 차이는 거의 없습니다.
20
+ - **소비는 배치마다 magic 을 봅니다**: 4(i32 길이 칸)와 5 를 모두 풀고, 한 응답에 섞여 있어도 됩니다.
21
+ - **깨진 v5 레코드**: 길이 칸이 5바이트를 넘거나, 적힌 값이 2^31 이상이거나, 잘렸거나, 값이 없으면(적힌 값 0) 그 배치는
22
+ 0.8.0 의 "풀지 못한 배치" 규칙대로 그 자리에서 멈춥니다(`MIMQC_FATAL_ERROR` / `MIMQE_INTERNAL_ERROR`). 사유는
23
+ `invalid v5 varint (...)` / `invalid v5 record value (absent ...)` 입니다.
24
+ - **공개 API 변화 없음**: 설정 / 메서드 / 반환 모양이 그대로입니다. 크기 검사(`maxRequestSize` / `maxFrameLength` /
25
+ `bufferMemory` / `batchSize`)는 v5 부호화 길이로 셉니다 - 같은 설정에 레코드가 조금 더 들어갑니다.
26
+
13
27
  ## 0.9.0 변경 - 압축 코덱 lz4 / zstd, gzip 제거 (자바 v2.5.0 과 같음)
14
28
 
15
29
  - **코덱은 `none` / `lz4` / `zstd` 입니다**: gzip 을 빼고 lz4 를 넣었습니다(사용자 결정). 클라이언트 CPU·지연이 중요하면
@@ -1,6 +1,6 @@
1
1
  """iLink - Python client API for iLink M.O.M. (Message Oriented Middleware)."""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.10.0"
4
4
 
5
5
  # --- Admin / Service ---
6
6
  from .admin import ILAdminService, ILAdminQmgr, ILLogBrowser, ILQueueBrowser
@@ -1,6 +1,6 @@
1
1
  """배치 압축 코덱(0.8.0, 0.9.0 에서 gzip 을 빼고 lz4 를 넣었다). **내부 모듈이다** - 발행과 소비가 함께 쓴다.
2
2
 
3
- 발행 v2 파티션 배치(= 저장 v4 배치)의 레코드 열을 압축하고 푼다. 와이어(엔진 CMPR-1):
3
+ 발행 v2 파티션 배치(= 저장 배치, 레코드 v4 / v5 공통)의 레코드 열을 압축하고 푼다. 와이어(엔진 CMPR-1):
4
4
 
5
5
  - 배치 헤더 52바이트는 압축하지 않는다. 레코드 열 ``[52, batchLength)`` 만 압축한다.
6
6
  - 헤더 ``attributes`` 의 하위 3비트가 코덱이다: 0 없음 / 3 lz4 / 4 zstd. 나머지 비트는 0 이다.
@@ -252,10 +252,11 @@ class ILC:
252
252
  #: 0.7.0: 저장 v4 엔진은 보내지 않는다(배치 read 응답은 2042). 구독은 받지 않고 ILTopicFrameMsg 의 해석만 남긴다
253
253
  MIMQ_TOPIC_BATCH_MESSAGE = 2024
254
254
  #: 저장 v4 배치 read 응답(엔진 DESIGN-storv4 §3) = IL 프레임 하나. 본문(이진 BE) = u16 version=4, u16 partCount,
255
- #: partCount x (i32 partition, i64 startOffset), i32 batchCount, v4 배치 x batchCount. 단건 read 는 그대로 2022 다.
255
+ #: partCount x (i32 partition, i64 startOffset), i32 batchCount, 배치 x batchCount. 단건 read 는 그대로 2022 다.
256
+ #: 0.10.0: 배치 헤더 magic 4 = 레코드 v4(i32 길이 칸), 5 = 레코드 v5(LEB128 길이 칸, 엔진 REC5-1) - 배치마다 가른다.
256
257
  MIMQ_TOPIC_BATCH_V4_MESSAGE = 2042
257
258
  #: 발행 v2 봉투(엔진 DESIGN-pubv2). IL 헤더 + 이진 big-endian 본문(version, acks, flags, corrId, topicEpoch,
258
- #: 토픽, 파티션 배치 N개 - 배치 = 저장 v4 배치, CRC-32).
259
+ #: 토픽, 파티션 배치 N개 - 배치 = 저장 배치 헤더 52바이트 + 레코드 열, CRC-32). 0.10.0 부터 producer 는 magic 5(레코드 v5)만 쓴다.
259
260
  MIMQ_TOPIC_PUBLISH_V2_MESSAGE = 2040
260
261
  #: 발행 v2 ACK. corrId + 봉투 code / 사유 + 배치 수 + 배치마다 {partition, code, baseOffset, timestamp,
261
262
  #: expectedSeq, 사유}. 봉투 헤더를 못 읽으면 봉투 code / 사유만 싣고 배치 수 0 이다.
@@ -605,20 +605,148 @@ _V4_I = struct.Struct(">i")
605
605
  _V4_BH = struct.Struct(">qiqbbHIiqqi")
606
606
 
607
607
 
608
+ def _records_v4(rb, r: int, rend: int, count: int, first: int, part: int, base: int, ts: int, make, append,
609
+ topic_name) -> int:
610
+ # 레코드 v4(배치 magic 4): [i32 keyLen][key][i32 propsLen][props][i32 valueLen][value], -1 = 없음(길이 칸이 레코드당
611
+ # 12바이트). 0.10.0 에 _decode 에서 떼어 냈다 - 검사와 오류 문구는 그대로다. first 앞 레코드는 길이만 걸어 넘긴다(키를
612
+ # 풀지 않는다). 돌려주는 값 = 레코드 열이 끝난 자리
613
+ i32 = _V4_I.unpack_from
614
+ for i in range(count):
615
+ if r + 4 > rend:
616
+ raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (4, r, rend))
617
+ (kl,) = i32(rb, r)
618
+ r += 4
619
+ if kl > 0:
620
+ if r + kl > rend:
621
+ raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (kl, r, rend))
622
+ key = rb[r:r + kl].decode("utf-8") if i >= first else None
623
+ r += kl
624
+ else:
625
+ key = ""
626
+ if r + 4 > rend:
627
+ raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (4, r, rend))
628
+ (pl,) = i32(rb, r)
629
+ r += 4
630
+ if pl > 0:
631
+ if r + pl > rend:
632
+ raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (pl, r, rend))
633
+ props = rb[r:r + pl]
634
+ r += pl
635
+ else:
636
+ props = None
637
+ if r + 4 > rend:
638
+ raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (4, r, rend))
639
+ (vl,) = i32(rb, r)
640
+ r += 4
641
+ if vl < 0:
642
+ raise ValueError("invalid record value length %d" % vl)
643
+ if r + vl > rend:
644
+ raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (vl, r, rend))
645
+ if i >= first:
646
+ append(make(part, base + i, ts, key, rb[r:r + vl], props, topic_name))
647
+ r += vl
648
+ return r
649
+
650
+
651
+ def _v5_len(rb, r: int, rend: int, what: str):
652
+ # 0.10.0 레코드 v5 길이 칸 하나를 첫 바이트부터 읽는다 - (적힌 값 = 길이 + 1, 다음 자리). 한 / 두 바이트짜리는 부르는
653
+ # 쪽이 먼저 처리하고 여기서는 잘림 / 세 바이트 이상 / 넘침을 본다. 거절(엔진 REC5-1 1절): 5바이트를 넘는 부호화, 적힌 값이
654
+ # 2^31 이상, 잘림. 가장 짧은 부호화는 강제하지 않는다(쓰는 쪽은 늘 가장 짧게 쓴다)
655
+ at = r
656
+ v = 0
657
+ shift = 0
658
+ while True:
659
+ if r >= rend:
660
+ raise ValueError("invalid v5 varint (%s length truncated at %d, records end at %d)" % (what, at, rend))
661
+ c = rb[r]
662
+ r += 1
663
+ v |= (c & 0x7F) << shift
664
+ if c < 0x80:
665
+ break
666
+ shift += 7
667
+ if shift >= 35:
668
+ raise ValueError("invalid v5 varint (%s length longer than 5 bytes at %d)" % (what, at))
669
+ if v > 0x7FFFFFFF:
670
+ raise ValueError("invalid v5 varint (%s length field %d >= 2^31 at %d)" % (what, v, at))
671
+ return v, r
672
+
673
+
674
+ def _records_v5(rb, r: int, rend: int, count: int, first: int, part: int, base: int, ts: int, make, append,
675
+ topic_name) -> int:
676
+ # 0.10.0 레코드 v5(배치 magic 5, 엔진 REC5-1): [uv keyLen+1][key][uv propsLen+1][props][uv valueLen+1][value],
677
+ # uv = unsigned LEB128. 적힌 값 0 = 없음, 1 = 빈 값. 한 바이트 / 두 바이트짜리(적힌 값 16,383 까지 - 두 바이트에서는
678
+ # 넘침이 없다)는 여기서 곧바로 읽고, 그 밖(잘림 포함)은 _v5_len 이 거절 규칙과 함께 읽는다. 키 / 속성의 뜻은 v4 와 같다
679
+ # (키 없음 / 빈 키 = "", 속성 없음 / 빈 속성 = None). 값 없음(0)은 거절한다(v4 의 valueLen < 0 과 같다). 잘린 본문 / 끝
680
+ # 자리 검사와 문구는 v4 와 같다
681
+ for i in range(count):
682
+ kl = rb[r] if r < rend else 0x80
683
+ if kl < 0x80:
684
+ r += 1
685
+ elif r + 1 < rend and rb[r + 1] < 0x80:
686
+ kl = (kl & 0x7F) | (rb[r + 1] << 7)
687
+ r += 2
688
+ else:
689
+ kl, r = _v5_len(rb, r, rend, "key")
690
+ if kl > 1:
691
+ kl -= 1
692
+ if r + kl > rend:
693
+ raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (kl, r, rend))
694
+ key = rb[r:r + kl].decode("utf-8") if i >= first else None
695
+ r += kl
696
+ else:
697
+ key = ""
698
+ pl = rb[r] if r < rend else 0x80
699
+ if pl < 0x80:
700
+ r += 1
701
+ elif r + 1 < rend and rb[r + 1] < 0x80:
702
+ pl = (pl & 0x7F) | (rb[r + 1] << 7)
703
+ r += 2
704
+ else:
705
+ pl, r = _v5_len(rb, r, rend, "props")
706
+ if pl > 1:
707
+ pl -= 1
708
+ if r + pl > rend:
709
+ raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (pl, r, rend))
710
+ props = rb[r:r + pl]
711
+ r += pl
712
+ else:
713
+ props = None
714
+ vl = rb[r] if r < rend else 0x80
715
+ if vl < 0x80:
716
+ r += 1
717
+ elif r + 1 < rend and rb[r + 1] < 0x80:
718
+ vl = (vl & 0x7F) | (rb[r + 1] << 7)
719
+ r += 2
720
+ else:
721
+ vl, r = _v5_len(rb, r, rend, "value")
722
+ if not vl:
723
+ raise ValueError("invalid v5 record value (absent - length field 0, record %d)" % i)
724
+ vl -= 1
725
+ if r + vl > rend:
726
+ raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (vl, r, rend))
727
+ if i >= first:
728
+ append(make(part, base + i, ts, key, rb[r:r + vl], props, topic_name))
729
+ r += vl
730
+ return r
731
+
732
+
608
733
  def _v4_tuple(partition, offset, timestamp, key, value, property_bytes, _topic_name):
609
734
  # records() 의 튜플 - _decode 의 make 자리에 들어간다
610
735
  return (partition, offset, timestamp, key, value, property_bytes)
611
736
 
612
737
 
613
738
  class ILTopicBatchV4Msg(ILInnerMsg):
614
- """소비자 배치 read 응답(``MIMQ_TOPIC_BATCH_V4_MESSAGE`` 2042, 엔진 저장 v4). **수신 전용**이다.
739
+ """소비자 배치 read 응답(``MIMQ_TOPIC_BATCH_V4_MESSAGE`` 2042). **수신 전용**이다.
615
740
 
616
741
  본문은 이진 big-endian 이다 - ``u16 version=4 | u16 partCount | partCount x (i32 partition, i64 startOffset) |
617
- i32 batchCount | v4 배치 x batchCount``. 배치는 발행 v2 의 파티션 배치와 같은 형식(52바이트 헤더 + 레코드)이고
742
+ i32 batchCount | 배치 x batchCount``(배치마다 레코드 magic 4 / 5). 배치는 발행 v2 의 파티션 배치와 같은 형식(52바이트 헤더 + 레코드)이고
618
743
  엔진이 baseOffset / timestamp 를 채웠다. 레코드 오프셋은 baseOffset + 순번, 시각은 배치 timestamp 다.
619
744
 
620
745
  0.8.0: 배치는 압축돼 있을 수 있다(헤더 attributes 하위 3비트 = 코덱: 0 없음 / 3 lz4 / 4 zstd). 엔진은 저장한
621
746
  바이트를 그대로 보내므로 배치마다 코덱을 보고 레코드 열을 푼다 - 한 응답에 코덱이 섞여도 된다.
747
+
748
+ 0.10.0: 레코드 형식은 배치 헤더 magic 으로 가른다 - 4 는 길이 칸이 i32 셋(레코드당 12바이트), 5 는 길이 칸이
749
+ unsigned LEB128 셋(길이 + 1 을 1~5바이트로, 엔진 REC5-1)이다. 배치마다 보므로 한 응답에 섞여도 된다.
622
750
  """
623
751
 
624
752
  def __init__(self, content: bytes) -> None:
@@ -642,8 +770,10 @@ class ILTopicBatchV4Msg(ILInnerMsg):
642
770
  ValueError: 형식이 깨졌거나 배치 CRC 가 맞지 않을 때. 압축 배치를 풀지 못할 때 - 모르는 코덱
643
771
  ``MIMQE_TOPIC_UNSUPPORTED_CODEC (codec N)``, zstd 모듈이 없음 ``MIMQE_NOT_SUPPORTED`` (설치 안내
644
772
  ``pip install "jetstream-api[zstd]"`` 를 싣는다), 깨졌거나 잘렸거나 128MiB 를 넘음
645
- ``MIMQE_TOPIC_CODEC_ERROR (...)``, 푼 레코드 수 / 길이가 헤더와 다름. 배치를 풀지 못한 오류는 문구에 그
646
- 배치의 파티션과 baseOffset 을 싣는다(``(partition P, base B)``).
773
+ ``MIMQE_TOPIC_CODEC_ERROR (...)``, 푼 레코드 수 / 길이가 헤더와 다름, 레코드 v5 길이 칸이 깨짐
774
+ ``invalid v5 varint (...)``(5바이트를 넘음 / 적힌 값 2^31 이상 / 잘림) 또는 값 없음
775
+ ``invalid v5 record value (absent ...)``. 배치를 풀지 못한 오류는 문구에 그 배치의 파티션과 baseOffset 을
776
+ 싣는다(``(partition P, base B)``).
647
777
  """
648
778
  return self._decode(_v4_tuple, None)
649
779
 
@@ -681,7 +811,6 @@ class ILTopicBatchV4Msg(ILInnerMsg):
681
811
  out = []
682
812
  append = out.append
683
813
  mv = memoryview(b)
684
- i32 = _V4_I.unpack_from
685
814
  head = _V4_BH.unpack_from
686
815
  crc32 = zlib.crc32
687
816
  decompress = _codec.decompress
@@ -695,7 +824,7 @@ class ILTopicBatchV4Msg(ILInnerMsg):
695
824
  self._cut(first_at, p, start, dead, exc, fails)
696
825
  return out
697
826
  base, blen, ts, magic, attr, _resv, crc, part, _pid, _base_seq, count = head(b, p)
698
- if magic != 4 or blen < 52 or blen > n - p or count < 0:
827
+ if (magic != 5 and magic != 4) or blen < 52 or blen > n - p or count < 0:
699
828
  exc = ValueError("invalid topic batch header (magic %d, length %d, records %d)" % (magic, blen, count))
700
829
  if fails is None:
701
830
  raise exc
@@ -724,40 +853,9 @@ class ILTopicBatchV4Msg(ILInnerMsg):
724
853
  r = p + 52
725
854
  rend = end
726
855
  first = 0 if low is None or low <= base else low - base
727
- for i in range(count):
728
- if r + 4 > rend:
729
- raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (4, r, rend))
730
- (kl,) = i32(rb, r)
731
- r += 4
732
- if kl > 0:
733
- if r + kl > rend:
734
- raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (kl, r, rend))
735
- key = rb[r:r + kl].decode("utf-8") if i >= first else None
736
- r += kl
737
- else:
738
- key = ""
739
- if r + 4 > rend:
740
- raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (4, r, rend))
741
- (pl,) = i32(rb, r)
742
- r += 4
743
- if pl > 0:
744
- if r + pl > rend:
745
- raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (pl, r, rend))
746
- props = rb[r:r + pl]
747
- r += pl
748
- else:
749
- props = None
750
- if r + 4 > rend:
751
- raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (4, r, rend))
752
- (vl,) = i32(rb, r)
753
- r += 4
754
- if vl < 0:
755
- raise ValueError("invalid record value length %d" % vl)
756
- if r + vl > rend:
757
- raise ValueError("truncated topic batch (need %d bytes at %d, have %d)" % (vl, r, rend))
758
- if i >= first:
759
- append(make(part, base + i, ts, key, rb[r:r + vl], props, topic_name))
760
- r += vl
856
+ # 0.10.0: 배치마다 magic 으로 레코드 형식을 가른다(4 = i32 길이 칸, 5 = LEB128 길이 칸 - 한 응답에 섞여도 된다)
857
+ r = (_records_v5 if magic == 5 else _records_v4)(rb, r, rend, count, first, part, base, ts, make, append,
858
+ topic_name)
761
859
  if r != rend:
762
860
  if codec:
763
861
  raise ValueError("topic batch length mismatch (records end at %d of %d decompressed bytes, codec %d)"
@@ -300,8 +300,9 @@ class ILProducerConfig:
300
300
 
301
301
  Note:
302
302
  봉투는 준비된 파티션 배치들을 이 크기까지 싣는다. 레코드 하나가 혼자 실린 봉투
303
- (봉투 헤더 24 + 토픽 이름 + 배치 헤더 52 + 레코드 고정 12 + key + 속성 + 값)가 이 값보다
304
- 크면 :meth:`ILTopicProducer.send` 가 즉시 거부한다. key 와 속성은 각각 99,999바이트까지다.
303
+ (봉투 헤더 24 + 토픽 이름 + 배치 헤더 52 + 레코드 길이 칸 셋 + key + 속성 + 값)가 이 값보다
304
+ 크면 :meth:`ILTopicProducer.send` 가 즉시 거부한다. 길이 칸은 key / 속성 / 값마다 1~5바이트다(0.10.0
305
+ 레코드 v5 - 126바이트까지 1바이트, 16,382바이트까지 2바이트). key 와 속성은 각각 99,999바이트까지다.
305
306
 
306
307
  배치는 토픽의 세그먼트 크기(SEGMENTBYTES, producer 생성 때 받아 둔다)에도 맞춰 끊는다 -
307
308
  엔진은 레코드마다 약 (160 + 토픽 이름 길이) 바이트를 더 붙여 저장하므로 그만큼을 더해
@@ -1047,12 +1048,14 @@ def _extract_leader_hint(exc: Exception) -> str | None:
1047
1048
  _IDEM_PROTOCOL = "3"
1048
1049
 
1049
1050
  # 발행 v2 와이어(엔진 DESIGN-pubv2 2장). 숫자는 모두 이진 big-endian 이다.
1050
- #: 파티션 배치 헤더 = 저장 v4 배치 헤더
1051
+ #: 파티션 배치 헤더 = 저장 배치 헤더(v4 / v5 가 같다)
1051
1052
  _BATCH_HEADER = 52
1052
1053
  #: 배치 CRC 가 덮는 구간의 시작(partition 칸부터 배치 끝까지)
1053
1054
  _CRC_FROM = 28
1054
- #: 레코드 고정 칸: keyLen 4 + propsLen 4 + valueLen 4
1055
- _RECORD_OVERHEAD = 12
1055
+ #: 0.10.0: 배치 머리 magic - 레코드 v5(엔진 REC5-1). 쓰기는 v5 만 한다(소비 쪽은 4 / 5 둘 다 읽는다)
1056
+ _MAGIC = 5
1057
+ #: 레코드 v5 의 가장 작은 크기: 길이 칸 셋이 각 1바이트(key 없음 / 속성 없음 / 빈 값)
1058
+ _RECORD_MIN = 3
1056
1059
  #: 봉투 본문 고정 칸: version 1 + acks 1 + flags 2 + corrId 8 + topicEpoch 8 + topicLen 2 + batchCount 2(토픽 바이트 제외)
1057
1060
  _ENVELOPE_FIXED = 24
1058
1061
  #: 1단계 엔진이 v3 레코드로 옮길 수 있는 key / props 길이 상한(v3 칸 폭 5자리)
@@ -1071,11 +1074,12 @@ _SIZE_SUSPECT_BYTES = ILC.MIMQ_MIN_MAX_FRAME_LENGTH
1071
1074
  #: 재시도 보호: 큰 봉투에 실려 응답 없이 끊긴 뒤 혼자 보냈는데도 또 끊긴 배치는 실패로 올린다(연속 횟수).
1072
1075
  _SIZE_DROP_LIMIT = 2
1073
1076
 
1074
- _I32 = struct.Struct(">i")
1075
- #: 0.7.3: 레코드를 배치 버퍼에 곧바로 붙일 때 - (keyLen, propsLen, valueLen) / (propsLen, valueLen)
1076
- _III = struct.Struct(">iii")
1077
- _II = struct.Struct(">ii")
1078
- _NEG1 = _I32.pack(-1)
1077
+ #: 0.10.0 레코드 v5 길이 칸 = unsigned LEB128(한 바이트에 7비트, 낮은 자리 먼저, 윗비트 1 = 다음 바이트 있음)로 적은
1078
+ #: 길이 + 1(0 = 없음, 1 = 빈 값). 늘 가장 짧게 쓴다. 한 바이트짜리(적는 값 127 까지)는 이 표에서 꺼낸다
1079
+ _UV1 = tuple(bytes((i,)) for i in range(0x80))
1080
+ #: 없음 칸 하나 / 둘(key 없음 + 속성 없음)
1081
+ _ABSENT = _UV1[0]
1082
+ _ABSENT2 = _UV1[0] * 2
1079
1083
  _U16 = struct.Struct(">H")
1080
1084
  #: 배치 헤더 앞 24바이트: baseOffset(엔진이 채움), batchLength, timestamp(엔진이 채움), magic, attributes, reserved
1081
1085
  _HEAD_A = struct.Struct(">qiqbbH")
@@ -1105,6 +1109,39 @@ _CODE_NAMES = {
1105
1109
  }
1106
1110
 
1107
1111
 
1112
+ #: 0.10.0: 속성 없는 레코드의 머리 조각 캐시. 값 길이 -> [속성 없음 0][값 길이 칸] / [key 없음 0][속성 없음 0][값 길이 칸].
1113
+ #: 같은 크기 값이 되풀이되는 흔한 경우 길이 칸을 만들지 않고 한 조각으로 붙인다(0.9.0 의 pack 한 번보다 싸다).
1114
+ #: 여러 producer 가 함께 쓴다 - dict 한 번의 읽기 / 쓰기라 잠금 없이 둔다. 4096개가 차면 비운다
1115
+ _HEAD1: dict = {}
1116
+ _HEAD2: dict = {}
1117
+ _HEAD_CACHE_MAX = 4096
1118
+
1119
+
1120
+ def _heads(vl: int) -> tuple:
1121
+ """속성 없는 레코드의 머리 조각 둘(key 있음 / 없음)을 만들어 캐시에 넣는다(0.10.0). ``vl`` = 값 길이."""
1122
+ vh = _uv(vl + 1)
1123
+ if len(_HEAD1) >= _HEAD_CACHE_MAX:
1124
+ _HEAD1.clear()
1125
+ _HEAD2.clear()
1126
+ h1 = _HEAD1[vl] = _ABSENT + vh
1127
+ h2 = _HEAD2[vl] = _ABSENT2 + vh
1128
+ return h1, h2
1129
+
1130
+
1131
+ def _uv(n: int) -> bytes:
1132
+ """레코드 v5 길이 칸 하나(0.10.0). ``n`` = 길이 + 1 을 가장 짧은 LEB128 로 적는다(1~5바이트)."""
1133
+ if n < 0x80:
1134
+ return _UV1[n]
1135
+ if n < 0x4000:
1136
+ return _U16.pack(0x8000 | ((n & 0x7F) << 8) | (n >> 7))
1137
+ out = bytearray()
1138
+ while n >= 0x80:
1139
+ out.append((n & 0x7F) | 0x80)
1140
+ n >>= 7
1141
+ out.append(n)
1142
+ return bytes(out)
1143
+
1144
+
1108
1145
  def _ascii(value: int, width: int) -> bytes:
1109
1146
  """IL 헤더 숫자 칸: 좌측 정렬 ASCII 10진 + 나머지 0x00."""
1110
1147
  return str(value).encode("ascii").ljust(width, b"\x00")
@@ -1323,7 +1360,7 @@ class ILTopicProducer:
1323
1360
  self._max_env_content = min(cfg.get_max_request_size(),
1324
1361
  cfg.get_max_frame_length() - ILC.SZ_IL_HEADER)
1325
1362
  #: 배치 하나가 레코드를 더 받지 않는 크기 = min(batchSize, 봉투 본문 상한 - 봉투 고정 칸)
1326
- self._batch_limit = max(_BATCH_HEADER + _RECORD_OVERHEAD,
1363
+ self._batch_limit = max(_BATCH_HEADER + _RECORD_MIN,
1327
1364
  min(cfg.get_batch_size(),
1328
1365
  self._max_env_content - _ENVELOPE_FIXED - len(self._topic_bytes)))
1329
1366
  #: 레코드 1건의 엔진 저장 부담 = 160 + 토픽 이름 바이트
@@ -1360,7 +1397,7 @@ class ILTopicProducer:
1360
1397
  self._used = 0
1361
1398
  self._sticky = -1
1362
1399
  self._sticky_bytes = 0
1363
- #: 키 캐시(0.7.2, 0.7.3 에 모양을 바꿈). 키(str) -> [UTF-8 바이트, 레코드 앞머리(keyLen+key), 파티션, 그때의 파티션 수].
1400
+ #: 키 캐시(0.7.2, 0.7.3 에 모양을 바꿈). 키(str) -> [UTF-8 바이트, 레코드 앞머리(v5 길이 칸 + key), 파티션, 그때의 파티션 수].
1364
1401
  #: 파티션 수가 바뀌면 비운다. 256바이트를 넘는 키는 넣지 않는다, 16384개가 차면 비운다
1365
1402
  self._key_parts: dict = {}
1366
1403
  #: 깨울 신호를 이미 보냈고 I/O 스레드가 아직 비우지 않았다(0.7.2 - 신호를 합친다)
@@ -1599,14 +1636,14 @@ class ILTopicProducer:
1599
1636
  raise ILOperationException(
1600
1637
  "MIMQE_INVALID_ARGUMENT (record topic '%s' is not the producer topic '%s' - a producer publishes"
1601
1638
  " only to the topic it was created for)" % (record.topic, self.topic))
1602
- # 0.7.3: 키 캐시 항목 [UTF-8 바이트, 레코드 앞머리(keyLen+key), 파티션, 그때의 파티션 수]. 바이트 / 앞머리는 파티션
1639
+ # 0.7.3: 키 캐시 항목 [UTF-8 바이트, 레코드 앞머리(v5 길이 칸 + key), 파티션, 그때의 파티션 수]. 바이트 / 앞머리는 파티션
1603
1640
  # 수와 무관해 잠금 밖에서 읽고, 파티션은 잠금 안에서만 읽고 쓴다(파티션 수가 다르면 다시 계산)
1604
1641
  key_s = record.key
1605
1642
  if key_s:
1606
1643
  ent = self._key_parts.get(key_s)
1607
1644
  if ent is None:
1608
1645
  key = key_s.encode("utf-8")
1609
- ent = [key, _I32.pack(len(key)) + key, -1, -1]
1646
+ ent = [key, _uv(len(key) + 1) + key, -1, -1]
1610
1647
  if len(key) <= _KEY_CACHE_MAX_KEY:
1611
1648
  kp = self._key_parts
1612
1649
  if len(kp) >= _KEY_CACHE_MAX:
@@ -1629,7 +1666,21 @@ class ILTopicProducer:
1629
1666
  if pl > _MAX_PROPS_BYTES:
1630
1667
  raise ILOperationException("MIMQE_MSG_SIZE_OVER (properties too large: %d bytes > %d)" % (pl, _MAX_PROPS_BYTES))
1631
1668
  vl = len(value)
1632
- rec_size = _RECORD_OVERHEAD + kl + pl + vl
1669
+ # 0.10.0 레코드 v5: [길이 칸][key][길이 칸][속성][길이 칸][값], 길이 칸 = 길이 + 1 의 LEB128(1~5바이트). key 칸은 키 캐시
1670
+ # 항목의 앞머리(ent[1])를 그대로 쓴다. 크기 검사(maxRequestSize / maxFrameLength / 세그먼트 / bufferMemory / batchSize)는
1671
+ # 이 부호화 길이 그대로 센다. 속성 없는 흔한 경우는 [속성 없음][값 길이 칸] 머리를 값 길이별 캐시(_HEAD1 / _HEAD2)에서
1672
+ # 꺼내 한 조각으로 붙인다
1673
+ if props:
1674
+ ph = _uv(pl + 1)
1675
+ vh = _uv(vl + 1)
1676
+ head = None
1677
+ rec_size = (len(ent[1]) if ent is not None else 1) + len(ph) + pl + len(vh) + vl
1678
+ elif ent is None:
1679
+ head = _HEAD2.get(vl) or _heads(vl)[1]
1680
+ rec_size = len(head) + vl
1681
+ else:
1682
+ head = _HEAD1.get(vl) or _heads(vl)[0]
1683
+ rec_size = len(ent[1]) + len(head) + vl
1633
1684
  # 이 레코드 혼자 실린 봉투의 본문 크기
1634
1685
  alone = self._alone_base + rec_size
1635
1686
  if alone > self._max_request_size:
@@ -1713,16 +1764,17 @@ class ILTopicProducer:
1713
1764
  self._queued += 1
1714
1765
  self._used += _BATCH_HEADER
1715
1766
  wake = True
1716
- # 0.7.3: 레코드를 따로 만들지 않고 배치 버퍼에 곧바로 붙인다(속성 없는 흔한 경우는 pack 한 번). 와이어 바이트는 같다
1767
+ # 0.7.3: 레코드를 따로 만들지 않고 배치 버퍼에 곧바로 붙인다. 0.10.0: 길이 칸은 위에서 만든 v5 칸(head 또는 ph / vh)
1717
1768
  buf = b.buf
1718
- if not props:
1719
- if ent is None:
1720
- buf += _III.pack(-1, -1, vl)
1721
- else:
1769
+ if head is not None:
1770
+ if ent is not None:
1722
1771
  buf += ent[1]
1723
- buf += _II.pack(-1, vl)
1772
+ buf += head
1724
1773
  else:
1725
- buf += b"".join((_I32.pack(kl) if key else _NEG1, key or b"", _I32.pack(pl), props, _I32.pack(vl)))
1774
+ buf += ent[1] if ent is not None else _ABSENT
1775
+ buf += ph
1776
+ buf += props
1777
+ buf += vh
1726
1778
  buf += value
1727
1779
  self._used += rec_size
1728
1780
  future = ILPublishFuture(self._delivery_timeout_ms, b.result, b.count)
@@ -2143,13 +2195,13 @@ class ILTopicProducer:
2143
2195
  buf = b.buf
2144
2196
  body = b.body
2145
2197
  if body is None:
2146
- _HEAD_A.pack_into(buf, 0, 0, len(buf), 0, 4, 0, 0)
2198
+ _HEAD_A.pack_into(buf, 0, 0, len(buf), 0, _MAGIC, 0, 0)
2147
2199
  _HEAD_B.pack_into(buf, _CRC_FROM, b.partition, use_pid, b.base_seq if use_pid >= 0 else -1, b.count)
2148
2200
  _CRC.pack_into(buf, 24, zlib.crc32(memoryview(buf)[_CRC_FROM:]) & 0xFFFFFFFF)
2149
2201
  else:
2150
2202
  # 0.8.0 압축 배치: batchLength = 52 + 압축 길이, attributes = 코덱, recordCount = 원래 건수,
2151
2203
  # CRC = [28, 52) 헤더 + 압축한 바이트(엔진 CMPR-1). 헤더를 다시 쓸 때마다(pid / baseSeq) CRC 도 다시 센다
2152
- _HEAD_A.pack_into(buf, 0, 0, _BATCH_HEADER + len(body), 0, 4, b.attr, 0)
2204
+ _HEAD_A.pack_into(buf, 0, 0, _BATCH_HEADER + len(body), 0, _MAGIC, b.attr, 0)
2153
2205
  _HEAD_B.pack_into(buf, _CRC_FROM, b.partition, use_pid, b.base_seq if use_pid >= 0 else -1, b.count)
2154
2206
  _CRC.pack_into(buf, 24,
2155
2207
  zlib.crc32(body, zlib.crc32(memoryview(buf)[_CRC_FROM:_BATCH_HEADER])) & 0xFFFFFFFF)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.9.0
3
+ Version: 0.10.0
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,20 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.10.0 변경 - 토픽 레코드 v5: 길이 칸 varint (자바 v2.6.0 과 같음)
64
+
65
+ - **레코드 v5**: producer 가 파티션 배치를 magic 5 로 씁니다. 레코드의 길이 칸 셋(key / 속성 / 값)이 4바이트 고정에서
66
+ unsigned LEB128(길이 + 1, 1~5바이트)로 바뀌었습니다 - 126바이트까지 1바이트, 16,382바이트까지 2바이트입니다. 배치 헤더
67
+ 52바이트, CRC, 압축 코덱과 압축 범위, 오프셋, 멱등 seq 는 그대로입니다.
68
+ - **레코드당 저장 바이트 감소**: 값 100B + uuid 키 36B + 속성 없음 = 148 → 139B(-6.1%), 값 1KB = 1,072 → 1,064B(-0.75%).
69
+ 압축을 켜면 차이는 거의 없습니다.
70
+ - **소비는 배치마다 magic 을 봅니다**: 4(i32 길이 칸)와 5 를 모두 풀고, 한 응답에 섞여 있어도 됩니다.
71
+ - **깨진 v5 레코드**: 길이 칸이 5바이트를 넘거나, 적힌 값이 2^31 이상이거나, 잘렸거나, 값이 없으면(적힌 값 0) 그 배치는
72
+ 0.8.0 의 "풀지 못한 배치" 규칙대로 그 자리에서 멈춥니다(`MIMQC_FATAL_ERROR` / `MIMQE_INTERNAL_ERROR`). 사유는
73
+ `invalid v5 varint (...)` / `invalid v5 record value (absent ...)` 입니다.
74
+ - **공개 API 변화 없음**: 설정 / 메서드 / 반환 모양이 그대로입니다. 크기 검사(`maxRequestSize` / `maxFrameLength` /
75
+ `bufferMemory` / `batchSize`)는 v5 부호화 길이로 셉니다 - 같은 설정에 레코드가 조금 더 들어갑니다.
76
+
63
77
  ## 0.9.0 변경 - 압축 코덱 lz4 / zstd, gzip 제거 (자바 v2.5.0 과 같음)
64
78
 
65
79
  - **코덱은 `none` / `lz4` / `zstd` 입니다**: gzip 을 빼고 lz4 를 넣었습니다(사용자 결정). 클라이언트 CPU·지연이 중요하면
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jetstream-api"
7
- version = "0.9.0"
7
+ version = "0.10.0"
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