jetstream-api 0.20.1__tar.gz → 0.20.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/PKG-INFO +30 -1
  2. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/README.md +29 -0
  3. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/__init__.py +11 -1
  4. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/admin.py +517 -0
  5. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/common.py +22 -0
  6. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/properties.py +280 -2
  7. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/jetstream_api.egg-info/PKG-INFO +30 -1
  8. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/pyproject.toml +1 -1
  9. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/LICENSE.txt +0 -0
  10. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/_codec.py +0 -0
  11. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/_consumer.py +0 -0
  12. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/_groupwire.py +0 -0
  13. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/_qleader.py +0 -0
  14. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/admintopic.py +0 -0
  15. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/connectivity.py +0 -0
  16. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/exception.py +0 -0
  17. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/msg.py +0 -0
  18. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/partitioner.py +0 -0
  19. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/pattern.py +0 -0
  20. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/producer.py +0 -0
  21. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/py.typed +0 -0
  22. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/qmgr.py +0 -0
  23. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/topic.py +0 -0
  24. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/ilink/util.py +0 -0
  25. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/jetstream_api.egg-info/SOURCES.txt +0 -0
  26. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/jetstream_api.egg-info/dependency_links.txt +0 -0
  27. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/jetstream_api.egg-info/requires.txt +0 -0
  28. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/jetstream_api.egg-info/top_level.txt +0 -0
  29. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/setup.cfg +0 -0
  30. {jetstream_api-0.20.1 → jetstream_api-0.20.2}/tests/test_example.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.20.1
3
+ Version: 0.20.2
4
4
  Summary: Python client API for iLink M.O.M. (Message Oriented Middleware)
5
5
  Author: snowjeans
6
6
  License: Boost Software License - Version 1.0 - August 17th, 2003
@@ -60,6 +60,35 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.20.2 변경 - 관제 명령 관리 API · 강제 선출 · 리더십 표 · 격리본 (자바 v2.16.2 와 같음, 짝 엔진 7.0.1.3512 이상 - 리더십 표 · 격리본 · 키 단위 강제 선출은 7.0.1.3552 이상)
64
+
65
+ - **리더십 표 · 격리본**(엔진 7.0.1.3552 이상, 그 전 엔진은 `MIMQE_INVALID_REQ_CODE`):
66
+ - `ILAdminService.getClusterLeadTable(cluster)`(VIEW CLUSTER LEADERS, 3423): 원문. `getClusterLeadTableParsed` 는
67
+ `ILClusterLeadTable`(노드 · 파티션 · 묶음 행, 강제 선출 기록, 오프라인 행)로 풀어 준다. 모르는 줄은 건너뛰고, 판(`ILLT1`)이 다르면 거절.
68
+ - `ILAdminQmgr.getQuarantineList()` / `scanQuarantine(dir, queue, since)`(VIEW QUARANTINE, 3425 / 3426): 원문.
69
+ - `reinjectQuarantine(dir, queue, since)`(REINJECT, 3427): `ILQuarantineReinjectResult`(건수 · 리더 · 멈춘 사유). ☠ OLD 까지
70
+ 넣으면 중복 가능, 넣은 건은 큐 맨 뒤에 붙는다.
71
+
72
+ - **ilsc 관제 명령을 관리 API 로**(엔진이 이미 받는 요청에 클라 메서드만 없던 것):
73
+ - `ILAdminService.shutdownService(clusterWide=False)`(SHUTDOWN, 3002): 응답 없이 끊기면 False(엔진이 내려가며 닫음), 시간 초과는 예외.
74
+ - `patchService(fileName, data)`(PATCH, 3335): 성공하면 서비스가 다시 기동하고 연결이 닫힌다.
75
+ - `diagnoseClusterDefinition(clusterName)`(DIAG CLUSTER 의 정의 대조, 3395): 텍스트 그대로.
76
+ - `ILAdminQmgr.getLoadMonitoringInfo(level)`(VIEW LOAD, 3204), `reloadQueue`(RELOAD QUEUE, 3315),
77
+ `setChannelStartMode(channel, autoStart)`(가동 중 STARTMODE, 3035), `removeMessagesFifo(queue, count)`(앞에서부터 지우기, 3345).
78
+ - `ILClusterProperty.placement` / `autoRebalance`(PLACEMENT · AUTOREBALANCE): `None` 은 보낼 때 "그대로 둔다",
79
+ `"DEFAULT"` 는 미설정으로 되돌린다.
80
+
81
+ - **내부 테스트 자동화 전용** `ILAdminService.checkForceElectLeader` / `forceElectLeader`(+ snake 별칭): 엔진 7.0.1.3551 의 운영자
82
+ 강제 선출(`ELECT CLUSTER(C) LEADER(..) FORCE`, 요청 3422)을 관리 API 로 부른다. 확인(위험만 봄)과 실행(☠ 유실 감수)을 나눴고,
83
+ 키(`"<토픽>/<파티션>"` / `"Q:<묶음>/0"`, 도우미 `forceElectPartitionKey` / `forceElectGroupKey`)로 하나만 강제할 수도 있다 -
84
+ 키 단위는 엔진 7.0.1.3552 이상(그 전 엔진은 키를 버리고 노드 단위로 실행하므로 옛 엔진임이 확실하면 보내기 전에 막는다).
85
+ 결과는 `ILClusterForceElectResult`(target · state · risk). 클라는 다시 보내지 않는다(실행의 결과 모름은 확인부터).
86
+ 지금은 내부 시험 자동화에서만 쓴다 - 운영자의 강제 선출은 ilsc 로 한다.
87
+
88
+ - `removeNode` 문서: `MIMQE_CLUSTER_NOT_COMMITTED (not committed - retry)` 는 실패가 아니라 **결과 모름**입니다(컨트롤러가 8초 안에
89
+ 확정을 못 봄, 늦게 확정될 수 있음). 클라는 스스로 다시 보내지 않습니다. 다시 불러도 안전하고, 그때 `MIMQE_OBJECT_NOT_FOUND` 면
90
+ 먼저 것이 확정돼 이미 빠진 것입니다. 클라 동작은 바뀌지 않습니다.
91
+
63
92
  ## 0.20.1 변경 - 큐 묶음 이동 중 재시도 (자바 v2.16.1 과 같음, 짝 엔진 7.0.1.3512 이상 - 묶음 이동은 7.0.1.3550 이상의 묶음 모드)
64
93
 
65
94
  - 큐 묶음 이동(엔진 7.0.1.3550 3-b3, `ALTER QUEUE … LEADERGROUP` 등)이 진행 중이면 엔진이 그 큐의 요청을
@@ -10,6 +10,35 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
10
10
  ## 설치 방법
11
11
  pip install jetstream-api
12
12
 
13
+ ## 0.20.2 변경 - 관제 명령 관리 API · 강제 선출 · 리더십 표 · 격리본 (자바 v2.16.2 와 같음, 짝 엔진 7.0.1.3512 이상 - 리더십 표 · 격리본 · 키 단위 강제 선출은 7.0.1.3552 이상)
14
+
15
+ - **리더십 표 · 격리본**(엔진 7.0.1.3552 이상, 그 전 엔진은 `MIMQE_INVALID_REQ_CODE`):
16
+ - `ILAdminService.getClusterLeadTable(cluster)`(VIEW CLUSTER LEADERS, 3423): 원문. `getClusterLeadTableParsed` 는
17
+ `ILClusterLeadTable`(노드 · 파티션 · 묶음 행, 강제 선출 기록, 오프라인 행)로 풀어 준다. 모르는 줄은 건너뛰고, 판(`ILLT1`)이 다르면 거절.
18
+ - `ILAdminQmgr.getQuarantineList()` / `scanQuarantine(dir, queue, since)`(VIEW QUARANTINE, 3425 / 3426): 원문.
19
+ - `reinjectQuarantine(dir, queue, since)`(REINJECT, 3427): `ILQuarantineReinjectResult`(건수 · 리더 · 멈춘 사유). ☠ OLD 까지
20
+ 넣으면 중복 가능, 넣은 건은 큐 맨 뒤에 붙는다.
21
+
22
+ - **ilsc 관제 명령을 관리 API 로**(엔진이 이미 받는 요청에 클라 메서드만 없던 것):
23
+ - `ILAdminService.shutdownService(clusterWide=False)`(SHUTDOWN, 3002): 응답 없이 끊기면 False(엔진이 내려가며 닫음), 시간 초과는 예외.
24
+ - `patchService(fileName, data)`(PATCH, 3335): 성공하면 서비스가 다시 기동하고 연결이 닫힌다.
25
+ - `diagnoseClusterDefinition(clusterName)`(DIAG CLUSTER 의 정의 대조, 3395): 텍스트 그대로.
26
+ - `ILAdminQmgr.getLoadMonitoringInfo(level)`(VIEW LOAD, 3204), `reloadQueue`(RELOAD QUEUE, 3315),
27
+ `setChannelStartMode(channel, autoStart)`(가동 중 STARTMODE, 3035), `removeMessagesFifo(queue, count)`(앞에서부터 지우기, 3345).
28
+ - `ILClusterProperty.placement` / `autoRebalance`(PLACEMENT · AUTOREBALANCE): `None` 은 보낼 때 "그대로 둔다",
29
+ `"DEFAULT"` 는 미설정으로 되돌린다.
30
+
31
+ - **내부 테스트 자동화 전용** `ILAdminService.checkForceElectLeader` / `forceElectLeader`(+ snake 별칭): 엔진 7.0.1.3551 의 운영자
32
+ 강제 선출(`ELECT CLUSTER(C) LEADER(..) FORCE`, 요청 3422)을 관리 API 로 부른다. 확인(위험만 봄)과 실행(☠ 유실 감수)을 나눴고,
33
+ 키(`"<토픽>/<파티션>"` / `"Q:<묶음>/0"`, 도우미 `forceElectPartitionKey` / `forceElectGroupKey`)로 하나만 강제할 수도 있다 -
34
+ 키 단위는 엔진 7.0.1.3552 이상(그 전 엔진은 키를 버리고 노드 단위로 실행하므로 옛 엔진임이 확실하면 보내기 전에 막는다).
35
+ 결과는 `ILClusterForceElectResult`(target · state · risk). 클라는 다시 보내지 않는다(실행의 결과 모름은 확인부터).
36
+ 지금은 내부 시험 자동화에서만 쓴다 - 운영자의 강제 선출은 ilsc 로 한다.
37
+
38
+ - `removeNode` 문서: `MIMQE_CLUSTER_NOT_COMMITTED (not committed - retry)` 는 실패가 아니라 **결과 모름**입니다(컨트롤러가 8초 안에
39
+ 확정을 못 봄, 늦게 확정될 수 있음). 클라는 스스로 다시 보내지 않습니다. 다시 불러도 안전하고, 그때 `MIMQE_OBJECT_NOT_FOUND` 면
40
+ 먼저 것이 확정돼 이미 빠진 것입니다. 클라 동작은 바뀌지 않습니다.
41
+
13
42
  ## 0.20.1 변경 - 큐 묶음 이동 중 재시도 (자바 v2.16.1 과 같음, 짝 엔진 7.0.1.3512 이상 - 묶음 이동은 7.0.1.3550 이상의 묶음 모드)
