queue-aiops 0.2.0__tar.gz → 0.2.1__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.
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/PKG-INFO +14 -12
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/README.md +13 -11
- queue_aiops-0.2.1/RELEASE_NOTES.md +26 -0
- queue_aiops-0.2.1/docs/VERIFICATION.md +34 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/pyproject.toml +1 -1
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/ops/_util.py +24 -1
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/ops/rabbit_reads.py +10 -10
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/ops/redis_reads.py +19 -19
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/server.json +2 -2
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_reads.py +31 -1
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/uv.lock +1 -1
- queue_aiops-0.2.0/RELEASE_NOTES.md +0 -48
- queue_aiops-0.2.0/docs/VERIFICATION.md +0 -200
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/.github/workflows/mcp-publish.yml +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/.github/workflows/publish.yml +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/.gitignore +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/CHANGELOG.md +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/LICENSE +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/SECURITY.md +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/__init__.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/_shared.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/server.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/tools/__init__.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/tools/analysis.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/tools/overview.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/tools/rabbit_reads.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/tools/redis_reads.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/tools/undo.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/mcp_server/tools/writes.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/__init__.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/__init__.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/_common.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/_root.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/analyze.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/doctor.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/init.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/overview.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/rabbit_cmds.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/redis_cmds.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/secret.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/cli/undo.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/config.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/connection.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/doctor.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/__init__.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/audit.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/budget.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/decorators.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/paths.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/patterns.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/policy.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/readonly.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/sanitize.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/governance/undo.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/ops/__init__.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/ops/analysis.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/ops/overview.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/ops/writes.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/platform.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/queue_aiops/secretstore.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/skills/queue-aiops/SKILL.md +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/skills/queue-aiops/references/agent-guardrails.md +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/skills/queue-aiops/references/capabilities.md +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/skills/queue-aiops/references/cli-reference.md +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/skills/queue-aiops/references/setup-guide.md +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/smithery.yaml +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/conftest.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/fakes.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_analysis.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_cli_rabbit_writes.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_cli_reads.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_cli_secret.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_cli_writes.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_connection.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_doctor.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_gov_audit.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_gov_decorators.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_gov_patterns.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_gov_policy.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_governance_persistence.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_init.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_mcp_reads.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_optional_fields.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_platform.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_readonly.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_secretstore.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_smoke.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_undo_executor.py +0 -0
- {queue_aiops-0.2.0 → queue_aiops-0.2.1}/tests/test_writes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queue-aiops
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Governed AI-ops for redis + rabbitmq: memory-pressure/latency/queue-backlog/connection-churn RCAs, slowlog and SCAN-budgeted big-key sampling, queues/policies/nodes, and governed writes (config set, client kill, purge/delete queue, policies) with a built-in governance harness (audit, budget, undo, risk tiers)
|
|
5
5
|
Author-email: wei <zhouwei008@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -192,17 +192,19 @@ queue-aiops analyze memory|latency|backlog|churn
|
|
|
192
192
|
|
|
193
193
|
## Verification status
|
|
194
194
|
|
|
195
|
-
**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
195
|
+
**Live-verified against Redis 7.4.9 (2026-07-19); RabbitMQ still mock-only.**
|
|
196
|
+
Connectivity, every Redis read (cross-checked against `redis-cli` ground truth), all
|
|
197
|
+
four analyses, and the full governance loop (real `redis_config_set` → audit row →
|
|
198
|
+
undo restoring the prior value) were exercised against a real server. That run found
|
|
199
|
+
and fixed a real defect: integer quantities — key counts, client counts, byte totals —
|
|
200
|
+
were rendered as floats (`202.0` keys), which equality assertions cannot catch.
|
|
201
|
+
|
|
202
|
+
The **entire RabbitMQ command group** remains unverified against a live broker, as do
|
|
203
|
+
Redis cluster/sentinel topologies and AUTH/TLS connections.
|
|
204
|
+
|
|
205
|
+
[docs/VERIFICATION.md](docs/VERIFICATION.md) records exactly what was checked and what
|
|
206
|
+
is still open. `queue-aiops doctor` is the fastest live check.
|
|
207
|
+
|
|
206
208
|
|
|
207
209
|
## Contributing
|
|
208
210
|
|
|
@@ -175,17 +175,19 @@ queue-aiops analyze memory|latency|backlog|churn
|
|
|
175
175
|
|
|
176
176
|
## Verification status
|
|
177
177
|
|
|
178
|
-
**
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
**Live-verified against Redis 7.4.9 (2026-07-19); RabbitMQ still mock-only.**
|
|
179
|
+
Connectivity, every Redis read (cross-checked against `redis-cli` ground truth), all
|
|
180
|
+
four analyses, and the full governance loop (real `redis_config_set` → audit row →
|
|
181
|
+
undo restoring the prior value) were exercised against a real server. That run found
|
|
182
|
+
and fixed a real defect: integer quantities — key counts, client counts, byte totals —
|
|
183
|
+
were rendered as floats (`202.0` keys), which equality assertions cannot catch.
|
|
184
|
+
|
|
185
|
+
The **entire RabbitMQ command group** remains unverified against a live broker, as do
|
|
186
|
+
Redis cluster/sentinel topologies and AUTH/TLS connections.
|
|
187
|
+
|
|
188
|
+
[docs/VERIFICATION.md](docs/VERIFICATION.md) records exactly what was checked and what
|
|
189
|
+
is still open. `queue-aiops doctor` is the fastest live check.
|
|
190
|
+
|
|
189
191
|
|
|
190
192
|
## Contributing
|
|
191
193
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Release notes — queue-aiops 0.2.1
|
|
2
|
+
|
|
3
|
+
Previous release: 0.2.0.
|
|
4
|
+
|
|
5
|
+
## Fixed: integer quantities were rendered as floats
|
|
6
|
+
|
|
7
|
+
Key counts, client counts and byte totals were routed through the float coercion
|
|
8
|
+
helper, so a live server reported `202.0` keys and `1.0` connected clients. The
|
|
9
|
+
values were arithmetically right but semantically wrong — a count cannot be
|
|
10
|
+
fractional, and a reader should not have to wonder whether `.0` means the number was
|
|
11
|
+
rounded.
|
|
12
|
+
|
|
13
|
+
These now use a new `as_int()` helper and come back as integers. Genuinely fractional
|
|
14
|
+
values (`hitRatePct`, `usedPctOfMax`, `opsPerSec`, `expiresPct`) are unchanged. Note
|
|
15
|
+
that equality assertions cannot catch this class of bug (`202 == 202.0`), so the
|
|
16
|
+
regression test asserts the *type*.
|
|
17
|
+
|
|
18
|
+
If you parse these fields, the JSON shape changes from `202.0` to `202`.
|
|
19
|
+
|
|
20
|
+
## Live-verified
|
|
21
|
+
|
|
22
|
+
This release was exercised end-to-end against a real **Redis 7.4.9** server: every
|
|
23
|
+
Redis read cross-checked against `redis-cli` ground truth, all four analyses, and the
|
|
24
|
+
full governance loop (real `redis_config_set` → audit row → `undo_apply` restoring the
|
|
25
|
+
prior value). See [docs/VERIFICATION.md](docs/VERIFICATION.md) — **RabbitMQ remains
|
|
26
|
+
mock-only** and is now the largest gap in this repo.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Live verification status
|
|
2
|
+
|
|
3
|
+
This document records what has and has not been validated against real Redis /
|
|
4
|
+
RabbitMQ servers, so the maturity claim is auditable rather than a vibe.
|
|
5
|
+
|
|
6
|
+
## Already live-verified ✅ — Redis 7.4.9, 2026-07-19
|
|
7
|
+
|
|
8
|
+
Exercised end-to-end against a real **Redis 7.4.9** server (Docker) seeded with
|
|
9
|
+
202 keys and a list-backed job queue:
|
|
10
|
+
|
|
11
|
+
- `doctor` against a live server: PING OK, no-AUTH lab path reported clearly.
|
|
12
|
+
- Every Redis read cross-checked against `redis-cli` ground truth:
|
|
13
|
+
`overview`, `redis info/memory/clients/slowlog/keyspace/bigkeys`
|
|
14
|
+
(key count matched `DBSIZE` exactly).
|
|
15
|
+
- All four analyses ran clean: `analyze memory/latency/backlog/churn`.
|
|
16
|
+
- Governance loop end-to-end: `redis_config_set` really changed
|
|
17
|
+
`maxmemory-policy`, captured `noeviction` as `priorState`, wrote an audit row
|
|
18
|
+
with approver, and `undo_apply` restored the prior value on the live server.
|
|
19
|
+
|
|
20
|
+
**A real defect was found and fixed by this run**: integer quantities were
|
|
21
|
+
rendered as floats — `202.0` keys, `1.0` connected clients, float byte counts —
|
|
22
|
+
because they were routed through `num()`. Values were arithmetically right but
|
|
23
|
+
semantically wrong, and equality assertions could not catch it
|
|
24
|
+
(`202 == 202.0`). Fixed with `as_int()` across the Redis and RabbitMQ reads,
|
|
25
|
+
plus a regression test that asserts the *type*, keeping genuine ratios
|
|
26
|
+
(`hitRatePct`, `usedPctOfMax`, `opsPerSec`) as floats.
|
|
27
|
+
|
|
28
|
+
## Not yet live-verified ⚠️
|
|
29
|
+
|
|
30
|
+
- **RabbitMQ** — the entire `rabbitmq` command group and its management-API
|
|
31
|
+
shapes are unit-tested only. This is now the largest gap in this repo.
|
|
32
|
+
- Redis **cluster / sentinel** topologies (only standalone was exercised).
|
|
33
|
+
- AUTH-enabled Redis and TLS connections.
|
|
34
|
+
- `kill-client` against a real blocked client.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "queue-aiops"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Governed AI-ops for redis + rabbitmq: memory-pressure/latency/queue-backlog/connection-churn RCAs, slowlog and SCAN-budgeted big-key sampling, queues/policies/nodes, and governed writes (config set, client kill, purge/delete queue, policies) with a built-in governance harness (audit, budget, undo, risk tiers)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -50,13 +50,36 @@ def pick(row: dict, *keys: str, default: Any = None) -> Any:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def num(value: Any) -> float:
|
|
53
|
-
"""Coerce a numeric cell to float; 0.0 when absent/non-numeric.
|
|
53
|
+
"""Coerce a numeric cell to float; 0.0 when absent/non-numeric.
|
|
54
|
+
|
|
55
|
+
Use this only for genuinely fractional values (rates, ratios, percentages).
|
|
56
|
+
For integer quantities — key counts, byte counts, client counts, whole
|
|
57
|
+
seconds — use :func:`as_int`, which keeps them integers.
|
|
58
|
+
"""
|
|
54
59
|
try:
|
|
55
60
|
return float(value)
|
|
56
61
|
except (TypeError, ValueError):
|
|
57
62
|
return 0.0
|
|
58
63
|
|
|
59
64
|
|
|
65
|
+
def as_int(value: Any) -> int:
|
|
66
|
+
"""Coerce an integer quantity (counts, bytes, whole seconds) to ``int``.
|
|
67
|
+
|
|
68
|
+
Redis ``INFO`` and the RabbitMQ management API hand these back as strings;
|
|
69
|
+
routing them through :func:`num` rendered them as ``202.0`` keys or
|
|
70
|
+
``1.0`` connected clients — arithmetically right but semantically wrong,
|
|
71
|
+
since a count cannot be fractional. A reader (human or model) should not
|
|
72
|
+
have to wonder whether a ``.0`` means the value was rounded.
|
|
73
|
+
|
|
74
|
+
Non-numeric/absent yields ``0`` to match :func:`num`'s contract for these
|
|
75
|
+
always-present INFO fields.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
return int(float(value))
|
|
79
|
+
except (TypeError, ValueError):
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
60
83
|
def rate(row: Any, default: float = 0.0) -> float:
|
|
61
84
|
"""Read a management-API ``*_details.rate`` cell (``{"rate": 1.2}``)."""
|
|
62
85
|
if isinstance(row, dict):
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
-
from queue_aiops.ops._util import as_obj, num, opt, pick, rate, s
|
|
13
|
+
from queue_aiops.ops._util import as_int, as_obj, num, opt, pick, rate, s
|
|
14
14
|
|
|
15
15
|
MAX_ROWS = 200
|
|
16
16
|
|
|
@@ -28,9 +28,9 @@ def broker_overview(conn: Any) -> dict:
|
|
|
28
28
|
"version": opt(pick(obj, "rabbitmq_version", "product_version")),
|
|
29
29
|
"clusterName": opt(obj.get("cluster_name")),
|
|
30
30
|
"node": opt(obj.get("node")),
|
|
31
|
-
"messagesReady":
|
|
32
|
-
"messagesUnacked":
|
|
33
|
-
"messagesTotal":
|
|
31
|
+
"messagesReady": as_int(totals.get("messages_ready")),
|
|
32
|
+
"messagesUnacked": as_int(totals.get("messages_unacknowledged")),
|
|
33
|
+
"messagesTotal": as_int(totals.get("messages")),
|
|
34
34
|
"queues": num(objects.get("queues")),
|
|
35
35
|
"connections": num(objects.get("connections")),
|
|
36
36
|
"channels": num(objects.get("channels")),
|
|
@@ -56,11 +56,11 @@ def _queue_row(q: dict) -> dict:
|
|
|
56
56
|
"vhost": opt(q.get("vhost"), 64),
|
|
57
57
|
"state": opt(q.get("state"), 32),
|
|
58
58
|
"durable": bool(q.get("durable")),
|
|
59
|
-
"messages":
|
|
60
|
-
"messagesReady":
|
|
61
|
-
"messagesUnacked":
|
|
59
|
+
"messages": as_int(q.get("messages")),
|
|
60
|
+
"messagesReady": as_int(q.get("messages_ready")),
|
|
61
|
+
"messagesUnacked": as_int(q.get("messages_unacknowledged")),
|
|
62
62
|
"consumers": num(q.get("consumers")),
|
|
63
|
-
"memoryBytes":
|
|
63
|
+
"memoryBytes": as_int(q.get("memory")),
|
|
64
64
|
"publishRate": rate(stats.get("publish_details")),
|
|
65
65
|
"deliverRate": rate(stats.get("deliver_get_details")),
|
|
66
66
|
"ackRate": rate(stats.get("ack_details")),
|
|
@@ -220,8 +220,8 @@ def node_health(conn: Any) -> dict:
|
|
|
220
220
|
"memUsedBytes": mem_used,
|
|
221
221
|
"memLimitBytes": mem_limit,
|
|
222
222
|
"memAlarm": bool(n.get("mem_alarm")),
|
|
223
|
-
"diskFreeBytes":
|
|
224
|
-
"diskFreeLimitBytes":
|
|
223
|
+
"diskFreeBytes": as_int(n.get("disk_free")),
|
|
224
|
+
"diskFreeLimitBytes": as_int(n.get("disk_free_limit")),
|
|
225
225
|
"diskAlarm": bool(n.get("disk_free_alarm")),
|
|
226
226
|
"fdUsed": num(n.get("fd_used")),
|
|
227
227
|
"fdTotal": num(n.get("fd_total")),
|
|
@@ -15,7 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
from typing import Any
|
|
17
17
|
|
|
18
|
-
from queue_aiops.ops._util import as_obj, num, opt, pct, s
|
|
18
|
+
from queue_aiops.ops._util import as_int, as_obj, num, opt, pct, s
|
|
19
19
|
|
|
20
20
|
# Hard bounds for the SCAN-based big-key sample (never KEYS *).
|
|
21
21
|
SCAN_BUDGET_KEYS = 10_000 # max keys walked per call
|
|
@@ -35,14 +35,14 @@ def server_info(conn: Any) -> dict:
|
|
|
35
35
|
"platform": conn.target.platform,
|
|
36
36
|
"version": opt(info.get("redis_version")),
|
|
37
37
|
"mode": opt(info.get("redis_mode")),
|
|
38
|
-
"uptimeSeconds":
|
|
38
|
+
"uptimeSeconds": as_int(info.get("uptime_in_seconds")),
|
|
39
39
|
"role": opt(info.get("role")),
|
|
40
|
-
"connectedClients":
|
|
41
|
-
"blockedClients":
|
|
40
|
+
"connectedClients": as_int(info.get("connected_clients")),
|
|
41
|
+
"blockedClients": as_int(info.get("blocked_clients")),
|
|
42
42
|
"opsPerSec": num(info.get("instantaneous_ops_per_sec")),
|
|
43
43
|
"totalCommands": num(info.get("total_commands_processed")),
|
|
44
|
-
"keyspaceHits":
|
|
45
|
-
"keyspaceMisses":
|
|
44
|
+
"keyspaceHits": as_int(info.get("keyspace_hits")),
|
|
45
|
+
"keyspaceMisses": as_int(info.get("keyspace_misses")),
|
|
46
46
|
"hitRatePct": pct(
|
|
47
47
|
num(info.get("keyspace_hits")),
|
|
48
48
|
num(info.get("keyspace_hits")) + num(info.get("keyspace_misses")),
|
|
@@ -56,8 +56,8 @@ def memory_stats(conn: Any) -> dict:
|
|
|
56
56
|
"""[READ] Memory posture: used vs maxmemory, policy, fragmentation, peaks."""
|
|
57
57
|
try:
|
|
58
58
|
info = as_obj(conn.redis_info("memory"))
|
|
59
|
-
used =
|
|
60
|
-
maxmem =
|
|
59
|
+
used = as_int(info.get("used_memory"))
|
|
60
|
+
maxmem = as_int(info.get("maxmemory"))
|
|
61
61
|
stats = as_obj(conn.redis_memory_stats())
|
|
62
62
|
return {
|
|
63
63
|
"usedBytes": used,
|
|
@@ -66,11 +66,11 @@ def memory_stats(conn: Any) -> dict:
|
|
|
66
66
|
"usedPctOfMax": pct(used, maxmem),
|
|
67
67
|
"maxmemoryPolicy": opt(info.get("maxmemory_policy")),
|
|
68
68
|
"fragmentationRatio": num(info.get("mem_fragmentation_ratio")),
|
|
69
|
-
"rssBytes":
|
|
70
|
-
"peakBytes":
|
|
71
|
-
"keysCount":
|
|
72
|
-
"overheadBytes":
|
|
73
|
-
"datasetBytes":
|
|
69
|
+
"rssBytes": as_int(info.get("used_memory_rss")),
|
|
70
|
+
"peakBytes": as_int(info.get("used_memory_peak")),
|
|
71
|
+
"keysCount": as_int(stats.get("keys.count")),
|
|
72
|
+
"overheadBytes": as_int(stats.get("overhead.total")),
|
|
73
|
+
"datasetBytes": as_int(stats.get("dataset.bytes")),
|
|
74
74
|
}
|
|
75
75
|
except Exception as exc: # noqa: BLE001 — report as partial
|
|
76
76
|
return {"error": s(exc, 200)}
|
|
@@ -85,8 +85,8 @@ def list_clients(conn: Any) -> dict:
|
|
|
85
85
|
"id": opt(c.get("id"), 32),
|
|
86
86
|
"addr": opt(c.get("addr"), 64),
|
|
87
87
|
"name": opt(c.get("name"), 64),
|
|
88
|
-
"ageSeconds":
|
|
89
|
-
"idleSeconds":
|
|
88
|
+
"ageSeconds": as_int(c.get("age")),
|
|
89
|
+
"idleSeconds": as_int(c.get("idle")),
|
|
90
90
|
"lastCommand": opt(c.get("cmd"), 64),
|
|
91
91
|
"db": opt(c.get("db"), 8),
|
|
92
92
|
"flags": opt(c.get("flags"), 16),
|
|
@@ -183,15 +183,15 @@ def keyspace(conn: Any) -> dict:
|
|
|
183
183
|
dbs = []
|
|
184
184
|
for name, cell in info.items():
|
|
185
185
|
cell = as_obj(cell) if isinstance(cell, dict) else _parse_keyspace_cell(cell)
|
|
186
|
-
keys =
|
|
187
|
-
expires =
|
|
186
|
+
keys = as_int(cell.get("keys"))
|
|
187
|
+
expires = as_int(cell.get("expires"))
|
|
188
188
|
dbs.append(
|
|
189
189
|
{
|
|
190
190
|
"db": s(name, 16),
|
|
191
191
|
"keys": keys,
|
|
192
192
|
"expires": expires,
|
|
193
193
|
"expiresPct": pct(expires, keys),
|
|
194
|
-
"avgTtlMs":
|
|
194
|
+
"avgTtlMs": as_int(cell.get("avg_ttl")),
|
|
195
195
|
}
|
|
196
196
|
)
|
|
197
197
|
return {"databases": dbs, "totalKeys": sum(d["keys"] for d in dbs)}
|
|
@@ -235,7 +235,7 @@ def big_key_sample(conn: Any, top: int = TOP_KEYS) -> dict:
|
|
|
235
235
|
for key in sampled:
|
|
236
236
|
usage = conn.redis_memory_usage(key)
|
|
237
237
|
if usage is not None:
|
|
238
|
-
sized.append({"key": s(key, 128), "bytes":
|
|
238
|
+
sized.append({"key": s(key, 128), "bytes": int(usage)})
|
|
239
239
|
sized.sort(key=lambda e: e["bytes"], reverse=True)
|
|
240
240
|
|
|
241
241
|
return {
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/AIops-tools/Queue-AIops",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.2.
|
|
10
|
+
"version": "0.2.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "queue-aiops",
|
|
15
|
-
"version": "0.2.
|
|
15
|
+
"version": "0.2.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
@@ -97,6 +97,36 @@ def test_keyspace_parses_raw_cells():
|
|
|
97
97
|
assert out["databases"][0]["expiresPct"] == 40.0
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
@pytest.mark.unit
|
|
101
|
+
def test_integer_quantities_are_ints_not_floats():
|
|
102
|
+
"""Counts and byte totals must be int; only ratios stay float.
|
|
103
|
+
|
|
104
|
+
Regression from live verification against Redis 7.4: INFO values routed
|
|
105
|
+
through ``num()`` rendered as ``202.0`` keys and ``1.0`` clients. Equality
|
|
106
|
+
assertions cannot catch this (``202 == 202.0``), so assert the *type*.
|
|
107
|
+
"""
|
|
108
|
+
conn = redis_conn(
|
|
109
|
+
info={
|
|
110
|
+
"keyspace": {"db0": "keys=100,expires=40,avg_ttl=5000"},
|
|
111
|
+
"memory": {"used_memory": "1374096", "maxmemory": "0"},
|
|
112
|
+
"clients": {"connected_clients": "1", "blocked_clients": "0"},
|
|
113
|
+
"server": {"uptime_in_seconds": "1790"},
|
|
114
|
+
"stats": {"keyspace_hits": "7", "keyspace_misses": "0"},
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
ks = redis_reads.keyspace(conn)
|
|
118
|
+
assert isinstance(ks["totalKeys"], int)
|
|
119
|
+
assert isinstance(ks["databases"][0]["keys"], int)
|
|
120
|
+
assert isinstance(ks["databases"][0]["expires"], int)
|
|
121
|
+
# …but a genuine ratio must stay float.
|
|
122
|
+
assert isinstance(ks["databases"][0]["expiresPct"], float)
|
|
123
|
+
|
|
124
|
+
mem = redis_reads.memory_stats(conn)
|
|
125
|
+
assert isinstance(mem["usedBytes"], int)
|
|
126
|
+
assert isinstance(mem["maxmemoryBytes"], int)
|
|
127
|
+
assert isinstance(mem["usedPctOfMax"], float)
|
|
128
|
+
|
|
129
|
+
|
|
100
130
|
@pytest.mark.unit
|
|
101
131
|
def test_big_key_sample_scans_and_sizes_topmost():
|
|
102
132
|
conn = redis_conn(
|
|
@@ -107,7 +137,7 @@ def test_big_key_sample_scans_and_sizes_topmost():
|
|
|
107
137
|
out = redis_reads.big_key_sample(conn)
|
|
108
138
|
assert out["scannedKeys"] == 3
|
|
109
139
|
assert out["coveragePct"] == 100.0
|
|
110
|
-
assert out["topKeys"][0] == {"key": "k2", "bytes": 50_000_000
|
|
140
|
+
assert out["topKeys"][0] == {"key": "k2", "bytes": 50_000_000}
|
|
111
141
|
|
|
112
142
|
|
|
113
143
|
@pytest.mark.unit
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# Release notes — queue-aiops 0.2.0
|
|
2
|
-
|
|
3
|
-
Previous release: 0.1.1.
|
|
4
|
-
|
|
5
|
-
## Headline: read-only mode
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
export QUEUE_READ_ONLY=1
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
With this set the **8 write tools are never registered** — an MCP
|
|
12
|
-
client lists **20 tools instead of 28**. The writes are not hidden
|
|
13
|
-
behind a flag and not merely refused on call: they are absent from the session,
|
|
14
|
-
so a model cannot invoke one and cannot be argued into one. For a reviewer this
|
|
15
|
-
is checkable rather than promised — connect, list the tools, and the writes are
|
|
16
|
-
not there.
|
|
17
|
-
|
|
18
|
-
Enforcement is two layers deep: the `@governed_tool` harness refuses every
|
|
19
|
-
non-read operation (covering the CLI and in-process callers too), and the MCP
|
|
20
|
-
server removes write tools from `list_tools()`. Changing entry point does not
|
|
21
|
-
get around it.
|
|
22
|
-
|
|
23
|
-
## BREAKING — return shapes changed
|
|
24
|
-
|
|
25
|
-
This release changes payloads that callers may be parsing. Both changes exist
|
|
26
|
-
to stop a result from misrepresenting itself:
|
|
27
|
-
|
|
28
|
-
1. **Absent fields are now `null`, not `""`.** A missing value and an empty value
|
|
29
|
-
were previously indistinguishable, which invited consumers to invent the
|
|
30
|
-
difference. Keys are still always present — only the value may be null.
|
|
31
|
-
2. **Anything with a `limit` now returns an envelope** —
|
|
32
|
-
`{"<items>": [...], "returned": N, "limit": L, "truncated": bool}`. Truncation is
|
|
33
|
-
*measured* (one extra row is fetched), never inferred from the page happening to
|
|
34
|
-
be full. Where a genuine pre-cap total is knowable it is reported as `total`;
|
|
35
|
-
where it isn't, `total` is deliberately omitted rather than echoing `returned`.
|
|
36
|
-
|
|
37
|
-
## Also in this release
|
|
38
|
-
|
|
39
|
-
- **`docs/VERIFICATION.md`** — what the mock suite actually guarantees, a live
|
|
40
|
-
verification checklist, and the criteria for claiming this tool verified.
|
|
41
|
-
- **`skills/queue-aiops/references/agent-guardrails.md`** — for driving this tool with a
|
|
42
|
-
smaller / local model: which guardrails are now enforced for you, and a
|
|
43
|
-
ready-made system prompt for the rest.
|
|
44
|
-
- Expanded operator playbooks in the skill documentation.
|
|
45
|
-
- The advertised tool count now matches what an MCP client actually lists
|
|
46
|
-
(it includes `undo_list` / `undo_apply`), and a release gate keeps it honest.
|
|
47
|
-
- The `(preview)` label has been dropped. It never meant unreleased; verification
|
|
48
|
-
status now lives in `docs/VERIFICATION.md` where it can be specific.
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# Live verification — redis / rabbitmq
|
|
2
|
-
|
|
3
|
-
`queue-aiops` is exercised by a **mock-only** test suite (`uv run pytest`, no real
|
|
4
|
-
broker). It has **not** yet been validated end-to-end against a live redis instance or
|
|
5
|
-
rabbitmq broker. This document says exactly what the mock suite already guarantees, and
|
|
6
|
-
what a live run has to prove before anyone may describe this tool as verified against a
|
|
7
|
-
real broker.
|
|
8
|
-
|
|
9
|
-
It is deliberately checklist-shaped so the result is reproducible and auditable — not a
|
|
10
|
-
subjective "seems fine".
|
|
11
|
-
|
|
12
|
-
## What the mock suite already guarantees
|
|
13
|
-
|
|
14
|
-
- Every module imports; the CLI builds; **all 28 MCP tools** carry the `@governed_tool`
|
|
15
|
-
harness marker (`tests/test_smoke.py`, which also asserts the tool count and that
|
|
16
|
-
`__version__` matches `pyproject.toml`).
|
|
17
|
-
- The four flagship analyses (`redis_memory_pressure_rca`, `redis_latency_rca`,
|
|
18
|
-
`rabbitmq_queue_backlog_rca`, `connection_churn_analysis`) are unit-tested against
|
|
19
|
-
synthetic telemetry: each cause classification fires on the right signal
|
|
20
|
-
(noeviction-near-limit vs active eviction vs fragmentation vs swapping; O(N) command
|
|
21
|
-
patterns and fork/AOF stalls; no-consumers vs unacked pile-up vs rate deficit),
|
|
22
|
-
findings cite the measured number, and partial/missing fields do not crash.
|
|
23
|
-
- The **platform registry** resolves each tool to the correct redis (RESP, via the
|
|
24
|
-
`redis` Python client) or rabbitmq (management HTTP API, HTTP Basic) shape.
|
|
25
|
-
- **Safety invariants** are tested: the redis surface is a typed command **allow-list**
|
|
26
|
-
with no generic passthrough; big-key sampling is `SCAN`-based under a hard budget and
|
|
27
|
-
**never** issues `KEYS *`; and rabbitmq path segments (queue/policy names, and the
|
|
28
|
-
default vhost `/` → `%2F`) are percent-encoded centrally.
|
|
29
|
-
- Reversible writes record a faithful **inverse** undo descriptor built from a fetched
|
|
30
|
-
before-state (`redis_config_set` restores the prior value from `CONFIG GET`;
|
|
31
|
-
`set_policy` / `delete_policy` restore the captured definition; `declare_queue`'s undo
|
|
32
|
-
deletes it only when newly created). `purge_queue` declares **no** undo;
|
|
33
|
-
`delete_queue`'s undo re-declares the captured definition and the tests assert that
|
|
34
|
-
**messages are not claimed to be restored**.
|
|
35
|
-
- Governance persistence is tested against a real on-disk SQLite audit DB: calls land as
|
|
36
|
-
rows, failures record `status=error` and no undo, and the secure-by-default approver
|
|
37
|
-
gate refuses high-risk ops when no `rules.yaml` exists.
|
|
38
|
-
|
|
39
|
-
What it does **not** guarantee: that the concrete `INFO` field names, `SLOWLOG` entry
|
|
40
|
-
shape, and rabbitmq management-API JSON match a real broker build. Those are modelled
|
|
41
|
-
from each project's public documentation and are the **largest verification debt in this
|
|
42
|
-
repo** — `INFO` field names in particular have shifted across redis 5/6/7.
|
|
43
|
-
|
|
44
|
-
## Prerequisites for a live run
|
|
45
|
-
|
|
46
|
-
Both platforms are free and trivially containerised:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
docker run -d --name redis-verify -p 6379:6379 redis:7 \
|
|
50
|
-
redis-server --maxmemory 128mb --maxmemory-policy noeviction
|
|
51
|
-
docker run -d --name rabbit-verify -p 5672:5672 -p 15672:15672 rabbitmq:3-management
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
The rabbitmq **management plugin** must be enabled (the `-management` image does this),
|
|
55
|
-
and the API user needs the `monitoring` or `management` tag.
|
|
56
|
-
|
|
57
|
-
Use a **throwaway broker with throwaway data**. The checklist purges and deletes queues,
|
|
58
|
-
kills clients, and changes the running config — `purge_queue` destroys real messages and
|
|
59
|
-
records **no undo**. Never point it at a broker carrying traffic you care about.
|
|
60
|
-
|
|
61
|
-
Set the lab up so the reads have something to find:
|
|
62
|
-
|
|
63
|
-
- Fill redis past 80% of a small `maxmemory` and leave the policy at `noeviction`, so the
|
|
64
|
-
most dangerous finding is reachable.
|
|
65
|
-
- Create a few deliberately oversized keys (a large hash and a large list).
|
|
66
|
-
- Run some `KEYS`/`SMEMBERS` calls so the SLOWLOG has genuine O(N) entries.
|
|
67
|
-
- On rabbitmq, create three queues: one with **no consumer** and messages published, one
|
|
68
|
-
with a consumer that **never acks**, and one healthy — the three backlog causes.
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
uv tool install queue-aiops
|
|
72
|
-
queue-aiops init # wizard: pick platform, optional encrypted secret
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Record the exact versions tested (e.g. "redis 7.4, rabbitmq 3.13") — a tick is only
|
|
76
|
-
meaningful with the build it was ticked against.
|
|
77
|
-
|
|
78
|
-
## Verification checklist
|
|
79
|
-
|
|
80
|
-
Tick every box, **per platform**. A box that cannot be ticked is a verification gap —
|
|
81
|
-
record it, do not silently pass.
|
|
82
|
-
|
|
83
|
-
### 1. Connectivity (the fastest live gate)
|
|
84
|
-
- [ ] `queue-aiops doctor` → green on each target: config parsed, secret store unlocks,
|
|
85
|
-
and a real `PING` / `/api/overview` returns.
|
|
86
|
-
- [ ] `queue-aiops doctor --skip-auth` → passes offline (config/secret checks only).
|
|
87
|
-
- [ ] An **auth-less** lab redis connects with no stored secret (this is a supported
|
|
88
|
-
configuration and a common mock-vs-reality divergence).
|
|
89
|
-
- [ ] One config spanning both platforms works with `--target` switching.
|
|
90
|
-
|
|
91
|
-
### 2. Reads return real, well-shaped data
|
|
92
|
-
- [ ] `queue-aiops overview` → real version/role, memory posture, client counts, ops/sec,
|
|
93
|
-
hit rate and key count (redis); version, queue/message totals, connection/channel/
|
|
94
|
-
consumer counts and node alarms (rabbitmq). Confirm a **partial** failure degrades
|
|
95
|
-
into the `errors` list rather than failing the whole call.
|
|
96
|
-
- [ ] `queue-aiops redis info` / `redis memory` → fields match `redis-cli INFO`
|
|
97
|
-
**on each of redis 6 and 7**, since field names have moved between versions.
|
|
98
|
-
- [ ] `queue-aiops redis clients` → matches `CLIENT LIST`; `redis keyspace` matches
|
|
99
|
-
`DBSIZE` per database.
|
|
100
|
-
- [ ] `queue-aiops redis slowlog --limit 50` → matches `SLOWLOG GET`, with timings
|
|
101
|
-
parsed correctly.
|
|
102
|
-
- [ ] `queue-aiops redis config-get maxmemory*` → glob patterns work and match
|
|
103
|
-
`CONFIG GET`.
|
|
104
|
-
- [ ] `queue-aiops redis bigkeys --count 500` → finds the oversized keys you planted, and
|
|
105
|
-
reports a **coverage %** consistent with the keyspace size. Confirm with
|
|
106
|
-
`MONITOR` or the slowlog that it issued `SCAN`, **never** `KEYS *` — this is the
|
|
107
|
-
single most important safety check in the file.
|
|
108
|
-
- [ ] `queue-aiops rabbitmq queues` / `queue <name>` / `connections` / `channels` /
|
|
109
|
-
`policies` / `nodes` → match the management UI, including ready-vs-unacked splits.
|
|
110
|
-
- [ ] A queue and a policy whose **names need percent-encoding** (a `/`, a space, a `%`)
|
|
111
|
-
are read correctly, and the default vhost `/` resolves as `%2F`.
|
|
112
|
-
|
|
113
|
-
### 3. The analyses are right, not just non-crashing
|
|
114
|
-
- [ ] With redis near `maxmemory` under `noeviction`,
|
|
115
|
-
`queue-aiops analyze memory --used-pct 85` raises the **noeviction-near-limit**
|
|
116
|
-
finding and cites the real used/max numbers. Switch to `allkeys-lru`, drive
|
|
117
|
-
evictions, and confirm the finding changes to active eviction.
|
|
118
|
-
- [ ] Fragmentation vs swapping are distinguished correctly (do not accept "flagged
|
|
119
|
-
something" — check it names the right one).
|
|
120
|
-
- [ ] `queue-aiops analyze latency --slow-us 10000` digests the planted O(N) entries by
|
|
121
|
-
**command pattern** and suggests the incremental variant (`KEYS` → `SCAN`).
|
|
122
|
-
- [ ] Trigger a background save on a large dataset; the fork/AOF stall signal appears.
|
|
123
|
-
- [ ] `queue-aiops analyze backlog --top 20` classifies each of your three planted
|
|
124
|
-
queues correctly: **no consumers**, **unacked pile-up**, and healthy.
|
|
125
|
-
- [ ] Drive the node past its memory watermark; the alarm is reported as blocking **all**
|
|
126
|
-
publishers, and outranks the per-queue findings.
|
|
127
|
-
- [ ] `queue-aiops analyze churn` on both platforms pins reconnect churn to the right
|
|
128
|
-
client source.
|
|
129
|
-
|
|
130
|
-
### 4. A reversible write + its undo (governance closes the loop)
|
|
131
|
-
- [ ] `queue-aiops redis config-set maxmemory-policy allkeys-lru --dry-run` → prints the
|
|
132
|
-
call, `CONFIG GET` shows the value unchanged.
|
|
133
|
-
- [ ] `queue-aiops redis config-set maxmemory-policy allkeys-lru` → the running config
|
|
134
|
-
changes, the result carries an `_undo_id`, and a row lands in
|
|
135
|
-
`~/.queue-aiops/audit.db`.
|
|
136
|
-
- [ ] `queue-aiops undo list` shows it; `undo apply <id>` restores the **prior** policy —
|
|
137
|
-
verify starting from a non-default value, where a naive undo would wrongly set
|
|
138
|
-
`noeviction`.
|
|
139
|
-
- [ ] `queue-aiops rabbitmq set-policy <name> <pattern> '{"max-length":1000}'` over an
|
|
140
|
-
**existing** policy, then `undo apply` → the prior definition returns intact (not
|
|
141
|
-
a deletion).
|
|
142
|
-
- [ ] `queue-aiops rabbitmq declare-queue <new> --dry-run` then for real, then
|
|
143
|
-
`undo apply` → the newly created queue is deleted. Repeat against a queue that
|
|
144
|
-
**already existed**: the undo must **not** delete it.
|
|
145
|
-
|
|
146
|
-
### 5. Irreversible / asymmetric writes behave as declared
|
|
147
|
-
- [ ] `queue-aiops rabbitmq purge <queue> --dry-run` → reports the message count about to
|
|
148
|
-
be destroyed; the real run destroys exactly that many and records **no** undo.
|
|
149
|
-
- [ ] `queue-aiops rabbitmq delete-queue <queue>` on a queue **with messages**, then
|
|
150
|
-
`undo apply` → the queue is re-declared with the right durable/auto-delete flags,
|
|
151
|
-
and the **messages are gone**. Confirm the tool never claimed otherwise.
|
|
152
|
-
- [ ] `queue-aiops redis kill-client --addr <ip:port>` drops that connection, records no
|
|
153
|
-
undo, and is audited.
|
|
154
|
-
|
|
155
|
-
### 6. Governance actually gates
|
|
156
|
-
- [ ] With no `~/.queue-aiops/rules.yaml`, `purge_queue` and `delete_queue` are
|
|
157
|
-
**refused** unless `QUEUE_AUDIT_APPROVED_BY` is set (secure-by-default); with it
|
|
158
|
-
plus `QUEUE_AUDIT_RATIONALE`, both appear in the audit row.
|
|
159
|
-
- [ ] A tight poll loop trips the runaway budget guard rather than hammering the broker.
|
|
160
|
-
- [ ] A failed call (nonexistent queue) is audited `status=error` and records no undo.
|
|
161
|
-
- [ ] Attempting a redis command outside the typed allow-list is rejected **before** the
|
|
162
|
-
client sends anything — confirm against the live server that nothing executes.
|
|
163
|
-
|
|
164
|
-
### 7. Cleanup
|
|
165
|
-
- [ ] Restore every config value you changed, delete the throwaway queues and policies,
|
|
166
|
-
and flush the lab redis.
|
|
167
|
-
- [ ] `queue-aiops overview` matches the baseline you captured before starting.
|
|
168
|
-
- [ ] Skim `~/.queue-aiops/audit.db` — every write is there with the right risk tier.
|
|
169
|
-
|
|
170
|
-
## Criteria to consider it live-verified
|
|
171
|
-
|
|
172
|
-
All of the following must hold:
|
|
173
|
-
|
|
174
|
-
1. Every box above is ticked against **both** a real redis and a real rabbitmq, with the
|
|
175
|
-
exact builds recorded (e.g. "redis 7.4 + rabbitmq 3.13"), and the redis reads
|
|
176
|
-
re-checked on at least **two major redis versions** given the `INFO` field drift.
|
|
177
|
-
2. Every field-name or API-shape mismatch found is **fixed and covered by a regression
|
|
178
|
-
test**, so the mock suite would now catch it.
|
|
179
|
-
3. Section 4 passed, including the asymmetric `declare_queue` case (undo must delete only
|
|
180
|
-
a queue it created). Recording an undo descriptor is not the same as the undo
|
|
181
|
-
working, and this product line has shipped broken undo pairs before.
|
|
182
|
-
4. The big-key sampler was confirmed **live** to issue `SCAN` under budget and never
|
|
183
|
-
`KEYS *` — a mock cannot prove this, and getting it wrong stalls a production redis.
|
|
184
|
-
5. The run is written up in the release notes / product-line memory with the date and
|
|
185
|
-
package version, matching how the line records its other live-verified tools.
|
|
186
|
-
|
|
187
|
-
Until then, this repo says only what is true: mock-validated, live-unverified. Claiming
|
|
188
|
-
otherwise would break that promise.
|
|
189
|
-
|
|
190
|
-
## Notes for maintainers
|
|
191
|
-
|
|
192
|
-
- `queue-aiops doctor` is the single fastest live entry point; start there.
|
|
193
|
-
- Weight the run toward **redis `INFO` field names** and **rabbitmq percent-encoded path
|
|
194
|
-
segments** — those are the two places the mocks are least able to model.
|
|
195
|
-
- Managed cloud queue services (ElastiCache, Amazon MQ, CloudAMQP, …) are a **separate**
|
|
196
|
-
verification target: wire-compatible reads may work, but `CONFIG SET` and
|
|
197
|
-
`CLIENT KILL` are commonly restricted. Do not infer managed support from a self-hosted
|
|
198
|
-
run.
|
|
199
|
-
- Add this tool's result to the product-line verification ledger once green, so the
|
|
200
|
-
central "verification debt" list stays accurate.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|