Files
minerops/backend/app/miner_control/repository.py
T
2026-08-26 15:48:59 +00:00

141 lines
5.4 KiB
Python

"""PostgreSQL persistence for typed miner control operations."""
from collections.abc import Callable
from typing import Any
from uuid import UUID
import psycopg
from psycopg.errors import UniqueViolation
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from app.config import settings
from app.miner_control.exceptions import MinerControlConflict
ConnectionFactory = Callable[[], psycopg.Connection]
LIVE_CONTROL_CONSTRAINT = "uq_agent_queue_live_miner_control"
def default_connection_factory() -> psycopg.Connection:
return psycopg.connect(
host=settings.postgres_host,
port=settings.postgres_port,
dbname=settings.postgres_db,
user=settings.postgres_user,
password=settings.postgres_password,
connect_timeout=5,
)
class MinerControlRepository:
"""Resolve control facts and persist queue/audit rows atomically."""
def __init__(self, connection_factory: ConnectionFactory | None = None):
self._connection_factory = connection_factory or default_connection_factory
def get_control_context(self, miner_id: UUID) -> dict | None:
connection = self._connection_factory()
try:
with connection.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT miners.id, miners.site_id, miners.agent_zone_id,
zones.id IS NOT NULL AS agent_zone_exists,
zones.is_enabled AS agent_zone_enabled,
host(miners.current_ip) AS current_ip,
miners.status AS runtime_state,
miners.configured_performance_mode,
miners.api3_supported, miners.api3_api_switch,
agents.agent_id, agents.last_checkin_at,
agents.version AS agent_version, agents.last_payload
FROM miners
LEFT JOIN agent_zones AS zones
ON zones.id = miners.agent_zone_id
LEFT JOIN minerops_site_agents AS agents
ON agents.agent_zone_id = miners.agent_zone_id
WHERE miners.id = %s
""",
(miner_id,),
)
row = cur.fetchone()
return dict(row) if row else None
finally:
connection.close()
def list_recent(self, miner_id: UUID, limit: int = 5) -> list[dict]:
connection = self._connection_factory()
try:
with connection.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
SELECT id AS operation_id, command_status AS status,
request_payload, result_payload, created_at,
claimed_at, completed_at
FROM minerops_agent_command_queue
WHERE command_type = 'miner_control'
AND request_payload ->> 'miner_id' = %s
AND request_payload ->> 'schema_version' = '1'
ORDER BY created_at DESC, id DESC
LIMIT %s
""",
(str(miner_id), limit),
)
return [dict(row) for row in cur.fetchall()]
finally:
connection.close()
def enqueue_with_audit(
self,
*,
operation_id: str,
miner_id: UUID,
agent_id: str,
actor_user_id: UUID,
actor_username: str,
request_payload: dict[str, Any],
audit_payload: dict[str, Any],
) -> dict:
connection = self._connection_factory()
try:
with connection.cursor(row_factory=dict_row) as cur:
cur.execute(
"""
INSERT INTO minerops_agent_command_queue (
id, agent_id, command_type, command_status,
requested_by_user_id, requested_by_username, request_payload
)
VALUES (%s, %s, 'miner_control', 'pending', %s, %s, %s)
RETURNING id AS operation_id, command_status AS status, created_at
""",
(
operation_id, agent_id, str(actor_user_id), actor_username,
Jsonb(request_payload),
),
)
operation = dict(cur.fetchone())
cur.execute(
"""
INSERT INTO audit_logs (
actor_user_id, action, entity_type, entity_id,
new_value_json, related_miner_asset_id
)
VALUES (%s, 'miner.control.queued', 'miner', %s, %s, %s)
""",
(actor_user_id, miner_id, Jsonb(audit_payload), miner_id),
)
connection.commit()
return operation
except UniqueViolation as exc:
connection.rollback()
if exc.diag.constraint_name == LIVE_CONTROL_CONSTRAINT:
raise MinerControlConflict(
"A pending or running control operation already exists for this miner"
) from exc
raise
except Exception:
connection.rollback()
raise
finally:
connection.close()