postgres-aiops 0.10.3__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.claude-plugin/plugin.json +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.mcp.json +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/CHANGELOG.md +33 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/PKG-INFO +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/RELEASE_NOTES.md +3 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/SECURITY.md +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/openclaw.plugin.json +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/package.json +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/_root.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/query.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/remediate.py +8 -8
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/undo.py +3 -3
- postgres_aiops-0.12.0/postgres_aiops/mcp_server/_annotations.py +66 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/_shared.py +2 -2
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/server.py +8 -3
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/activity.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/analysis.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/indexes.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/queries.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/remediation.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/replication.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/server.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/tables.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/undo.py +4 -4
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/analysis.py +28 -8
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/pyproject.toml +4 -4
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/server.json +2 -2
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/agent-guardrails.md +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/capabilities.md +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_analysis.py +45 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_audited.py +3 -2
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_governed_exit.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_remediate.py +3 -3
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_writes.py +4 -4
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_connection_lost.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_gov_audit.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_gov_decorators.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_gov_patterns.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_governance_persistence.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_mcp_tools.py +18 -18
- postgres_aiops-0.12.0/tests/test_package_layout.py +62 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_smoke.py +15 -15
- postgres_aiops-0.12.0/tests/test_tool_annotations.py +139 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_truncation.py +2 -2
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_undo_executor.py +5 -5
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_undo_replay.py +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/uv.lock +1 -1
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.github/workflows/mcp-publish.yml +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.github/workflows/publish.yml +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.gitignore +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/Dockerfile +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/LICENSE +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/README.md +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/docs/VERIFICATION.md +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/glama.json +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/__init__.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/__init__.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/_common.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/activity.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/analyze.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/doctor.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/index.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/init.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/overview.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/replication.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/secret.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/server.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/table.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/config.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/connection.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/doctor.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/__init__.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/audit.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/budget.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/decorators.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/outcome.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/paths.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/patterns.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/policy.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/sanitize.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/undo.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/__init__.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/__init__.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/__init__.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/_util.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/activity.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/indexes.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/overview.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/queries.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/remediation.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/replication.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/server.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/tables.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/secretstore.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/SKILL.md +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/cli-reference.md +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/setup-guide.md +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/smithery.yaml +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/conftest.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_reads.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_secret.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_config.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_connection.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_connection_more.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_doctor.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_gov_policy.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_init.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_ops_more.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_optional_fields.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_reads.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_secretstore.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_secretstore_more.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_self_lockout.py +0 -0
- {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_writes.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "postgres-aiops",
|
|
4
4
|
"displayName": "Postgres AIops",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.12.0",
|
|
6
6
|
"description": "Governed PostgreSQL DBA ops: slow-query RCA, bloat/vacuum & blocking-lock analysis; 35 MCP tools.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Wei Zhou",
|
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.12.0 — 2026-10-15
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- The MCP server moved from a top-level `mcp_server` package to
|
|
7
|
+
`postgres_aiops.mcp_server`. Every AIops tool used to ship its own top-level
|
|
8
|
+
`mcp_server`, so installing two of them into one environment merged the two
|
|
9
|
+
into one package: `postgres-aiops-mcp` could start another tool's server, and
|
|
10
|
+
`postgres-aiops undo` wrote audit and undo rows into the other tool's home
|
|
11
|
+
directory. Installs through `uvx`, the Claude Code plugin or the OpenClaw
|
|
12
|
+
bundle were isolated and unaffected. The `postgres-aiops-mcp` command is
|
|
13
|
+
unchanged; only code that imported `mcp_server` directly must now import
|
|
14
|
+
`postgres_aiops.mcp_server`.
|
|
15
|
+
|
|
16
|
+
## v0.11.0 — 2026-10-15
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- MCP tool annotations derived from each tool's governance `risk_level`, so
|
|
20
|
+
clients can gate approval from `tools/list` alone. `low` tools advertise
|
|
21
|
+
`readOnlyHint: true`; every other tool advertises `readOnlyHint: false` and
|
|
22
|
+
`destructiveHint: true`. The `medium` tier includes deletes and hard
|
|
23
|
+
power-offs, so a tool is never advertised as non-destructive on the strength
|
|
24
|
+
of its tier. `idempotentHint` only repeats an explicit declaration. The exact
|
|
25
|
+
level is published in `_meta` under `io.github.AIops-tools/riskLevel`. These
|
|
26
|
+
are hints for the client; audit and undo are unchanged.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- `blocking_lock_chain_rca`: the chain root's state was dropped and a missing
|
|
30
|
+
query became `""`. For a backend that is `idle in transaction`, `rootQuery` is
|
|
31
|
+
the last statement it ran, not a running one, so the recommended
|
|
32
|
+
`cancel_query` had nothing to cancel. Roots now carry `rootState` and
|
|
33
|
+
`rootIdle` (true for any `idle*` state, including plain `idle` with session-level locks), a missing query stays `null`, and the action for an
|
|
34
|
+
idle root points at `terminate_backend` (which rolls the transaction back).
|
|
35
|
+
|
|
3
36
|
## v0.10.3 — 2026-09-15
|
|
4
37
|
|
|
5
38
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: postgres-aiops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Governed AI-ops for PostgreSQL DBA operations: slow-query RCA, bloat/vacuum & blocking-lock analysis with a built-in governance harness (audit, budget, undo, risk tiers)
|
|
5
5
|
Author-email: Wei Zhou <zhouwei008@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Release notes — postgres-aiops 0.6.0
|
|
2
2
|
|
|
3
|
+
> Historical — this file covers the 0.6.0 release only and is not updated.
|
|
4
|
+
> For every release since, see [CHANGELOG.md](CHANGELOG.md) and the GitHub releases.
|
|
5
|
+
|
|
3
6
|
Previous release: 0.5.0.
|
|
4
7
|
|
|
5
8
|
## BREAKING — the authorization layer is removed
|
|
@@ -70,7 +70,7 @@ def query_reset(target: TargetOption = None, dry_run: DryRunOption = False) -> N
|
|
|
70
70
|
``@governed_tool``-wrapped MCP function, so each is audited on the same
|
|
71
71
|
governance path as MCP calls.
|
|
72
72
|
"""
|
|
73
|
-
from mcp_server.tools import queries as gov
|
|
73
|
+
from postgres_aiops.mcp_server.tools import queries as gov
|
|
74
74
|
|
|
75
75
|
if dry_run:
|
|
76
76
|
dry_run_preview(
|
|
@@ -32,7 +32,7 @@ def remediate_terminate(
|
|
|
32
32
|
dry_run: DryRunOption = False,
|
|
33
33
|
) -> None:
|
|
34
34
|
"""Terminate a backend (no undo; dry-run + confirm)."""
|
|
35
|
-
from mcp_server.tools import remediation as gov
|
|
35
|
+
from postgres_aiops.mcp_server.tools import remediation as gov
|
|
36
36
|
|
|
37
37
|
if dry_run:
|
|
38
38
|
# Through the governed call: terminate_backend refuses this tool's own
|
|
@@ -54,7 +54,7 @@ def remediate_cancel(
|
|
|
54
54
|
dry_run: DryRunOption = False,
|
|
55
55
|
) -> None:
|
|
56
56
|
"""Cancel a backend's running query (no undo; dry-run + confirm)."""
|
|
57
|
-
from mcp_server.tools import remediation as gov
|
|
57
|
+
from postgres_aiops.mcp_server.tools import remediation as gov
|
|
58
58
|
|
|
59
59
|
if dry_run:
|
|
60
60
|
dry_run_preview(
|
|
@@ -76,7 +76,7 @@ def remediate_vacuum(
|
|
|
76
76
|
dry_run: DryRunOption = False,
|
|
77
77
|
) -> None:
|
|
78
78
|
"""VACUUM a table (dry-run + confirm)."""
|
|
79
|
-
from mcp_server.tools import remediation as gov
|
|
79
|
+
from postgres_aiops.mcp_server.tools import remediation as gov
|
|
80
80
|
|
|
81
81
|
if dry_run:
|
|
82
82
|
dry_run_preview(
|
|
@@ -97,7 +97,7 @@ def remediate_analyze(
|
|
|
97
97
|
dry_run: DryRunOption = False,
|
|
98
98
|
) -> None:
|
|
99
99
|
"""ANALYZE a table (dry-run + confirm)."""
|
|
100
|
-
from mcp_server.tools import remediation as gov
|
|
100
|
+
from postgres_aiops.mcp_server.tools import remediation as gov
|
|
101
101
|
|
|
102
102
|
if dry_run:
|
|
103
103
|
dry_run_preview(
|
|
@@ -120,7 +120,7 @@ def remediate_create_index(
|
|
|
120
120
|
dry_run: DryRunOption = False,
|
|
121
121
|
) -> None:
|
|
122
122
|
"""Create an index (reversible; dry-run + confirm)."""
|
|
123
|
-
from mcp_server.tools import remediation as gov
|
|
123
|
+
from postgres_aiops.mcp_server.tools import remediation as gov
|
|
124
124
|
|
|
125
125
|
if dry_run:
|
|
126
126
|
# Through the governed call: create_index validates the table/columns vs
|
|
@@ -146,7 +146,7 @@ def remediate_drop_index(
|
|
|
146
146
|
dry_run: DryRunOption = False,
|
|
147
147
|
) -> None:
|
|
148
148
|
"""Drop an index (reversible; captures the definition first; dry-run + confirm)."""
|
|
149
|
-
from mcp_server.tools import remediation as gov
|
|
149
|
+
from postgres_aiops.mcp_server.tools import remediation as gov
|
|
150
150
|
|
|
151
151
|
if dry_run:
|
|
152
152
|
dry_run_preview(
|
|
@@ -168,7 +168,7 @@ def remediate_reindex(
|
|
|
168
168
|
dry_run: DryRunOption = False,
|
|
169
169
|
) -> None:
|
|
170
170
|
"""REINDEX an index/table/schema (dry-run + confirm)."""
|
|
171
|
-
from mcp_server.tools import remediation as gov
|
|
171
|
+
from postgres_aiops.mcp_server.tools import remediation as gov
|
|
172
172
|
|
|
173
173
|
if dry_run:
|
|
174
174
|
dry_run_preview(
|
|
@@ -195,7 +195,7 @@ def remediate_set(
|
|
|
195
195
|
dry_run: DryRunOption = False,
|
|
196
196
|
) -> None:
|
|
197
197
|
"""ALTER SYSTEM SET a parameter, or --reset it (reversible; dry-run + confirm)."""
|
|
198
|
-
from mcp_server.tools import remediation as gov
|
|
198
|
+
from postgres_aiops.mcp_server.tools import remediation as gov
|
|
199
199
|
|
|
200
200
|
action = f"ALTER SYSTEM RESET {name}" if reset else f"ALTER SYSTEM SET {name} ="
|
|
201
201
|
if dry_run:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""``postgres-aiops undo`` — list recorded undo tokens and apply them.
|
|
2
2
|
|
|
3
3
|
Real execution is delegated to the ``@governed_tool``-wrapped functions in
|
|
4
|
-
``mcp_server.tools.undo`` so an applied undo is audited on the SAME governance
|
|
4
|
+
``postgres_aiops.mcp_server.tools.undo`` so an applied undo is audited on the SAME governance
|
|
5
5
|
path as any other write (the inverse tool it dispatches is itself re-gated).
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -38,7 +38,7 @@ def undo_list_cmd(
|
|
|
38
38
|
target: TargetOption = None,
|
|
39
39
|
) -> None:
|
|
40
40
|
"""List recorded, not-yet-applied undo tokens."""
|
|
41
|
-
from mcp_server.tools import undo as gov
|
|
41
|
+
from postgres_aiops.mcp_server.tools import undo as gov
|
|
42
42
|
|
|
43
43
|
console.print_json(json.dumps(gov.undo_list(limit=limit, target=target)))
|
|
44
44
|
|
|
@@ -49,7 +49,7 @@ def undo_apply_cmd(
|
|
|
49
49
|
undo_id: UndoIdArg, target: TargetOption = None, dry_run: DryRunOption = False
|
|
50
50
|
) -> None:
|
|
51
51
|
"""Apply a recorded undo (dispatches its inverse tool; dry-run + confirm)."""
|
|
52
|
-
from mcp_server.tools import undo as gov
|
|
52
|
+
from postgres_aiops.mcp_server.tools import undo as gov
|
|
53
53
|
|
|
54
54
|
if dry_run:
|
|
55
55
|
# undo_apply refuses an unknown or already-applied token, and refuses one
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Derive MCP tool annotations from the governance risk level.
|
|
2
|
+
|
|
3
|
+
Every tool already declares ``risk_level`` on ``@governed_tool``; this module
|
|
4
|
+
publishes it to MCP clients as the standard ``ToolAnnotations`` hints plus the
|
|
5
|
+
exact level under ``_meta``, so a client can gate approval without reading our
|
|
6
|
+
docstrings.
|
|
7
|
+
|
|
8
|
+
Only the safe direction is ever asserted:
|
|
9
|
+
|
|
10
|
+
- ``low`` → ``readOnlyHint: true``. Backed by the smoke-test invariant that
|
|
11
|
+
``risk_level == "low"`` exactly when the docstring says ``[READ]``.
|
|
12
|
+
- anything else → ``readOnlyHint: false, destructiveHint: true``. The
|
|
13
|
+
``medium`` tier is NOT non-destructive — it holds deletes, hard power-offs,
|
|
14
|
+
state kills and stat resets — so claiming ``destructiveHint: false`` from the
|
|
15
|
+
tier would tell a client it may skip approval for a delete. Over-warning
|
|
16
|
+
costs a confirmation prompt; under-warning costs data.
|
|
17
|
+
- ``idempotentHint`` only echoes an explicit ``@governed_tool(idempotent=True)``
|
|
18
|
+
declaration; it is never inferred.
|
|
19
|
+
- ``openWorldHint`` is left unset (spec default ``true``): every tool talks to
|
|
20
|
+
an external system.
|
|
21
|
+
|
|
22
|
+
This file is identical across the AIops line; keep it that way.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from mcp.types import ToolAnnotations
|
|
28
|
+
|
|
29
|
+
RISK_META_KEY = "io.github.AIops-tools/riskLevel"
|
|
30
|
+
RISK_LEVELS = frozenset({"low", "medium", "high", "critical"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def annotations_for(risk_level: str, idempotent: bool) -> ToolAnnotations:
|
|
34
|
+
"""Return the MCP hints that a tool of ``risk_level`` may honestly claim."""
|
|
35
|
+
if risk_level not in RISK_LEVELS:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"unknown risk_level {risk_level!r}; expected one of {sorted(RISK_LEVELS)}"
|
|
38
|
+
)
|
|
39
|
+
if risk_level == "low":
|
|
40
|
+
return ToolAnnotations(read_only_hint=True)
|
|
41
|
+
return ToolAnnotations(
|
|
42
|
+
read_only_hint=False,
|
|
43
|
+
destructive_hint=True,
|
|
44
|
+
idempotent_hint=idempotent,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def apply_risk_annotations(server: Any) -> None:
|
|
49
|
+
"""Replace every registered tool with a copy carrying its risk annotations.
|
|
50
|
+
|
|
51
|
+
Fails fast on a tool without ``@governed_tool`` metadata: an unannotated
|
|
52
|
+
tool would be advertised with the spec defaults, i.e. as a destructive
|
|
53
|
+
write with no stated risk level — wrong for a read, silent for a write.
|
|
54
|
+
"""
|
|
55
|
+
tools = server._tool_manager._tools # same private registry undo.py relies on
|
|
56
|
+
for name, tool in list(tools.items()):
|
|
57
|
+
risk_level = getattr(tool.fn, "_risk_level", None)
|
|
58
|
+
if risk_level is None:
|
|
59
|
+
raise RuntimeError(f"tool {name!r} has no @governed_tool risk_level")
|
|
60
|
+
idempotent = bool(getattr(tool.fn, "_idempotent", False))
|
|
61
|
+
tools[name] = tool.model_copy(
|
|
62
|
+
update={
|
|
63
|
+
"annotations": annotations_for(risk_level, idempotent),
|
|
64
|
+
"meta": {**(tool.meta or {}), RISK_META_KEY: risk_level},
|
|
65
|
+
}
|
|
66
|
+
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Shared MCP server primitives: the MCPServer instance, connection helper,
|
|
2
2
|
error sanitisation, and the ``@tool_errors`` decorator.
|
|
3
3
|
|
|
4
|
-
Tool modules under ``mcp_server/tools/`` import ``mcp`` from here and register
|
|
5
|
-
their ``@mcp.tool()`` functions onto it. ``mcp_server/server.py`` then imports
|
|
4
|
+
Tool modules under ``postgres_aiops/mcp_server/tools/`` import ``mcp`` from here and register
|
|
5
|
+
their ``@mcp.tool()`` functions onto it. ``postgres_aiops/mcp_server/server.py`` then imports
|
|
6
6
|
those modules and runs the server.
|
|
7
7
|
|
|
8
8
|
Keep ``Optional[X]`` (never PEP 604 ``X | None``) in any reflected tool
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""MCP server wrapping postgres-aiops operations (stdio transport).
|
|
2
2
|
|
|
3
|
-
Thin adapter layer: each ``@mcp.tool()`` function (in ``mcp_server/tools/``)
|
|
3
|
+
Thin adapter layer: each ``@mcp.tool()`` function (in ``postgres_aiops/mcp_server/tools/``)
|
|
4
4
|
delegates to the ``postgres_aiops`` ops package and is wrapped with the
|
|
5
5
|
postgres-aiops ``@governed_tool`` harness (audit / budget / undo / risk-tier).
|
|
6
6
|
|
|
@@ -14,11 +14,12 @@ License: MIT
|
|
|
14
14
|
import logging
|
|
15
15
|
import os
|
|
16
16
|
|
|
17
|
-
from mcp_server.
|
|
17
|
+
from postgres_aiops.mcp_server._annotations import apply_risk_annotations
|
|
18
|
+
from postgres_aiops.mcp_server._shared import _safe_error, mcp, tool_errors
|
|
18
19
|
|
|
19
20
|
# Importing the tool modules registers every @mcp.tool() onto the shared
|
|
20
21
|
# `mcp` instance. Order does not matter; each module is self-contained.
|
|
21
|
-
from mcp_server.tools import ( # noqa: F401 — side effects
|
|
22
|
+
from postgres_aiops.mcp_server.tools import ( # noqa: F401 — side effects
|
|
22
23
|
activity,
|
|
23
24
|
analysis,
|
|
24
25
|
indexes,
|
|
@@ -30,6 +31,10 @@ from mcp_server.tools import ( # noqa: F401 — side effects
|
|
|
30
31
|
undo,
|
|
31
32
|
)
|
|
32
33
|
|
|
34
|
+
# Publish each tool's governance risk level as MCP annotations. Runs after
|
|
35
|
+
# every tool module has registered, so no tool is advertised unannotated.
|
|
36
|
+
apply_risk_annotations(mcp)
|
|
37
|
+
|
|
33
38
|
__all__ = ["mcp", "main", "_safe_error", "tool_errors"]
|
|
34
39
|
|
|
35
40
|
logger = logging.getLogger(__name__)
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
5
|
from postgres_aiops.governance import governed_tool
|
|
6
|
+
from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from postgres_aiops.ops import activity as ops
|
|
8
8
|
|
|
9
9
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, Optional
|
|
4
4
|
|
|
5
|
-
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
5
|
from postgres_aiops.governance import governed_tool
|
|
6
|
+
from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from postgres_aiops.ops import activity as activity_ops
|
|
8
8
|
from postgres_aiops.ops import analysis as ops
|
|
9
9
|
from postgres_aiops.ops import queries as query_ops
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
5
|
from postgres_aiops.governance import governed_tool
|
|
6
|
+
from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from postgres_aiops.ops import indexes as ops
|
|
8
8
|
|
|
9
9
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
5
|
from postgres_aiops.governance import governed_tool
|
|
6
|
+
from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from postgres_aiops.ops import queries as ops
|
|
8
8
|
|
|
9
9
|
|
{postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/remediation.py
RENAMED
|
@@ -12,8 +12,8 @@ Risk tiers:
|
|
|
12
12
|
|
|
13
13
|
from typing import Any, Optional
|
|
14
14
|
|
|
15
|
-
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
16
15
|
from postgres_aiops.governance import governed_tool
|
|
16
|
+
from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
17
17
|
from postgres_aiops.ops import remediation as ops
|
|
18
18
|
|
|
19
19
|
# ── undo descriptors (built from the fetched before-state) ──────────────────
|
{postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/replication.py
RENAMED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
5
|
from postgres_aiops.governance import governed_tool
|
|
6
|
+
from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from postgres_aiops.ops import replication as ops
|
|
8
8
|
|
|
9
9
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
5
|
from postgres_aiops.governance import governed_tool
|
|
6
|
+
from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from postgres_aiops.ops import overview as overview_ops
|
|
8
8
|
from postgres_aiops.ops import server as ops
|
|
9
9
|
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
5
|
from postgres_aiops.governance import governed_tool
|
|
6
|
+
from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from postgres_aiops.ops import tables as ops
|
|
8
8
|
|
|
9
9
|
|
|
@@ -15,26 +15,26 @@ design, inverses in this line key off ids/names, not credentials.
|
|
|
15
15
|
import json
|
|
16
16
|
from typing import Any, Optional
|
|
17
17
|
|
|
18
|
-
from mcp_server._shared import mcp, tool_errors
|
|
19
18
|
from postgres_aiops.governance import governed_tool
|
|
20
19
|
from postgres_aiops.governance.undo import get_undo_store
|
|
20
|
+
from postgres_aiops.mcp_server._shared import mcp, tool_errors
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _resolve_tool(name: str) -> Any:
|
|
24
24
|
"""Return the governed callable registered under ``name`` (or None).
|
|
25
25
|
|
|
26
26
|
The CLI reaches this through ``undo apply``, whose process only imports
|
|
27
|
-
``mcp_server.tools.undo`` — every other tool module is imported lazily inside
|
|
27
|
+
``postgres_aiops.mcp_server.tools.undo`` — every other tool module is imported lazily inside
|
|
28
28
|
its own CLI command and so is absent here. Without forcing a full load,
|
|
29
29
|
``undo_apply`` would fail with "inverse tool not registered" for every write
|
|
30
30
|
tool when driven from the CLI (the MCP entry point loads all modules, so it
|
|
31
|
-
never hit this). If the first lookup misses, import ``mcp_server.server`` —
|
|
31
|
+
never hit this). If the first lookup misses, import ``postgres_aiops.mcp_server.server`` —
|
|
32
32
|
which registers every ``@mcp.tool()`` as an import side effect — and retry.
|
|
33
33
|
Imported lazily to avoid the undo↔server cycle.
|
|
34
34
|
"""
|
|
35
35
|
tool = mcp._tool_manager._tools.get(name)
|
|
36
36
|
if tool is None:
|
|
37
|
-
import mcp_server.server # noqa: F401 — registers all tools as a side effect
|
|
37
|
+
import postgres_aiops.mcp_server.server # noqa: F401 — registers all tools as a side effect
|
|
38
38
|
|
|
39
39
|
tool = mcp._tool_manager._tools.get(name)
|
|
40
40
|
return getattr(tool, "fn", None) if tool else None
|
|
@@ -201,6 +201,22 @@ def _descendants(root: int, children: dict[int, list[int]]) -> set[int]:
|
|
|
201
201
|
return seen
|
|
202
202
|
|
|
203
203
|
|
|
204
|
+
def _lock_root_action(pid: int, idle: bool | None) -> str:
|
|
205
|
+
if idle:
|
|
206
|
+
return (
|
|
207
|
+
f"Backend {pid} heads the chain but is not running a statement (see "
|
|
208
|
+
"rootState: an open transaction, or session-level locks): rootQuery is "
|
|
209
|
+
"the last statement it ran, not a running one. cancel_query has nothing "
|
|
210
|
+
f"to cancel; terminate_backend({pid}) ends the session, rolling back any "
|
|
211
|
+
"transaction and releasing its locks — confirm with its owner first."
|
|
212
|
+
)
|
|
213
|
+
return (
|
|
214
|
+
f"Backend {pid} is the head of the chain — investigate its transaction; "
|
|
215
|
+
f"cancel_query({pid}) to stop its statement, or terminate_backend({pid}) to "
|
|
216
|
+
"end the session and release the locks."
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
204
220
|
def blocking_lock_chain_rca(pairs: list[dict]) -> dict:
|
|
205
221
|
"""[READ] Build the wait-for tree from blocking pairs and name the root blocker.
|
|
206
222
|
|
|
@@ -217,7 +233,8 @@ def blocking_lock_chain_rca(pairs: list[dict]) -> dict:
|
|
|
217
233
|
return {"blockedBackends": 0, "roots": [], "note": "No blocking detected."}
|
|
218
234
|
|
|
219
235
|
children: dict[int, list[int]] = {}
|
|
220
|
-
blocking_query: dict[int, str] = {}
|
|
236
|
+
blocking_query: dict[int, str | None] = {}
|
|
237
|
+
blocking_state: dict[int, str | None] = {}
|
|
221
238
|
blocked_pids: set[int] = set()
|
|
222
239
|
blocking_pids: set[int] = set()
|
|
223
240
|
for e in edges:
|
|
@@ -225,7 +242,8 @@ def blocking_lock_chain_rca(pairs: list[dict]) -> dict:
|
|
|
225
242
|
children.setdefault(g, []).append(b)
|
|
226
243
|
blocked_pids.add(b)
|
|
227
244
|
blocking_pids.add(g)
|
|
228
|
-
blocking_query.setdefault(g, e.get("blockingQuery")
|
|
245
|
+
blocking_query.setdefault(g, e.get("blockingQuery"))
|
|
246
|
+
blocking_state.setdefault(g, e.get("blockingState"))
|
|
229
247
|
|
|
230
248
|
root_pids = [pid for pid in blocking_pids if pid not in blocked_pids]
|
|
231
249
|
if not root_pids:
|
|
@@ -242,16 +260,18 @@ def blocking_lock_chain_rca(pairs: list[dict]) -> dict:
|
|
|
242
260
|
roots = []
|
|
243
261
|
for pid in root_pids:
|
|
244
262
|
blocked = _descendants(pid, children)
|
|
263
|
+
state = blocking_state.get(pid)
|
|
264
|
+
# Any idle state means no statement is running: "idle in transaction"
|
|
265
|
+
# holds row locks, plain "idle" can still hold session-level advisory locks.
|
|
266
|
+
idle = None if state is None else str(state).startswith("idle")
|
|
245
267
|
roots.append({
|
|
246
268
|
"rootPid": pid,
|
|
247
269
|
"blockedCount": len(blocked),
|
|
248
270
|
"blockedPids": sorted(blocked),
|
|
249
|
-
"rootQuery": blocking_query.get(pid
|
|
250
|
-
"
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
"terminate_backend(pid) to end the session and release the locks."
|
|
254
|
-
),
|
|
271
|
+
"rootQuery": blocking_query.get(pid),
|
|
272
|
+
"rootState": state,
|
|
273
|
+
"rootIdle": idle,
|
|
274
|
+
"action": _lock_root_action(pid, idle),
|
|
255
275
|
})
|
|
256
276
|
roots.sort(key=lambda r: r["blockedCount"], reverse=True)
|
|
257
277
|
return {
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "postgres-aiops"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.12.0"
|
|
8
8
|
description = "Governed AI-ops for PostgreSQL DBA operations: slow-query RCA, bloat/vacuum & blocking-lock analysis with a built-in governance harness (audit, budget, undo, risk tiers)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -28,10 +28,10 @@ dev = [
|
|
|
28
28
|
|
|
29
29
|
[project.scripts]
|
|
30
30
|
postgres-aiops = "postgres_aiops.cli:app"
|
|
31
|
-
postgres-aiops-mcp = "mcp_server.server:main"
|
|
31
|
+
postgres-aiops-mcp = "postgres_aiops.mcp_server.server:main"
|
|
32
32
|
|
|
33
33
|
[tool.hatch.build.targets.wheel]
|
|
34
|
-
packages = ["postgres_aiops"
|
|
34
|
+
packages = ["postgres_aiops"]
|
|
35
35
|
|
|
36
36
|
[tool.ruff]
|
|
37
37
|
line-length = 100
|
|
@@ -47,7 +47,7 @@ select = ["E", "F", "I", "N", "W", "UP"]
|
|
|
47
47
|
"postgres_aiops/governance/*.py" = ["E501", "N818"]
|
|
48
48
|
# MCP server files are reflected by FastMCP/Pydantic; PEP 604 unions (X | None)
|
|
49
49
|
# crash on older mcp/pydantic, so they intentionally use Optional[X].
|
|
50
|
-
"mcp_server/**/*.py" = ["UP007", "UP045"]
|
|
50
|
+
"postgres_aiops/mcp_server/**/*.py" = ["UP007", "UP045"]
|
|
51
51
|
# The MCP entry keeps an explicit Python-version guard even though the declared
|
|
52
52
|
# minimum is already 3.11.
|
|
53
53
|
"postgres_aiops/cli/_root.py" = ["UP036"]
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/AIops-tools/Postgres-AIops",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.12.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "postgres-aiops",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.12.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
{postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/agent-guardrails.md
RENAMED
|
@@ -28,7 +28,7 @@ recorded on the audit row; they are never required and never block a call.
|
|
|
28
28
|
## What still needs a prompt
|
|
29
29
|
|
|
30
30
|
These are model-behaviour problems the harness cannot fix from the outside.
|
|
31
|
-
|
|
31
|
+
Recommended setup for a local model (adapt it to your agent client):
|
|
32
32
|
|
|
33
33
|
```text
|
|
34
34
|
You operate a PostgreSQL server through the postgres-aiops MCP tools.
|
{postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/capabilities.md
RENAMED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
| `wal_status` | WAL fns + `pg_stat_archiver` | currentLsn, walLevel, max/minWalSize, archiver |
|
|
33
33
|
| `slow_query_rca` | pg_stat_statements + EXPLAIN | worst{}, findings[] (cited cause/action) |
|
|
34
34
|
| `bloat_and_vacuum_analysis` | table-bloat rows | recommendations[] (cited reasons + action) |
|
|
35
|
-
| `blocking_lock_chain_rca` | `pg_blocking_pids` pairs | roots[], worstRootPid, deadlockSuspected |
|
|
35
|
+
| `blocking_lock_chain_rca` | `pg_blocking_pids` pairs | roots[] (rootQuery, rootState, rootIdle — true for any `idle*` state; rootQuery is then the last statement run, not a running one — and action), worstRootPid, deadlockSuspected |
|
|
36
36
|
| `undo_list` | local undo store | recorded, not-yet-applied reversible writes: undoId, ts, originalTool, inverseTool, note |
|
|
37
37
|
|
|
38
38
|
The flagship analyses accept injected records (`statements=` / `tables=` /
|
|
@@ -82,3 +82,48 @@ def test_blocking_chain_detects_cycle():
|
|
|
82
82
|
def test_blocking_chain_no_blocking():
|
|
83
83
|
out = analysis.blocking_lock_chain_rca([])
|
|
84
84
|
assert out["blockedBackends"] == 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── lock-chain root that is idle inside an open transaction ──────────────────
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _pg_pairs(state, query="UPDATE t SET v=1 WHERE id=1"):
|
|
91
|
+
return [{"blockedPid": 201, "blockingPid": 100, "blockedQuery": "DELETE FROM t",
|
|
92
|
+
"blockingQuery": query, "blockingState": state}]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_idle_in_transaction_root_is_named_and_cancel_is_ruled_out():
|
|
96
|
+
from postgres_aiops.ops import analysis as a
|
|
97
|
+
|
|
98
|
+
(root,) = a.blocking_lock_chain_rca(_pg_pairs("idle in transaction"))["roots"]
|
|
99
|
+
assert root["rootState"] == "idle in transaction"
|
|
100
|
+
assert root["rootIdle"] is True
|
|
101
|
+
# The query is the LAST statement it ran, not a running one.
|
|
102
|
+
assert "last statement" in root["action"]
|
|
103
|
+
assert "terminate_backend(100)" in root["action"]
|
|
104
|
+
assert "nothing to cancel" in root["action"]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_active_root_offers_cancel_first():
|
|
108
|
+
from postgres_aiops.ops import analysis as a
|
|
109
|
+
|
|
110
|
+
(root,) = a.blocking_lock_chain_rca(_pg_pairs("active"))["roots"]
|
|
111
|
+
assert root["rootIdle"] is False
|
|
112
|
+
assert "cancel_query(100)" in root["action"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_missing_root_query_stays_null():
|
|
116
|
+
from postgres_aiops.ops import analysis as a
|
|
117
|
+
|
|
118
|
+
(root,) = a.blocking_lock_chain_rca(_pg_pairs(None, query=None))["roots"]
|
|
119
|
+
assert root["rootQuery"] is None
|
|
120
|
+
assert root["rootState"] is None
|
|
121
|
+
assert root["rootIdle"] is None # state unknown
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_plain_idle_root_with_session_locks_is_not_offered_cancel():
|
|
125
|
+
from postgres_aiops.ops import analysis as a
|
|
126
|
+
|
|
127
|
+
(root,) = a.blocking_lock_chain_rca(_pg_pairs("idle"))["roots"]
|
|
128
|
+
assert root["rootIdle"] is True
|
|
129
|
+
assert "terminate_backend(100)" in root["action"]
|