jetstream-api 0.4.3__tar.gz → 0.4.4__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.egg-info → jetstream_api-0.4.4}/PKG-INFO +25 -8
  2. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/README.md +24 -7
  3. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/__init__.py +1 -1
  4. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/admin.py +116 -19
  5. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/common.py +13 -0
  6. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/msg.py +7 -0
  7. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/properties.py +114 -21
  8. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/qmgr.py +252 -21
  9. {jetstream_api-0.4.3 → jetstream_api-0.4.4/jetstream_api.egg-info}/PKG-INFO +25 -8
  10. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/pyproject.toml +1 -1
  11. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/LICENSE.txt +0 -0
  12. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/admintopic.py +0 -0
  13. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/connectivity.py +0 -0
  14. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/exception.py +0 -0
  15. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/pattern.py +0 -0
  16. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/producer.py +0 -0
  17. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/py.typed +0 -0
  18. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/topic.py +0 -0
  19. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/ilink/util.py +0 -0
  20. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/jetstream_api.egg-info/SOURCES.txt +0 -0
  21. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/jetstream_api.egg-info/dependency_links.txt +0 -0
  22. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/jetstream_api.egg-info/requires.txt +0 -0
  23. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/jetstream_api.egg-info/top_level.txt +0 -0
  24. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/setup.cfg +0 -0
  25. {jetstream_api-0.4.3 → jetstream_api-0.4.4}/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.4
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,32 @@ 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)
143
146
  ```
144
147
 
145
- > `set_auto_reconnect`는 **transacted 세션에서는 동작하지 않습니다.** 미커밋 트랜잭션이
146
- > 재접속으로 유실되므로, 예외를 받고 앱이 트랜잭션을 처음부터 재수행해야 합니다
147
- > (`reconnect()` 직접 호출은 가능합니다). auto-commit 세션도 응답 유실 시점의 재시도로
148
- > **중복 put이 발생할 수 있습니다.**
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 이 생길 수 있습니다.** 멱등하지 않은 처리라면 메시지 ID 로 중복을 걸러야
165
+ > 합니다. 미커밋 트랜잭션이 없을 때만 재시도하므로 트랜잭션 유실은 없습니다.
149
166
 
150
167
  ## 0.4.0 신규 - 관리 연결 하나로 pub/sub
151
168
 
@@ -91,15 +91,32 @@ 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)
97
100
  ```
98
101
 
99
- > `set_auto_reconnect`는 **transacted 세션에서는 동작하지 않습니다.** 미커밋 트랜잭션이
100
- > 재접속으로 유실되므로, 예외를 받고 앱이 트랜잭션을 처음부터 재수행해야 합니다
101
- > (`reconnect()` 직접 호출은 가능합니다). auto-commit 세션도 응답 유실 시점의 재시도로
102
- > **중복 put이 발생할 수 있습니다.**
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 이 생길 수 있습니다.** 멱등하지 않은 처리라면 메시지 ID 로 중복을 걸러야
119
+ > 합니다. 미커밋 트랜잭션이 없을 때만 재시도하므로 트랜잭션 유실은 없습니다.
103
120
 
104
121
  ## 0.4.0 신규 - 관리 연결 하나로 pub/sub
105
122
 
@@ -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.4"
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:
@@ -446,6 +446,7 @@ class ILXmitQueueMinorProperty(ILQueueMinorProperty):
446
446
  super().__init__(name, ILC.MIMQ_CHANNEL_TRANSMISSION_QUEUE)
447
447
 
448
448
 
449
+
449
450
  class ILQueueProperty(_AutoAccessor):
450
451
  """큐의 **전체 속성**. 큐 생성/조회/변경에 모두 쓰이는 값 객체.
451
452
 
@@ -515,6 +516,11 @@ class ILQueueProperty(_AutoAccessor):
515
516
  maxMemorySize (int): 메모리 상주 한도(바이트). 기본 10485760(10MB).
516
517
  sharedMemorySize (int): 공유 메모리 크기. 기본 0.
517
518
  numberOfPartition (int): 파티션 수. 기본 10.
519
+ legacyBlob (bool): 이 객체가 **구버전(4096) 블롭**에서 만들어졌는지.
520
+ 엔진 rev 3155 미만이 보낸 응답이면 True 다. 그 경우 ``expiry`` /
521
+ ``expiredQueue`` / ``deliveryMode`` / ``maxMemorySize`` /
522
+ ``sharedMemorySize`` / ``numberOfPartition`` 은 **서버가 준 값이 아니라
523
+ 기본값**이다 - 구엔진에는 그 설정이 없다.
518
524
  """
519
525
 
520
526
  def __init__(
@@ -552,6 +558,8 @@ class ILQueueProperty(_AutoAccessor):
552
558
  self.maxMemorySize = 10485760
553
559
  self.sharedMemorySize = 0
554
560
  self.numberOfPartition = 10
561
+ #: 구버전(4096) 블롭에서 만들어졌는지. 내부 판별용.
562
+ self.legacyBlob = False
555
563
 
556
564
  if isinstance(name, (bytes, bytearray)):
557
565
  self._from_bytes(bytes(name))
@@ -575,6 +583,9 @@ class ILQueueProperty(_AutoAccessor):
575
583
  + str(len(ba))
576
584
  + "]"
577
585
  )
