From 1fff5117525cf312d72aa4dc5f96665fc5c177e4 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 10 Jul 2026 00:50:55 +0900 Subject: [PATCH] =?UTF-8?q?fix(realestate):=20compose=20INTERNAL=5FAPI=5FK?= =?UTF-8?q?EY(=EB=B0=B0=ED=8F=AC=20401=20=EB=B0=A9=EC=A7=80)=20+=20?= =?UTF-8?q?=EB=A7=A4=EC=B9=AD=C2=B7=EC=95=8C=EB=A6=BC=20=EA=B3=B5=EC=9C=A0?= =?UTF-8?q?=EB=9D=BD(=EC=A4=91=EB=B3=B5=20=ED=85=94=EB=A0=88=EA=B7=B8?= =?UTF-8?q?=EB=9E=A8=20=EB=B0=A9=EC=A7=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose.yml realestate-lab environment에 INTERNAL_API_KEY/NAVER_PAGE_LIMIT 누락 추가 (verify_internal_key 항상 401 → 워커 계약 전체 마비 방지, sibling image/video/music/insta-lab과 동형) - pipeline_lock.py 신설: cron _run_listing_pipeline과 워커 listings_ingest가 run_listing_matching()+notify_new_listings() 임계구역을 공유 threading.Lock으로 직렬화 (동시 실행 시 동일 매물 중복 텔레그램 발송 방지). 느린 collect는 락 밖 유지. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01EqCYBhvTcdeCTUDX3RhWx9 --- docker-compose.yml | 2 ++ realestate-lab/app/internal_router.py | 8 +++++--- realestate-lab/app/main.py | 6 +++++- realestate-lab/app/pipeline_lock.py | 5 +++++ realestate-lab/tests/test_internal_ingest.py | 13 +++++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 realestate-lab/app/pipeline_lock.py diff --git a/docker-compose.yml b/docker-compose.yml index 1360613..1d92fb5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -206,6 +206,8 @@ services: - DATA_GO_KR_API_KEY=${DATA_GO_KR_API_KEY:-} - CORS_ALLOW_ORIGINS=${CORS_ALLOW_ORIGINS:-http://localhost:3007,http://localhost:8080} - AGENT_OFFICE_URL=${AGENT_OFFICE_URL:-http://agent-office:8000} + - INTERNAL_API_KEY=${INTERNAL_API_KEY:-} + - NAVER_PAGE_LIMIT=${NAVER_PAGE_LIMIT:-2} - PYTHONPATH=/app:/shared volumes: - ${RUNTIME_PATH}/data/realestate:/app/data diff --git a/realestate-lab/app/internal_router.py b/realestate-lab/app/internal_router.py index 95c68e6..8d9dd12 100644 --- a/realestate-lab/app/internal_router.py +++ b/realestate-lab/app/internal_router.py @@ -11,6 +11,7 @@ from .lawd_codes import naver_cortar from .listing_collector import _parse_naver_article from .listing_matcher import run_listing_matching from .notifier import notify_new_listings +from .pipeline_lock import match_notify_lock logger = logging.getLogger("realestate-lab") router = APIRouter() @@ -61,7 +62,8 @@ def listings_ingest(body: ListingsIngest): logger.warning("네이버 ingest 파싱 실패: %s", e) matched = 0 if received: - run_listing_matching() - noti = notify_new_listings() - matched = int(noti.get("sent", 0)) if isinstance(noti, dict) else 0 + 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} diff --git a/realestate-lab/app/main.py b/realestate-lab/app/main.py index 122e1b5..6490804 100644 --- a/realestate-lab/app/main.py +++ b/realestate-lab/app/main.py @@ -23,6 +23,7 @@ from .matcher import run_matching from .notifier import notify_new_matches, notify_new_listings from .listing_collector import collect_listings from .listing_matcher import run_listing_matching, compute_safety, compute_valuation +from .pipeline_lock import match_notify_lock from . import finance_rules from .finance_rules_config import DISCLAIMER from .models import ( @@ -73,7 +74,10 @@ def _run_listing_pipeline(): if not _listing_collect_lock.acquire(blocking=False): logger.info("매물 수집 이미 진행 중 — 건너뜀"); return try: - collect_listings(); run_listing_matching(); notify_new_listings() + collect_listings() + with match_notify_lock: + run_listing_matching() + notify_new_listings() finally: _listing_collect_lock.release() diff --git a/realestate-lab/app/pipeline_lock.py b/realestate-lab/app/pipeline_lock.py new file mode 100644 index 0000000..ac9349d --- /dev/null +++ b/realestate-lab/app/pipeline_lock.py @@ -0,0 +1,5 @@ +"""매물 매칭+알림 임계구역 직렬화 락 — cron 파이프라인과 워커 ingest가 공유해 +run_listing_matching()+notify_new_listings() 동시 실행(중복 텔레그램)을 방지한다.""" +import threading + +match_notify_lock = threading.Lock() diff --git a/realestate-lab/tests/test_internal_ingest.py b/realestate-lab/tests/test_internal_ingest.py index 31aa9ea..4dfb16e 100644 --- a/realestate-lab/tests/test_internal_ingest.py +++ b/realestate-lab/tests/test_internal_ingest.py @@ -44,3 +44,16 @@ def test_ingest_idempotent(monkeypatch): c.post("/api/internal/realestate/listings-ingest", headers={"X-Internal-Key": "SECRET"}, json=payload) r2 = c.post("/api/internal/realestate/listings-ingest", headers={"X-Internal-Key": "SECRET"}, json=payload) assert r2.json()["new"] == 0 # 재push → 신규 0 + +def test_ingest_holds_match_notify_lock(monkeypatch): + from app import internal_router as ir + from app.pipeline_lock import match_notify_lock + c = _client(monkeypatch) + holds = {} + def fake_notify(): + holds["locked"] = match_notify_lock.locked() + return {"sent": 0} + monkeypatch.setattr(ir, "notify_new_listings", fake_notify) + c.post("/api/internal/realestate/listings-ingest", headers={"X-Internal-Key": "SECRET"}, + json={"batches": [{"dong": "신대방동", "articles": [_ARTICLE]}]}) + assert holds["locked"] is True # 알림 시점에 락 보유(임계구역 직렬화 증명)