Skip to content

Commit 8b68117

Browse files
committed
fix(uyap): eliminate 1-hour bulk hang - skip stale viewer tabs + step watchdog
Root cause of the hour-long hang (no output, then stray TargetClosedError): _find_gecmis_page called page.content() on EVERY open tab. A stale viewer/blob tab left by the previous crashed run has a dead renderer, and page.content() has no timeout -> it blocked forever, before the first progress line (hence zero [UYAP BULK] output). Fix 1 (root cause): _find_gecmis_page now filters tabs by the cheap, non-hanging p.url first and skips blob:/data:/file:/about:/chrome:/devtools:/view-source: tabs entirely; content() is only called on real app tabs. Proven via fake-page test: stale blob tab's content() is never called. Fix 2 (safety net): added a per-step heartbeat (_beat, stderr, flushed) + a background watchdog thread (_start_watchdog). If any live step fails to progress within --stall-timeout seconds (default 120), it prints an actionable diagnostic (which step, open-tab count, 'close stale tabs' remedy) and os._exit(4)s instead of hanging silently. Checkpoints are saved page-by-page so no data loss. Heartbeats now show live progress at each step (connect, find page, per window/page/record). Also: _print flushes; new --stall-timeout CLI flag threaded to UyapBulkCollector(stall_seconds=). Pure functions unchanged; frozen structural core untouched; non-mutating. Tests: 510 passed (uyap/bulk/cli/collect/discovery subset), 39 bulk unit tests green.
1 parent 73d9294 commit 8b68117

2 files changed

Lines changed: 81 additions & 3 deletions

File tree

