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.
- {jetstream_api-0.9.0/jetstream_api.egg-info → jetstream_api-0.10.0}/PKG-INFO +15 -1
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/README.md +14 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/__init__.py +1 -1
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/_codec.py +1 -1
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/common.py +3 -2
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/msg.py +138 -40
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/producer.py +76 -24
- {jetstream_api-0.9.0 → jetstream_api-0.10.0/jetstream_api.egg-info}/PKG-INFO +15 -1
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/pyproject.toml +1 -1
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/LICENSE.txt +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/admin.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/admintopic.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/connectivity.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/exception.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/partitioner.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/pattern.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/properties.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/py.typed +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/qmgr.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/topic.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/ilink/util.py +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/jetstream_api.egg-info/SOURCES.txt +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/jetstream_api.egg-info/dependency_links.txt +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/jetstream_api.egg-info/requires.txt +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/jetstream_api.egg-info/top_level.txt +0 -0
- {jetstream_api-0.9.0 → jetstream_api-0.10.0}/setup.cfg +0 -0
- {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.
|
|
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
|
"""배치 압축 코덱(0.8.0, 0.9.0 에서 gzip 을 빼고 lz4 를 넣었다). **내부 모듈이다** - 발행과 소비가 함께 쓴다.
|
|
2
2
|
|
|
3
|
-
발행 v2 파티션 배치(= 저장 v4
|
|
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,
|
|
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개 - 배치 = 저장
|
|
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
|
|
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 |
|
|
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
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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 + 레코드
|
|
304
|
-
크면 :meth:`ILTopicProducer.send` 가 즉시 거부한다. key
|
|
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
|
-
#: 레코드
|
|
1055
|
-
|
|
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
|
-
|
|
1075
|
-
#: 0
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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 +
|
|
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 바이트, 레코드 앞머리(
|
|
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 바이트, 레코드 앞머리(
|
|
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,
|
|
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
|
-
|
|
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: 레코드를 따로 만들지 않고 배치 버퍼에 곧바로
|
|
1767
|
+
# 0.7.3: 레코드를 따로 만들지 않고 배치 버퍼에 곧바로 붙인다. 0.10.0: 길이 칸은 위에서 만든 v5 칸(head 또는 ph / vh)
|
|
1717
1768
|
buf = b.buf
|
|
1718
|
-
if not
|
|
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
|
-
|
|
1772
|
+
buf += head
|
|
1724
1773
|
else:
|
|
1725
|
-
buf +=
|
|
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,
|
|
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,
|
|
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.
|
|
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·지연이 중요하면
|
|
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
|