141 lines
5.4 KiB
Python
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()
|