7cd8cfde0a
A-3 migrations 319-323 (news_sources 9컬럼 + source_channel 'crawl' + process_stage 'fulltext' + source_health) A-1 조건부 GET(ETag/Last-Modified 그대로 재전송)+콘텐츠 해시 변경감지, A-4 politeness 코어(per-domain 직렬+robots+정직UA), A-2+A-7 fulltext_worker(4-tier 재사용·NAS crawl_raw gzip 보존·격하 경로·03:40 reconcile 안전망), A-5 circuit breaker(3/10 임계, enabled 미터치), A-6 포털 전재 2차 dedup(제목+3일, 12자 게이트). 기존 소스 fulltext_policy='none' 기본 = 무회귀. plan crawl-24x7-1, 예외 박제 crawl-24x7-exec1-20260610.md
37 lines
1.7 KiB
Python
37 lines
1.7 KiB
Python
"""source_health 테이블 ORM (A-5, plan crawl-24x7-1)
|
|
|
|
news_sources 와 1:1. 소스별 fetch 성공/실패 기록 + circuit breaker 상태.
|
|
silent skip 누적 방지의 가시성 기반 — A-8 헬스 패널이 읽는다.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String, Text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from core.database import Base
|
|
|
|
|
|
class SourceHealth(Base):
|
|
__tablename__ = "source_health"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
source_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("news_sources.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
consecutive_failures: Mapped[int] = mapped_column(Integer, default=0)
|
|
total_fetches: Mapped[int] = mapped_column(BigInteger, default=0)
|
|
total_failures: Mapped[int] = mapped_column(BigInteger, default=0)
|
|
last_success_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
last_error: Mapped[str | None] = mapped_column(Text)
|
|
last_error_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
last_fetch_items: Mapped[int | None] = mapped_column(Integer)
|
|
# 200 인데 entries 0 인 연속 fetch 횟수 (304/해시동일은 미집계 — 피드 부패 신호 전용)
|
|
empty_streak: Mapped[int] = mapped_column(Integer, default=0)
|
|
# closed(정상) / open(연속 실패 → 지수 backoff) / disabled(임계 초과, 수동 복구 대상)
|
|
circuit_state: Mapped[str] = mapped_column(String(10), default="closed")
|
|
circuit_opened_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=datetime.now
|
|
)
|