jetstream-api 0.4.3__tar.gz → 0.4.5__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 (25) hide show
  1. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/PKG-INFO +134 -8
  2. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/README.md +133 -7
  3. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/__init__.py +1 -1
  4. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/admin.py +116 -19
  5. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/common.py +13 -0
  6. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/msg.py +7 -0
  7. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/properties.py +114 -21
  8. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/qmgr.py +272 -21
  9. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/jetstream_api.egg-info/PKG-INFO +134 -8
  10. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/pyproject.toml +1 -1
  11. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/LICENSE.txt +0 -0
  12. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/admintopic.py +0 -0
  13. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/connectivity.py +0 -0
  14. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/exception.py +0 -0
  15. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/pattern.py +0 -0
  16. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/producer.py +0 -0
  17. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/py.typed +0 -0
  18. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/topic.py +0 -0
  19. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/ilink/util.py +0 -0
  20. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/jetstream_api.egg-info/SOURCES.txt +0 -0
  21. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/jetstream_api.egg-info/dependency_links.txt +0 -0
  22. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/jetstream_api.egg-info/requires.txt +0 -0
  23. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/jetstream_api.egg-info/top_level.txt +0 -0
  24. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/setup.cfg +0 -0
  25. {jetstream_api-0.4.3 → jetstream_api-0.4.5}/tests/test_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.4.3
3
+ Version: 0.4.5
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
@@ -137,15 +137,141 @@ print(adm.getTopicList())
137
137
  클러스터 큐 관리자에 접속하면 후보 주소를 캐싱해 두었다가, 리더가 바뀌어도 따라갑니다.
138
138
 