586
+ # 엔진 6.3.3 미만은 4096 레이아웃을 보낸다. 길이로 가른다 -
587
+ # 엔진의 QueueProperty::IsLegacyBlob(nDataSize) 와 같은 판정이다.
588
+ self.legacyBlob = len(ba) >= ILC.MIMQ_VO_QUEUE_LEN_LEGACY
578
589
  offset = 0
579
590
  self.name = ba_pick_string(ba, offset, 70)
580
591
  offset += 70
@@ -603,19 +614,27 @@ class ILQueueProperty(_AutoAccessor):
603
614
  offset += 10
604
615
  self.traceEnabled = ba_pick_string(ba, offset, 1).lower() == "t"
605
616
  offset += 2
606
- self.expiry = ba_pick_long(ba, offset, 20)
607
- offset += 20
608
- self.expiredQueue = ba_pick_string(ba, offset, 70)
609
- offset += 70
610
- self.deliveryMode = ba_pick_int(ba, offset, 2)
611
- offset += 2
612
- self.maxMemorySize = ba_pick_long(ba, offset, 20)
613
- offset += 20
614
- self.sharedMemorySize = ba_pick_long(ba, offset, 20)
615
- offset += 20
616
- self.numberOfPartition = ba_pick_int(ba, offset, 10)
617
- offset += 10
618
- offset += 228
617
+ # ---- 여기(1772)부터 레이아웃이 갈린다 ----
618
+ if self.legacyBlob:
619
+ # 구버전 4096: 이 자리는 사용 현황 통계다. 통계는
620
+ # ILQueueRealTimeInfo 가 같은 블롭에서 따로 꺼내가므로 여기서는
621
+ # 건너뛴다. 신규 설정 항목은 애초에 없으니 기본값을 유지한다.
622
+ offset = ILC.QUEUE_LEGACY_TAIL_OFFSET
623
+ else:
624
+ self.expiry = ba_pick_long(ba, offset, 20)
625
+ offset += 20
626
+ self.expiredQueue = ba_pick_string(ba, offset, 70)
627
+ offset += 70
628
+ self.deliveryMode = ba_pick_int(ba, offset, 2)
629
+ offset += 2
630
+ self.maxMemorySize = ba_pick_long(ba, offset, 20)
631
+ offset += 20
632
+ self.sharedMemorySize = ba_pick_long(ba, offset, 20)
633
+ offset += 20
634
+ self.numberOfPartition = ba_pick_int(ba, offset, 10)
635
+ offset += 10
636
+ offset += 228
637
+ # ---- 공통 접미 (2142) ----
619
638
  self.incomingDelayDepth = ba_pick_int(ba, offset, 10)
620
639
  offset += 10
621
640
  self.incomingDelay = ba_pick_int(ba, offset, 10)
@@ -623,16 +642,27 @@ class ILQueueProperty(_AutoAccessor):
623
642
  except Exception as exc:
624
643
  raise ILException(exc) from exc
625
644
 
626
- def get_serialized(self) -> bytes:
645
+ def get_serialized(self, legacy: bool = False) -> bytes:
627
646
  """이 객체를 서버 규격의 **고정폭 바이트**로 직렬화한다.
628
647
 
648
+ Args:
649
+ legacy (bool): True 면 **구버전(4096) 레이아웃**으로 만든다.
650
+ 엔진 rev 3155 미만에 보낼 때 쓴다. 기본 False(현행 2200).
651
+
629
652
  Returns:
630
653
  bytes: 서버로 그대로 보낼 수 있는 바이트열.
631
654
 
632
655
  Note:
633
656
  라이브러리가 요청을 보낼 때 내부적으로 부른다. 응용이 직접 부를
634
- 일은 거의 없다.
657
+ 일은 거의 없다 - 어느 레이아웃으로 보낼지는 접속한 엔진 리비전을
658
+ 보고 :class:`ILAdminQmgr` 가 정한다.
659
+
660
+ 구버전 레이아웃에는 ``expiry`` / ``expiredQueue`` / ``deliveryMode`` /
661
+ ``maxMemorySize`` / ``sharedMemorySize`` / ``numberOfPartition`` 자리가
662
+ **없다.** 그 값들은 실리지 않고 버려진다 - 구엔진이 모르는 설정이다.
635
663
  """
664
+ if legacy:
665
+ return self._get_serialized_legacy()
636
666
  ba = bytearray(ILC.MIMQ_VO_QUEUE_LEN)
637
667
  offset = 0
638
668
  ba_merge(self.name, ba, offset)
@@ -681,6 +711,18 @@ class ILQueueProperty(_AutoAccessor):
681
711
  offset += 10
682
712
  return bytes(ba)
683
713
 
