# Conflicts: # .env.example # solution/backend/common/enums.py # solution/backend/requirements.txt # solution/backend/services/site_payload.py # solution/backend/services/snapshot.py # solution/backend/services/weather_notes.json # solution/shared/src/types/site-payload.ts # solution/site/src/layouts/editorial/Shell.tsx # solution/site/src/lib/use-live-weather.ts # solution/site/src/pages/HomePage.tsx # solution/site/src/sections/SiteFooter.tsx # solution/site/src/sections/WeatherSection.tsx # solution/site/src/sections/index.ts
84 lines
3.7 KiB
Python
84 lines
3.7 KiB
Python
"""스케줄 잡 로직(what). '언제 도느냐'(scheduler/__init__.py)와 분리된, 잡이 실제로 하는 일.
|
|
|
|
잡은 '대상을 고르는 것'까지만 하고, 실제 처리는 도메인 service 가 책임진다.
|
|
(지역정보 갱신 · 수집 재시도 · 개별 사이트 재빌드가 여기로 들어온다.)
|
|
"""
|
|
from common.logger import LOG
|
|
|
|
"""예약 실행 진입점. 복구 전이는 DB 조건부 UPDATE로 여러 프로세스에서도 안전하다."""
|
|
from crud.social_crud import sweep
|
|
|
|
async def sweep_social_posts():
|
|
await sweep()
|
|
|
|
async def sweep_alert_outbox():
|
|
"""대기 중인 알림을 실제로 보낸다(services/alert_service.process_outbox)."""
|
|
from services import alert_service
|
|
|
|
try:
|
|
await alert_service.process_outbox()
|
|
except Exception as ex: # noqa: BLE001 — 스윕 실패가 스케줄러를 죽이면 안 된다(다음 주기 재시도)
|
|
LOG.w(f"[scheduler] 알림 발송 스윕 실패: {type(ex).__name__}: {ex}")
|
|
|
|
|
|
async def sweep_queue_health():
|
|
"""잡 큐가 막혔는지 주기적으로 본다 — dead-letter 누적·좀비 실행·오래 밀린 PENDING.
|
|
|
|
★ 왜 필요한가: 개별 잡의 DEAD 전이는 worker/runner.py 가 그 자리에서 바로 알린다. 이건
|
|
그것과 다른 신호다 — 잡 하나하나는 재시도 중(아직 DEAD 아님)인데 **큐 전체가 정체**된
|
|
경우(워커 프로세스가 죽었거나 DB 순단이 길어지는 경우)는 개별 잡 알림만으로는 안 보인다.
|
|
★ 복구되면 한 번만 알린다 — send_alert/resolve_alert 의 dedupe_key 가 그 판단을 한다."""
|
|
from crud.job_crud import JobQueue
|
|
from services import alert_service
|
|
|
|
try:
|
|
snap = await JobQueue().ops()
|
|
except Exception as ex: # noqa: BLE001
|
|
LOG.w(f"[scheduler] 큐 상태 조회 실패: {type(ex).__name__}: {ex}")
|
|
return
|
|
|
|
# 기준값: dead-letter 가 최근 1시간에 쌓였거나, 좀비 실행이 있거나, 가장 오래된 PENDING 이
|
|
# 30분 넘게 안 집혔다(정상 워커라면 대기 잡을 몇 초 안에 claim 한다).
|
|
problems = []
|
|
if snap.get("dead_1h", 0) > 0:
|
|
problems.append(f"최근 1시간 dead-letter {snap['dead_1h']}건")
|
|
if snap.get("stuck_running", 0) > 0:
|
|
problems.append(f"좀비 실행 {snap['stuck_running']}건(lease 만료 또는 10분 초과)")
|
|
if snap.get("oldest_pending_sec", 0) > 1800:
|
|
problems.append(f"가장 오래된 대기 잡이 {snap['oldest_pending_sec'] // 60}분째 안 집힘")
|
|
|
|
dedupe_key = "queue_health"
|
|
if problems:
|
|
await alert_service.send_alert(
|
|
kind="queue_stuck",
|
|
title="잡 큐 정체",
|
|
detail=" · ".join(problems) + f"\n{snap}",
|
|
dedupe_key=dedupe_key,
|
|
)
|
|
else:
|
|
await alert_service.resolve_alert(dedupe_key, "잡 큐 정상으로 돌아옴")
|
|
|
|
|
|
async def sweep_blog_drafts():
|
|
"""미니 블로그 재고 채우기(services/blog_jobs.generate_drafts)."""
|
|
from services import blog_jobs
|
|
|
|
try:
|
|
made = await blog_jobs.generate_drafts()
|
|
if made:
|
|
LOG.i(f"[scheduler] 미니 블로그 초안 {made}건 생성")
|
|
except Exception as ex: # noqa: BLE001 — 생성 실패가 스케줄러를 죽이면 안 된다
|
|
LOG.w(f"[scheduler] 미니 블로그 생성 실패: {type(ex).__name__}: {ex}")
|
|
|
|
|
|
async def sweep_blog_mail():
|
|
"""검수를 통과한 글을 사장님에게 보낸다(services/blog_jobs.send_reviewed)."""
|
|
from services import blog_jobs
|
|
|
|
try:
|
|
sent = await blog_jobs.send_reviewed()
|
|
if sent:
|
|
LOG.i(f"[scheduler] 미니 블로그 메일 {sent}통 발송")
|
|
except Exception as ex: # noqa: BLE001
|
|
LOG.w(f"[scheduler] 미니 블로그 발송 실패: {type(ex).__name__}: {ex}")
|