139
139
  ```python
140
- qmgr.set_auto_reconnect(True) # auto-commit 세션에서만 동작
141
- print(qmgr.get_cluster_addresses())
142
- qmgr.reconnect() # 새 리더를 찾아 재접속
140
+ qmgr.connect("10.0.0.1", 5000, "APP", True) # 주소 하나면 됩니다
141
+ print(qmgr.get_cluster_addresses()) # ['10.0.0.1:5000', '10.0.0.2:5000']
142
+ qmgr.reconnect() # 새 리더를 찾아 재접속
143
+
144
+ # 첫 접속 시점의 장애까지 대비하려면 목록으로 (포트 인자 없음)
145
+ qmgr.connect("10.0.0.1:5000,10.0.0.2:5000", "APP", True)
146
+ ```
147
+
148
+ 접속에 성공하면 핸드셰이크 직후 나머지 노드 주소를 서버에서 받아 캐싱하므로
149
+ **주소는 하나만 주면 됩니다.** 목록은 첫 접속 시점의 장애까지 대비할 때 씁니다.
150
+
151
+ ### 0.4.4 변경 - 자동 재접속은 옵션이 아니라 기본 동작
152
+
153
+ 캐싱된 endpoint 목록이 있으면(= 클러스터) 통신 장애로 실패한 put/get 을 페일오버
154
+ 재접속 후 **한 번 자동 재시도합니다.** `set_auto_reconnect()` 는 더 이상 필요 없으며,
155
+ 불러도 무시됩니다(기존 코드 호환용으로만 남겨 두었습니다).
156
+
157
+ | 상황 | 동작 |
158
+ |---|---|
159
+ | 미커밋 트랜잭션 **있음** | 예외를 올립니다. 앱이 `reconnect()` 후 트랜잭션을 처음부터 다시 수행 |
160
+ | 미커밋 트랜잭션 **없음** (auto-commit 포함) | 내부에서 재접속 후 1회 재시도 |
161
+ | endpoint 목록 없음 (비클러스터/구엔진) | 예외를 올립니다 - 갈 곳이 없습니다 |
162
+
163
+ > 재시도는 **at-least-once** 입니다. 서버가 처리한 뒤 응답이 유실된 시점에 재시도하면
164
+ > **중복 put 이 생길 수 있습니다.** 재시도는 같은 msgId 로 나가므로 수신측에서 메시지 ID
165
+ > 로 걸러낼 수 있습니다. 미커밋 트랜잭션이 없을 때만 재시도하므로 트랜잭션 유실은 없습니다.
166
+
167
+ ### 장애 전환(failover) 시 앱이 해야 할 일
168
+
169
+ 리더가 죽으면 새 리더가 뽑힐 때까지 **아무도 접속을 받지 않습니다.** 실측(2노드 클러스터,
170
+ 엔진 7.0.1.3341) **약 15~20초**가 걸립니다. 그 사이의 재접속 시도는 정상적으로 실패하므로,
171
+ **한 번 실패했다고 끝내지 말고 재시도**해야 합니다.
172
+
173
+ 세션 종류에 따라 앱이 할 일이 갈립니다.
174
+
175
+ | 세션 | 실패 시점에 미커밋 | 앱이 할 일 |
176
+ |---|---|---|
177
+ | auto-commit | 없음 | **아무것도 안 해도 됩니다.** 그 연산을 다시 부르기만 하면 라이브러리가 재접속·재시도합니다 |
178
+ | transacted | 없음 | 위와 같습니다 |
179
+ | transacted | 있음 | `reconnect()` 로 자리를 옮기고 **트랜잭션을 처음부터 다시** 수행해야 합니다 |
180
+
181
+ `access_queue()` 는 **자동 재접속 대상이 아닙니다.** 끊긴 세션에서 부르면 그대로 실패하니,
182
+ 재접속에 성공한 **뒤에** 핸들을 다시 얻으십시오. 기존 핸들을 계속 쓰는 쪽은 스스로 복구됩니다.
183
+
184
+ #### (1) 트랜잭션 재수행이 필요 없는 경우 — auto-commit
185
+
186
+ ```python
187
+ import time
188
+ from ilink.qmgr import ILQmgr
189
+ from ilink.exception import ILException, ILSessionException, ILOperationException
190
+
191
+ q = ILQmgr()
192
+ q.set_endpoint_cache_file("/var/run/myapp/ilink-endpoints.txt") # 재기동 대비(선택)
193
+ q.connect("10.0.0.1", 5000, "collector", True) # auto-commit
194
+ queue = q.access_queue("APP.EVENTS") # 핸들은 한 번만 얻는다
195
+
196
+ def publish(payload):
197
+ """페일오버가 나도 이 함수는 그대로 둔다 - 라이브러리가 복구한다."""
198
+ for attempt in range(1, 11):
199
+ try:
200
+ return queue.put(payload) # 실패하면 내부에서 재접속 + 1회 재시도
201
+ except ILOperationException:
202
+ raise # 서버가 거절 - 재시도해도 같다
203
+ except (ILException, ILSessionException):
204
+ if attempt == 10:
205
+ raise # 승격이 20초 넘게 안 끝났다
206
+ time.sleep(3) # 리더 승격을 기다렸다가 다시
207
+ ```
208
+
209
+ 앱 코드에 `reconnect()` 가 없다는 점이 요지입니다. 같은 큐 핸들로 `put` 을 다시 부르기만
210
+ 하면, 승격이 끝난 시점의 호출이 새 리더에 붙어 성공합니다(실측 20.6초에 복구).
211
+
212
+ #### (2) 트랜잭션 재수행이 필요한 경우 — transacted
213
+
214
+ ```python
215
+ import time
216
+ from ilink.qmgr import ILQmgr
217
+ from ilink.exception import ILException, ILSessionException, ILOperationException
218
+
219
+ MAX_RETRY = 10
220
+
221
+ def process_batch(records):
222
+ """한 번의 트랜잭션 = 전부 커밋되거나 전부 없던 일이 된다."""
223
+ q = ILQmgr()
224
+ q.set_endpoint_cache_file("/var/run/myapp/ilink-endpoints.txt")
225
+ q.connect("10.0.0.1", 5000, "billing-svc", False) # transacted
226
+ try:
227
+ for attempt in range(1, MAX_RETRY + 1):
228
+ try:
229
+ queue = q.access_queue("APP.ORDERS") # 재접속 뒤 핸들을 다시 얻는다
230
+ for rec in records:
231
+ queue.put(rec)
232
+ q.commit() # 여기까지 와야 확정된다
233
+ return
234
+ except ILOperationException:
235
+ q.rollback() # 서버 거절 - 재시도 무의미
236
+ raise
237
+ except (ILException, ILSessionException):
238
+ if attempt == MAX_RETRY:
239
+ raise
240
+ try:
241
+ q.reconnect() # 승격 전이면 여기서 또 실패한다
242
+ except Exception:
243
+ time.sleep(3) # 기다렸다가 다음 회차에 다시
244
+ # 루프 처음으로 -> 배치 전체를 다시 넣는다
245
+ finally:
246
+ q.disconnect()
143
247
  ```
144
248
 