714
+ def _get_serialized_legacy(self) -> bytes:
715
+ """구버전(4096) 레이아웃으로 직렬화한다. 엔진 rev 3155 미만 전송용."""
716
+ cur = self.get_serialized() # 현행 2200
717
+ ba = bytearray(ILC.MIMQ_VO_QUEUE_LEN_LEGACY)
718
+ d = ILC.QUEUE_LEGACY_DIVERGE_OFFSET
719
+ t = ILC.QUEUE_LEGACY_TAIL_OFFSET
720
+ # 공통 접두(0~1772)와 공통 접미(2142~2162)만 옮긴다. 그 사이는 구버전에서
721
+ # 사용 현황 통계 자리라 설정값을 넣으면 통계로 오독된다 - 0 으로 남긴다.
722
+ ba[0:d] = cur[0:d]
723
+ ba[t:t + 20] = cur[t:t + 20]
724
+ return bytes(ba)
725
+
684
726
  def get_type_string(self) -> str:
685
727
  """종류 코드를 사람이 읽는 문자열로 바꾼다.
686
728
 
@@ -1739,16 +1781,30 @@ class ILSecureFileProperty(_AutoAccessor):
1739
1781
  class ILQueueRealTimeInfo(_AutoAccessor):
1740
1782
  """큐의 **실시간 상태.** :meth:`ILAdminQmgr.getQueueRealTimeInfo` 가 돌려준다.
1741
1783
 
1742
- "소비자가 멈춘 것 같다" 를 진단할 때 ``lastGetTime`` / ``lastGetClient`` 가
1743
- 결정적인 단서가 된다.
1784
+ "소비자가 멈춘 것 같다" 를 진단할 때 깊이와 미커밋 건수를 함께 보면 된다.
1785
+
1786
+ Note:
1787
+ **마지막 put/get 4항목은 6.3.3 미만 엔진 전용이다** - ``lastPutTime`` /
1788
+ ``lastGetTime`` / ``lastPutClient`` / ``lastGetClient`` 넷을 말한다.
1789
+ 6.3.3 에서 실시간 속성이 별도 조회로 분리되면서 엔진의 실시간 정보
1790
+ 구조체에서 이 4개가 빠졌다 - 전송 규격에 자리는 남아 있지만 서버가
1791
+ 채우지 않는다(직렬화에서 그 180바이트를 건너뛴다). 그래서 구엔진에
1792
+ 붙었을 때만 값이 온다.
1793
+
1794
+ 6.3.3+ 에서 "누가 마지막으로 이 큐를 건드렸는지" 를 보려면 세션 쪽을
1795
+ 보라 - :meth:`ILAdminQmgr.getSessionList` 가 주는
1796
+ :class:`ILSessionProperty` 의 ``lastAccessObject`` 가 그 정보를 세션
1797
+ 기준으로 담고 있다.
1744
1798
 
1745
1799
  Attributes:
1746
1800
  curDepth (int): 현재 깊이(메시지 수). 기본 0.
1747
1801
  openTime (int): 큐가 열린 시각(epoch 밀리초). 기본 0.
1748
- lastPutTime (int): 마지막 put 시각(epoch 밀리초). 없으면 0.
1749
- lastGetTime (int): 마지막 get 시각(epoch 밀리초). 없으면 0.
1750
- lastPutClient (str): 마지막으로 put 한 클라이언트 이름. 기본 ``""``.
1751
- lastGetClient (str): 마지막으로 get 한 클라이언트 이름. 기본 ``""``.
1802
+ lastPutTime (int): 마지막 put 시각(epoch 밀리초). **엔진 6.3.3+ 는 항상 0**.
1803
+ lastGetTime (int): 마지막 get 시각(epoch 밀리초). **엔진 6.3.3+ 는 항상 0**.
1804
+ lastPutClient (str): 마지막으로 put 한 클라이언트 이름.
1805
+ **엔진 6.3.3+ 는 항상** ``""``.
1806
+ lastGetClient (str): 마지막으로 get 한 클라이언트 이름.
1807
+ **엔진 6.3.3+ 는 항상** ``""``.
1752
1808
  numberOfClient (int): 이 큐를 열고 있는 클라이언트 수. 기본 0.
1753
1809
  numberOfChannel (int): 이 큐를 열고 있는 채널 수. 기본 0.
1754
1810
  uncommittedPut (int): 아직 커밋되지 않은 put 건수. 기본 0.
@@ -1889,6 +1945,43 @@ class ILQueueRealTimeInfo(_AutoAccessor):
1889
1945
  ret += " " + str(partition)
1890
1946
  return ret
1891
1947
 
