mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
The handler fired ~2+2N read round-trips and N+N write transactions per SQS batch, pinning RDS CPU under ~32 concurrent containers on pool_size=1. Reads: merge the duplicate property query and add overrides_for_many / SolarRepository.get_many so overrides, solar, and property rows each load in one query (2+2N -> 3). Writes: buffer each modelled property's persistence intent in memory (_PropertyWrite) during the loop, then flush the whole batch in one PostgresUnitOfWork with a single commit, and run the baseline orchestrator once for all written ids (N+N -> 2 transactions). Per-property modelling failures stay isolated in the loop; the batch write is all-or-nothing and retried via SQS (saves are idempotent upserts). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
4.3 KiB
Python
107 lines
4.3 KiB
Python
"""Postgres adapter for the ``property_overrides`` read side.
|
|
|
|
Read-only and uow-independent: ``property_overrides`` is committed reference
|
|
data the ``bulk_upload_finaliser`` Lambda writes at Finalise, long before First
|
|
Run executes — there is no transactional coupling to the ingestion run, so the
|
|
standalone reader opens its own short read session per call via the injected
|
|
session factory (mirroring the composition root's ``lambda: Session(engine)``).
|
|
|
|
Inside a Unit of Work the overrides must instead be read on the UoW's *own*
|
|
session (``OpenSessionPropertyOverridesReader``): a second session opened
|
|
concurrently checks out a second connection, which deadlocks the modelling_e2e
|
|
Lambda's single-connection pool while the UoW's read transaction is still open.
|
|
Reading committed reference data inside the UoW's transaction is correct — it
|
|
sees the same committed rows — and keeps the invocation on one connection.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
|
|
from sqlmodel import Session, col, select
|
|
|
|
from infrastructure.postgres.property_override_table import PropertyOverrideRow
|
|
from repositories.property.property_overrides_reader import (
|
|
PropertyOverridesReader,
|
|
ResolvedPropertyOverride,
|
|
ResolvedPropertyOverrides,
|
|
)
|
|
|
|
|
|
def _resolve_overrides(session: Session, property_id: int) -> ResolvedPropertyOverrides:
|
|
rows = session.exec(
|
|
select(PropertyOverrideRow).where(
|
|
col(PropertyOverrideRow.property_id) == property_id
|
|
)
|
|
).all()
|
|
return ResolvedPropertyOverrides(
|
|
rows=tuple(
|
|
ResolvedPropertyOverride(
|
|
override_component=row.override_component,
|
|
building_part=row.building_part,
|
|
override_value=row.override_value,
|
|
)
|
|
for row in rows
|
|
)
|
|
)
|
|
|
|
|
|
def _resolve_overrides_many(
|
|
session: Session, property_ids: list[int]
|
|
) -> dict[int, ResolvedPropertyOverrides]:
|
|
"""Resolve overrides for many Properties in one query. Returns an entry for
|
|
every requested id; a Property with no rows resolves to an empty snapshot."""
|
|
rows = session.exec(
|
|
select(PropertyOverrideRow).where(
|
|
col(PropertyOverrideRow.property_id).in_(property_ids)
|
|
)
|
|
).all()
|
|
grouped: dict[int, list[ResolvedPropertyOverride]] = {
|
|
property_id: [] for property_id in property_ids
|
|
}
|
|
for row in rows:
|
|
grouped.setdefault(row.property_id, []).append(
|
|
ResolvedPropertyOverride(
|
|
override_component=row.override_component,
|
|
building_part=row.building_part,
|
|
override_value=row.override_value,
|
|
)
|
|
)
|
|
return {
|
|
property_id: ResolvedPropertyOverrides(rows=tuple(overrides))
|
|
for property_id, overrides in grouped.items()
|
|
}
|
|
|
|
|
|
class PropertyOverridesPostgresReader(PropertyOverridesReader):
|
|
"""Opens its own short read session per call — for standalone use outside a
|
|
Unit of Work, where there is no shared session/connection to reuse."""
|
|
|
|
def __init__(self, session_factory: Callable[[], Session]) -> None:
|
|
self._session_factory = session_factory
|
|
|
|
def overrides_for(self, property_id: int) -> ResolvedPropertyOverrides:
|
|
with self._session_factory() as session:
|
|
return _resolve_overrides(session, property_id)
|
|
|
|
def overrides_for_many(
|
|
self, property_ids: list[int]
|
|
) -> dict[int, ResolvedPropertyOverrides]:
|
|
"""Every requested Property's resolved overrides in one query — the batch
|
|
form of ``overrides_for``. The returned dict has an entry for every
|
|
requested id; a Property with no rows resolves to an empty snapshot
|
|
(exactly as ``overrides_for`` returns for a Property with no rows)."""
|
|
with self._session_factory() as session:
|
|
return _resolve_overrides_many(session, property_ids)
|
|
|
|
|
|
class OpenSessionPropertyOverridesReader(PropertyOverridesReader):
|
|
"""Reads on a caller-owned, already-open session without closing it — for use
|
|
inside a Unit of Work so resolving overrides reuses the UoW's single
|
|
connection instead of checking out a second one."""
|
|
|
|
def __init__(self, session: Session) -> None:
|
|
self._session = session
|
|
|
|
def overrides_for(self, property_id: int) -> ResolvedPropertyOverrides:
|
|
return _resolve_overrides(self._session, property_id)
|