145
- > `set_auto_reconnect`는 **transacted 세션에서는 동작하지 않습니다.** 미커밋 트랜잭션이
146
- > 재접속으로 유실되므로, 예외를 받고 앱이 트랜잭션을 처음부터 재수행해야 합니다
147
- > (`reconnect()` 직접 호출은 가능합니다). auto-commit 세션도 응답 유실 시점의 재시도로
148
- > **중복 put이 발생할 수 있습니다.**
249
+ 읽어야 할 포인트 셋입니다.
250
+
251
+ 1. **재시도 단위는 트랜잭션 전체**입니다. 실패 지점부터 이어붙이면 안 됩니다 — 앞서 넣은
252
+ 것들도 커밋되지 않았으므로 함께 사라집니다(실측: 미커밋 1건 유실, 재수행 후 depth 2 정상).
253
+ 2. **`reconnect()` 자체가 실패할 수 있습니다.** 승격 전에 부르면 실패하는 게 정상이라,
254
+ 루프 안에서 간격을 두고 다시 불러야 합니다(실측 2번째 시도, 16.5초에 성공).
255
+ 3. **`ILOperationException` 과 통신 예외를 갈라 잡습니다.** 전자는 서버가 판단해 거절한
256
+ 것이라 재시도가 무의미하고, 후자만 재접속 대상입니다. 파이썬의 예외 클래스는 평면
257
+ 구조라 `ILSessionException` 은 `ILException` 의 하위 타입이 **아닙니다** — 통신 장애를
258
+ 잡으려면 **둘 다** 적어야 합니다(자바는 모두 `ILException` 으로 감싸므로 하나면 됩니다).
259
+
260
+ #### 앱이 재기동되는 경우
261
+
262
+ 캐싱은 메모리에만 있으므로 프로세스가 죽으면 사라집니다. 설정에 남은 옛 리더 주소로 다시
263
+ 붙을 때 이렇게 갈립니다.
264
+
265
+ | 옛 리더의 상태 | 결과 |
266
+ |---|---|
267
+ | 살아 있고 **팔로워로 강등** | 그 노드가 리더 힌트를 주므로 **자동으로 새 리더에 접속** |
268
+ | **죽어 있음** | 갈 곳이 없어 실패 — `set_endpoint_cache_file()` 을 쓰거나 주소를 목록으로 주십시오 |
269
+
270
+ ```python
271
+ q = ILQmgr()
272
+ q.set_endpoint_cache_file("/var/run/myapp/ilink-endpoints.txt")
273
+ q.connect("10.0.0.1", 5000, "APP", True) # 이 주소가 죽어 있어도 파일 후보로 붙는다
274
+ ```
149
275
 
150
276
  ## 0.4.0 신규 - 관리 연결 하나로 pub/sub
151
277
 
@@ -91,15 +91,141 @@ print(adm.getTopicList())
91
91
  클러스터 큐 관리자에 접속하면 후보 주소를 캐싱해 두었다가, 리더가 바뀌어도 따라갑니다.
92
92
 