1948
+ @classmethod
1949
+ def from_legacy_queue_blob(cls, ba: bytes) -> "ILQueueRealTimeInfo":
1950
+ """**구버전(4096) 큐 속성 블롭**에서 실시간 정보를 뽑아 만든다.
1951
+
1952
+ Args:
1953
+ ba (bytes): 엔진 rev 3155 미만이 보낸 큐 속성 블롭(4096).
1954
+
1955
+ Returns:
1956
+ ILQueueRealTimeInfo: 채워진 실시간 정보 객체.
1957
+
1958
+ Note:
1959
+ 구엔진은 실시간 정보 요청(``MIMQ_GET_QUEUE_LIVE_PROPERTY``)에 **큐 속성
1960
+ 블롭 그대로** 답한다. 사용 현황 통계가 그 블롭 안에 들어 있기 때문이다
1961
+ (6.3.3 에서 별도 조회로 분리됐다). 그래서 같은 응답으로 속성 객체와
1962
+ 이 객체를 모두 채울 수 있다.
1963
+
1964
+ :attr:`partitions` 는 **빈 목록**이 된다. 파티셔닝은 6.3.3 에서 들어온
1965
+ 기능이라 구엔진 큐에는 파티션이라는 개념 자체가 없다 - 누락이 아니라
1966
+ 사실이다.
1967
+ """
1968
+ o = ILC.QUEUE_LEGACY_DIVERGE_OFFSET
1969
+ info = cls()
1970
+ info.curDepth = ba_pick_int(ba, 1740, 10)
1971
+ info.openTime = ba_pick_long(ba, o, 20)
1972
+ info.lastPutTime = ba_pick_long(ba, o + 20, 20)
1973
+ info.lastGetTime = ba_pick_long(ba, o + 40, 20)
1974
+ info.lastPutClient = ba_pick_string(ba, o + 60, 70)
1975
+ info.lastGetClient = ba_pick_string(ba, o + 130, 70)
1976
+ info.numberOfClient = ba_pick_int(ba, o + 200, 10)
1977
+ info.numberOfChannel = ba_pick_int(ba, o + 210, 10)
1978
+ # o+220 ~ o+300 : accumulated put/get/putSize/getSize (누적 총량).
1979
+ # 현행 유량 조회(ILQueueFlow)는 **구간별** 값이라 의미가 달라 쓰지 않는다.
1980
+ info.uncommittedPut = ba_pick_int(ba, o + 300, 10)
1981
+ info.uncommittedGet = ba_pick_int(ba, o + 310, 10)
1982
+ info.partitions = []
1983
+ return info
1984
+
1892
1985
 
1893
1986
  class ILQueueFlow(_AutoAccessor):
1894
1987
  """큐의 유입/유출량 스냅샷 하나.
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import os
4
5
  import time
5
6
  import uuid
6
7
  from typing import List
@@ -66,7 +67,12 @@ class ILQmgr:
66
67
  # HA-3 클라이언트 페일오버: 접속 성공 시 캐싱한 클러스터 후보("host:port")
67
68
  self.clusterAddresses: list[str] | None = None
68
69
  self.clusterLeaderNodeId = -1
69
- self.autoReconnect = False
70
+ # endpoint 목록을 남겨 둘 파일. **지정했을 때만** 읽고 쓴다(기본 없음).
71
+ self.endpointCacheFile: str | None = None
72
+ # 트랜잭션 세션에서 **커밋되지 않은 작업이 걸려 있는지**.
73
+ # 성공한 put/get 마다 켜고 commit/rollback/접속 시 끈다.
74
+ # 자동 재접속을 허용할지 판단하는 데 쓴다(걸린 게 없으면 잃을 것도 없다).
75
+ self.txnPending = False
70
76
 
71
77
  # --- context manager ---------------------------------------------------
72
78
  def __enter__(self) -> "ILQmgr":
@@ -226,12 +232,13 @@ class ILQmgr:
226
232
  def connect(
227
233
  self,
228
234
  host: str,
229
- port: int,
230
- client_name: str = "",
235
+ port: int | str | bool | None = None,
236
+ client_name: str | bool = "",
231
237
  auto_commit: bool = False,
232
238
  *,
233
239
  clientName: str | None = None,
234
240
  isAutoCommit: bool | None = None,
241
+ _allow_redirect: bool = True,
235
242
  ) -> None:
236
243
  """큐 관리자에 접속해 세션을 연다.
237
244
 
@@ -240,9 +247,13 @@ class ILQmgr:
240
247
  찾아갈 수 있게 한다.
241
248
 
242
249
  Args:
243
- host (str): 큐 관리자의 IP 또는 호스트명.
250
+ host (str): 큐 관리자의 IP 또는 호스트명. **``"host:port"`` 또는 콤마로
251
+ 이은 목록** (``"h1:p1,h2:p2"``)을 줄 수도 있다 - 그 경우 ``port`` 는
252
+ 생략하고 뒤 인자가 한 칸씩 당겨진다
253
+ (``connect("h1:p1,h2:p2", "APP", True)``).
244
254
  port (int): 큐 관리자의 리스너 포트. **관리 서비스(ilsvc) 포트가 아니다** -
245
255
  관리 포트(보통 9998)는 :class:`~ilink.admin.ILAdminService` 쪽이다.
256
+ 주소 목록 형태로 줬으면 지정하지 않는다.
246
257
  client_name (str): 이 세션의 클라이언트 이름. 기본 ``""`` 이면 **호출한
247
258
  모듈 이름을 자동으로** 쓴다. 큐 관리자 세션 목록에 그대로 보이므로
248
259
  운영상 알아볼 수 있는 이름을 주는 편이 좋다.
@@ -260,13 +271,63 @@ class ILQmgr:
260
271
  세 번째 인자에 ``bool`` 을 주면 ``auto_commit``
261
272
  으로 해석한다 - ``connect(host, port, True)`` 가 동작한다.
262
273
 
