feat: add safe miner control foundation
This commit is contained in:
1 parent
6cd876b4b3
commit
f5965af914
39 files changed
+2952
-491
No files matched your search
@@ -0,0 +1,135 @@
|
||||
"""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, 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 minerops_site_agents AS agents
|
||||
ON agents.site_id = miners.site_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()
|
||||
Reference in new issue
Block a user