gendesign/tradein-mvp/backend/app/tasks/refresh_search_matview.py
lekss361 09698c5124 fix(tradein): PR #469 BLOCK — h.reviews_count + DSN psycopg strip
deep-code-reviewer caught 2 critical bugs that would cause silent prod regression:

1. 050_search_optimization.sql: h.ratings_count → h.reviews_count
   (alias house_ratings_count preserved for API contract)
   houses.ratings_count does not exist; matview CREATE would abort,
   and deploy-tradein.yml swallows the error (|| echo) — Celery task
   would then fail at runtime with "relation listings_search_mv does not exist".

2. refresh_search_matview.py: strip +psycopg dialect prefix from DSN
   tradein-mvp/docker-compose.prod.yml uses postgresql+psycopg:// (SQLAlchemy
   dialect form) for DATABASE_URL. libpq accepts only postgresql:// — without
   the strip, psycopg.connect() raises OperationalError on every Celery beat tick.

Refs PR #469 review (deep-code-reviewer, 2026-05-23).
2026-05-23 16:58:14 +03:00

70 lines
2.7 KiB
Python

"""Refresh listings_search_mv materialized view (daily 3 AM).
Currently tradein-mvp has no Celery infrastructure (verified — no celery_app.py exists
in tradein-mvp/backend/app/). This module provides both:
- A Celery task wrapper (active only if celery is installed and celery_app present).
- A plain async refresh_search_matview() callable for cron / one-shot invocation.
NEEDS COORDINATION: main session must decide whether to bootstrap Celery in tradein-mvp
or run via external scheduler (systemd timer / OS cron). Until then, no Beat schedule.
"""
from __future__ import annotations
import logging
import time
import psycopg
from app.core.config import settings
logger = logging.getLogger(__name__)
def refresh_search_matview() -> None:
"""REFRESH MATERIALIZED VIEW CONCURRENTLY listings_search_mv (psycopg v3, sync).
Logs duration. Idempotent. Safe to run during read traffic (CONCURRENTLY).
"""
start = time.monotonic()
# DATABASE_URL is SQLAlchemy dialect form (postgresql+psycopg://...) in tradein-mvp
# (see tradein-mvp/docker-compose.prod.yml). libpq / psycopg.connect() accepts only
# postgresql:// or postgres:// — strip the +psycopg dialect prefix.
dsn = settings.database_url.replace("postgresql+psycopg://", "postgresql://", 1)
with psycopg.connect(dsn, autocommit=True) as conn:
with conn.cursor() as cur:
cur.execute("REFRESH MATERIALIZED VIEW CONCURRENTLY listings_search_mv")
elapsed = time.monotonic() - start
logger.info("listings_search_mv refresh completed in %.2fs", elapsed)
# Celery-task wrapper — only registers if celery_app exists.
try:
from app.celery_app import celery_app # type: ignore[import-not-found]
except ImportError:
logger.info(
"Celery not configured in tradein-mvp — refresh_search_matview() callable directly. "
"Bootstrap app/celery_app.py to enable scheduled execution.",
)
else:
@celery_app.task(name="tradein.refresh_search_matview") # type: ignore[misc]
def refresh_search_matview_task() -> None:
"""Celery wrapper for refresh_search_matview()."""
refresh_search_matview()
# =============================================================================
# Beat schedule snippet — paste into app/celery_app.py if/when Celery is bootstrapped:
#
# from celery.schedules import crontab
#
# celery_app.conf.beat_schedule = {
# 'tradein-refresh-matview': {
# 'task': 'tradein.refresh_search_matview',
# 'schedule': crontab(hour=3, minute=0),
# },
# # ... other tradein beat entries
# }
#
# Per master plan sec 10.1 ('refresh-search-matview-daily': crontab(minute=0, hour=3)).
# =============================================================================