274
+ .. note:: 클러스터에는 **주소 하나면 된다**
275
+
276
+ 접속에 성공하면 핸드셰이크 직후 나머지 노드 주소를 서버에서 받아
277
+ 캐싱한다(:meth:`get_cluster_addresses` 로 확인). 이후 리더가 바뀌어도
278
+ :meth:`reconnect` 가 그 후보로 새 리더를 찾아간다.
279
+
280
+ **주소 목록은 최초 접속 부트스트랩용이다.** 캐싱은 접속에 성공한
281
+ 뒤에 일어나므로, 하나만 준 그 주소가 첫 접속 시점에 죽어 있으면 갈
282
+ 곳이 없다. 목록을 주면 순회하며 붙고, 팔로워가
283
+ ``MIMQE_FOLLOWER_NODE`` 로 거절하면 응답에 실린 리더 힌트를 맨 앞으로
284
+ 올려 재시도한다(최대 2회전).
285
+
286
+ **단일 주소 형태는 리더 힌트를 따라가지 않는다** - 팔로워를 지목하면
287
+ 그대로 예외가 난다. 그때는 :meth:`reconnect` 를 부르면 리더로 넘어간다.
288
+
263
289
  Example::
264
290
 
265
291
  qmgr = ILQmgr()
266
292
  qmgr.connect("192.168.0.10", 5000) # 트랜잭션 세션
267
293
  qmgr.connect("192.168.0.10", 5000, auto_commit=True) # 자동 커밋
268
294
  qmgr.connect("192.168.0.10", 5000, client_name="billing-svc")
295
+
296
+ # 클러스터 - 주소 하나면 충분하다(나머지는 접속 후 캐싱된다)
297
+ qmgr.connect("10.0.0.1", 5000, "APP", True)
298
+ qmgr.getClusterAddresses() # ['10.0.0.1:5000', '10.0.0.2:5000']
299
+
300
+ # 첫 접속 시점의 장애까지 대비하려면 목록으로 준다(포트 인자 없음)
301
+ qmgr.connect("10.0.0.1:5000,10.0.0.2:5000", "APP", True)
269
302
  """
303
+ # 주소 목록 형태 - 자바 connect(addressList, clientName, isAutoCommit) 대응.
304
+ # "host:port" 하나든 콤마로 이은 여럿이든 같은 경로로 처리한다.
305
+ # 포트를 따로 받지 않으므로 뒤 인자가 한 칸씩 당겨진 형태다.
306
+ # 포트를 int 로 함께 준 호출은 기존 경로 그대로 둔다(오탐 방지).
307
+ if isinstance(host, str) and ("," in host or (":" in host and not isinstance(port, int))):
308
+ if isinstance(port, bool):
309
+ auto_commit = port
310
+ elif isinstance(port, str):
311
+ if isinstance(client_name, bool):
312
+ auto_commit = client_name
313
+ client_name = port
314
+ elif port is not None:
315
+ raise ILSessionException(
316
+ ILRC.MIMQE_INVALID_ADDRESS,
317
+ "address list form takes no port argument : [%s:%s]" % (host, port),
318
+ )
319
+ if clientName is not None:
320
+ client_name = clientName
321
+ if isAutoCommit is not None:
322
+ auto_commit = isAutoCommit
323
+ candidates = [a.strip() for a in host.split(",") if a.strip()]
324
+ if not candidates:
325
+ raise ILSessionException(
326
+ ILRC.MIMQE_INVALID_ADDRESS, "empty address list"
327
+ )
328
+ self._connect_any(candidates, client_name or "", bool(auto_commit))
329
+ return
330
+
270
331
  # Support legacy positional bool for client_name (Java-style overload)
271
332
  if isinstance(client_name, bool):
272
333
  auto_commit = client_name
@@ -302,6 +363,7 @@ class ILQmgr:
302
363
 
303
364
  self.sessionId = conn_res_msg.getArg1()
304
365
  self.isAutoCommitFlag = auto_commit
366
+ self.txnPending = False
305
367
  if self.sessionId == "":
306
368
  self.disconnect()
307
369
  raise ILSessionException(
@@ -352,9 +414,36 @@ class ILQmgr:
352
414
  # 막으면 정상 클러스터에서 페일오버 후보 캐싱이 조용히 사라진다.
353
415
  if not (0 < self.revision < 3265):
354
416
  self._fetch_cluster_info()
355
- except _PASSTHROUGH_EXCEPTIONS:
356
- raise
417
+ self._save_endpoint_cache()
357
418
  except Exception as exc:
419
+ # 팔로워를 지목했으면 응답에 실린 리더 힌트로 **한 번** 옮겨 붙는다.
420
+ # 앱이 옛 리더 주소만 들고 재기동해도 승격된 리더를 찾아간다 -
421
+ # 주소를 하나만 줘도 되게 한다는 설계 의도가 여기까지다.
422
+ if _allow_redirect:
423
+ hint = self._extract_leader_hint(exc)
424
+ if hint:
425
+ colon = hint.rfind(":")
426
+ try:
427
+ self.service.shutdown()
428
+ except Exception:
429
+ pass
430
+ self.connect(
431
+ hint[:colon],
432
+ int(hint[colon + 1:]),
433
+ client_name,
434
+ auto_commit,
435
+ _allow_redirect=False,
436
+ )
437
+ return
438
+ # 힌트를 줄 상대조차 없으면(노드가 죽어 있음) 파일에 남겨 둔
439
+ # 후보로 한 번 더 돈다. 경로를 지정했을 때만 동작한다.
440
+ cached = [a for a in self._load_endpoint_cache()
441
+ if a != "%s:%s" % (host, port)]
442
+ if cached:
443
+ self._connect_any(cached, client_name, auto_commit)
444
+ return
445
+ if isinstance(exc, _PASSTHROUGH_EXCEPTIONS):
446
+ raise
358
447
  raise ILException(exc) from exc
359
448
 
360
449
  def disconnect(self) -> None:
@@ -428,6 +517,7 @@ class ILQmgr:
428
517
  committed = qmgr.commit() # 두 건이 원자적으로 확정된다
429
518
  """