src/sold/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,6 +2120,7 @@ def uyap_bulk_cmd(
21202120
diagnose_documents: bool = typer.Option(False, "--diagnose-documents", help="TANI: hedef kartın (--kayit-no) 'İhale Evrak Listesi'ni çalıştırıp belge-listesinin ne açtığını basar (admisyon YOK)"),
21212121
store_dir: Optional[str] = typer.Option(None, help="Çalışma deposu / kontrol noktası dizini"),
21222122
genuine_path: Optional[str] = typer.Option(None, help="genuine uyap.json yolu (DUPLICATE denetimi; admisyon YOK)"),
2123+
stall_timeout: int = typer.Option(120, "--stall-timeout", help="Canlı bir adım bu kadar saniye ilerlemezse GÜVENLE sonlandır (takılı sekme koruması)"),
21232124
) -> None:
21242125
"""UYAP TOPLU keşif+iterasyon — 'Geçmiş İlanlar'da Taşınmaz+İl+tarih ile SADECE Satıldı açık artırmalar.
21252126
@@ -2232,6 +2233,7 @@ def uyap_bulk_cmd(
22322233
try:
22332234
s = UyapBulkCollector(
22342235
cdp_endpoint=cdp_endpoint or "", store_dir=store_dir, genuine_path=genuine_path,
2236+
stall_seconds=stall_timeout,
22352237
).run(
22362238
province=province, date_from=date_from, date_to=date_to,
22372239
max_records=max_records, max_windows=max_windows,

src/sold/ingestion/uyap/bulk.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818

1919
import datetime as dt
2020
import json
21+
import os
2122
import re
23+
import sys
24+
import threading
25+
import time
2226
from pathlib import Path
2327

2428
from . import store
@@ -837,12 +841,20 @@ def __init__(
837841
genuine_path: Path | str | None = None,
838842
request_delay_ms: int = 900,
839843
result_timeout_ms: int = 20000,
844+
stall_seconds: int = 120,
840845
) -> None:
841846
self.cdp_endpoint = cdp_endpoint
842847
self.store_dir = store_dir
843848
self.genuine_path = genuine_path
844849
self.request_delay_ms = max(0, int(request_delay_ms))
845850
self.result_timeout_ms = max(1000, int(result_timeout_ms))
851+
# Canlı adım gözcüsü (watchdog): hiçbir adım stall_seconds'ı aşarsa net tanı ile GÜVENLE sonlandır
852+
# (önceki koşumdan kalan TAKILI görüntüleyici sekmesi page.content()'i sonsuza dek bloklayabilir).
853+
self.stall_seconds = max(15, int(stall_seconds))
854+
self._hb_step = "başlatılıyor"
855+
self._hb_ts = time.monotonic()
856+
self._tab_count = -1
857+
self._live_active = False
846858

847859
def diagnose_form(self) -> dict: # pragma: no cover - canlı tarayıcı gerektirir
848860
"""READ-ONLY tanı: oturuma bağlanıp 'Geçmiş İlanlar' formunun GERÇEK kontrol yapısını döndürür.
@@ -1012,12 +1024,24 @@ def run(
10121024
collector = BrowserCollector(cdp_endpoint=self.cdp_endpoint)
10131025
acquired_total = 0
10141026
with sync_playwright() as pw:
1027+
self._live_active = True
1028+
watchdog_stop = self._start_watchdog()
1029+
self._beat("CDP oturumuna bağlanılıyor")
10151030
browser = pw.chromium.connect_over_cdp(self.cdp_endpoint)
10161031
if not browser.contexts:
1032+
self._live_active = False
1033+
watchdog_stop.set()
10171034
raise RuntimeError("no_usable_browser_context: CDP oturumunda kullanılabilir bağlam yok.")
10181035
context = browser.contexts[0]
1036+
try:
1037+
self._tab_count = len(context.pages)
1038+
except Exception:
1039+
self._tab_count = -1
1040+
self._beat(f"'Geçmiş İlanlar' sekmesi aranıyor ({self._tab_count} açık sekme)")
10191041
page = self._find_gecmis_page(context)
10201042
if page is None:
1043+
self._live_active = False
1044+
watchdog_stop.set()
10211045
raise RuntimeError(
10221046
"no_gecmis_ilanlar_page: 'Geçmiş İlanlar' sekmesi bulunamadı. "
10231047
"Önce UYAP e-Satış → İhaleler → Geçmiş İlanlar sayfasını elle açın."
@@ -1048,6 +1072,8 @@ def _acquire(file_id, institution):
10481072
if stop == "MAX_RECORDS":
10491073
summary["stopped_reason"] = "max_records"
10501074
break
1075+
self._live_active = False
1076+
watchdog_stop.set()
10511077

10521078
for c in store.load_candidates(self.store_dir):
10531079
dec = (c.get("audit") or {}).get("decision")
@@ -1061,6 +1087,7 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
10611087
start_ui = format_uyap_ui_date(w["start"])
10621088
end_ui = format_uyap_ui_date(w["end"])
10631089
self._print(f"[UYAP BULK] pencere {w['start']}{w['end']} · {CATEGORY_TASINMAZ} · {province}")
1090+
self._beat(f"pencere {w['start']}{w['end']}: form dolduruluyor (kategori/il/tarih)")
10641091

10651092
self._dismiss_notices(page)
10661093
cat_ok = self._select_category_tasinmaz(page)
@@ -1077,6 +1104,7 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
10771104
save_bulk_state(state, self.store_dir)
10781105
self._print(" ARA (arama) kontrolü bulunamadı — pencere atlandı. `--diagnose` çıktısındaki aksiyon adaylarını paylaşın.")
10791106
return None
1107+
self._beat("ARA sonrası sonuç durumu bekleniyor")
10801108
result_html = self._wait_result_state(page)
10811109

10821110
exp = detect_session_expiration(result_html, page.url)
@@ -1117,6 +1145,7 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
11171145
processed_ids: set = set()
11181146
pages_failed: list = []
11191147
for pnum in pages_remaining(rec, valid_pages):
1148+
self._beat(f"sayfa {pnum} yükleniyor")
11201149
if not self._goto_page(page, pnum):
11211150
pages_failed.append(pnum)
11221151
self._print(f" sayfa {pnum} yüklenemedi/kart kümesi değişmedi — atlanıyor (pencere COMPLETE değil, tekrar denenir).")
@@ -1148,6 +1177,7 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
11481177
continue # hedefli edinim: yalnız istenen KAYIT NO işlenir
11491178
if max_records and summary["records_processed"] >= max_records:
11501179
return "MAX_RECORDS"
1180+
self._beat(f"KAYIT NO {card.get('kayit_no')}: belge ediniliyor (native)")
11511181
res = process_sold_auction(
11521182
card, acquire_documents=acquire, store_dir=self.store_dir,
11531183
genuine_path=self.genuine_path, discovery_only=discovery_only,
@@ -1190,8 +1220,21 @@ def _run_window(self, page, context, acquire, province, w, rec, state, summary,
11901220

11911221
# -- Canlı DOM yardımcıları (pragma) — gerçek gözlenen UYAP DOM'una uyarlanır ---------- #
11921222
def _find_gecmis_page(self, context): # pragma: no cover - canlı DOM
1193-
best = None
1223+
# ÖNCE URL'e göre ayıkla (ucuz, ASILMAZ): önceki koşumdan kalan TAKILI görüntüleyici/indirme
1224+
# sekmeleri (blob:/data:/file:/about:blank) ölü renderer'a sahip olabilir; page.content() ZAMAN
1225+
# AŞIMSIZ bloklar → o sekmelerin content()'ine HİÇ dokunma (bir saatlik asılmanın kök nedeni).
1226+
_skip_prefix = ("blob:", "data:", "file:", "chrome:", "about:", "devtools:", "view-source:")
1227+
app_pages = []
11941228
for p in context.pages:
1229+
try:
1230+
url = _fold(p.url or "")
1231+
except Exception:
1232+
continue
1233+
if not url or any(url.startswith(s) for s in _skip_prefix):
1234+
continue
1235+
app_pages.append(p)
1236+
best = None
1237+
for p in app_pages:
11951238
try:
11961239
fold = _fold(p.content())
11971240
except Exception:
@@ -1200,7 +1243,7 @@ def _find_gecmis_page(self, context): # pragma: no cover - canlı DOM
12001243
return p
12011244
if best is None and ("ilan" in fold or "ihale" in fold):
12021245
best = p
1203-
return best or (context.pages[0] if context.pages else None)
1246+
return best or (app_pages[0] if app_pages else None)
12041247

12051248
def _dismiss_notices(self, page): # pragma: no cover - canlı DOM
12061249
"""Bilgilendirme/duyuru pop-up'larını + açık kalmış Bootstrap modallarını KAPATIR.
@@ -1534,7 +1577,40 @@ def _report_auction(self, res: dict) -> None: # pragma: no cover - canlı çık
15341577
+ (f" · denetim={dec}" if dec else ""))
15351578

15361579
def _print(self, msg: str) -> None: # pragma: no cover - canlı çıktı
1537-
print(msg)
1580+
print(msg, flush=True)
1581+
1582+
def _beat(self, step: str) -> None: # pragma: no cover - canlı çıktı/heartbeat
1583+
"""Canlı adım nabzı: gözcü zamanlayıcısını sıfırlar + ilerlemeyi stderr'e (flush) yazar."""
1584+
self._hb_step = step
1585+
self._hb_ts = time.monotonic()
1586+
print(f"[UYAP BULK] · {step}", file=sys.stderr, flush=True)
1587+
1588+
def _start_watchdog(self): # pragma: no cover - zamanlayıcı iş parçacığı
1589+
"""Arka plan gözcüsü: canlı bir adım stall_seconds'ı aşarsa net tanı ile SERT sonlandır.
1590+
1591+
Takılı bir Playwright sync çağrısı (ör. ölü renderer'lı sekmede page.content()) ana iş parçacığını
1592+
kesintisiz bloklar; tek güvenli kurtarma os._exit'tir. Kontrol noktaları zaten sayfa-sayfa kaydedilir.
1593+
"""
1594+
stop = threading.Event()
1595+
1596+
def _mon():
1597+
while not stop.wait(5.0):
1598+
if not self._live_active:
1599+
continue
1600+
age = time.monotonic() - self._hb_ts
1601+
if age > self.stall_seconds:
1602+
sys.stderr.write(
1603+
f"\n[UYAP BULK] ZAMAN AŞIMI: '{self._hb_step}' adımı {int(age)}sn ilerlemedi "
1604+
f"(eşik {self.stall_seconds}sn; --stall-timeout ile ayarlanır). Açık sekme≈{self._tab_count}. "
1605+
f"Olası neden: önceki koşumdan kalan TAKILI görüntüleyici/indirme sekmesi 'page.content()'i "
1606+
f"blokluyor. ÇÖZÜM: Chrome'da YALNIZ 'Geçmiş İlanlar' sekmesini bırakıp diğer sekmeleri "
1607+
f"kapatın, sonra komutu tekrar çalıştırın. Kontrol noktaları kaydedildi; güvenle sonlandırılıyor.\n"
1608+
)
1609+
sys.stderr.flush()
1610+
os._exit(4)
1611+
1612+
threading.Thread(target=_mon, name="uyap-bulk-watchdog", daemon=True).start()
1613+
return stop
15381614

15391615
def _print_summary(self, s: dict) -> None: # pragma: no cover - canlı çıktı
15401616
self._print("\n[UYAP BULK] ÖZET")

0 commit comments

Comments
 (0)