93
93
  ```python
94
- qmgr.set_auto_reconnect(True) # auto-commit 세션에서만 동작
95
- print(qmgr.get_cluster_addresses())
96
- qmgr.reconnect() # 새 리더를 찾아 재접속
94
+ qmgr.connect("10.0.0.1", 5000, "APP", True) # 주소 하나면 됩니다
95
+ print(qmgr.get_cluster_addresses()) # ['10.0.0.1:5000', '10.0.0.2:5000']
96
+ qmgr.reconnect() # 새 리더를 찾아 재접속
97
+
98
+ # 첫 접속 시점의 장애까지 대비하려면 목록으로 (포트 인자 없음)
99
+ qmgr.connect("10.0.0.1:5000,10.0.0.2:5000", "APP", True)
100
+ ```
101
+
102
+ 접속에 성공하면 핸드셰이크 직후 나머지 노드 주소를 서버에서 받아 캐싱하므로
103
+ **주소는 하나만 주면 됩니다.** 목록은 첫 접속 시점의 장애까지 대비할 때 씁니다.
104
+
105
+ ### 0.4.4 변경 - 자동 재접속은 옵션이 아니라 기본 동작
106
+
107
+ 캐싱된 endpoint 목록이 있으면(= 클러스터) 통신 장애로 실패한 put/get 을 페일오버
108
+ 재접속 후 **한 번 자동 재시도합니다.** `set_auto_reconnect()` 는 더 이상 필요 없으며,
109
+ 불러도 무시됩니다(기존 코드 호환용으로만 남겨 두었습니다).
110
+
111
+ | 상황 | 동작 |
112
+ |---|---|
113
+ | 미커밋 트랜잭션 **있음** | 예외를 올립니다. 앱이 `reconnect()` 후 트랜잭션을 처음부터 다시 수행 |
114
+ | 미커밋 트랜잭션 **없음** (auto-commit 포함) | 내부에서 재접속 후 1회 재시도 |
115
+ | endpoint 목록 없음 (비클러스터/구엔진) | 예외를 올립니다 - 갈 곳이 없습니다 |
116
+
117
+ > 재시도는 **at-least-once** 입니다. 서버가 처리한 뒤 응답이 유실된 시점에 재시도하면
118
+ > **중복 put 이 생길 수 있습니다.** 재시도는 같은 msgId 로 나가므로 수신측에서 메시지 ID
119
+ > 로 걸러낼 수 있습니다. 미커밋 트랜잭션이 없을 때만 재시도하므로 트랜잭션 유실은 없습니다.
120
+
121
+ ### 장애 전환(failover) 시 앱이 해야 할 일
122
+
123
+ 리더가 죽으면 새 리더가 뽑힐 때까지 **아무도 접속을 받지 않습니다.** 실측(2노드 클러스터,
124
+ 엔진 7.0.1.3341) **약 15~20초**가 걸립니다. 그 사이의 재접속 시도는 정상적으로 실패하므로,
125
+ **한 번 실패했다고 끝내지 말고 재시도**해야 합니다.
126
+
127
+ 세션 종류에 따라 앱이 할 일이 갈립니다.
128
+
129
+ | 세션 | 실패 시점에 미커밋 | 앱이 할 일 |
130
+ |---|---|---|
131
+ | auto-commit | 없음 | **아무것도 안 해도 됩니다.** 그 연산을 다시 부르기만 하면 라이브러리가 재접속·재시도합니다 |
132
+ | transacted | 없음 | 위와 같습니다 |
133
+ | transacted | 있음 | `reconnect()` 로 자리를 옮기고 **트랜잭션을 처음부터 다시** 수행해야 합니다 |
134
+
135
+ `access_queue()` 는 **자동 재접속 대상이 아닙니다.** 끊긴 세션에서 부르면 그대로 실패하니,
136
+ 재접속에 성공한 **뒤에** 핸들을 다시 얻으십시오. 기존 핸들을 계속 쓰는 쪽은 스스로 복구됩니다.
137
+
138
+ #### (1) 트랜잭션 재수행이 필요 없는 경우 — auto-commit
139
+
140
+ ```python
141
+ import time
142
+ from ilink.qmgr import ILQmgr
143
+ from ilink.exception import ILException, ILSessionException, ILOperationException
144
+
145
+ q = ILQmgr()
146
+ q.set_endpoint_cache_file("/var/run/myapp/ilink-endpoints.txt") # 재기동 대비(선택)
147
+ q.connect("10.0.0.1", 5000, "collector", True) # auto-commit
148
+ queue = q.access_queue("APP.EVENTS") # 핸들은 한 번만 얻는다
149
+
150
+ def publish(payload):
151
+ """페일오버가 나도 이 함수는 그대로 둔다 - 라이브러리가 복구한다."""
152
+ for attempt in range(1, 11):
153
+ try:
154
+ return queue.put(payload) # 실패하면 내부에서 재접속 + 1회 재시도
155
+ except ILOperationException:
156
+ raise # 서버가 거절 - 재시도해도 같다
157
+ except (ILException, ILSessionException):
158
+ if attempt == 10:
159
+ raise # 승격이 20초 넘게 안 끝났다
160
+ time.sleep(3) # 리더 승격을 기다렸다가 다시
161
+ ```
162
+
163
+ 앱 코드에 `reconnect()` 가 없다는 점이 요지입니다. 같은 큐 핸들로 `put` 을 다시 부르기만
164
+ 하면, 승격이 끝난 시점의 호출이 새 리더에 붙어 성공합니다(실측 20.6초에 복구).
165
+
166
+ #### (2) 트랜잭션 재수행이 필요한 경우 — transacted
167
+
168
+ ```python
169
+ import time
170
+ from ilink.qmgr import ILQmgr
171
+ from ilink.exception import ILException, ILSessionException, ILOperationException
172
+
173
+ MAX_RETRY = 10
174
+
175
+ def process_batch(records):
176
+ """한 번의 트랜잭션 = 전부 커밋되거나 전부 없던 일이 된다."""
177
+ q = ILQmgr()
178
+ q.set_endpoint_cache_file("/var/run/myapp/ilink-endpoints.txt")
179
+ q.connect("10.0.0.1", 5000, "billing-svc", False) # transacted
180
+ try:
181
+ for attempt in range(1, MAX_RETRY + 1):
182
+ try:
183
+ queue = q.access_queue("APP.ORDERS") # 재접속 뒤 핸들을 다시 얻는다
184
+ for rec in records:
185
+ queue.put(rec)
186
+ q.commit() # 여기까지 와야 확정된다
187
+ return
188
+ except ILOperationException:
189
+ q.rollback() # 서버 거절 - 재시도 무의미
190
+ raise
191
+ except (ILException, ILSessionException):
192
+ if attempt == MAX_RETRY:
193
+ raise
194
+ try:
195
+ q.reconnect() # 승격 전이면 여기서 또 실패한다
196
+ except Exception:
197
+ time.sleep(3) # 기다렸다가 다음 회차에 다시
198
+ # 루프 처음으로 -> 배치 전체를 다시 넣는다
199
+ finally:
200
+ q.disconnect()
97
201
  ```
98
202
 