14
43
 
15
44
  - 큐 묶음 이동(엔진 7.0.1.3550 3-b3, `ALTER QUEUE … LEADERGROUP` 등)이 진행 중이면 엔진이 그 큐의 요청을
@@ -1,6 +1,6 @@
1
1
  """iLink - Python client API for iLink M.O.M. (Message Oriented Middleware)."""
2
2
 
3
- __version__ = "0.20.1"
3
+ __version__ = "0.20.2"
4
4
 
5
5
  # --- Admin / Service ---
6
6
  from .admin import ILAdminService, ILAdminQmgr, ILLogBrowser, ILQueueBrowser
@@ -29,6 +29,11 @@ from .properties import (
29
29
  ILClusterNode,
30
30
  ILClusterControlResult,
31
31
  ILClusterDissolveResult,
32
+ ILClusterForceElectResult,
33
+ ILClusterLeadTable,
34
+ ILClusterLeadEntry,
35
+ ILClusterForcedKey,
36
+ ILQuarantineReinjectResult,
32
37
  ILClusterJoinResult,
33
38
  ILClusterNodeAddResult,
34
39
  ILFollowerStatus,
@@ -90,6 +95,11 @@ __all__ = [
90
95
  "ILClusterNode",
91
96
  "ILClusterControlResult",
92
97
  "ILClusterDissolveResult",
98
+ "ILClusterForceElectResult",
99
+ "ILClusterLeadTable",
100
+ "ILClusterLeadEntry",
101
+ "ILClusterForcedKey",
102
+ "ILQuarantineReinjectResult",
93
103
  "ILClusterJoinResult",
94
104
  "ILClusterNodeAddResult",
95
105
  "ILFollowerStatus",
@@ -27,6 +27,9 @@ from .properties import (
27
27
  ILChannelDirection,
28
28
  ILClusterControlResult,
29
29
  ILClusterDissolveResult,
30
+ ILClusterForceElectResult,
31
+ ILClusterLeadTable,
32
+ ILQuarantineReinjectResult,
30
33
  ILClusterJoinResult,
31
34
  ILClusterNodeAddResult,
32
35
  ILReplicationStatus,
@@ -1241,6 +1244,261 @@ class ILAdminService:
1241
1244
  except Exception as exc:
1242
1245
  raise ILException(exc) from exc
1243
1246
 
1247
+ def checkForceElectLeader(self, clusterName: str, node, key: str | None = None) -> "ILClusterForceElectResult":
1248
+ """강제 선출을 **확인만** 한다 - 받아들여질지, 받아들여지면 무엇을 잃는지 본다(0.20.2, 엔진 7.0.1.3551).
1249
+
1250
+ ☠ **내부 테스트 자동화 전용.** 지금은 내부 시험 자동화(장애 재현 · 회귀)에서만 쓴다. 운영 절차나 응용에서 부르지
1251
+ 않는다 - 운영자의 강제 선출은 ilsc 의 ``ELECT CLUSTER(C) LEADER(..) FORCE`` 로 한다(위험을 보이고 y/n 을 묻는다).
1252
+
1253
+ :meth:`forceElectLeader` 앞에 부르는 단계다(ilsc 의 ``ELECT CLUSTER(C) LEADER(..) FORCE`` 가 위험을 보이고
1254
+ y/n 을 묻는 것과 같다). 아무것도 바꾸지 않는다.
1255
+
1256
+ Args:
1257
+ clusterName (str): 클러스터 이름.
1258
+ node (int | str): 리더로 세울 노드 - 번호(int 또는 숫자 문자열) 또는 큐 관리자 이름. 같은 이름이 여러
1259
+ 노드에 있으면(HA 에서는 정상) 번호로 다시 달라고 거절된다.
1260
+ key (str | None): 그 하나만 강제할 키 - **엔진 7.0.1.3552 이상**. 토픽 파티션은 ``"<토픽>/<파티션>"``, 큐 묶음은
1261
+ ``"Q:<묶음>/0"``(:meth:`forceElectPartitionKey` / :meth:`forceElectGroupKey` 로 만든다). ``None`` 이면 노드
1262
+ 단위(브로커 리더, 엔진 7.0.1.3551 이상). ☠ 그 전 엔진은 키를 버리고 **노드 단위로 실행**하므로, 옛 엔진임이
1263
+ 확실하면 보내기 전에 ``MIMQE_NOT_SUPPORTED`` 로 막는다.
1264
+
1265
+ Returns:
1266
+ ILClusterForceElectResult: 대상 번호 · 지금 현황 · 유실 위험.
1267
+
1268
+ Raises:
1269
+ ILOperationException: 엔진이 거절할 때 - 강제할 까닭이 없거나 할 수 없다(사유 문자열로 가른다).
1270
+ 리더가 서빙 중이면 ``MIMQE_CLUSTER_LEADER_SERVING (node N leads - nothing to force)``(스위치백이
1271
+ 유실 없는 길이다), 안전 후보가 있으면 ``MIMQE_CLUSTER_SAFE_CANDIDATE (node N is elected without loss)``
1272
+ (컨트롤러가 곧 유실 없이 뽑는다), 대상이 살아 있지 않으면
1273
+ ``MIMQE_INVALID_TARGET (node N is not alive - start it first)``, 대상이 없으면
1274
+ ``MIMQE_OBJECT_NOT_FOUND (...)``(키 단위면 키가 리더십 표에 없음), 키 형식이 틀렸거나 같은 이름의 노드가
1275
+ 여럿이면 ``MIMQE_INVALID_ARGUMENT (...)``, 키 단위인데 파티션 · 묶음이 배치된 표가 아니면
1276
+ ``MIMQE_INVALID_OPERATION (...)``(노드 단위를 쓴다), 컨트롤러 임기가 적용되기 전이면
1277
+ ``MIMQE_CLUSTER_NOT_READY (...)``, 컨트롤러 리더가 없으면 ``MIMQE_CLUSTER_NO_LEADER (...)``, 해산 중이면
1278
+ ``MIMQE_CLUSTER_DISSOLVED (...)``, 3422 를 모르는 엔진이면 ``MIMQE_INVALID_REQ_CODE (...)``. 키를 줬는데
1279
+ 옛 엔진이면 ``MIMQE_NOT_SUPPORTED (...)``. 클라이언트는 이 거절들을 다시 보내지 않는다.
1280
+ ILException: 통신 실패.
1281
+ """
1282
+ return self._force_elect(clusterName, node, key, False)
1283
+
1284
+ def forceElectLeader(self, clusterName: str, node, key: str | None = None) -> "ILClusterForceElectResult":
1285
+ """☠ **유실을 감수하고** 리더를 강제로 세운다(0.20.2, 엔진 7.0.1.3551). 비상 탈출구다.
1286
+
1287
+ ☠ **내부 테스트 자동화 전용.** 지금은 내부 시험 자동화(장애 재현 · 회귀)에서만 쓴다. 운영 절차나 응용에서 부르지
1288
+ 않는다 - 운영자의 강제 선출은 ilsc 의 ``ELECT CLUSTER(C) LEADER(..) FORCE`` 로 한다(위험을 보이고 y/n 을 묻는다).
1289
+
1290
+ 안전한 후보가 진짜 없을 때(혼자 커밋하던 리더가 돌아오지 않는 등)만 쓴다. 엔진은 자동으로는 절대 하지
1291
+ 않는다. **반드시 먼저** :meth:`checkForceElectLeader` 로 위험(``risk``)을 보고 사람이 판단한 뒤 부른다.
1292
+
1293
+ Args:
1294
+ clusterName (str): 클러스터 이름.
1295
+ node (int | str): 리더로 세울 노드 - 번호 또는 큐 관리자 이름(:meth:`checkForceElectLeader` 와 같다).
1296
+ key (str | None): 그 하나만 강제할 키(``"<토픽>/<파티션>"`` / ``"Q:<묶음>/0"``) - **엔진 7.0.1.3552 이상**.
1297
+ ``None`` 이면 노드 단위. 키 단위 실행은 그 키가 오프라인(리더 없음)일 때만 받는다.
1298
+
1299
+ Returns:
1300
+ ILClusterForceElectResult: 대상 번호 · 배정 결과(``state``) · 유실 위험. ``state`` 가
1301
+ ``commit pending`` 이면 배정은 냈으나 확정을 아직 못 본 것이다 - 리더십 현황을 다시 확인한다.
1302
+
1303
+ Raises:
1304
+ ILOperationException: 엔진이 거절할 때(:meth:`checkForceElectLeader` 와 같은 사유). 실행 순간 상황이
1305
+ 바뀌었으면(리더가 돌아왔다 · 안전 후보가 생겼다) 실행도 거절된다. ☠ ``MIMQE_CLUSTER_NOT_COMMITTED`` 나
1306
+ ``state`` 의 ``commit pending`` 은 **결과 모름**이다 - 다시 실행하면 강제 선출이 두 번 될 수 있으니,
1307
+ :meth:`checkForceElectLeader` 로 상태부터 본다.
1308
+ ILException: 통신 실패.
1309
+
1310
+ Note:
1311
+ ☠ **위험에 적힌 노드들만 가진 커밋은 사라진다.** 돌아온 옛 리더는 새 리더 밑으로 다시 붙으며 갈라진
1312
+ 몫을 잃는다(갈라진 기록은 잘려 나간다). 키 단위면 엔진이 갈라진 데이터를 ``<홈>/QUARANTINE/`` 아래로 격리한 뒤
1313
+ 새 리더 사본으로 맞춘다. 클라이언트는 이 요청을 **스스로 다시 보내지 않는다.**
1314
+
1315
+ Example::
1316
+
1317
+ chk = svc.checkForceElectLeader("C1", 2)
1318
+ print(chk.getState(), "잃을 수 있는 노드:", chk.getRisk())
1319
+ if input("강제로 세울까요? (y/n) ") == "y":
1320
+ print(svc.forceElectLeader("C1", 2))
1321
+ """
1322
+ return self._force_elect(clusterName, node, key, True)
1323
+
1324
+ def _force_elect(self, clusterName: str, node, key: str | None, execute: bool) -> "ILClusterForceElectResult":
1325
+ if isinstance(node, bool) or not isinstance(node, (int, str)):
1326
+ raise ILOperationException("MIMQE_INVALID_ARGUMENT (node must be a NodeId or a qmgr name)")
1327
+ target = str(node).strip()
1328
+ if not target:
1329
+ raise ILOperationException("MIMQE_INVALID_ARGUMENT (node must be a NodeId or a qmgr name)")
1330
+ if key is not None and not str(key).strip():
1331
+ raise ILOperationException("MIMQE_INVALID_ARGUMENT (key)")
1332
+ mode = ILC.MIMQ_FORCE_ELECT_EXEC if execute else ILC.MIMQ_FORCE_ELECT_CHECK
1333
+ try:
1334
+ self._checkClusterSupported()
1335
+ # ☠ 옛 엔진은 키(확장)를 버리고 노드 단위로 실행한다 - 확실히 옛 엔진일 때만 막는다(리비전을 모르면 보낸다)
1336
+ if key is not None and 0 < self.revision < ILC.FORCE_ELECT_KEY_MIN_REVISION:
1337
+ raise ILOperationException(
1338
+ "MIMQE_NOT_SUPPORTED (force elect by key requires engine rev %d+, current: %d - node-level would run)"
1339
+ % (ILC.FORCE_ELECT_KEY_MIN_REVISION, self.revision))
1340
+ if key is None:
1341
+ res = self.service.request(ILC.MIMQ_CLUSTER_FORCE_ELECT, clusterName, target, mode)
1342
+ else:
1343
+ res = self.service.request(ILC.MIMQ_CLUSTER_FORCE_ELECT, str(key).encode("utf-8"),
1344
+ clusterName, target, mode)
1345
+ if res is None or res.getCode() != ILC.MIMQ_SUCCESSFUL_CLUSTER_OPERATION:
1346
+ raise ILOperationException(
1347
+ (res.getArg2() or res.getArg1()) if res is not None else "no response")
1348
+ return ILClusterForceElectResult(res.getArg1(), res.getArg2(), res.getArg3())
1349
+ except _PASSTHROUGH_EXCEPTIONS:
1350
+ raise
1351
+ except Exception as exc:
1352
+ raise ILException(exc) from exc
1353
+
1354
+ def shutdownService(self, clusterWide: bool = False) -> bool:
1355
+ """접속한 **서비스(ilsvc)를 내린다** - ilsc 의 ``SHUTDOWN`` (0.20.2).
1356
+
1357
+ Args:
1358
+ clusterWide (bool): True 면 **클러스터 전체 정지**를 알린다(``MIMQ_SHUTDOWN_SCOPE_CLUSTER``) - 컨트롤러가
1359
+ 리더 반납을 기다리지 않는다. 기본 False 는 이 서비스만 내린다(종전 동작).
1360
+
1361
+ Returns:
1362
+ bool: 엔진의 성공 응답을 받았으면 True. **응답 없이 연결이 끊겼으면 False** 다 - 엔진이 정지하며
1363
+ SO_LINGER(1, 0) 로 소켓을 닫아 응답이 오지 않을 수 있다(엔진 C++ 관리 도구도 이 경우를 성공으로 본다).
1364
+ False 는 "정지 요청은 갔으나 확인 못 함" 이니, 확실히 하려면 다시 접속해 보거나 프로세스를 확인한다.
1365
+
1366
+ Raises:
1367
+ ILOperationException: 엔진이 거절했을 때.
1368
+ ILSessionException: 응답도 끊김도 없이 시간이 다했을 때(``MIMQE_WAIT_TIMEOUT`` / ``MIMQE_SESSION_TIMEOUT`` -
1369
+ 판정 불가라 성공으로 치지 않는다).
1370
+ ILException: 그 밖의 통신 실패.
1371
+
1372
+ Note:
1373
+ 어느 쪽이든 끝나면 이 관리 연결은 **닫힌다**(:meth:`isConnected` 가 False). 큐 관리자들도 함께 내려간다.
1374
+ """
1375
+ scope = ILC.MIMQ_SHUTDOWN_SCOPE_CLUSTER if clusterWide else ""
1376
+ try:
1377
+ self.service.request(ILC.MIMQ_EXIT_ALL_REQUEST, ILC.MIMQ_SUCCESSFUL_SET_EXIT_FLAG, scope, "", "")
1378
+ return True
1379
+ except ILSessionException as exc:
1380
+ if getattr(exc, "reason_code", None) in (ILRC.MIMQE_WAIT_TIMEOUT, ILRC.MIMQE_SESSION_TIMEOUT):
1381
+ raise # 시간이 다했다 - 끊김이 아니라 판정 불가다
1382
+ return False # 응답 없이 끊김 - 엔진이 내려가며 닫았다
1383
+ except _PASSTHROUGH_EXCEPTIONS:
1384
+ raise
1385
+ except Exception as exc:
1386
+ raise ILException(exc) from exc
1387
+ finally:
1388
+ try:
1389
+ self.service.shutdown()
1390
+ except Exception:
1391
+ pass
1392
+
1393
+ def patchService(self, fileName: str, data: bytes) -> None:
1394
+ """서비스에 **패치 파일을 보내 적용한다** - ilsc 의 ``PATCH FILEPATH(..)`` (0.20.2).
1395
+
1396
+ Args:
1397
+ fileName (str): 패치 파일 이름(경로 없이, ilsc 가 보내는 것과 같다). UTF-8 127바이트 이하.
1398
+ data (bytes): 파일 내용 그대로.
1399
+
1400
+ Raises:
1401
+ UnsupportedOperationException: 이름이 비었거나 127바이트를 넘을 때(보내기 전에 막는다).
1402
+ ILOperationException: 엔진이 거절했을 때, 또는 요청이 프레임 상한(기본 128MiB)을 넘을 때.
1403
+ ILException: 통신 실패.
1404
+
1405
+ Note:
1406
+ 성공하면 서비스가 파일을 갈아 끼우며 **다시 기동한다** - 이 관리 연결은 닫힌다. 잠시 뒤 다시 접속한다.
1407
+ 응답 대기는 관리 연결의 세션 타임아웃(기본 600초)이다.
1408
+ """
1409
+ name = (fileName or "").encode("utf-8")
1410
+ if not name or len(name) > ILC.MIMQ_PATCH_FILE_NAME_LENGTH - 1:
1411
+ raise UnsupportedOperationException(
1412
+ "[%s] is an unacceptable value. (acceptable length: 1 to %d bytes)"
1413
+ % (fileName, ILC.MIMQ_PATCH_FILE_NAME_LENGTH - 1))
1414
+ body = name + bytes(ILC.MIMQ_PATCH_FILE_NAME_LENGTH - len(name)) + bytes(data or b"")
1415
+ try:
1416
+ self.service.apply(ILC.MIMQ_UPDATE_REQUEST, body, ILC.MIMQ_SUCCESSFUL_UPDATE_REQUEST)
1417
+ except _PASSTHROUGH_EXCEPTIONS:
1418
+ raise
1419
+ except Exception as exc:
1420
+ raise ILException(exc) from exc
1421
+ try:
1422
+ self.service.shutdown()
1423
+ except Exception:
1424
+ pass
1425
+
1426
+ def diagnoseClusterDefinition(self, clusterName: str) -> str:
1427
+ """클러스터 **정의 진단** - 이 서비스의 정의 사본과 컨트롤러 정족수가 적용한 것을 대조한다(0.20.2).
1428
+
1429
+ ilsc ``DIAG CLUSTER(C)`` 의 앞부분이다(뒷부분은 큐 관리자마다의 :meth:`ILAdminQmgr.getClusterRuntime`).
1430
+ 0.18.1 에서 지운 판사 모델의 ``getClusterDefDiag`` (객체 반환)와는 다른 메서드다 - 엔진 3395 의 본문이 컨트롤러
1431
+ 정족수 모델의 텍스트가 되어 이름을 새로 붙였다.
1432
+
1433
+ Args:
1434
+ clusterName (str): 클러스터 이름.
1435
+
1436
+ Returns:
1437
+ str: 엔진이 준 텍스트 그대로. 줄 머리가 ``NOTE`` · ``STATE`` · ``HERE`` · ``DEF`` 다
1438
+ (예: ``DEF replicas .. minsync .. ctrlhb .. ctrlelect .. coords .. secret ..``). **형식은 계약이 아니다** -
1439
+ 사람이 읽거나 그대로 기록한다.
1440
+
1441
+ Raises:
1442
+ ILOperationException: 엔진이 거절했을 때(사유는 엔진 문구 그대로).
1443
+ ILException: 통신 실패.
1444
+ """
1445
+ try:
1446
+ self._checkClusterSupported()
1447
+ res_msg = self.service.inquire(
1448
+ ILMsgUtil.create_request_msg(ILC.MIMQ_GET_CLUSTER_DEFDIAG, clusterName, "", ""), False)
1449
+ if res_msg.getType() == ILC.MIMQ_REPORT_MESSAGE:
1450
+ raise ILOperationException(res_msg.getReportMsg().getArg2())
1451
+ return res_msg.getDataMsg().getData().decode("utf-8", "replace")
1452
+ except _PASSTHROUGH_EXCEPTIONS:
1453
+ raise
1454
+ except Exception as exc:
1455
+ raise ILException(exc) from exc
1456
+
1457
+ def getClusterLeadTable(self, clusterName: str) -> str:
1458
+ """클러스터 **리더십 표** 원문 - 노드 · 토픽 파티션 · 큐 묶음 리더와 강제 선출 기록(0.20.2, 엔진 7.0.1.3552).
1459
+
1460
+ ilsc ``VIEW CLUSTER(C) LEADERS`` 와 같다. 풀어 쓰려면 :meth:`getClusterLeadTableParsed` 나
1461
+ ``ILClusterLeadTable(text)`` 를 쓴다.
1462
+
1463
+ Args:
1464
+ clusterName (str): 클러스터 이름.
1465
+
1466
+ Returns:
1467
+ str: 엔진이 준 텍스트 그대로(첫 줄 ``ILLT1|<C|U>|<컨트롤러 리더>``, 칸 구분 ``|``, 빈 값 ``-``).
1468
+
1469
+ Raises:
1470
+ ILOperationException: 엔진이 거절했을 때 - 컨트롤러에 닿지 않으면 ``MIMQE_CLUSTER_UNREACHABLE (...)``, 아직 기록이
1471
+ 없으면 ``MIMQE_CLUSTER_NO_LEADER (...)``, 7.0.1.3552 전 엔진이면 ``MIMQE_INVALID_REQ_CODE (...)``.
1472
+ ILException: 통신 실패.
1473
+ """
1474
+ try:
1475
+ self._checkClusterSupported()
1476
+ res_msg = self.service.inquire(
1477
+ ILMsgUtil.create_request_msg(ILC.MIMQ_CLUSTER_LEAD_TABLE, clusterName, "", ""), False)
1478
+ if res_msg.getType() == ILC.MIMQ_REPORT_MESSAGE:
1479
+ raise ILOperationException(res_msg.getReportMsg().getArg2())
1480
+ return res_msg.getDataMsg().getData().decode("utf-8", "replace")
1481
+ except _PASSTHROUGH_EXCEPTIONS:
1482
+ raise
1483
+ except Exception as exc:
1484
+ raise ILException(exc) from exc
1485
+
1486
+ def getClusterLeadTableParsed(self, clusterName: str) -> "ILClusterLeadTable":
1487
+ """:meth:`getClusterLeadTable` 을 :class:`ILClusterLeadTable` 로 풀어 돌려준다(0.20.2).
1488
+
1489
+ Args:
1490
+ clusterName (str): 클러스터 이름.
1491
+
1492
+ Returns:
1493
+ ILClusterLeadTable: 풀린 표(원문은 ``raw``).
1494
+
1495
+ Raises:
1496
+ ILOperationException: :meth:`getClusterLeadTable` 과 같다.
1497
+ ILDeserializeException: 표의 판이 이 클라와 다를 때.
1498
+ ILException: 통신 실패.
1499
+ """
1500
+ return ILClusterLeadTable(self.getClusterLeadTable(clusterName))
1501
+
1244
1502
  def addNode(self, clusterName: str, node: ILClusterNode) -> "ILClusterNodeAddResult":
1245
1503
  """클러스터에 **노드 하나**(컨트롤러 전용 노드 또는 브로커)를 더한다. 투표권은 주지 않는다.
1246
1504
 
@@ -1316,8 +1574,19 @@ class ILAdminService:
1316
1574
  엔진이 거절할 때 - 아직 투표권이 있으면 ``... (node N still votes - revoke with COORDS)``,
1317
1575
  마지막 브로커일 때 ``... (the last broker cannot be removed)``, 그 브로커가 지금 리더일 때
1318
1576
  ``MIMQE_INVALID_OPERATION (node N leads - stop it first, then DELNODE)``.
1577
+ 컨트롤러 정족수가 확정해 주지 못할 때 ``MIMQE_CLUSTER_NOT_COMMITTED (not committed - retry)`` - 아래 Note.
1319
1578
  ILException: 통신 실패.
1320
1579
 
1580
+ Note:
1581
+ ☠ ``MIMQE_CLUSTER_NOT_COMMITTED (not committed - retry)`` 는 **실패가 아니라 결과 모름**이다. 컨트롤러 리더가
1582
+ 변경을 올리고 8초 안에 확정을 못 봤다는 뜻이고, 제안은 거둬지지 않아 **늦게 확정될 수 있다**. 얼마나 이어질지는
1583
+ 원인에 달렸다 - 컨트롤러 리더가 바뀌는 중이면 보통 몇 초, 투표자 컨트롤러가 죽어 있으면 돌아올 때까지 계속 난다.
1584
+ 클라이언트는 스스로 다시 보내지 않는다. 다시 부르면 안전하다(매번 정의를 새로 읽어 그 판으로 보낸다): 먼저 것이
1585
+ 그사이 확정됐으면 ``MIMQE_OBJECT_NOT_FOUND (node N in cluster C)`` 가 나는데 이것은 **이미 빠졌다는 뜻**(성공)이고,
1586
+ 다시 보낸 뒤에 먼저 것이 확정되면 ``MIMQE_CLUSTER_DEF_MOVED`` 로 갈려 두 번 적용되지 않는다.
1587
+ :meth:`getClusterProperty` 로 노드 목록을 보고 판단해도 된다. ``(earlier change still pending - retry)`` 는 이번
1588
+ 것을 올리지 않은 것이라 그대로 다시 부르면 된다.
1589
+
1321
1590
  Note:
1322
1591
  ★ **투표자를 빼려면 두 번 부른다** - 먼저 :meth:`setCoords` 로 투표권을 거두고, 그다음 이것으로 뺀다.
1323
1592
 
@@ -1649,6 +1918,55 @@ class ILAdminService:
1649
1918
  """:meth:`joinCluster` 의 snake 별칭."""
1650
1919
  return self.joinCluster(cluster_name, controller_addr, auto_start, seats)
1651
1920
 
1921
+ @staticmethod
1922
+ def forceElectPartitionKey(topic: str, partition: int) -> str:
1923
+ """토픽 파티션 하나의 강제 선출 키를 만든다(``"<토픽>/<파티션>"``). **내부 테스트 자동화 전용.**
1924
+
1925
+ Args:
1926
+ topic (str): 토픽 이름.
1927
+ partition (int): 파티션 번호.
1928
+
1929
+ Returns:
1930
+ str: 키.
1931
+ """
1932
+ return "%s/%d" % (topic, int(partition))
1933
+
1934
+ @staticmethod
1935
+ def forceElectGroupKey(group: str) -> str:
1936
+ """큐 묶음(LEADERGROUP) 하나의 강제 선출 키를 만든다(``"Q:<묶음>/0"`` - 묶음은 파티션 하나뿐인 토픽처럼 다룬다).
1937
+ **내부 테스트 자동화 전용.**
1938
+
1939
+ Args:
1940
+ group (str): 묶음 이름.
1941
+
1942
+ Returns:
1943
+ str: 키.
1944
+ """
1945
+ return "Q:%s/0" % group
1946
+
1947
+ force_elect_partition_key = forceElectPartitionKey
1948
+ force_elect_group_key = forceElectGroupKey
1949
+
1950
+ def get_cluster_lead_table(self, cluster_name: str) -> str:
1951
+ """:meth:`getClusterLeadTable` 의 snake 별칭."""
1952
+ return self.getClusterLeadTable(cluster_name)
1953
+
1954
+ def get_cluster_lead_table_parsed(self, cluster_name: str) -> "ILClusterLeadTable":
1955
+ """:meth:`getClusterLeadTableParsed` 의 snake 별칭."""
1956
+ return self.getClusterLeadTableParsed(cluster_name)
1957
+
1958
+ def diagnose_cluster_definition(self, cluster_name: str) -> str:
1959
+ """:meth:`diagnoseClusterDefinition` 의 snake 별칭."""
1960
+ return self.diagnoseClusterDefinition(cluster_name)
1961
+
1962
+ def check_force_elect_leader(self, cluster_name: str, node, key: str | None = None) -> "ILClusterForceElectResult":
1963
+ """:meth:`checkForceElectLeader` 의 snake 별칭."""
1964
+ return self.checkForceElectLeader(cluster_name, node, key)
1965
+
1966
+ def force_elect_leader(self, cluster_name: str, node, key: str | None = None) -> "ILClusterForceElectResult":
1967
+ """:meth:`forceElectLeader` 의 snake 별칭."""
1968
+ return self.forceElectLeader(cluster_name, node, key)
1969
+
1652
1970
  def dissolve_cluster(self, cluster_name: str) -> "ILClusterDissolveResult":
1653
1971
  """:meth:`dissolveCluster` 의 snake 별칭."""
1654
1972
  return self.dissolveCluster(cluster_name)
@@ -3894,6 +4212,182 @@ class ILAdminQmgr:
3894
4212
  except Exception as exc:
3895
4213
  raise ILException(exc) from exc
3896
4214
 
4215
+ def _quarantineText(self, code: int, *args: str) -> str:
4216
+ res_msg = self.service.inquire(
4217
+ ILMsgUtil.create_request_msg_with_qmgr(self.qmgrName, code, None, *args), False)
4218
+ if res_msg.getType() == ILC.MIMQ_REPORT_MESSAGE:
4219
+ raise ILOperationException(res_msg.getReportMsg().getArg2())
4220
+ return res_msg.getDataMsg().getData().decode("utf-8", "replace")
4221
+
4222
+ @staticmethod
4223
+ def _quarantineSince(since) -> str:
4224
+ if since is None:
4225
+ return ""
4226
+ if isinstance(since, bool):
4227
+ raise UnsupportedOperationException("[%s] is an unacceptable value. (since)" % (since,))
4228
+ return str(since)
4229
+
4230
+ def getQuarantineList(self) -> str:
4231
+ """이 큐 관리자의 **격리본 목록** - ilsc ``VIEW QUARANTINE`` (0.20.2, 엔진 7.0.1.3552).
4232
+
4233
+ 격리본은 강제 선출 뒤 돌아온 옛 리더가 새 리더 사본으로 맞추기 전에 갈라진 데이터를 ``<홈>/QUARANTINE/`` 아래로 옮겨 둔
4234
+ 것이다. 큐 관리자가 떠 있어야 하고, 팔로워 노드에서도 된다.
4235
+
4236
+ Returns:
4237
+ str: 엔진이 준 텍스트 그대로. 첫 줄 ``QL1|<격리 루트>``, 그다음
4238
+ ``D|<디렉터리>|<NODE|GROUP|TOPIC|?>|<파일 수>|<바이트>|<기준 시각 또는 ->|<FORK.txt 첫 줄 또는 ->`` 와
4239
+ ``Q|<디렉터리>|<큐>|<세그먼트 수>|<재주입 건수>`` 줄.
4240
+
4241
+ Raises:
4242
+ ILOperationException: 엔진이 거절했을 때(7.0.1.3552 전 엔진은 ``MIMQE_INVALID_REQ_CODE``).
4243
+ ILException: 통신 실패.
4244
+ """
4245
+ try:
4246
+ return self._quarantineText(ILC.MIMQ_QUARANTINE_LIST, "", "", "")
4247
+ except _PASSTHROUGH_EXCEPTIONS:
4248
+ raise
4249
+ except Exception as exc:
4250
+ raise ILException(exc) from exc
4251
+
4252
+ def scanQuarantine(self, directory: str, queueName: str | None = None, since=None) -> str:
4253
+ """격리본을 **검사만** 한다 - 재주입하면 무엇이 들어갈지 본다. 부수효과 없음(0.20.2, 엔진 7.0.1.3552).
4254
+
4255
+ Args:
4256
+ directory (str): 격리 디렉터리 이름(한 단계 - ``/`` ``\\`` ``:`` 금지).
4257
+ queueName (str | None): 큐. ``None`` 이면 그 디렉터리의 모든 큐 요약.
4258
+ since (str | int | None): 기준. ``None`` 이면 디렉터리 기준 시각, ``"ALL"`` 이면 전부,
4259
+ ``"yyyymmddhhmmss"`` 문자열이나 epoch 밀리초 숫자.
4260
+
4261
+ Returns:
4262
+ str: 엔진이 준 텍스트 그대로. 첫 줄 ``QS1|<디렉터리>|<큐 또는 *>|<기준>``, 큐마다
4263
+ ``S|<큐>|<전체>|<LIVE>|<CANDIDATE>|<OLD>|<DONE>``, 큐를 줬으면 재주입 대상 앞 500건
4264
+ ``M|C|<IndexId>|<PUT 시각>|<바이트>|<MsgId>``. LIVE = 지금 큐에 있음, CANDIDATE = 재주입 대상, OLD = 기준 전(소비됐을
4265
+ 가능성), DONE = 이미 재주입.
4266
+
4267
+ Raises:
4268
+ ILOperationException: 엔진이 거절했을 때(디렉터리 · 큐가 없으면 ``MIMQE_OBJECT_NOT_FOUND`` 등).
4269
+ ILException: 통신 실패.
4270
+ """
4271
+ try:
4272
+ return self._quarantineText(ILC.MIMQ_QUARANTINE_SCAN, directory or "", queueName or "",
4273
+ self._quarantineSince(since))
4274
+ except _PASSTHROUGH_EXCEPTIONS:
4275
+ raise
4276
+ except Exception as exc:
4277
+ raise ILException(exc) from exc
4278
+
4279
+ def reinjectQuarantine(self, directory: str, queueName: str, since=None) -> "ILQuarantineReinjectResult":
4280
+ """격리본의 재주입 대상(CANDIDATE)을 **그 큐의 지금 리더에 다시 넣는다** - ilsc ``REINJECT`` (0.20.2, 엔진 7.0.1.3552).
4281
+
4282
+ 원래 MsgId · CorrelId 로 PUT 하고 100건마다 커밋한다. 넣은 것은 기록되어 다시 넣지 않는다. **먼저**
4283
+ :meth:`scanQuarantine` 으로 무엇이 들어갈지 본다.
4284
+
4285
+ Args:
4286
+ directory (str): 격리 디렉터리 이름.
4287
+ queueName (str): 큐(필수).
4288
+ since (str | int | None): 기준(:meth:`scanQuarantine` 과 같다).
4289
+
4290
+ Returns:
4291
+ ILQuarantineReinjectResult: 넣은 건수 · 넣은 리더 · 메모. 메모가 ``stopped: ...`` 면 중간에 멈춘 것이다 - 넣은 만큼은
4292
+ 커밋됐고 다시 부르면 나머지만 넣는다.
4293
+
4294
+ Raises:
4295
+ UnsupportedOperationException: 큐를 주지 않았을 때(보내기 전에 막는다).
4296
+ ILOperationException: 엔진이 거절했을 때 - ``MIMQE_OBJECT_NOT_FOUND``, 토픽 격리본이면
4297
+ ``MIMQE_UNSUPPORTED_OPERATION``, 리더가 없으면 ``MIMQE_CLUSTER_NO_LEADER``, 리더 접속 실패 사유.
4298
+ ILException: 통신 실패.
4299
+
4300
+ Warning:
4301
+ ☠ **중복이 생길 수 있다** - ``since="ALL"`` 로 OLD(이미 소비됐을 수 있는 것)까지 넣으면 같은 메시지가 두 번 처리될 수
4302
+ 있다. 넣은 메시지는 **큐 맨 뒤에** 붙는다(원래 순서 자리로 가지 않는다). 건수에 비례해 오래 걸릴 수 있으니 관리 연결의
4303
+ 세션 타임아웃을 넉넉히 둔다(ilsc 는 120초).
4304
+ """
4305
+ if not queueName:
4306
+ raise UnsupportedOperationException("queueName is required for reinject")
4307
+ try:
4308
+ res = self.service.request(self.qmgrName, ILC.MIMQ_QUARANTINE_REINJECT, directory or "", queueName,
4309
+ self._quarantineSince(since))
4310
+ if res is None or res.getCode() != ILC.MIMQ_SUCCESSFUL_CLUSTER_OPERATION:
4311
+ raise ILOperationException(res.getArg2() if res is not None else "no response")
4312
+ return ILQuarantineReinjectResult(res.getArg1(), res.getArg2(), res.getArg3())
4313
+ except _PASSTHROUGH_EXCEPTIONS:
4314
+ raise
4315
+ except Exception as exc:
4316
+ raise ILException(exc) from exc
4317
+
4318
+ def removeMessagesFifo(self, queueName: str, count: int) -> None:
4319
+ """큐 **앞에서부터** ``count`` 건을 지운다(0.20.2). :meth:`flushQueue` 와 달리 일부만 지운다.
4320
+
4321
+ Args:
4322
+ queueName (str): 큐 이름.
4323
+ count (int): 지울 건수(1 이상). 큐에 그보다 적으면 있는 만큼 지운다.
4324
+
4325
+ Raises:
4326
+ UnsupportedOperationException: ``count`` 가 1 보다 작을 때(보내기 전에 막는다).
4327
+ ILOperationException: 그런 큐가 없거나 엔진이 거절했을 때. 클러스터 큐 묶음 모드에서는 그 큐 묶음의
4328
+ 리더가 아닌 큐 관리자로 보내면 ``MIMQE_FOLLOWER_NODE (...)`` 로 거절된다.
4329
+ ILException: 통신 실패.
4330
+
4331
+ Warning:
4332
+ **되돌릴 수 없다.** 다른 세션이 받으려던 메시지일 수 있다. 지운 건은 클러스터에서 팔로워에도 복제된다.
4333
+ """
4334
+ if isinstance(count, bool) or not isinstance(count, int) or count < 1:
4335
+ raise UnsupportedOperationException("[%s] is an unacceptable value. (acceptable: 1 or greater)" % (count,))
4336
+ try:
4337
+ self.service.request(self.qmgrName, ILC.MIMQ_REMOVE_MSG_FIFO, ILC.MIMQ_SUCCESSFUL_REMOVE_MSG,
4338
+ queueName, str(count))
4339
+ except _PASSTHROUGH_EXCEPTIONS:
4340
+ raise
4341
+ except Exception as exc:
4342
+ raise ILException(exc) from exc
4343
+
4344
+ def reloadQueue(self, queueName: str) -> None:
4345
+ """큐를 **디스크에서 다시 읽어 올린다** - ilsc 의 ``RELOAD QUEUE(q)`` (0.20.2).
4346
+
4347
+ Args:
4348
+ queueName (str): 큐 이름.
4349
+
4350
+ Raises:
4351
+ ILOperationException: 그런 큐가 없거나 엔진이 거절했을 때. 클러스터 큐 묶음 모드에서는 그 큐 묶음의
4352
+ 리더가 아닌 큐 관리자로 보내면 ``MIMQE_FOLLOWER_NODE (...)`` 로 거절된다.
4353
+ ILException: 통신 실패.
4354
+ """
4355
+ try:
4356
+ self.service.request(self.qmgrName, ILC.MIMQ_RELOAD_QUEUE, ILC.MIMQ_SUCCESSFUL_QUEUE_RELOAD, queueName)
4357
+ except _PASSTHROUGH_EXCEPTIONS:
4358
+ raise
4359
+ except Exception as exc:
4360
+ raise ILException(exc) from exc
4361
+
4362
+ def getLoadMonitoringInfo(self, level: int = 1) -> str:
4363
+ """큐 관리자 **부하 정보** - ilsc 의 ``VIEW LOAD [LEVEL(n)]`` (0.20.2).
4364
+
4365
+ Args:
4366
+ level (int): 자세한 정도 1 · 2 · 3. 기본 1.
4367
+
4368
+ Returns:
4369
+ str: 엔진이 준 텍스트 그대로. **형식은 계약이 아니다** - 사람이 읽거나 그대로 기록한다.
4370
+
4371
+ Raises:
4372
+ UnsupportedOperationException: ``level`` 이 1 · 2 · 3 이 아닐 때(보내기 전에 막는다).
4373
+ ILOperationException: 엔진이 거절했을 때.
4374
+ ILException: 통신 실패.
4375
+ """
4376
+ if isinstance(level, bool) or level not in (1, 2, 3):
4377
+ raise UnsupportedOperationException("[%s] is an unacceptable value. (acceptable: 1, 2, 3)" % (level,))
4378
+ try:
4379
+ res_msg = self.service.inquire(
4380
+ ILMsgUtil.create_request_msg_with_qmgr(
4381
+ self.qmgrName, ILC.MIMQ_GET_LOAD_MONITORING_INFO, None, str(level), "", ""),
4382
+ False)
4383
+ if res_msg.getType() == ILC.MIMQ_REPORT_MESSAGE:
4384
+ raise ILOperationException(res_msg.getReportMsg().getArg2())
4385
+ return res_msg.getDataMsg().getData().decode("utf-8", "replace")
4386
+ except _PASSTHROUGH_EXCEPTIONS:
4387
+ raise
4388
+ except Exception as exc:
4389
+ raise ILException(exc) from exc
4390
+
3897
4391
  def _queueBlob(self, prop: ILQueueProperty) -> bytes:
3898
4392
  """큐 속성을 **붙어 있는 엔진이 이해하는 레이아웃**으로 직렬화한다.
3899
4393
 
@@ -5271,6 +5765,29 @@ class ILAdminQmgr:
5271
5765
  pass
5272
5766
  return False
5273
5767
 
5768
+ def setChannelStartMode(self, channelName: str, autoStart: bool) -> None:
5769
+ """채널의 **자동 기동 여부**를 바꾼다 - 채널이 **가동 중이어도** 된다(ilsc ``ALTER CHANNEL STARTMODE``, 0.20.2).
5770
+
5771
+ :meth:`setChannelProperty` 는 채널이 멈춰 있을 때만 바꿀 수 있는 항목이 있다. 이 메서드는 기동 방식 하나만
5772
+ 가동 중에 바꾼다.
5773
+
5774
+ Args:
5775
+ channelName (str): 채널 이름.
5776
+ autoStart (bool): True 면 큐 관리자가 뜰 때 같이 기동한다.
5777
+
5778
+ Raises:
5779
+ ILOperationException: 그런 채널이 없거나 엔진이 거절했을 때.
5780
+ ILException: 통신 실패.
5781
+ """
5782
+ try:
5783
+ self.service.request(self.qmgrName, ILC.MIMQ_SET_CHANNEL_EXECUTION_MODE,
5784
+ ILC.MIMQ_SUCCESSFUL_SET_EXECUTION_MODE, channelName,
5785
+ "true" if autoStart else "false")
5786
+ except _PASSTHROUGH_EXCEPTIONS:
5787
+ raise
5788
+ except Exception as exc:
5789
+ raise ILException(exc) from exc
5790
+
5274
5791
  def setChannelTrace(self, channelName: str, enabled: bool) -> None:
5275
5792
  """특정 채널의 트레이스를 켜거나 끈다.
5276
5793
 
@@ -429,6 +429,28 @@ class ILC:
429
429
  #: 이미 있는 클러스터에 이 서비스를 참여시킨다(엔진 r3487). arg1 이름, arg2 ``host:ctrlPort``,
430
430
  #: arg3 STARTMODE(빈 값 = AUTO). 성공 arg1 = 노드 번호, arg2 = 로컬 컨트롤러 포트(0 = 없음).
431
431
  MIMQ_JOIN_CLUSTER = 3406
432
+ #: 운영자 강제 선출(엔진 7.0.1.3551 FORCELEAD-1 / FORCEKEY-1). arg1 클러스터, arg2 대상(NodeId 또는 큐 관리자 이름),
433
+ #: arg3 :attr:`MIMQ_FORCE_ELECT_CHECK` / :attr:`MIMQ_FORCE_ELECT_EXEC`, 확장 = 키(없으면 노드 단위).
434
+ #: 성공 4272 arg1 = 대상 NodeId, arg2 = 상태(EXEC 면 배정 결과), arg3 = 유실 위험. 거절 4273.
435
+ MIMQ_CLUSTER_FORCE_ELECT = 3422
436
+ #: SHUTDOWN(3002) arg1 - **클러스터 전체 정지**(엔진 CLSREL-1). 빈 값이면 이 서비스만 내린다(종전).
437
+ MIMQ_SHUTDOWN_SCOPE_CLUSTER = "C"
438
+ #: 리더십 표 조회(엔진 7.0.1.3552 LEADVIEW-1). ilsvc 요청, arg1 클러스터. 성공 LONG_REPORT 텍스트(첫 줄 ``ILLT1|...``).
439
+ MIMQ_CLUSTER_LEAD_TABLE = 3423
440
+ #: 격리본 목록(엔진 7.0.1.3552 QUARTOOL-1). 큐 관리자 중계. 성공 LONG_REPORT 텍스트(첫 줄 ``QL1|...``).
441
+ MIMQ_QUARANTINE_LIST = 3425
442
+ #: 격리본 검사(재주입의 확인, 부수효과 없음). arg1 디렉터리, arg2 큐, arg3 SINCE. 성공 LONG_REPORT(첫 줄 ``QS1|...``).
443
+ MIMQ_QUARANTINE_SCAN = 3426
444
+ #: 격리본 재주입. arg 는 검사와 같다(큐 필수). 성공 4272 arg1 넣은 건수, arg2 넣은 리더, arg3 멈춘 사유.
445
+ MIMQ_QUARANTINE_REINJECT = 3427
446
+ #: PATCH(3335) 본문 앞 파일 이름 칸 폭(ILINKBYTE128). 이름은 이 폭 - 1 바이트까지(널 종단).
447
+ MIMQ_PATCH_FILE_NAME_LENGTH = 128
448
+ #: 3422 arg3 - 확인만(거절 사유 또는 상태 · 위험).
449
+ MIMQ_FORCE_ELECT_CHECK = "CHECK"
450
+ #: 3422 arg3 - 실행(유실 감수).
451
+ MIMQ_FORCE_ELECT_EXEC = "EXEC"
452
+ #: 키 단위 강제 선출(3422 확장 칸)을 받는 엔진 리비전(FORCEKEY-1). 그 전 엔진은 확장을 버리고 **노드 단위로 실행**한다.
453
+ FORCE_ELECT_KEY_MIN_REVISION = 3552
432
454
  #: REMOVE CLUSTER 의 arg2. 이 값이면 **클러스터 전체 해산**이다(엔진 r3487). 빈 값이면 이 서비스
433
455
  #: 사본만 지운다. ☠ 옛 엔진은 arg2 를 무시해 사본만 지운다 - 성공 arg1 이 빈 값이면 그것이다.
434
456
  MIMQ_CLUSTER_DISSOLVE_ALL = "ALL"
@@ -5305,6 +5305,267 @@ class ILClusterDissolveResult(_AutoAccessor):
5305
5305
  return s
5306
5306
 
5307
5307
 
5308
+ class ILClusterForceElectResult(_AutoAccessor):
5309
+ """:meth:`ILAdminService.checkForceElectLeader` / :meth:`ILAdminService.forceElectLeader` 의 결과(0.20.2). **내부 테스트 자동화 전용.**
5310
+
5311
+ Attributes:
5312
+ target (int): 대상 노드 번호(큐 관리자 이름으로 불렀어도 엔진이 푼 번호).
5313
+ state (str): 확인이면 지금 리더십 현황, 실행이면 배정 결과(``assigned epoch N - committed`` 또는
5314
+ ``... - commit pending (check VIEW CLUSTER)``). 사람이 읽는 문구이고 **형식은 계약이 아니다.**
5315
+ risk (str): 유실 위험 - 대상 말고 확정 리더 · 확정 ISR · 보유자 노드. **그 노드들만 가진 커밋이 사라진다.**
5316
+ 사람이 읽는 문구이고 형식은 계약이 아니다.
5317
+ """
5318
+
5319
+ def __init__(self, arg1: str = "", arg2: str = "", arg3: str = "") -> None:
5320
+ """응답 arg 세 개를 담는다. **응용이 직접 만들 일은 없다.**
5321
+
5322
+ Args:
5323
+ arg1 (str): 대상 노드 번호.
5324
+ arg2 (str): 상태.
5325
+ arg3 (str): 유실 위험.
5326
+ """
5327
+ try:
5328
+ self.target = int(str(arg1 or "").strip() or 0)
5329
+ except ValueError:
5330
+ self.target = 0
5331
+ self.state = str(arg2 or "")
5332
+ self.risk = str(arg3 or "")
5333
+
5334
+ def __str__(self) -> str:
5335
+ """사람이 읽는 한 줄 요약을 돌려준다.
5336
+
5337
+ Returns:
5338
+ str: 대상 · 상태 · 위험. 형식은 고정이 아니므로 파싱하지 말 것.
5339
+ """
5340
+ return "force elect node %d: %s [risk: %s]" % (self.target, self.state, self.risk)
5341
+
5342
+
5343
+ def _lt_int(v: str, default: int = 0) -> int:
5344
+ v = (v or "").strip()
5345
+ if v in ("", "-"):
5346
+ return default
5347
+ try:
5348
+ return int(v)
5349
+ except ValueError:
5350
+ return default
5351
+
5352
+
5353
+ def _lt_ids(v: str) -> list:
5354
+ v = (v or "").strip()
5355
+ if v in ("", "-"):
5356
+ return []
5357
+ out = []
5358
+ for x in v.split(","):
5359
+ x = x.strip()
5360
+ if x.lstrip("-").isdigit():
5361
+ out.append(int(x))
5362
+ return out
5363
+
5364
+
5365
+ class ILClusterLeadEntry(_AutoAccessor):
5366
+ """리더십 표의 **토픽 파티션 하나 또는 큐 묶음 하나**(0.20.2). :class:`ILClusterLeadTable` 의 항목이다.
5367
+
5368
+ Attributes:
5369
+ key (str): 토픽 파티션은 ``"<토픽>/<파티션>"``, 큐 묶음은 묶음 이름.
5370
+ leader (int): 리더 노드 번호. **0 이면 오프라인**(앞 ISR 이 모두 죽음 - 키 단위 강제 선출 대상).
5371
+ epoch (int): 리더 epoch.
5372
+ isr (list[int]): 확정 ISR 노드 번호.
5373
+ replicas (list[int]): 복제본 노드 번호.
5374
+ """
5375
+
5376
+ def __init__(self, key: str = "", leader: int = 0, epoch: int = 0,
5377
+ isr: list | None = None, replicas: list | None = None) -> None:
5378
+ """값을 담는다. **응용이 직접 만들 일은 없다.**
5379
+
5380
+ Args:
5381
+ key (str): 키.
5382
+ leader (int): 리더.
5383
+ epoch (int): epoch.
5384
+ isr (list | None): ISR.
5385
+ replicas (list | None): 복제본.
5386
+ """
5387
+ self.key = key
5388
+ self.leader = leader
5389
+ self.epoch = epoch
5390
+ self.isr = list(isr or [])
5391
+ self.replicas = list(replicas or [])
5392
+
5393
+ def is_offline(self) -> bool:
5394
+ """리더가 없는가(리더 0).
5395
+
5396
+ Returns:
5397
+ bool: 오프라인이면 True.
5398
+ """
5399
+ return self.leader == 0
5400
+
5401
+ isOffline = is_offline
5402
+
5403
+ def __str__(self) -> str:
5404
+ """사람이 읽는 한 줄을 돌려준다.
5405
+
5406
+ Returns:
5407
+ str: 형식은 고정이 아니므로 파싱하지 말 것.
5408
+ """
5409
+ return "%s leader %d e%d isr%s replicas%s" % (self.key, self.leader, self.epoch, self.isr, self.replicas)
5410
+
5411
+
5412
+ class ILClusterForcedKey(_AutoAccessor):
5413
+ """리더십 표의 **강제 선출 기록 하나**(키 단위, 0.20.2).
5414
+
5415
+ Attributes:
5416
+ key (str): 강제 선출한 키(``"<토픽>/<파티션>"`` 또는 ``"Q:<묶음>/0"``).
5417
+ epoch (int): 강제 선출로 배정한 epoch.
5418
+ risk (list[int]): 그때 잃을 수 있던 노드 번호.
5419
+ """
5420
+
5421
+ def __init__(self, key: str = "", epoch: int = 0, risk: list | None = None) -> None:
5422
+ """값을 담는다. **응용이 직접 만들 일은 없다.**
5423
+
5424
+ Args:
5425
+ key (str): 키.
5426
+ epoch (int): epoch.
5427
+ risk (list | None): 위험 노드.
5428
+ """
5429
+ self.key = key
5430
+ self.epoch = epoch
5431
+ self.risk = list(risk or [])
5432
+
5433
+
5434
+ class ILClusterLeadTable(_AutoAccessor):
5435
+ """:meth:`ILAdminService.getClusterLeadTable` 원문을 푼 **리더십 표**(0.20.2, 엔진 7.0.1.3552 LEADVIEW-1).
5436
+
5437
+ Attributes:
5438
+ committed (bool): 컨트롤러 정족수가 확정한 표면 True(``C``). False(``U``)면 리더가 없어 한 컨트롤러가 적용한 판이라
5439
+ **낡았을 수 있다.**
5440
+ controllerLeader (int): 컨트롤러 리더 노드 번호(없으면 0).
5441
+ nodeLeader (int): 노드 단위(브로커) 리더. 0 이면 없음.
5442
+ nodeEpoch (int): 노드 리더 epoch.
5443
+ nodeIsr (list[int]): 노드 단위 확정 ISR.
5444
+ nodeHolders (list[int]): 보유자.
5445
+ nodeForcedEpoch (int): 노드 단위 강제 선출 epoch(없으면 0).
5446
+ nodeRisk (list[int]): 그 강제 선출의 위험 노드.
5447
+ partitionSeq (int | None): 파티션 절 순번. 파티션 절이 없으면 None.
5448
+ placed (bool): 파티션 · 묶음이 배치된 표인가(키 단위 강제 선출을 받는 표).
5449
+ partitions (list[ILClusterLeadEntry]): 토픽 파티션 행.
5450
+ groups (list[ILClusterLeadEntry]): 큐 묶음 행.
5451
+ forced (list[ILClusterForcedKey]): 키 단위 강제 선출 기록.
5452
+ raw (str): 엔진이 준 원문.
5453
+
5454
+ Note:
5455
+ 모르는 머리글 줄은 건너뛴다(엔진 뒤 판이 줄 종류를 늘릴 수 있다). 첫 줄이 ``ILLT1|`` 이 아니면 거절한다 -
5456
+ 판이 다른 표를 조용히 잘못 읽지 않도록.
5457
+ """
5458
+
5459
+ def __init__(self, text: str) -> None:
5460
+ """원문을 푼다.
5461
+
5462
+ Args:
5463
+ text (str): :meth:`ILAdminService.getClusterLeadTable` 원문.
5464
+
5465
+ Raises:
5466
+ ILDeserializeException: 첫 줄이 ``ILLT1|`` 이 아닐 때.
5467
+ """
5468
+ self.raw = text or ""
5469
+ lines = [ln.rstrip("\r") for ln in self.raw.split("\n") if ln.strip()]
5470
+ if not lines or not lines[0].startswith("ILLT1|"):
5471
+ raise ILDeserializeException(
5472
+ "unsupported lead table : first line %r - expected 'ILLT1|...' (upgrade the client)"
5473
+ % (lines[0][:20] if lines else ""))
5474
+ head = lines[0].split("|")
5475
+ self.committed = len(head) > 1 and head[1] == "C"
5476
+ self.controllerLeader = _lt_int(head[2]) if len(head) > 2 else 0
5477
+ self.nodeLeader = 0
5478
+ self.nodeEpoch = 0
5479
+ self.nodeIsr: list = []
5480
+ self.nodeHolders: list = []
5481
+ self.nodeForcedEpoch = 0
5482
+ self.nodeRisk: list = []
5483
+ self.partitionSeq = None
5484
+ self.placed = False
5485
+ self.partitions: list = []
5486
+ self.groups: list = []
5487
+ self.forced: list = []
5488
+ for ln in lines[1:]:
5489
+ f = ln.split("|")
5490
+ kind = f[0]
5491
+ f = f + [""] * 7
5492
+ if kind == "N":
5493
+ self.nodeLeader = _lt_int(f[1])
5494
+ self.nodeEpoch = _lt_int(f[2])
5495
+ self.nodeIsr = _lt_ids(f[3])
5496
+ self.nodeHolders = _lt_ids(f[4])
5497
+ self.nodeForcedEpoch = _lt_int(f[5])
5498
+ self.nodeRisk = _lt_ids(f[6])
5499
+ elif kind == "PT":
5500
+ self.partitionSeq = _lt_int(f[1])
5501
+ self.placed = f[2].strip() == "1"
5502
+ elif kind in ("P", "G"):
5503
+ e = ILClusterLeadEntry(f[1], _lt_int(f[2]), _lt_int(f[3]), _lt_ids(f[4]), _lt_ids(f[5]))
5504
+ (self.partitions if kind == "P" else self.groups).append(e)
5505
+ elif kind == "K":
5506
+ self.forced.append(ILClusterForcedKey(f[1], _lt_int(f[2]), _lt_ids(f[3])))
5507
+ # 모르는 줄은 건너뛴다
5508
+
5509
+ def get_offline(self) -> list:
5510
+ """리더가 없는(오프라인) 파티션 · 묶음 행.
5511
+
5512
+ Returns:
5513
+ list[ILClusterLeadEntry]: 키 단위 강제 선출 대상.
5514
+ """
5515
+ return [e for e in self.partitions + self.groups if e.leader == 0]
5516
+
5517
+ getOffline = get_offline
5518
+
5519
+ def __str__(self) -> str:
5520
+ """원문을 돌려준다.
5521
+
5522
+ Returns:
5523
+ str: 엔진이 준 원문.
5524
+ """
5525
+ return self.raw
5526
+
5527
+
5528
+ class ILQuarantineReinjectResult(_AutoAccessor):
5529
+ """:meth:`ILAdminQmgr.reinjectQuarantine` 의 결과(0.20.2).
5530
+
5531
+ Attributes:
5532
+ count (int): 넣은 건수.
5533
+ leader (str): 넣은 리더(``"node <id> <host:port>"``).
5534
+ note (str): ``""`` 이면 다 넣었다. ``"stopped: <사유>"`` 면 중간에 멈췄다(넣은 만큼은 커밋됐다 - 다시 부르면 나머지만
5535
+ 넣는다). ``"nothing to reinject"`` 면 넣을 것이 없었다.
5536
+ """
5537
+
5538
+ def __init__(self, arg1: str = "", arg2: str = "", arg3: str = "") -> None:
5539
+ """응답 arg 세 개를 담는다. **응용이 직접 만들 일은 없다.**
5540
+
5541
+ Args:
5542
+ arg1 (str): 넣은 건수.
5543
+ arg2 (str): 넣은 리더.
5544
+ arg3 (str): 메모.
5545
+ """
5546
+ self.count = _lt_int(arg1)
5547
+ self.leader = str(arg2 or "")
5548
+ self.note = str(arg3 or "")
5549
+
5550
+ def is_stopped(self) -> bool:
5551
+ """중간에 멈췄는가.
5552
+
5553
+ Returns:
5554
+ bool: ``note`` 가 ``"stopped:"`` 로 시작하면 True.
5555
+ """
5556
+ return self.note.startswith("stopped:")
5557
+
5558
+ isStopped = is_stopped
5559
+
5560
+ def __str__(self) -> str:
5561
+ """사람이 읽는 한 줄 요약을 돌려준다.
5562
+
5563
+ Returns:
5564
+ str: 형식은 고정이 아니므로 파싱하지 말 것.
5565
+ """
5566
+ return "reinjected %d to %s%s" % (self.count, self.leader, (" [" + self.note + "]") if self.note else "")
5567
+
5568
+
5308
5569
  class ILClusterControlResult(_AutoAccessor):
5309
5570
  """:meth:`ILAdminService.startCluster` / :meth:`ILAdminService.stopCluster` 의 결과.
5310
5571
 
@@ -5379,7 +5640,10 @@ _CLUSTER_FIXED_FIELDS = (
5379
5640
  # actingCoordinator 는 **폐기 예정**이다(0.18.1): r3481~r3483 은 controllerLeader 의 사본이었고,
5380
5641
  # r3484 부터는 엔진이 채우지 않는다. 이 12바이트 중 앞 6바이트(583..588)는 **영구 결번** -
5381
5642
  # 다른 값에 다시 쓰이지 않는다(옛 클라가 새 값을 이 이름으로 조용히 잘못 읽지 않도록).
5382
- ("actingCoordinator", 6), ("localCtrlPort", 6), (None, 173),
5643
+ ("actingCoordinator", 6), ("localCtrlPort", 6),
5644
+ # 엔진 PLACEMENT-1: 예약 앞 20바이트에 **정의 칸 둘**. 판 표시는 V2 그대로 - 옛 블롭에는 0x00 이라
5645
+ # 미설정으로 읽힌다. 빈칸으로 보내면 엔진이 저장값을 유지하고, "DEFAULT" 를 보내면 미설정으로 되돌린다.
5646
+ ("placement", 12), ("autoRebalance", 8), (None, 153),
5383
5647
  )
5384
5648
 
5385
5649
  _CLUSTER_OFF, _CLUSTER_LEN = _field_offsets(_CLUSTER_FIXED_FIELDS)
@@ -5390,7 +5654,7 @@ _CLUSTER_OFF, _CLUSTER_LEN = _field_offsets(_CLUSTER_FIXED_FIELDS)
5390
5654
  _CLUSTER_WRITTEN = ("layoutVersion", "name", "startMode",
5391
5655
  "controllerHeartbeatIntervalMs", "controllerElectionTimeoutMs",
5392
5656
  "heartbeatIntervalMs", "heartbeatMissCount", "electionDelayMs",
5393
- "replicas", "minSync")
5657
+ "replicas", "minSync", "placement", "autoRebalance")
5394
5658
 
5395
5659
  # 표에서 계산한 길이와 공개 상수가 어긋나면 **여기서 터진다.** 한쪽만 고치는 일을 막는다 -
5396
5660
  # 상수를 써서 오프셋을 잡는 코드가 밖에 있어서 조용히 어긋나면 잘못된 자리를 파게 된다.
@@ -5821,6 +6085,12 @@ class ILClusterProperty(_AutoAccessor):
5821
6085
  친다 - 노드를 더하면 RF 도 따라 는다. 숫자를 명시하면 그 진술이 고정되어
5822
6086
  노드 수가 달라질 때 거절된다. ★ **ALTER 에서 ``None``(빈칸)은 "그대로 둔다"** 이고, 명시해 둔 값을
5823
6087
  **지우려면 0 을 보낸다** - 미설정으로 되돌아가 다시 노드 수를 따른다(``minSync`` 와 같은 규칙).
6088
+ placement (str | None): 파티션 배치(문법의 ``PLACEMENT``) - ``"OFF"`` · ``"ROUNDROBIN"`` · ``"LOAD"``(0.20.2).
6089
+ ``None`` 은 미설정(엔진이 ``OFF`` 로 친다)이고, **보낼 때는 "말하지 않았다"** 라 엔진이 저장값을 유지한다.
6090
+ 미설정으로 되돌리려면 ``"DEFAULT"`` 를 보낸다. 켜면(``ROUNDROBIN`` · ``LOAD``) ``replicas`` 가 파티션마다의 복제본
6091
+ 수가 된다. 값 검사는 엔진이 한다(``MIMQE_INVALID_PROPERTY (PLACEMENT must be OFF | ROUNDROBIN | LOAD)``).
6092
+ autoRebalance (str | None): 배치를 켠 정의에서 브로커가 바뀔 때 저절로 다시 배치할지(``AUTOREBALANCE``) -
6093
+ ``"ON"`` · ``"OFF"``(0.20.2). ``None`` 은 미설정(엔진이 ``ON`` 으로 친다), 보낼 때 규칙은 ``placement`` 와 같다.
5824
6094
  minSync (int | None): 쓰기를 받는 최소 확정 ISR 수. ``None`` 이면 엔진이 1 로 친다 -
5825
6095
  혼자 남은 리더도 계속 쓴다(가용성을 택하고 내구성을 내린다).
5826
6096
  **2 이상을 명시하면 Kafka 의 ``min.insync.replicas`` 처럼 동작한다** -
@@ -5942,6 +6212,8 @@ class ILClusterProperty(_AutoAccessor):
5942
6212
  self.controllerElectionTimeoutMs = None
5943
6213
  self.replicas = None
5944
6214
  self.minSync = None
6215
+ self.placement = None
6216
+ self.autoRebalance = None
5945
6217
  self.clusterId = ""
5946
6218
  self.status = ILC.MIMQ_STOPPED
5947
6219
  self.statusReason = ""
@@ -5996,6 +6268,8 @@ class ILClusterProperty(_AutoAccessor):
5996
6268
  self.electionDelayMs = ba_pick_int(ba, o["electionDelayMs"][0], o["electionDelayMs"][1])
5997
6269
  self.replicas = ba_pick_int_opt(ba, o["replicas"][0], o["replicas"][1])
5998
6270
  self.minSync = ba_pick_int_opt(ba, o["minSync"][0], o["minSync"][1])
6271
+ self.placement = ba_pick_string(ba, o["placement"][0], o["placement"][1]).strip() or None
6272
+ self.autoRebalance = ba_pick_string(ba, o["autoRebalance"][0], o["autoRebalance"][1]).strip() or None
5999
6273
  self.secretFile = ba_pick_string(ba, o["secretFile"][0], o["secretFile"][1])
6000
6274
  self.controllerLeader = ba_pick_int_opt(ba, o["controllerLeader"][0],
6001
6275
  o["controllerLeader"][1])
@@ -6214,6 +6488,10 @@ class ILClusterProperty(_AutoAccessor):
6214
6488
  _v = getattr(self, _name)
6215
6489
  if _v is not None: # 0 은 "0" 으로, None 은 빈칸으로
6216
6490
  ba_merge(_v, ba, o[_name][0], o[_name][1])
6491
+ for _name in ("placement", "autoRebalance"):
6492
+ _v = getattr(self, _name)
6493
+ if _v: # None / "" 은 빈칸 = 엔진이 저장값 유지
6494
+ ba_merge(str(_v), ba, o[_name][0], o[_name][1])
6217
6495
  offset = _CLUSTER_LEN
6218
6496
  ba_merge(len(self.nodes), ba, offset, ILC.MIMQ_LIST_SIZE_LENGTH)
6219
6497
  offset += ILC.MIMQ_LIST_SIZE_LENGTH
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jetstream-api
3
- Version: 0.20.1
3
+ Version: 0.20.2
4
4
  Summary: Python client API for iLink M.O.M. (Message Oriented Middleware)
5
5
  Author: snowjeans
6
6
  License: Boost Software License - Version 1.0 - August 17th, 2003
@@ -60,6 +60,35 @@ lz4 배치 압축을 쓰려면 `pip install "jetstream-api[lz4]"`, zstd 배치
60
60
  ## 설치 방법
61
61
  pip install jetstream-api
62
62
 
63
+ ## 0.20.2 변경 - 관제 명령 관리 API · 강제 선출 · 리더십 표 · 격리본 (자바 v2.16.2 와 같음, 짝 엔진 7.0.1.3512 이상 - 리더십 표 · 격리본 · 키 단위 강제 선출은 7.0.1.3552 이상)
64
+
65
+ - **리더십 표 · 격리본**(엔진 7.0.1.3552 이상, 그 전 엔진은 `MIMQE_INVALID_REQ_CODE`):
66
+ - `ILAdminService.getClusterLeadTable(cluster)`(VIEW CLUSTER LEADERS, 3423): 원문. `getClusterLeadTableParsed` 는
67
+ `ILClusterLeadTable`(노드 · 파티션 · 묶음 행, 강제 선출 기록, 오프라인 행)로 풀어 준다. 모르는 줄은 건너뛰고, 판(`ILLT1`)이 다르면 거절.
68
+ - `ILAdminQmgr.getQuarantineList()` / `scanQuarantine(dir, queue, since)`(VIEW QUARANTINE, 3425 / 3426): 원문.
69
+ - `reinjectQuarantine(dir, queue, since)`(REINJECT, 3427): `ILQuarantineReinjectResult`(건수 · 리더 · 멈춘 사유). ☠ OLD 까지
70
+ 넣으면 중복 가능, 넣은 건은 큐 맨 뒤에 붙는다.
71
+
72
+ - **ilsc 관제 명령을 관리 API 로**(엔진이 이미 받는 요청에 클라 메서드만 없던 것):
73
+ - `ILAdminService.shutdownService(clusterWide=False)`(SHUTDOWN, 3002): 응답 없이 끊기면 False(엔진이 내려가며 닫음), 시간 초과는 예외.
74
+ - `patchService(fileName, data)`(PATCH, 3335): 성공하면 서비스가 다시 기동하고 연결이 닫힌다.
75
+ - `diagnoseClusterDefinition(clusterName)`(DIAG CLUSTER 의 정의 대조, 3395): 텍스트 그대로.
76
+ - `ILAdminQmgr.getLoadMonitoringInfo(level)`(VIEW LOAD, 3204), `reloadQueue`(RELOAD QUEUE, 3315),
77
+ `setChannelStartMode(channel, autoStart)`(가동 중 STARTMODE, 3035), `removeMessagesFifo(queue, count)`(앞에서부터 지우기, 3345).
78
+ - `ILClusterProperty.placement` / `autoRebalance`(PLACEMENT · AUTOREBALANCE): `None` 은 보낼 때 "그대로 둔다",
79
+ `"DEFAULT"` 는 미설정으로 되돌린다.
80
+
81
+ - **내부 테스트 자동화 전용** `ILAdminService.checkForceElectLeader` / `forceElectLeader`(+ snake 별칭): 엔진 7.0.1.3551 의 운영자
82
+ 강제 선출(`ELECT CLUSTER(C) LEADER(..) FORCE`, 요청 3422)을 관리 API 로 부른다. 확인(위험만 봄)과 실행(☠ 유실 감수)을 나눴고,
83
+ 키(`"<토픽>/<파티션>"` / `"Q:<묶음>/0"`, 도우미 `forceElectPartitionKey` / `forceElectGroupKey`)로 하나만 강제할 수도 있다 -
84
+ 키 단위는 엔진 7.0.1.3552 이상(그 전 엔진은 키를 버리고 노드 단위로 실행하므로 옛 엔진임이 확실하면 보내기 전에 막는다).
85
+ 결과는 `ILClusterForceElectResult`(target · state · risk). 클라는 다시 보내지 않는다(실행의 결과 모름은 확인부터).
86
+ 지금은 내부 시험 자동화에서만 쓴다 - 운영자의 강제 선출은 ilsc 로 한다.
87
+
88
+ - `removeNode` 문서: `MIMQE_CLUSTER_NOT_COMMITTED (not committed - retry)` 는 실패가 아니라 **결과 모름**입니다(컨트롤러가 8초 안에
89
+ 확정을 못 봄, 늦게 확정될 수 있음). 클라는 스스로 다시 보내지 않습니다. 다시 불러도 안전하고, 그때 `MIMQE_OBJECT_NOT_FOUND` 면
90
+ 먼저 것이 확정돼 이미 빠진 것입니다. 클라 동작은 바뀌지 않습니다.
91
+
63
92
  ## 0.20.1 변경 - 큐 묶음 이동 중 재시도 (자바 v2.16.1 과 같음, 짝 엔진 7.0.1.3512 이상 - 묶음 이동은 7.0.1.3550 이상의 묶음 모드)
64
93
 
65
94
  - 큐 묶음 이동(엔진 7.0.1.3550 3-b3, `ALTER QUEUE … LEADERGROUP` 등)이 진행 중이면 엔진이 그 큐의 요청을
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jetstream-api"
7
- version = "0.20.1"
7
+ version = "0.20.2"
8
8
  description = "Python client API for iLink M.O.M. (Message Oriented Middleware)"
9
9
  authors = [{name = "snowjeans"}]
10
10
  readme = "README.md"
File without changes