430
519
  try:
520
+ self.txnPending = False
431
521
  report_msg = self.service.request(ILC.MIMQ_COMMIT_TRANSACTION_REQUEST)
432
522
  if report_msg is None:
433
523
  raise ILOperationException("MIMQE_INTERNAL_ERROR")
@@ -460,6 +550,7 @@ class ILQmgr:
460
550
  """
461
551
  try:
462
552
  self.set_last_access_time()
553
+ self.txnPending = False
463
554
  report_msg = self.service.request(ILC.MIMQ_ROLLBACK_REQUEST)
464
555
  if (
465
556
  report_msg is not None
@@ -651,27 +742,110 @@ class ILQmgr:
651
742
  return list(self.clusterAddresses) if self.clusterAddresses else None
652
743
 
653
744
  def set_auto_reconnect(self, value: bool) -> None:
654
- """연결이 끊겼을 때 자동으로 재접속할지 정한다. (기본 False)
745
+ """**더 이상 쓰이지 않는다 - 불러도 아무 일도 일어나지 않는다.**
655
746
 
656
747
  Args:
657
- value (bool): True 면 통신 장애 시 라이브러리가 스스로 재접속을 시도한다.
748
+ value (bool): 무시된다.
658
749
 
659
- Warning:
660
- **트랜잭션 세션에서는 켜도 동작하지 않는다.** 재접속하면 미커밋
661
- 트랜잭션이 유실되므로, 예외를 그대로 올려 앱이 트랜잭션을 처음부터
662
- 다시 수행하게 한다(:meth:`reconnect` 직접 호출은 언제든 가능).
663
- 자동 커밋 세션이라도 **응답을 못 받은 시점에 재시도하면 중복 put 이
664
- 생길 수 있다** - 멱등하지 않은 처리라면 주의하라.
750
+ Note:
751
+ 0.4.4 부터 자동 재접속은 **옵션이 아니라 기본 동작**이다. 접속 때
752
+ 받아 둔 endpoint 목록이 있으면(= 클러스터) 통신 장애로 실패한
753
+ put/get 을 페일오버 재접속 후 1회 자동 재시도한다. 다만 **미커밋
754
+ 트랜잭션이 걸려 있으면** 재접속으로 그것이 유실되므로 예외를 그대로
755
+ 올린다 - 앱이 트랜잭션을 처음부터 다시 수행해야 한다.
756
+
757
+ 메서드 자체는 기존 코드가 깨지지 않도록 남겨 둔 것이다.
758
+
759
+ .. deprecated:: 0.4.4
760
+ 자동 재접속은 항상 켜져 있다.
665
761
  """
666
- self.autoReconnect = bool(value)
762
+ # no-op : 자동 재접속은 이제 조건부 기본 동작이다(endpoint 목록 + 미커밋 없음).
667
763
 
668
764
  def is_auto_reconnect(self) -> bool:
669
- """자동 재접속이 켜져 있는지. 기본 False.
765
+ """**항상 True 다.** 0.4.4 부터 자동 재접속은 옵션이 아니다.
670
766
 
671
767
  Returns:
