در یک run واقعی ممکن است ۱۰٬۰۰۰ URL هدف داشته باشید؛ ۹٬۷۰۰ موفق و ۳۰۰ شکست. سیستم درست ۹٬۷۰۰ را commit می‌کند، ۳۰۰ را با جزئیات لاگ می‌کند، در صورت نیاز در صف «بررسی بعدی» (dead-letter) نگه می‌دارد و به تیم alert می‌دهد — نه rollback کل batch و نه نادیده گرفتن خطاها. این مقاله الگوهای resilience در پروژه استخراج داده را جمع می‌کند؛ زمان‌بندی job در اتوماسیون زمان‌بندی‌شده.

خلاصه
  • خطا را دسته‌بندی کنید: گذرا (retry) در برابر دائمی (fix + DLQ).
  • ۴۲۹ و ۵xx با backoff؛ ۴۰۰ معمولاً retry بی‌فایده است.
  • تغییر layout → parser failure — circuit breaker تا dataset خراب نشود.
  • partial success + idempotency برای upsert امن.

دسته‌بندی خطا

شبکه: timeout، DNS، TLS — اغلب گذرا؛ با retry محدود و jitter درست می‌شود.
HTTP: ۴۰۴ یعنی URL اشتباه یا حذف شده؛ ۴۰۱/۴۰۳ یعنی session یا مجوز؛ ۴۲۹ یعنی rate limit — سرعت را کم کنید نه فقط retry سریع.
۵xx: خطای سرور منبع — retry با backoff؛ اگر پایدار شد منبع down است.
Parser: selector چیزی پیدا نکرد یا schema عوض شد — معمولاً دائمی تا patch کد.
Database: deadlock، disk full — جدا از scrape؛ ممکن است run را fail کنید اما دادهٔ fetch شده را از دست ندهید (buffer یا فایل موقت).
Partial success: در یک batch بعضی URLها OK و بعضی نه — رفتار پیش‌فرض باید per-URL باشد نه all-or-nothing.

Retry، backoff و timeout

Retry فقط برای خطاهایی که با تکرار ممکن است برطرف شوند. الگوی رایج: exponential backoff (مثلاً ۱s، ۲s، ۴s) به‌اضافه jitter تا همه workerها هم‌زمان منبع را نزنند. سقف تلاش (مثلاً ۵ بار) تعریف کنید؛ بعد از آن URL به DLQ.

Timeout را جدا تنظیم کنید: connect کوتاه‌تر، read بلندتر برای صفحات سنگین. job بدون timeout می‌تواند worker را ساعت‌ها block کند و overlap job بعدی را خراب کند.

اشتباه رایج

retry بی‌حد روی ۴۲۹ همان منبع را سریع‌تر بلاک می‌کند. اول throttle یا کاهش concurrency.

Logging و alerting

هر run یک run_id داشته باشد. برای هر شکست: URL، HTTP status، مرحله (fetch/parse/store)، پیام کوتاه. نمونهٔ خطا برای QA ذخیره کنید (نه همیشه کل HTML).

هشدار روی SLO: مثلاً اگر کمتر از ۹۹٪ URL در یک اجرا موفق شد یا اگر ۱۰۰٪ فیلد قیمت null شد (نشانهٔ تغییر schema). این با اعتبارسنجی پس از یکدست‌سازی هم‌راستا است.

Idempotency

همان URL ممکن است دو بار پردازش شود (retry، requeue). ذخیره باید upsert روی کلید یکتا باشد نه insert تکراری. برای side effect بیرونی (ارسال ایمیل، webhook) از idempotency key استفاده کنید.

Dead-letter queue (مفهومی)

URLهایی که بعد از N تلاش شکست خوردند از صف اصلی خارج و در DLQ می‌مانند تا انسان یا job جداگانه بعد از fix selector دوباره پردازش کند. بدون DLQ، یا خطاها گم می‌شوند یا همان URL بی‌نهایت retry می‌شود.

مثال: ۱۰٬۰۰۰ URL، ۳۰۰ شکست

  1. ۹٬۷۰۰ رکورد validate و در DB commit.
  2. ۳۰۰ رکورد: status=failed، reason، last_http_code در جدول run_errors.
  3. Dashboard: نرخ موفقیت ۹۷٪؛ اگر زیر آستانه → alert.
  4. ۳۰۰ مورد: ۵۰ تا ۴۲۹ → requeue با delay؛ ۲۰۰ تا parse → DLQ تا patch؛ ۵۰ تا ۴۰۴ → archive (حذف از لیست هدف).
python">
# شبه‌کد — پردازش per-URL
for url in urls:
    try:
        html = fetch(url, timeout=(5, 60))
        row = parse(html)
        validate(row)
        upsert(row, key=row["source_id"])
        ok += 1
    except TransientError as e:
        requeue(url, delay=backoff(attempts[url]))
    except ParseError as e:
        dlq.append({"url": url, "error": str(e)})
    except ValidationError as e:
        quarantine.append(row)
stats.emit(run_id, ok=ok, failed=len(dlq) + len(quarantine))

تغییر schema و circuit breaker

اگر layout عوض شود ممکن است parser «موفق» باشد اما همه قیمت‌ها null — خطرناک‌تر از fail صریح. rule: اگر بیش از X٪ رکورد یک فیلد اجباری خالی شد، run را متوقف کنید (circuit breaker)، on-call selector را patch کند، سپس reprocess از DLQ یا snapshot خام.

صفحات JS: خطای timeout گاهی با wait strategy در Playwright کم می‌شود؛ اگر منبع API دارد اول مسیر API را بررسی کنید.

در عمل چه انتخابی کنیم؟

همیشه partial success + DLQ + metric؛ هرگز «یا همه یا هیچ» برای ده‌ها هزار URL مگر business واقعاً بخواهد. MVP: retry ساده + فایل لاگ شکست‌ها. Production: DLQ، SLO alert، idempotent upsert، circuit breaker روی فیلدهای حیاتی. برای اجرای شبانه پایدار: زمان‌بندی و queue.