fix(realestate): listings-ingest async(BackgroundTask)+매칭/알림 타이밍 로그 + notify 홍수방지(cap+baseline)

This commit is contained in:
2026-07-10 11:11:36 +09:00
parent 28418b9f5d
commit 8e28ce9ae5
5 changed files with 56 additions and 13 deletions

View File

@@ -1,8 +1,9 @@
"""naver-fetch 워커 내부 계약: targets 조회 + listings ingest."""
import os
import time
import logging
from typing import List, Dict, Any
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, BackgroundTasks
from pydantic import BaseModel
from .auth import verify_internal_key
@@ -45,7 +46,7 @@ class ListingsIngest(BaseModel):
@router.post("/api/internal/realestate/listings-ingest",
dependencies=[Depends(verify_internal_key)])
def listings_ingest(body: ListingsIngest):
def listings_ingest(body: ListingsIngest, background_tasks: BackgroundTasks):
received = new = 0
for batch in body.batches:
for raw in batch.articles:
@@ -60,10 +61,18 @@ def listings_ingest(body: ListingsIngest):
new += 1
except Exception as e:
logger.warning("네이버 ingest 파싱 실패: %s", e)
matched = 0
if received:
with match_notify_lock:
run_listing_matching()
noti = notify_new_listings()
matched = int(noti.get("sent", 0)) if isinstance(noti, dict) else 0
return {"received": received, "new": new, "matched": matched}
background_tasks.add_task(_match_and_notify)
return {"received": received, "new": new, "queued": bool(received)}
def _match_and_notify():
"""ingest 후처리(백그라운드): 매칭+알림을 공유 락으로 직렬화. 타이밍 로그로 병목 관측."""
with match_notify_lock:
t0 = time.time()
run_listing_matching()
t1 = time.time()
noti = notify_new_listings()
t2 = time.time()
sent = (noti or {}).get("sent") if isinstance(noti, dict) else noti
logger.info("ingest 후처리: match=%.1fs notify=%.1fs sent=%s", t1 - t0, t2 - t1, sent)

View File

@@ -10,6 +10,8 @@ logger = logging.getLogger("realestate-lab")
AGENT_OFFICE_URL = os.getenv("AGENT_OFFICE_URL", "http://agent-office:8000")
NOTIFY_TIMEOUT_SECONDS = int(os.getenv("REALESTATE_NOTIFY_TIMEOUT", "15"))
LISTING_NOTIFY_MAX = int(os.getenv("LISTING_NOTIFY_MAX", "8"))
LISTING_NOTIFY_TIMEOUT = int(os.getenv("LISTING_NOTIFY_TIMEOUT", "60"))
def notify_new_matches() -> dict:
@@ -49,21 +51,28 @@ def notify_new_matches() -> dict:
def notify_new_listings() -> dict:
"""임계 통과 미알림 매물을 agent-office로 push (notify_new_matches 대칭).
passed 매물은 전 안전등급 알림(위험도 회피 목적 유용). criteria.min_safety_tier 설정 시 그 이상만."""
passed 매물은 전 안전등급 알림(위험도 회피 목적 유용). criteria.min_safety_tier 설정 시 그 이상만.
콜드스타트로 backlog가 쌓이면 한 사이클 최대 LISTING_NOTIFY_MAX건만 알림 발송하고
나머지는 조용히 notified_at 마킹(baseline)해 스팸·재전송 루프를 막는다."""
crit = get_listing_criteria()
if not crit.get("notify_enabled"):
return {"sent": 0, "skipped": "notify_disabled"}
listings = get_unnotified_listing_matches(min_tier=crit.get("min_safety_tier"))
if not listings:
return {"sent": 0}
to_send = listings[:LISTING_NOTIFY_MAX]
to_baseline = listings[LISTING_NOTIFY_MAX:]
if to_baseline: # 콜드스타트 backlog: 조용히 baseline 마킹(알림 X)
mark_listings_notified([m["id"] for m in to_baseline])
logger.info("매물 알림 baseline(무알림) 마킹: %d", len(to_baseline))
url = f"{AGENT_OFFICE_URL}/api/agent-office/realestate/notify-listing"
try:
resp = requests.post(url, json={"listings": listings}, timeout=NOTIFY_TIMEOUT_SECONDS)
resp = requests.post(url, json={"listings": to_send}, timeout=LISTING_NOTIFY_TIMEOUT)
resp.raise_for_status()
body = resp.json()
except requests.RequestException as e:
logger.error("agent-office 매물 push 실패: %s", e)
return {"sent": 0, "error": str(e)}
return {"sent": 0, "error": str(e), "baselined": len(to_baseline)}
sent_ids = body.get("sent_ids") or []
if sent_ids:
mark_listings_notified(sent_ids)