99
- > `set_auto_reconnect`는 **transacted 세션에서는 동작하지 않습니다.** 미커밋 트랜잭션이
100
- > 재접속으로 유실되므로, 예외를 받고 앱이 트랜잭션을 처음부터 재수행해야 합니다
101
- > (`reconnect()` 직접 호출은 가능합니다). auto-commit 세션도 응답 유실 시점의 재시도로
102
- > **중복 put이 발생할 수 있습니다.**
203
+ 읽어야 할 포인트 셋입니다.
204
+
205
+ 1. **재시도 단위는 트랜잭션 전체**입니다. 실패 지점부터 이어붙이면 안 됩니다 — 앞서 넣은
206
+ 것들도 커밋되지 않았으므로 함께 사라집니다(실측: 미커밋 1건 유실, 재수행 후 depth 2 정상).
207
+ 2. **`reconnect()` 자체가 실패할 수 있습니다.** 승격 전에 부르면 실패하는 게 정상이라,
208
+ 루프 안에서 간격을 두고 다시 불러야 합니다(실측 2번째 시도, 16.5초에 성공).
209
+ 3. **`ILOperationException` 과 통신 예외를 갈라 잡습니다.** 전자는 서버가 판단해 거절한
210
+ 것이라 재시도가 무의미하고, 후자만 재접속 대상입니다. 파이썬의 예외 클래스는 평면
211
+ 구조라 `ILSessionException` 은 `ILException` 의 하위 타입이 **아닙니다** — 통신 장애를
212
+ 잡으려면 **둘 다** 적어야 합니다(자바는 모두 `ILException` 으로 감싸므로 하나면 됩니다).
213
+
214
+ #### 앱이 재기동되는 경우
215
+
216
+ 캐싱은 메모리에만 있으므로 프로세스가 죽으면 사라집니다. 설정에 남은 옛 리더 주소로 다시
217
+ 붙을 때 이렇게 갈립니다.
218
+
219
+ | 옛 리더의 상태 | 결과 |
220
+ |---|---|
221
+ | 살아 있고 **팔로워로 강등** | 그 노드가 리더 힌트를 주므로 **자동으로 새 리더에 접속** |
222
+ | **죽어 있음** | 갈 곳이 없어 실패 — `set_endpoint_cache_file()` 을 쓰거나 주소를 목록으로 주십시오 |
223
+
224
+ ```python
225
+ q = ILQmgr()
226
+ q.set_endpoint_cache_file("/var/run/myapp/ilink-endpoints.txt")
227
+ q.connect("10.0.0.1", 5000, "APP", True) # 이 주소가 죽어 있어도 파일 후보로 붙는다
228
+ ```
103
229
 
104
230
  ## 0.4.0 신규 - 관리 연결 하나로 pub/sub
105
231
 
@@ -1,6 +1,6 @@
1
1
  """iLink - Python client API for iLink M.O.M. (Message Oriented Middleware)."""
2
2
 
3
- __version__ = "0.4.3"
3
+ __version__ = "0.4.5"
4
4
 
5
5
  # --- Admin / Service ---
6
6
  from .admin import ILAdminService, ILAdminQmgr, ILLogBrowser, ILQueueBrowser
@@ -2989,12 +2989,20 @@ class ILAdminQmgr:
2989
2989
  qmgr.startChannel("TO.QM2")
2990
2990
 
2991
2991
  qmgr2.createQueue(ILQueueProperty("APP.ORDERS")) # 받는 쪽
2992
+
2993
+ .. note:: 구엔진(6.3.3 미만) 호환
2994
+
2995
+ 큐 속성은 엔진 6.3.3 에서 전송 레이아웃이 한 번 바뀐 **유일한**
2996
+ 속성이다(4096 -> 2200). 붙어 있는 엔진의 리비전을 보고 알아서 옛
2997
+ 레이아웃으로 보내므로 호출부는 달라질 게 없다. 다만 구엔진에는
2998
+ 만료(``expiry``)/영속성(``deliveryMode``)/파티션 설정이 아예 없어
2999
+ **그 값들은 무시된다** - 설정해도 반영되지 않는다.
2992
3000
  """
2993
3001
  try:
2994
3002
  self.service.apply(
2995
3003
  self.qmgrName,
2996
3004
  ILC.MIMQ_CREATE_QUEUE_REQUEST,
2997
- prop.get_serialized(),
3005
+ self._queueBlob(prop),
2998
3006
  ILC.MIMQ_SUCCESSFUL_QUEUE_CREATION,
2999
3007
  )
3000
3008
  except _PASSTHROUGH_EXCEPTIONS:
@@ -3097,6 +3105,41 @@ class ILAdminQmgr:
3097
3105
  except Exception as exc:
3098
3106
  raise ILException(exc) from exc
3099
3107
 
3108
+ def _queueBlob(self, prop: ILQueueProperty) -> bytes:
3109
+ """큐 속성을 **붙어 있는 엔진이 이해하는 레이아웃**으로 직렬화한다.
3110
+
3111
+ Args:
3112
+ prop (ILQueueProperty): 보낼 큐 속성.
3113
+
3114
+ Returns:
3115
+ bytes: rev 3155 미만이면 구버전(4096), 그 외에는 현행(2200) 블롭.
3116
+
3117
+ Note:
3118
+ 판정을 ``0 < revision`` 으로 한정한 이유는 다른 버전 게이트와 같다.
3119
+ 버전 문자열을 파싱하지 못하면 revision 이 0 으로 남는데, 그때 구버전
3120
+ 취급을 하면 최신 엔진에 4096 을 보내 조용히 깨진다.
3121
+ """
3122
+ return prop.get_serialized(
3123
+ legacy=0 < self.revision < ILC.QUEUE_LAYOUT_MIN_REVISION
3124
+ )
3125
+
3126
+ def _requireQueueFlowSupport(self) -> None:
3127
+ """유량 조회를 지원하지 않는 엔진이면 그 자리에서 실패시킨다.
3128
+
3129
+ Raises:
3130
+ ILOperationException: 엔진이 6.3.4(rev 3299) 미만일 때.
3131
+
3132
+ Note:
3133
+ 유량 통계는 6.3.4 에서 들어왔다. 구엔진은 요청 코드 자체를 모르므로
3134
+ 응답이 없거나 엉뚱한 리포트가 온다. 무엇이 잘못됐는지 알 수 없는
3135
+ 타임아웃 대신 **왜 안 되는지 적힌 예외**로 끊는다.
3136
+ """
3137
+ if 0 < self.revision < ILC.QUEUE_FLOW_MIN_REVISION:
3138
+ raise ILOperationException(
3139
+ "MIMQE_NOT_SUPPORTED (queue flow statistics require engine 6.3.4, "
3140
+ f"rev {ILC.QUEUE_FLOW_MIN_REVISION}+, current: {self.revision})"
3141
+ )
3142
+
3100
3143
  def getQueueProperty(self, queueName: str) -> ILQueueProperty:
3101
3144
  """큐 하나의 전체 속성을 조회한다(종류 무관).