672
- bool: 자동 재접속이 켜져 있는지. 기본 False.
768
+ bool: 항상 True.
769
+
770
+ Note:
771
+ .. deprecated:: 0.4.4
772
+ 자동 재접속은 항상 켜져 있다.
673
773
  """
674
- return self.autoReconnect
774
+ return True
775
+
776
+ def set_endpoint_cache_file(self, path: str | None) -> None:
777
+ """클러스터 endpoint 목록을 남겨 둘 **파일 경로**를 지정한다.
778
+
779
+ Args:
780
+ path (str | None): 파일 경로. None 이면 파일을 쓰지도 읽지도 않는다(기본).
781
+
782
+ Note:
783
+ **접속하기 전에 지정해야 한다.** 지정하면
784
+
785
+ * :meth:`connect` 가 설정 주소로 붙지 못했을 때 이 파일의 주소들을
786
+ **보조 후보**로 써서 한 번 더 시도한다.
787
+ * 접속에 성공해 서버에서 받은 목록이 달라졌으면 파일을 새로 쓴다.
788
+
789
+ 설정 주소를 **항상 먼저** 쓴다 - 파일이 낡아 이미 빠진 노드를 담고
790
+ 있어도 정상 경로가 그것 때문에 느려지지 않는다.
791
+
792
+ 입출력 실패는 **전부 조용히 무시한다.** 캐시 때문에 접속이 깨지면
793
+ 안 되기 때문이다(경로가 없거나 권한이 없어도 그냥 동작한다).
794
+
795
+ 한 줄에 ``host:port`` 하나씩 적힌 평범한 텍스트라 사람이 열어
796
+ 고쳐도 된다. ``#`` 으로 시작하는 줄은 주석이다.
797
+
798
+ Example::
799
+
800
+ q = ILQmgr()
801
+ q.set_endpoint_cache_file("/var/run/myapp/ilink-cluster.txt")
802
+ q.connect("10.0.0.1", 5000, "APP", True)
803
+ """
804
+ self.endpointCacheFile = path
805
+
806
+ def setEndpointCacheFile(self, path: str | None) -> None:
807
+ """:meth:`set_endpoint_cache_file` 의 camelCase 별칭.
808
+
809
+ Args:
810
+ path (str | None): 파일 경로.
811
+ """
812
+ self.set_endpoint_cache_file(path)
813
+
814
+ def _load_endpoint_cache(self) -> list[str]:
815
+ # 파일에서 보조 후보를 읽는다. 없거나 못 읽으면 빈 목록.
816
+ if not self.endpointCacheFile:
817
+ return []
818
+ try:
819
+ out = []
820
+ with open(self.endpointCacheFile, "r", encoding="utf-8") as fp:
821
+ for line in fp:
822
+ line = line.strip()
823
+ if not line or line.startswith("#"):
824
+ continue
825
+ if line.rfind(":") > 0 and line not in out:
826
+ out.append(line)
827
+ return out
828
+ except Exception:
829
+ return []
830
+
831
+ def _save_endpoint_cache(self) -> None:
832
+ # 받은 목록이 파일과 다르면 원자적으로 바꿔 쓴다(임시 파일 + rename).
833
+ if not self.endpointCacheFile or not self.clusterAddresses:
834
+ return
835
+ try:
836
+ if self._load_endpoint_cache() == list(self.clusterAddresses):
837
+ return
838
+ tmp = "%s.%d.tmp" % (self.endpointCacheFile, os.getpid())
839
+ with open(tmp, "w", encoding="utf-8", newline="\n") as fp:
840
+ fp.write("# ilink cluster endpoints - 라이브러리가 자동으로 갱신한다\n")
841
+ for addr in self.clusterAddresses:
842
+ fp.write(addr + "\n")
843
+ os.replace(tmp, self.endpointCacheFile)
844
+ except Exception:
845
+ try:
846
+ os.remove(tmp)
847
+ except Exception:
848
+ pass
675
849
 
676
850
  @staticmethod
677
851
  def _extract_leader_hint(exc: Exception) -> str | None:
@@ -714,7 +888,7 @@ class ILQmgr:
714
888
  except Exception:
715
889
  i += 1; continue
716
890
  try:
717
- self.connect(host, port, client_name, auto_commit)
891
+ self.connect(host, port, client_name, auto_commit, _allow_redirect=False)
718
892
  return
719
893
  except Exception as exc:
720
894
  last_error = exc
@@ -757,9 +931,26 @@ class ILQmgr:
757
931
  candidates.insert(0, last)
758
932
  self._connect_any(candidates, self.clientName, self.isAutoCommitFlag)
759
933
 
934
+ @staticmethod
935
+ def _is_connection_error(exc: BaseException) -> bool:
936
+ # 자바 ILQueue.isConnectionError 대응 - 원인 사슬에 통신 계열이 있으면 참.
937
+ t: BaseException | None = exc
938
+ seen = 0
939
+ while t is not None and seen < 10:
940
+ if isinstance(t, (OSError, ILSessionException)):
941
+ return True
942
+ t = getattr(t, "__cause__", None)
943
+ seen += 1
944
+ return False
945
+
760
946
  def _try_auto_reconnect(self) -> bool:
761
- # 통신 장애 시 내부 호출. **오토커밋 + 활성화** 일 때만 시도한다.
762
- if not self.autoReconnect or not self.isAutoCommitFlag:
947
+ # 통신 장애 시 내부 호출. 옵션이 아니라 기본 동작이다.
948
+ # 접속 때 받아 둔 endpoint 목록이 없으면 갈 곳이 없다(비클러스터/구엔진).
949
+ # 미커밋 트랜잭션이 걸려 있으면 재접속으로 그것이 유실되므로 예외를 그대로
950
+ # 올린다 - 앱이 트랜잭션을 처음부터 다시 수행해야 한다.
951
+ if not self.clusterAddresses:
952
+ return False
953
+ if not self.isAutoCommitFlag and self.txnPending:
763
954
  return False
764
955
  try:
765
956
  self.reconnect()
@@ -1243,6 +1434,17 @@ class ILQueue:
1243
1434
  raise TypeError("unsupported message type")
1244
1435
 
1245
1436
  def _put_msg(self, msg: ILDataMsg) -> str:
1437
+ try:
1438
+ return self._put_msg_once(msg)
1439
+ except Exception as exc:
1440
+ # HA-3: 통신 장애 + 오토커밋 세션이면 페일오버 재접속 후 1회 재시도.
1441
+ # endpoint 목록이 있고 미커밋 트랜잭션이 없을 때만 재접속한다.
1442
+ if ILQmgr._is_connection_error(exc) and self.qmgr._try_auto_reconnect():
1443
+ self.conn = self.qmgr.get_conn()
1444
+ return self._put_msg_once(msg)
1445
+ raise
1446
+
1447
+ def _put_msg_once(self, msg: ILDataMsg) -> str:
1246
1448
  try:
1247
1449
  msg_id = msg.getHeader().getMsgId()
1248
1450
  if msg_id == "":
@@ -1260,6 +1462,11 @@ class ILQueue:
1260
1462
  if report_msg.getReportMsg().getCode() != ILC.MIMQ_SUCCESSFUL_PUT:
1261
1463
  raise ILOperationException(report_msg.getReportMsg().getArg2())
1262
1464
  self.qmgr.set_last_access_time()
1465
+ # 트랜잭션 세션이면 이제 **커밋 안 된 작업이 걸려 있다.**
1466
+ # 실패한 연산은 표시하지 않는다 - 세션이 끊기면 서버가 버리므로
1467
+ # 재접속 후 다시 보내도 중복이 생기지 않는다.
1468
+ if not self.qmgr.is_auto_commit():
1469
+ self.qmgr.txnPending = True
1263
1470
  return msg_id
1264
1471
  except _PASSTHROUGH_EXCEPTIONS:
1265
1472
  raise
@@ -1333,6 +1540,11 @@ class ILQueue:
1333
1540
  if report_msg.getReportMsg().getCode() != ILC.MIMQ_SUCCESSFUL_PUT:
1334
1541
  raise ILOperationException(report_msg.getReportMsg().getArg2())
1335
1542
  self.qmgr.set_last_access_time()
1543
+ # 트랜잭션 세션이면 이제 **커밋 안 된 작업이 걸려 있다.**
1544
+ # 실패한 연산은 표시하지 않는다 - 세션이 끊기면 서버가 버리므로
1545
+ # 재접속 후 다시 보내도 중복이 생기지 않는다.
1546
+ if not self.qmgr.is_auto_commit():
1547
+ self.qmgr.txnPending = True
1336
1548
  return msg_ids
1337
1549
  except _PASSTHROUGH_EXCEPTIONS:
1338
1550
  raise
@@ -1403,6 +1615,21 @@ class ILQueue:
1403
1615
  msg = q.get("abc-123", MatchOption.MATCH_MSG_ID) # 메시지 ID 로
1404
1616
  msg = q.get("corr-456", MatchOption.MATCH_CORREL_ID) # 상관 ID 로
1405
1617
  """
