Skip to content

[FEATURE] Host signal scoring 기반 동적 High↔Normal priority 결정 (#380 Sub B) #382

Description

@juhy0987

부모

부모 이슈: #380 (메타)
의존: Sub A (#381 — CategoryBased + Retry) 머지 후

배경

Sub A 의 정적 카테고리 매핑은 host metadata 가 부재한 신규 host / 미분류 host 에 대해 모두 Low 로 fallback. 또한 같은 카테고리 안에서도 host 별 신뢰도 / 비용 / 시의성이 다른데 일률적으로 동일 priority 부여됨.

본 Sub 는 host 단위 signal 을 누적하여 High 와 Normal 사이의 동적 분기를 자동화한다. Low 후보 (Sub A 가 결정) 는 본 scoring 대상 X — Low → Normal/High 승급 없음.

Scoring 모델

입력 signal (host 단위, 시계열 누적)

Signal 의미 출처
`freshness` publish→detect lag — 짧을수록 높음 metric (validator 의 published_at vs created_at)
`impact` 카테고리 type 가중치 (정치/경제=高, 연예=中) host metadata + 카테고리 매핑 (Sub A 와 공유)
`cost` chromedp 비율 + LLM 재학습 빈도 + 평균 fetch latency — 비용 ↑ → 점수 ↓ metrics registry
`reliability` host_breaker 발동율 + HTTP 4xx/5xx 비율 — 안정 ↑ → 점수 ↑ circuit breaker + metrics
`host_trust` 호스트 누적 신뢰도 (passed/rejected validation 비율, 운영자 평가) DB aggregate

집계

score = w_fresh × freshness
      + w_impact × impact
      + w_trust × host_trust
      − w_cost × cost
      − w_reliability_penalty × (1 − reliability)
  • score 는 [0, 1] 정규화
  • weight 는 cluster-wide env (per-host 는 Sub C 에서 확장)
  • mapping 은 High vs Normal 2-tier 만: score ≥ θ → High, 그 외 → Normal
  • θ (threshold) 도 env 노출 (default 0.5)

Cold-start

신규 host 는 signal 누적 부족 → score 계산 skip → Sub A 의 결정에 그대로 위임 (대부분 Low 또는 매핑된 카테고리)

인프라

1. 신규 테이블 `host_scoring_state`

```sql
host TEXT PRIMARY KEY,
score REAL, -- 0~1
signals JSONB, -- 입력값 스냅샷
window_minutes INT,
calculated_at TIMESTAMPTZ
```

2. Scorer goroutine

  • 5분 주기 polling (env: `SCORING_INTERVAL`, default 5m)
  • 모든 host (또는 active subset) 에 대해 signal 집계 → score 계산 → state 갱신
  • 별도 stage (parserStage 류) 또는 main goroutine 내 부속

3. Redis cache

  • key: `host_score:` → JSON `{score, priority, calculated_at}`
  • TTL: scorer 주기와 동일 (5분)
  • Resolver 핫패스가 직접 lookup (1ms 이하)

4. `DynamicScorePriorityResolver`

  • Redis lookup → 없으면 DB lookup → 둘 다 없으면 CanResolve=false (다음 resolver 로 위임)
  • score 가 있어도 카테고리가 명시적으로 Low 인 job 은 본 resolver 거치지 않음 (Sub A 가 chain 상 먼저 결정)

Resolver chain 위치

Sub A 의 CategoryBased 다음 — Low 가 이미 결정된 job 은 본 resolver 진입 안 함:

```
... → CategoryBasedResolver → DynamicScorePriorityResolver → SourcePriorityResolver → ...
```

CategoryBased 가 Normal 또는 High 매핑한 후 본 resolver 가 host 신호로 재조정.
단, 본 resolver 가 결정한 후 chain 종료 (이후 resolver 는 skip) — score 결정이 가장 정밀한 신호.

운영 결정 (사용자 확정)

  • Weight 운영 인터페이스: env-only
  • per-host weight 는 Sub C 의 scope
  • Low 후보는 그대로 (scoring 대상 X)

영향 / 위험

효과

  • 같은 카테고리 안에서도 신뢰도/비용 기반 정교한 분배
  • 비용 높은 host (chromedp/LLM 비중 大) 는 Normal 로 강등 → high pool 의 자원 보존
  • 시의성 높은 host (실시간 update 多) 는 High 로 승급

위험

  • Scoring 인프라 자체 비용 (DB row scan + Redis IO + goroutine) — 5분 주기 cap 으로 흡수
  • Signal 수집의 정확성 의존 — metrics registry 의 host dim 확장 필요
  • Cold-start 처리 (score 없음 → CategoryBased 결정 그대로) 가 운영자 기대와 다를 수 있음

완료 조건

  • host signal 수집 — metrics 확장 + DB aggregate query
  • `host_scoring_state` 테이블 + migration
  • Scorer goroutine 구현 + 5분 주기 polling
  • Redis cache wiring
  • `DynamicScorePriorityResolver` + chain 등록
  • env weight + threshold 노출
  • 단위 테스트 (signal 0/1 host scoring, cold-start)
  • 라이브 1 cycle 검증 — 같은 카테고리 안에서 high/normal 분배 관찰

참고

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions