fix(pipeline): 조용한 실패 3건 — 빈 추출/요약 success 박제 + misfire 침묵 스킵 차단

H1 marker_worker: PDF arm + split arm 에 빈 md_content 가드(office arm 동형 raise → queue 재시도 후
failed). 빈 추출(스캔/이미지 PDF)을 md_status=success+빈 md 로 박제하던 불변식 위반 제거.
H2 summarize_worker: 빈/think-only 요약을 ai_summary= 로 박제(completed 마크)하던 것 raise 로 가시화
+ briefing/digest loader 에 length(ai_summary)>0 방어(기존 누출  행도 배제).
H4 main.py: AsyncIOScheduler job_defaults misfire_grace_time 1s→45s — 단일 루프 1초 혼잡에 1분 컨슈머
틱이 run time missed 로 침묵 스킵하던 것 차단(coalesce 유지).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyungi
2026-06-20 05:03:03 +00:00
parent 403b05d971
commit dc9cbcc669
5 changed files with 21 additions and 2 deletions
+6 -1
View File
@@ -94,7 +94,12 @@ async def lifespan(app: FastAPI):
)
# APScheduler: 백그라운드 작업
scheduler = AsyncIOScheduler(timezone="Asia/Seoul")
scheduler = AsyncIOScheduler(
timezone="Asia/Seoul",
# 2026-06-20 H4: 기본 misfire_grace_time=1s 는 단일 asyncio 루프가 1초만 혼잡해도
# 1분 컨슈머 틱을 run time missed 로 침묵 스킵(에러·failed row 0). 45s 완화 + coalesce.
job_defaults={"misfire_grace_time": 45, "coalesce": True, "max_instances": 1},
)
# 상시 실행
scheduler.add_job(consume_queue, "interval", minutes=1, id="queue_consumer")
# PR-DocSrv-Markdown-Consumer-Split-1: markdown(marker) 전용 consumer.
+2
View File
@@ -42,6 +42,7 @@ _NEWS_WINDOW_SQL = text(f"""
AND d.created_at < :window_end
AND d.embedding IS NOT NULL
AND d.ai_summary IS NOT NULL
AND length(d.ai_summary) > 0
-- 안전 자료실 B-4: licensed_restricted 발행 차단 (digest 와 동일 공유 술어, 경로 일관성)
AND {restricted_exclude_sql("d")}
""")
@@ -66,6 +67,7 @@ _HISTORICAL_CANDIDATES_SQL = text(f"""
AND d.created_at < :hist_end
AND d.embedding IS NOT NULL
AND d.ai_summary IS NOT NULL
AND length(d.ai_summary) > 0
-- 안전 자료실 B-4: licensed_restricted 발행 차단 (공유 술어)
AND {restricted_exclude_sql("d")}
""")
+1
View File
@@ -42,6 +42,7 @@ _NEWS_WINDOW_SQL = text(f"""
AND d.created_at < :window_end
AND d.embedding IS NOT NULL
AND d.ai_summary IS NOT NULL
AND length(d.ai_summary) > 0
-- 안전 자료실 B-4: licensed_restricted 발행 차단 (모든 경로 공유 술어 = license_filter).
-- news 채널엔 현재 restricted 부재 = 방어적 게이트(미래 유료 news 소스 대비, 경로 누락 방지).
AND {restricted_exclude_sql("d")}
+6
View File
@@ -307,6 +307,10 @@ async def _process_single(
# ---- (7) image persist + md_content rewrite (Phase 1B.5) ----
md_content_raw = data["md_content"]
# 2026-06-20 H1: 빈 추출(스캔/이미지 PDF)을 md_status=success + 빈 md 로 박제 X
# (계약: md_status in {success,partial} => md 非공백). office arm 동형 raise → queue 재시도 후 failed.
if not md_content_raw.strip():
raise ValueError("empty md_content (blank extraction) — success 박제 차단")
images_resp = data.get("images") if MARKDOWN_IMAGE_PERSIST else None
saved_images: list[dict[str, Any]] = []
@@ -653,6 +657,8 @@ async def _process_split(
md_status = "success" if not failed else "partial"
stitched = "\n\n".join(b["md"] for b in succeeded)
if not stitched.strip():
raise ValueError("empty stitched md_content (all batches blank) — success 박제 차단")
md_content = _build_large_md_content(stitched[:LARGE_DOC_MD_CONTENT_HEAD_CHARS], manifest)
quality = _compute_quality(stitched, doc.extracted_text or "", {"page_count": page_count})
+6 -1
View File
@@ -91,7 +91,12 @@ async def process(document_id: int, session: AsyncSession, *, use_deep: bool = F
# sleep-안전 불변식: 쓰기는 전체 완주 후에만 — 중간 절단은 StageDeferred 로 빠져
# 이 지점에 도달하지 않는다 (carry 는 로컬 변수, doc 무변경).
doc.ai_summary = strip_thinking(summary)
final_summary = strip_thinking(summary)
# 2026-06-20 H2: 빈/think-only 요약을 ai_summary 빈문자열로 박제 → completed 마크 → briefing/digest 누출.
# raise → queue 재시도 후 failed(가시화). 기존 raise 계약(not-found·empty-text)과 동형.
if not final_summary.strip():
raise ValueError(f"empty ai_summary after strip (document_id={document_id})")
doc.ai_summary = final_summary
doc.ai_model_version = used_cfg.model
doc.ai_processed_at = datetime.now(timezone.utc)
logger.info(