3102
3145
 
@@ -3258,12 +3301,20 @@ class ILAdminQmgr:
3258
3301
  prop = qmgr.getQueueProperty("APP.ORDERS")
3259
3302
  prop.setMaxDepth(50000)
3260
3303
  qmgr.setQueueProperty(prop)
3304
+
3305
+ .. note:: 구엔진(6.3.3 미만) 호환
3306
+
3307
+ 큐 속성은 엔진 6.3.3 에서 전송 레이아웃이 한 번 바뀐 **유일한**
3308
+ 속성이다(4096 -> 2200). 붙어 있는 엔진의 리비전을 보고 알아서 옛
3309
+ 레이아웃으로 보내므로 호출부는 달라질 게 없다. 다만 구엔진에는
3310
+ 만료(``expiry``)/영속성(``deliveryMode``)/파티션 설정이 아예 없어
3311
+ **그 값들은 무시된다** - 설정해도 반영되지 않는다.
3261
3312
  """
3262
3313
  try:
3263
3314
  self.service.apply(
3264
3315
  self.qmgrName,
3265
3316
  ILC.MIMQ_SET_QUEUE_PROPERTY,
3266
- prop.get_serialized(),
3317
+ self._queueBlob(prop),
3267
3318
  ILC.MIMQ_SUCCESSFUL_SET_QUEUE_PROPERTY,
3268
3319
  )
3269
3320
  except _PASSTHROUGH_EXCEPTIONS:
@@ -3442,23 +3493,39 @@ class ILAdminQmgr:
3442
3493
  ``getIncomingDataSize()`` / ``getOutgoingDataSize()`` / ``getDepth()``).
3443
3494
 
3444
3495
  Raises:
3445
- ILOperationException: 그런 큐가 없을 때.
3496
+ ILOperationException: 그런 큐가 없을 때. 또는 엔진이 6.3.4(rev 3299)
3497
+ 미만이라 유량 통계 자체를 지원하지 않을 때.
3446
3498
  ILException: 통신 실패.
3447
3499
 
3448
3500
  Note:
3449
3501
  **한 시점의 스냅샷**이다. 시간에 따른 추이를 보려면
3450
3502
  :meth:`getQueueFlowInfoArray` 를 쓴다.
3451
3503
 
3452
- .. warning:: 갓 만든 큐는 잠시 조회되지 않는다
3504
+ .. note:: 값은 **1초 주기**로 채워진다
3505
+
3506
+ 유량 스냅샷은 서버가 1초마다 적재한다. 그래서 갓 만든 큐도 **조회
3507
+ 자체는 즉시 되지만**(엔진 r3336+) 유입/유출 값은 첫 적재가 돌 때까지
3508
+ 0 이다 - 실측: put 직후 0초 시점 0, 1.2초 시점 5건/2695B (r3336).
3509
+ 즉시 깊이가 필요하면 :meth:`getQueueRealTimeInfo` 를 쓰라. 그쪽은
3510
+ 적재 주기와 무관하게 현재 값을 준다.
3453
3511
 
3454
- 유량 집계 테이블은 **주기적으로 갱신**된다. 큐를 만든 직후에는 아직
3455
- 등록되지 않아 ``MIMQE_OBJECT_NOT_FOUND`` 가 나고, **약 1분 뒤부터**
3456
- 정상 조회된다(실서버 확인: 생성 직후 실패 -> 35초 뒤 성공).
3457
- put/get 을 해도 앞당겨지지 않는다. **서버 동작이라 클라이언트에서
3458
- 우회할 수 없다** - 방금 만든 큐의 현황은 :meth:`getQueueRealTimeInfo`
3459
- 로 보라(이쪽은 즉시 조회된다).
3512
+ **엔진 r3336 미만에서는 갓 만든 큐가 최대 1초간 조회되지 않는다** -
3513
+ 첫 적재 전까지 ``MIMQE_OBJECT_NOT_FOUND`` 가 난다(r3335 실측
3514
+ 0.1~1.1초, 단건·배열 50회). put/get 을 해도 앞당겨지지 않는다.
3515
+
3516
+ .. versionchanged:: 0.1.6
3517
+ 이전 문서는 "유량 테이블은 주기적(약 1분) 갱신"이라고 적었으나
3518
+ **사실이 아니다**. 실서버에서 관측된 35초 지연은 적재 주기가
3519
+ 아니라 **적재 스레드가 분 단위 통계 리포트에 붙들려 있던 것**으로,
3520
+ 엔진 r3331 통계 컴팩션으로 해소됐다.
3521
+
3522
+ .. versionchanged:: 0.4.4
3523
+ 엔진 r3336 이 큐 생성 시점에 유량 항목을 미리 등재하도록 바뀌어
3524
+ **생성 직후 조회 실패가 사라졌다**(24/24 첫 시도 성공, 값은 0).
3525
+ 재시도 코드는 구엔진 대비로만 남기면 된다.
3460
3526
 
3461
3527
  """
