232 lines
9.8 KiB
Python
232 lines
9.8 KiB
Python
"""Set-based PostgreSQL reads for Mine Map v1."""
|
|
|
|
from collections.abc import Callable, Iterator
|
|
from contextlib import contextmanager
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
import psycopg
|
|
from psycopg.rows import dict_row
|
|
|
|
from app.config import settings
|
|
|
|
|
|
ConnectionFactory = Callable[[], psycopg.Connection]
|
|
ACTIVE_REPAIR_STATUSES = (
|
|
"OPEN",
|
|
"IN_PROGRESS",
|
|
"WAITING_FOR_PARTS",
|
|
"READY_FOR_TEST",
|
|
)
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
LATEST_TELEMETRY_SQL = """
|
|
LEFT JOIN LATERAL (
|
|
SELECT snapshots.observed_at,
|
|
ROUND((snapshots.mhs_av / 1000000.0)::numeric, 2)::float
|
|
AS hashrate_ths,
|
|
ROUND(snapshots.power_w::numeric, 2)::float AS power_w,
|
|
ROUND(snapshots.chip_temp_max_c::numeric, 2)::float
|
|
AS chip_temp_max_c,
|
|
ROUND(snapshots.chip_temp_avg_c::numeric, 2)::float
|
|
AS chip_temp_avg_c,
|
|
snapshots.hash_stable, snapshots.pool_rejected_percent
|
|
FROM miner_status_snapshots snapshots
|
|
WHERE snapshots.miner_id = miners.id
|
|
ORDER BY snapshots.observed_at DESC, snapshots.id DESC
|
|
LIMIT 1
|
|
) latest ON true
|
|
"""
|
|
|
|
ACTIVE_REPAIR_SQL = """
|
|
LEFT JOIN LATERAL (
|
|
SELECT repairs.id, repairs.status, repairs.reason
|
|
FROM repairs
|
|
WHERE repairs.miner_id = miners.id AND repairs.status = ANY(%s)
|
|
ORDER BY repairs.opened_at, repairs.id
|
|
LIMIT 1
|
|
) active_repair ON true
|
|
"""
|
|
|
|
|
|
class LocationMapRepository:
|
|
"""Own all persistence used by the Mine Map read model."""
|
|
|
|
def __init__(
|
|
self,
|
|
connection_factory: ConnectionFactory | None = None,
|
|
*,
|
|
connection: psycopg.Connection | None = None,
|
|
):
|
|
self._connection_factory = connection_factory or default_connection_factory
|
|
self._connection = connection
|
|
|
|
@contextmanager
|
|
def _connection_scope(self) -> Iterator[psycopg.Connection]:
|
|
if self._connection is not None:
|
|
yield self._connection
|
|
return
|
|
connection = self._connection_factory()
|
|
try:
|
|
yield connection
|
|
finally:
|
|
connection.close()
|
|
|
|
def read_map(
|
|
self,
|
|
*,
|
|
site_id: UUID | None = None,
|
|
room_id: UUID | None = None,
|
|
) -> dict[str, list[dict[str, Any]]]:
|
|
position_where = []
|
|
position_params: list[Any] = [list(ACTIVE_REPAIR_STATUSES)]
|
|
virtual_where = [
|
|
"COALESCE(mls.asset_state, 'waiting_placement') = ANY(%s)",
|
|
"NOT EXISTS (SELECT 1 FROM location_positions assigned "
|
|
"WHERE assigned.miner_id = miners.id)",
|
|
]
|
|
virtual_params: list[Any] = [
|
|
list(ACTIVE_REPAIR_STATUSES),
|
|
["waiting_placement", "storage", "repair", "decommissioned"],
|
|
]
|
|
if site_id is not None:
|
|
position_where.append("rooms.site_id = %s")
|
|
position_params.append(site_id)
|
|
virtual_where.append("miners.site_id = %s")
|
|
virtual_params.append(site_id)
|
|
if room_id is not None:
|
|
position_where.append("lp.room_id = %s")
|
|
position_params.append(room_id)
|
|
virtual_where.append("miners.room_id = %s")
|
|
virtual_params.append(room_id)
|
|
position_filter = (
|
|
"WHERE " + " AND ".join(position_where) if position_where else ""
|
|
)
|
|
virtual_filter = "WHERE " + " AND ".join(virtual_where)
|
|
|
|
with self._connection_scope() as connection:
|
|
with connection.cursor(row_factory=dict_row) as cur:
|
|
cur.execute("SELECT id, name FROM sites ORDER BY lower(name), id")
|
|
sites = [dict(row) for row in cur.fetchall()]
|
|
cur.execute(
|
|
"""
|
|
SELECT rooms.id, rooms.site_id, sites.name AS site_name,
|
|
rooms.name, rooms.sort_order
|
|
FROM rooms JOIN sites ON sites.id = rooms.site_id
|
|
ORDER BY lower(sites.name), rooms.sort_order,
|
|
lower(rooms.name), rooms.id
|
|
"""
|
|
)
|
|
rooms = [dict(row) for row in cur.fetchall()]
|
|
cur.execute(
|
|
f"""
|
|
SELECT lp.id AS location_position_id, sites.id AS site_id,
|
|
sites.name AS site_name, rooms.id AS room_id,
|
|
COALESCE(rooms.name, lp.room_name) AS room_name,
|
|
lp.rack_name, lp.shelf_name, lp.shelf_index,
|
|
lp.position_index, lp.position_label, lp.position_kind,
|
|
lp.position_status, (miners.id IS NOT NULL) AS occupied,
|
|
lp.expected_mac, lp.equipment_label, lp.notes,
|
|
lp.source_file, lp.source_cell, lp.conflict_reason,
|
|
miners.id AS miner_id, miners.asset_tag AS asset_id,
|
|
miners.current_mac, host(miners.current_ip) AS current_ip,
|
|
miners.status AS miner_status, miners.hostname,
|
|
miners.miner_sn, miners.power_sn, miners.model,
|
|
miners.miner_type, miners.firmware_version,
|
|
miners.lifecycle_status, miners.last_seen_at,
|
|
COALESCE(mls.asset_state, 'unassigned') AS asset_state,
|
|
latest.observed_at, latest.hashrate_ths, latest.power_w,
|
|
latest.chip_temp_max_c, latest.chip_temp_avg_c,
|
|
latest.hash_stable, latest.pool_rejected_percent,
|
|
active_repair.id AS active_repair_id,
|
|
active_repair.status AS active_repair_status,
|
|
active_repair.reason AS active_repair_reason
|
|
FROM location_positions lp
|
|
LEFT JOIN rooms ON rooms.id = lp.room_id
|
|
LEFT JOIN sites ON sites.id = rooms.site_id
|
|
LEFT JOIN miners ON miners.id = lp.miner_id
|
|
LEFT JOIN miner_location_states mls ON mls.miner_id = miners.id
|
|
{LATEST_TELEMETRY_SQL}
|
|
{ACTIVE_REPAIR_SQL}
|
|
{position_filter}
|
|
ORDER BY lower(sites.name) NULLS LAST,
|
|
rooms.sort_order NULLS LAST,
|
|
lower(COALESCE(rooms.name, lp.room_name)),
|
|
lower(lp.rack_name), lp.shelf_index NULLS LAST,
|
|
lower(lp.shelf_name), lp.position_index, lp.id
|
|
""",
|
|
position_params,
|
|
)
|
|
positions = [dict(row) for row in cur.fetchall()]
|
|
cur.execute(
|
|
f"""
|
|
SELECT miners.id AS miner_id, miners.site_id, miners.room_id,
|
|
miners.asset_tag AS asset_id,
|
|
COALESCE(mls.asset_state, 'waiting_placement') AS asset_state,
|
|
miners.current_mac, host(miners.current_ip) AS current_ip,
|
|
miners.status AS miner_status, miners.status, miners.hostname,
|
|
miners.miner_sn, miners.power_sn, miners.model,
|
|
miners.miner_type, miners.firmware_version,
|
|
miners.lifecycle_status, miners.last_seen_at,
|
|
latest.observed_at, latest.hashrate_ths, latest.power_w,
|
|
latest.chip_temp_max_c, latest.chip_temp_avg_c,
|
|
latest.hash_stable,
|
|
active_repair.id AS active_repair_id,
|
|
active_repair.status AS active_repair_status,
|
|
active_repair.reason AS active_repair_reason
|
|
FROM miners
|
|
LEFT JOIN miner_location_states mls ON mls.miner_id = miners.id
|
|
{LATEST_TELEMETRY_SQL}
|
|
{ACTIVE_REPAIR_SQL}
|
|
{virtual_filter}
|
|
ORDER BY asset_state, lower(miners.asset_tag), miners.id
|
|
""",
|
|
virtual_params,
|
|
)
|
|
virtual_miners = [dict(row) for row in cur.fetchall()]
|
|
return {
|
|
"sites": sites,
|
|
"rooms": rooms,
|
|
"positions": positions,
|
|
"virtual_miners": virtual_miners,
|
|
}
|
|
|
|
def summarize_locations(self, *, include_miner_counts: bool) -> dict[str, Any]:
|
|
miner_counts = (
|
|
"count(DISTINCT lp.miner_id)::integer AS mapped_miners, "
|
|
"(SELECT count(*)::integer FROM miners WHERE NOT EXISTS ("
|
|
"SELECT 1 FROM location_positions assigned "
|
|
"WHERE assigned.miner_id = miners.id)) AS unmapped_miners"
|
|
if include_miner_counts
|
|
else "NULL::integer AS mapped_miners, NULL::integer AS unmapped_miners"
|
|
)
|
|
with self._connection_scope() as connection:
|
|
with connection.cursor(row_factory=dict_row) as cur:
|
|
cur.execute(
|
|
f"""
|
|
SELECT (SELECT count(*)::integer FROM sites) AS sites,
|
|
(SELECT count(*)::integer FROM rooms) AS rooms,
|
|
count(lp.id)::integer AS positions,
|
|
count(lp.id) FILTER (
|
|
WHERE lp.miner_id IS NOT NULL
|
|
)::integer AS occupied_positions,
|
|
count(lp.id) FILTER (
|
|
WHERE lp.miner_id IS NULL
|
|
)::integer AS empty_positions,
|
|
{miner_counts}
|
|
FROM location_positions AS lp
|
|
"""
|
|
)
|
|
return dict(cur.fetchone())
|