1618
+ try:
1619
+ return self._get_once(matchId, option)
1620
+ except ILNoMsgException:
1621
+ raise
1622
+ except Exception as exc:
1623
+ # HA-3: 통신 장애 + 오토커밋 세션이면 페일오버 재접속 후 1회 재시도.
1624
+ # endpoint 목록이 있고 미커밋 트랜잭션이 없을 때만 재접속한다.
1625
+ if ILQmgr._is_connection_error(exc) and self.qmgr._try_auto_reconnect():
1626
+ self.conn = self.qmgr.get_conn()
1627
+ return self._get_once(matchId, option)
1628
+ raise
1629
+
1630
+ def _get_once(
1631
+ self, matchId: str | None = None, option: MatchOption = MatchOption.NONE
1632
+ ) -> ILDataMsg:
1406
1633
  try:
1407
1634
  ext = None
1408
1635
  if matchId:
@@ -1426,6 +1653,8 @@ class ILQueue:
1426
1653
  self.qmgr.set_last_access_time()
1427
1654
  msg = self.conn.recv(timeout)
1428
1655
  if msg.getType() == ILC.MIMQ_DATA_MESSAGE:
1656
+ if not self.qmgr.is_auto_commit():
1657
+ self.qmgr.txnPending = True
1429
1658
  return msg.getDataMsg()
1430
1659
 
1431
1660
  if msg.getType() != ILC.MIMQ_REPORT_MESSAGE:
@@ -1500,6 +1729,8 @@ class ILQueue:
1500
1729
  offset += length
1501
1730
  temp_msg = ILMsg(ba)
1502
1731
  data_msgs.append(temp_msg.getDataMsg())
1732
+ if data_msgs and not self.qmgr.is_auto_commit():
1733
+ self.qmgr.txnPending = True
1503
1734
  return data_msgs
1504
1735
 
1505
1736
  if msg.getType() != ILC.MIMQ_REPORT_MESSAGE:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.4.3
3
+ Version: 0.4.4
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,32 @@ 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)
143
146
  ```
144
147
 
145
- > `set_auto_reconnect`는 **transacted 세션에서는 동작하지 않습니다.** 미커밋 트랜잭션이
146
- > 재접속으로 유실되므로, 예외를 받고 앱이 트랜잭션을 처음부터 재수행해야 합니다
147
- > (`reconnect()` 직접 호출은 가능합니다). auto-commit 세션도 응답 유실 시점의 재시도로
148
- > **중복 put이 발생할 수 있습니다.**
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 이 생길 수 있습니다.** 멱등하지 않은 처리라면 메시지 ID 로 중복을 걸러야
165
+ > 합니다. 미커밋 트랜잭션이 없을 때만 재시도하므로 트랜잭션 유실은 없습니다.
149
166
 
150
167
  ## 0.4.0 신규 - 관리 연결 하나로 pub/sub
151
168
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jetstream-api"
7
- version = "0.4.3"
7
+ version = "0.4.4"
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