3528
+ self._requireQueueFlowSupport()
3462
3529
  try:
3463
3530
  report_msg = self.service.request(
3464
3531
  self.qmgrName, ILC.MIMQ_GET_QUEUE_ACCESS_INFO, queueName
@@ -3495,23 +3562,39 @@ class ILAdminQmgr:
3495
3562
  실제 기록이 그보다 적으면 0 으로 채운 항목이 들어간다.
3496
3563
 
3497
3564
  Raises:
3498
- ILOperationException: 그런 큐가 없을 때.
3565
+ ILOperationException: 그런 큐가 없을 때. 또는 엔진이 6.3.4(rev 3299)
3566
+ 미만이라 유량 통계 자체를 지원하지 않을 때.
3499
3567
  ILException: 통신 실패.
3500
3568
 
3501
3569
  Note:
3502
3570
  0 으로 채워진 항목과 실제로 유량이 0 이었던 구간은 값만으로 구분되지
3503
3571
  않는다. 적체 추세만 보는 용도로 쓰라.
3504
3572
 
3505
- .. warning:: 갓 만든 큐는 잠시 조회되지 않는다
3573
+ .. note:: 값은 **1초 주기**로 채워진다
3574
+
3575
+ 유량 스냅샷은 서버가 1초마다 적재한다. 그래서 갓 만든 큐도 **조회
3576
+ 자체는 즉시 되지만**(엔진 r3336+) 유입/유출 값은 첫 적재가 돌 때까지
3577
+ 0 이다 - 실측: put 직후 0초 시점 0, 1.2초 시점 5건/2695B (r3336).
3578
+ 즉시 깊이가 필요하면 :meth:`getQueueRealTimeInfo` 를 쓰라. 그쪽은
3579
+ 적재 주기와 무관하게 현재 값을 준다.
3506
3580
 
3507
- 유량 집계 테이블은 **주기적으로 갱신**된다. 큐를 만든 직후에는 아직
3508
- 등록되지 않아 ``MIMQE_OBJECT_NOT_FOUND`` 가 나고, **약 1분 뒤부터**
3509
- 정상 조회된다(실서버 확인: 생성 직후 실패 -> 35초 뒤 성공).
3510
- put/get 을 해도 앞당겨지지 않는다. **서버 동작이라 클라이언트에서
3511
- 우회할 수 없다** - 방금 만든 큐의 현황은 :meth:`getQueueRealTimeInfo`
3512
- 로 보라(이쪽은 즉시 조회된다).
3581
+ **엔진 r3336 미만에서는 갓 만든 큐가 최대 1초간 조회되지 않는다** -
3582
+ 첫 적재 전까지 ``MIMQE_OBJECT_NOT_FOUND`` 가 난다(r3335 실측
3583
+ 0.1~1.1초, 단건·배열 50회). put/get 을 해도 앞당겨지지 않는다.
3584
+
3585
+ .. versionchanged:: 0.1.6
3586
+ 이전 문서는 "유량 테이블은 주기적(약 1분) 갱신"이라고 적었으나
3587
+ **사실이 아니다**. 실서버에서 관측된 35초 지연은 적재 주기가
3588
+ 아니라 **적재 스레드가 분 단위 통계 리포트에 붙들려 있던 것**으로,
3589
+ 엔진 r3331 통계 컴팩션으로 해소됐다.
3590
+
3591
+ .. versionchanged:: 0.4.4
3592
+ 엔진 r3336 이 큐 생성 시점에 유량 항목을 미리 등재하도록 바뀌어
3593
+ **생성 직후 조회 실패가 사라졌다**(24/24 첫 시도 성공, 값은 0).
3594
+ 재시도 코드는 구엔진 대비로만 남기면 된다.
3513
3595
 
3514
3596
  """
3597
+ self._requireQueueFlowSupport()
3515
3598
  try:
3516
3599
  res_msg = self.service.inquire(
3517
3600
  self.qmgrName, ILC.MIMQ_GET_QUEUE_ACCESS_INFO_ARRAY, queueName
@@ -4885,6 +4968,15 @@ class ILAdminQmgr:
4885
4968
  Note:
4886
4969
  **누가 언제 마지막으로 이 큐를 건드렸는지** 알 수 있어 소비자가
4887
4970
  멈춘 상황을 진단할 때 유용하다.
4971
+
4972
+ .. note:: 구엔진(6.3.3 미만)에 붙었을 때
4973
+
4974
+ 그 시절 엔진은 이 요청에 **큐 속성 블롭을 통째로** 답한다. 사용 현황
4975
+ 통계가 속성 안에 들어 있었기 때문이다. 클라이언트가 알아서 판별해
4976
+ 같은 필드를 채우므로 호출부는 달라질 게 없다. 다만
4977
+ :attr:`~ilink.properties.ILQueueRealTimeInfo.partitions` 는 **빈
4978
+ 목록**이다 - 파티셔닝은 6.3.3 에서 들어온 기능이라 구엔진 큐에는
4979
+ 파티션이라는 개념 자체가 없다.
4888
4980
  """
4889
4981
  try:
4890
4982
  res_msg = self.service.inquire(
@@ -4892,7 +4984,12 @@ class ILAdminQmgr:
4892
4984
  )
4893
4985
  if res_msg.getType() == ILC.MIMQ_REPORT_MESSAGE:
4894
4986
  raise ILOperationException(res_msg.getReportMsg().getArg2())
4895
- return ILQueueRealTimeInfo(res_msg.getDataMsg().getData())
4987
+ data = res_msg.getDataMsg().getData() or b""
4988
+ # 구엔진(rev 3155 미만)은 이 요청에 **큐 속성 블롭 4096** 을 그대로
4989
+ # 답한다. 그 시절에는 사용 현황 통계가 속성 안에 들어 있었기 때문이다.
4990
+ if len(data) >= ILC.MIMQ_VO_QUEUE_LEN_LEGACY:
4991
+ return ILQueueRealTimeInfo.from_legacy_queue_blob(data)
4992
+ return ILQueueRealTimeInfo(data)
4896
4993
  except _PASSTHROUGH_EXCEPTIONS:
4897
4994
  raise
4898
4995
  except Exception as exc:
@@ -719,6 +719,19 @@ class ILC:
719
719
  MIMQ_VO_SERVER_LEN = 428
720
720
  MIMQ_VO_QMGR_LEN = 666
721
721
  MIMQ_VO_QUEUE_LEN = 2200
722
+ #: 큐 속성 블롭의 **구버전(4096) 길이**. 엔진 6.3.3 미만이 쓰던 레이아웃이다.
723
+ #: 0~1772 와 2142~2162 는 현행과 같고, 그 사이 370 바이트만 다르다 -
724
+ #: 구버전은 사용 현황 통계, 현행은 만료/영속성/파티셔닝 설정이 들어간다.
725
+ MIMQ_VO_QUEUE_LEN_LEGACY = 4096
726
+ #: 큐 속성이 2200 레이아웃으로 바뀐 **첫 엔진 리비전**(6.3.3.3155).
727
+ #: 이 값보다 낮으면 4096 레이아웃으로 주고받아야 한다.
728
+ QUEUE_LAYOUT_MIN_REVISION = 3155
729
+ #: 구버전(4096) 블롭에서 공통 접두가 끝나고 레이아웃이 갈리는 지점.
730
+ #: 큐 **유량 통계** 조회가 들어온 첫 엔진 리비전(6.3.4.3299).
731
+ QUEUE_FLOW_MIN_REVISION = 3299
732
+ QUEUE_LEGACY_DIVERGE_OFFSET = 1772
733
+ #: 구버전(4096) 블롭에서 incomingDelayDepth 가 있는 위치. 현행과 같다.
734
+ QUEUE_LEGACY_TAIL_OFFSET = 2142
722
735
  MIMQ_VO_CHANNEL_LEN = 2048
723
736
  MIMQ_VO_DEST_ITEM_LEN = 1024
724
737
  MIMQ_VO_QUEUE_INFO_LEN = 512
@@ -2524,7 +2524,14 @@ class ILMsgUtil:
2524
2524
  elif cls is ILQmgrProperty:
2525
2525
  serialized_length = ILC.MIMQ_VO_QMGR_LEN
2526
2526
  elif cls is ILQueueProperty:
2527
+ # 큐 속성만 컨테이너 길이가 바뀐 적이 있다(엔진 6.3.3 에서 4096 -> 2200).
2528
+ # 버전을 따지는 대신 **응답 길이에서 보폭을 역산**한다. 구엔진에 붙어도
2529
+ # 목록이 그대로 읽히고, 새 엔진에서는 계산 결과가 2200 이라 무해하다.
2527
2530
  serialized_length = ILC.MIMQ_VO_QUEUE_LEN
2531
+ if count > 0:
2532
+ stride = (len(ba) - count_length) // count
2533
+ if stride == ILC.MIMQ_VO_QUEUE_LEN_LEGACY:
2534
+ serialized_length = ILC.MIMQ_VO_QUEUE_LEN_LEGACY
2528
2535
  elif cls is ILChannelProperty:
2529
2536
  serialized_length = ILC.MIMQ_VO_CHANNEL_LEN
2530
2537
  elif cls is ILSpokeProperty: