wrongmove: fix DetachedInstanceError in daily market aggregator task

`compute_aggregate_snapshot` returns ORM rows that were created inside
a `with Session(engine)` block — by the time the Celery task tries to
serialise their attributes into the result dict the session has closed,
triggering SQLAlchemy's DetachedInstanceError. Combined with acks_late
this caused the task to be redelivered repeatedly (4× in the first
manual trigger).

Fix: drop the per-row dict-serialisation in the task return — keep just
`aggregates_written: int`. The per-band stats are already logged by the
aggregator's own info-level lines, so no observability is lost.

Caught when manually firing the task on prod to seed today's snapshot
before the 04:00 UTC daily fire. Aggregator itself ran fine (the rows
were written before the session closed); only the post-return access
was broken.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-05-16 12:53:26 +00:00
parent 9fdf7fd356
commit 01a940b9b6

View file

@ -32,26 +32,20 @@ def compute_daily_market_aggregates_task(self: Any) -> dict[str, Any]:
celery_logger.info("Starting daily market aggregator (task=%s)", self.request.id) celery_logger.info("Starting daily market aggregator (task=%s)", self.request.id)
per_listing = market_aggregator.update_per_listing_trend(engine) per_listing = market_aggregator.update_per_listing_trend(engine)
aggregates = market_aggregator.compute_aggregate_snapshot(engine) aggregates = market_aggregator.compute_aggregate_snapshot(engine)
# Materialise only the count — the row objects came from a session
# that's already closed, so accessing any lazy-loaded attribute would
# raise DetachedInstanceError. The aggregator's own logger lines have
# already printed the per-band stats.
aggregates_count = len(aggregates)
result = { result = {
"status": "ok", "status": "ok",
"per_listing": per_listing, "per_listing": per_listing,
"aggregates": [ "aggregates_written": aggregates_count,
{
"snapshot_date": a.snapshot_date.isoformat(),
"listing_type": a.listing_type,
"min_bedrooms": a.min_bedrooms,
"max_bedrooms": a.max_bedrooms,
"listing_count": a.listing_count,
"median_total_price": a.median_total_price,
"median_qmprice": a.median_qmprice,
}
for a in aggregates
],
} }
celery_logger.info( celery_logger.info(
"Daily market aggregator complete: rent_updated=%s buy_updated=%s aggregates=%s", "Daily market aggregator complete: rent_updated=%s buy_updated=%s aggregates=%d",
per_listing.get("rent_updated"), per_listing.get("rent_updated"),
per_listing.get("buy_updated"), per_listing.get("buy_updated"),
len(aggregates), aggregates_count,
) )
return result return result