Imported from yandex/pgconsul (
AGENTS.md). Install upstream withnpx skills add yandex/pgconsul. Copyright stays with the author.
AGENTS.md — AI Agent Guide for the pgconsul Project
Project Overview
pgconsul is a tool for maintaining High-Availability PostgreSQL cluster configurations. It is responsible for automatic cluster recovery in emergency situations, using ZooKeeper as a distributed coordinator.
Language: Python 3
License: PostgreSQL
Installation path: /opt/yandex/pgconsul (venv)
Architecture
Directory Structure
src/ # Main source code (pgconsul package)
├── __init__.py # Daemon bootstrap, configuration defaults, and logging
├── main.py # Main pgconsul class, primary iteration loop
├── pg.py # PostgreSQL interaction (psycopg2)
├── zk.py # ZooKeeper domain operations and locks
├── zk_client.py # Low-level KazooClient wrapper (ZK connection management)
├── replication_manager.py # Replication mode management (sync/async/quorum)
├── failover_election.py # Failover election logic
├── helpers.py # Utility functions
├── utils.py # Switchover, Failover classes
├── command_manager.py # External command management
├── cli.py # CLI interface (pgconsul-util)
├── types.py # Type aliases
├── exceptions.py # Custom exceptions
├── list_removal_strategy.py # Quorum list removal strategy
├── ssn_manager.py # SSN (Sync Standby Names) management
├── slot_manager.py # Replication slot lifecycle management
├── timings.py # Failover/switchover timing management
├── log_formatters.py # Log formatting
├── async_logging.py # Asynchronous logging
├── yapf_check.py # YAPF style-check helper
└── sdnotify.py # systemd integration
Core Components
| Component | File | Description |
|---|---|---|
pgconsul |
src/main.py |
Main class, primary loop (run_iteration) |
Postgres |
src/pg.py |
PostgreSQL abstraction layer |
Zookeeper |
src/zk.py |
ZooKeeper abstraction layer |
ReplicationManager |
src/replication_manager.py |
Replication type management |
FailoverElection |
src/failover_election.py |
New primary election |
CommandManager |
src/command_manager.py |
External command execution |
Data Flow (Main Loop)
Every second, pgconsul executes run_iteration():
- Fetches database state (
db.get_state()) - Fetches ZooKeeper state (
zk.get_state()) - Updates maintenance status
- Depending on the current role, calls:
primary_iter()— if the node is the primaryreplica_iter()— if the node is an HA replicanon_ha_replica_iter()— if the node is a cascading replicadead_iter()— if PostgreSQL is unavailable
Testing
Unit Tests (pytest)
Unit tests are located in tests/unit/ directory.
# Run all unit tests
make unit_test
# Or run directly with pytest
pytest tests/unit/ -v
pytest tests/unit/ --cov=src --cov-report=html --cov-report=term
Integration BDD Tests (behave)
# All tests
make check_test
# Specific feature file
TEST_ARGS='-i archive.feature' make check_test
# Specific scenario by line number
TEST_ARGS='-i kill_primary.feature:108' make check_test
# By tag
TEST_ARGS='--tags @fail_replication_source -i cascade.feature' make check_test
# With debug logs
DEBUG=1 TEST_ARGS='--tags @fail_replication_source -i cascade.feature' make check_test
# Continue on failure (unstoppable)
tox -e behave_unstoppable -- tests/features cascade.feature
Test Logs
logs/debug/test_execution.log— test execution details, timing, retrieslogs/<feature_file>/<line_number>/<hostname>/— container logs on failure
GitHub Actions Artifacts
When investigating CI failures, identify the run from the pull request's checks and verify its head SHA before reading logs. Use the workflow-run summary URL (/actions/runs/<run-id>), not an individual job URL or a run chosen only by branch name or title.
For every failed job under investigation:
- Record the pull request number, run ID, head SHA, job name, and failed scenario.
- Download its relevant archive using the Download link in the run summary's Artifacts section.
- Verify the archive name and SHA-256 digest against that section before extracting it.
- Treat the visible job log as supplementary evidence. If the archive cannot be retrieved, say so explicitly rather than inferring that it was examined.
Linting and Static Analysis
tox -e mypy
Note:
yapf,flake8,pylint, andbanditare currently broken and should not be run. Do not usemake lint. Onlymypyis required.
Style Rules
- Maximum line length: 200 characters (
.flake8) - Type checking: mypy with
ignore_missing_imports = True,check_untyped_defs = True - All new code must pass:
mypy
Configuration
Configuration is stored in an INI file (default: /etc/pgconsul.conf). Main sections:
| Section | Description |
|---|---|
[global] |
General parameters (ZK address, timeouts, priority, replication mode) |
[primary] |
Primary behavior (replication type switching, quorum) |
[replica] |
Replica behavior (recovery timeouts, failover) |
[commands] |
External commands (promote, rewind, pg_start/stop, etc.) |
[plugins] |
Plugin configuration |
Full reference: docs/CONFIG.md
Important Conventions
Coding
Cognitive complexity and readability are important. Creating new classes, state files, or ZooKeeper keys should be avoided when possible. Compact code and small changes should be preferred.
Comments
- Self-documenting code should be preferred in most cases
- Add comments only to explain complex behavior or non-obvious decisions
- Don't write comments just to document function or method signatures
- All added comments must be brief and in English
Error Handling
PostgreSQL Errors (src/exceptions.py)
PostgreSQL errors propagate as typed exceptions by default:
| Exception | When to raise |
|---|---|
PostgresException |
Base class; do not raise directly |
PostgresConnectionError |
Connection unavailable or dropped (psycopg2.OperationalError) |
PostgresConnectionTimeout |
Local connection attempt timed out; handled by the orchestrator grace-period policy |
PostgresQueryError |
Reserved for unexpected/invalid query results; not yet raised in production code |
Key convention: pg.py internal methods translate psycopg2.OperationalError into
PostgresConnectionError and let it propagate to the caller. By default, it reaches
run_iteration(), whose boundary in start() logs it and starts the next iteration.
PostgresConnectionTimeout must reach the liveness path in run_iteration() so the grace-period
policy can decide whether to act.
PROHIBITED in pg.py methods: catching PostgresConnectionError inside the method itself
and returning a safe default (e.g. return [], return ('async', None), return None).
This pattern hides DB errors from the iteration loop and prevents proper restart.
Local handling is allowed only for the documented critical sections, Best-Effort operations, and
special pg.py methods listed in ADR-0001
and ADR-0002.
@helpers.return_none_on_error is intentionally kept only on zk.noexcept_get() — that is
the only place where None as a return value is a valid "no data" signal. Do not apply this
decorator to new pg.py methods; raise PostgresConnectionError instead.
ZooKeeper Errors
zk.get()/zk.write()raiseZookeeperException— callers decide to propagate or handle.zk.noexcept_get()swallows exceptions and returnsNone— valid "soft" API for optional reads.
Working with ZooKeeper
- All ZK paths are defined as constants in the
Zookeeperclass (src/zk.py) - The primary lock is stored at
<prefix>/master(PRIMARY_LOCK_PATH) - Cluster state is synchronized via ZK on every iteration
- When ZK connectivity is lost, the primary stops the pooler and halts WAL archiving
- Layering (ADR-0003):
ZkClient(src/zk_client.py) is the transport layer — KazooClient lifecycle, primitive data ops, kazoo→ZkClientErrorexception translation.Zookeeper(src/zk.py) is the domain layer — path constants, lock ownership, business operations,ZkClientError → ZookeeperExceptiontranslation. New business operations go inzk.py; new transport primitives go inzk_client.py.Zookeepermust not importkazoo.*directly.
Replication
- Supported modes:
sync,async,quorum ReplicationManagerhandles switching between modesquorum_removal_delay(0–120 sec) — delay before removing a replica from the quorum list- When
quorum_commit = true, eitheruse_lwaldump = trueorallow_potential_data_loss = trueis required
Failover vs Switchover
- Failover — automatic emergency switch triggered when the primary becomes unavailable
- Switchover — planned switch initiated via
pgconsul-util switchover - Both processes are coordinated through ZK (
FAILOVER_STATE_PATH,SWITCHOVER_STATE_PATH)
Rewind-fail Flag
- If
pg_rewindfails more thanmax_rewind_retriestimes, the file.pgconsul_rewind_fail.flagis created - When this flag exists, pgconsul refuses to start — manual intervention is required
Architecture Decision Records (ADR)
Architectural decisions are documented in adr/ as Markdown files named ADR-NNNN-<slug>.md.
Existing ADRs
| File | Title | Status |
|---|---|---|
adr/ADR-0001-typed-postgres-exception-hierarchy.md |
Typed Exception Hierarchy for the PostgreSQL Layer | Accepted |
adr/ADR-0002-exception-propagation-to-iteration-boundary.md |
Exception Propagation Strategy to the Iteration Boundary | Accepted |
adr/ADR-0003-zk-client-zk-layering.md |
Layering and Responsibility Split between ZkClient and Zookeeper |
Accepted |
adr/ADR-0004-factory-config-builder-convention.md |
Factory + Config-Builder Convention for Infrastructure Components | Accepted |
When to create a new ADR
Don't create an ADR unless explicitly asked to. An ADR should be submitted as a separate PR and should not be mixed with implementation code.
ADR structure
Each ADR must contain the following sections:
# Context → # Decision → # Alternatives → # Consequences → # Links
Common Agent Tasks
Adding a New Configuration Parameter
- Add the parameter to the owning component's
*Configand config builder, following ADR-0004. - For
ReplicationManager, updateReplicationManagerConfigandbuild_replication_manager_config()insrc/replication_manager.py. For the orchestrator, updatePgconsulConfigandbuild_pgconsul_config()insrc/main.py. - Update the documentation in
docs/CONFIG.md - Add a default value to the test config
tests/conf/pgconsul.conf
Adding a Unit Test
- Test files:
tests/unit/test_*.py - Run:
pytest tests/unit/ -vormake unit_test - Uses standard
pytest; mocking viaunittest.mock
Adding a BDD Test
- Feature files:
tests/features/*.feature - Step definitions:
tests/steps/*.py - Run:
TEST_ARGS='-i <feature>.feature' make check_test
Changing Replication Logic
- Core logic:
src/replication_manager.py - Configuration:
ReplicationManagerConfigandbuild_replication_manager_config()insrc/replication_manager.py - SSN management:
src/ssn_manager.py - Replication slot lifecycle:
src/slot_manager.py - Tests:
tests/unit/test_replication_manager_*.py,tests/unit/test_ssn_manager.py,tests/unit/test_slot_manager.py
