refactor(board): 처리 머신 보드 나스+맥미니 2노드 재구성
2026-07-02 컷오버 반영 — GPU 서버 퇴역, 맥북 night-drain 보류(06-29 결정). - 레인 2개: 나스(추출/마크다운/청크·임베딩 등 DS 본체 Docker 스테이지), 맥미니(분류/요약/심층분석 — 단일 생성 LLM 허브 + bge-m3/리랭크) - summarize 풀 분리(summarize_by_machine·ai_model_version 조인 SQL) 제거 — FE 유일 소비자 확인 후 응답 스키마에서 정리 (5쿼리 -> 4쿼리) - 맥북 전제 UI 제거: 요약 오프로드 분담막대·요약 합류 칩·번다운 합류 변곡점 마커·잠듦 문구·전역 스트립 맥북 칩(맥미니 칩으로 대체) - deferred_pending = LLM 백오프 신호로 맥미니 카드 귀속 (기능 보존) - 번다운 차트·정직 ETA·실패 드로어·백그라운드 작업 등 머신 무관 기능 보존 - background_jobs 머신 귀속 기본값 gpu -> nas - 단위테스트 2노드 기준 재작성 (27 passed) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+66
-156
@@ -4,6 +4,8 @@ services/queue_overview 의 SQL 수집부와 분리된 순수 판정 함수
|
||||
(stage_machine_map / build_machines / build_summarize_eta / build_trend /
|
||||
build_totals / compute_eta_minutes / rows_to_* / display_title) 를
|
||||
mock 행으로 검증한다. 통합(실 SQL)은 배포 후 라이브 smoke 로 확인.
|
||||
|
||||
2026-07-02 컷오버 후 2노드(나스+맥미니) 기준 — 구 3노드 레인은 제거됨.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
@@ -18,7 +20,6 @@ from services.queue_overview import (
|
||||
compute_eta_minutes,
|
||||
display_title,
|
||||
rows_to_stage_stats,
|
||||
rows_to_summarize_split,
|
||||
stage_machine_map,
|
||||
)
|
||||
|
||||
@@ -36,186 +37,115 @@ def _stage(**kw) -> dict:
|
||||
return base
|
||||
|
||||
|
||||
def _split(macbook: dict | None = None, macmini: dict | None = None) -> dict:
|
||||
"""summarize 풀 완료 실적 split — 미지정 0."""
|
||||
zero = {"done_1h": 0, "done_today": 0, "done_15m": 0}
|
||||
return {
|
||||
"macbook": {**zero, **(macbook or {})},
|
||||
"macmini": {**zero, **(macmini or {})},
|
||||
}
|
||||
|
||||
|
||||
def _machine(machines: list[dict], key: str) -> dict:
|
||||
return next(m for m in machines if m["key"] == key)
|
||||
|
||||
|
||||
# ─── stage→machine 귀속 맵 ────────────────────────────────────────────────────
|
||||
|
||||
def test_stage_machine_map_deep_enabled():
|
||||
smap = stage_machine_map(deep_enabled=True)
|
||||
def test_stage_machine_map_two_nodes():
|
||||
smap = stage_machine_map()
|
||||
for s in ("extract", "embed", "chunk", "markdown", "preview", "thumbnail", "fulltext", "stt"):
|
||||
assert smap[s] == "gpu"
|
||||
assert smap[s] == "nas"
|
||||
assert smap["classify"] == "macmini"
|
||||
assert smap["summarize"] == "macmini"
|
||||
assert smap["deep_summary"] == "macbook"
|
||||
|
||||
|
||||
def test_stage_machine_map_deep_disabled():
|
||||
"""deep 슬롯 부재 시 deep_summary 도 macmini 귀속."""
|
||||
smap = stage_machine_map(deep_enabled=False)
|
||||
assert smap["deep_summary"] == "macmini"
|
||||
|
||||
|
||||
# ─── 머신 카드 귀속 합산 ──────────────────────────────────────────────────────
|
||||
|
||||
def test_gpu_stage_counts_attribution():
|
||||
def test_nas_stage_counts_attribution():
|
||||
stats = {
|
||||
"extract": _stage(pending=3, processing=1, done_1h=5, done_today=9, done_15m=1),
|
||||
"stt": _stage(failed=2, done_1h=1, done_today=2),
|
||||
}
|
||||
machines = build_machines(stats, _split(), [], deep_enabled=True)
|
||||
gpu = _machine(machines, "gpu")
|
||||
assert (gpu["pending"], gpu["processing"], gpu["failed"]) == (3, 1, 2)
|
||||
assert (gpu["done_1h"], gpu["done_today"]) == (6, 11)
|
||||
# gpu 의 stages 는 정적 8종 전부 (집계 0 이어도 표시)
|
||||
assert gpu["stages"] == [
|
||||
machines = build_machines(stats, [])
|
||||
nas = _machine(machines, "nas")
|
||||
assert (nas["pending"], nas["processing"], nas["failed"]) == (3, 1, 2)
|
||||
assert (nas["done_1h"], nas["done_today"]) == (6, 11)
|
||||
# nas 의 stages 는 정적 8종 전부 (집계 0 이어도 표시)
|
||||
assert nas["stages"] == [
|
||||
"extract", "embed", "chunk", "markdown",
|
||||
"preview", "thumbnail", "fulltext", "stt",
|
||||
]
|
||||
|
||||
|
||||
def test_summarize_pool_split_attribution():
|
||||
"""summarize pending/failed = macmini 귀속, 완료 실적은 split 로 분리 —
|
||||
stage-level summarize done 수치는 카드에 이중 합산되지 않는다."""
|
||||
def test_macmini_llm_stages_attribution():
|
||||
"""classify/summarize/deep_summary 전부 macmini 귀속 (단일 생성 LLM 허브)."""
|
||||
stats = {
|
||||
"classify": _stage(done_1h=2, done_today=3),
|
||||
"summarize": _stage(pending=7, failed=1, done_1h=10, done_today=20),
|
||||
"deep_summary": _stage(pending=2, processing=1, done_1h=3, done_today=4),
|
||||
}
|
||||
split = _split(macbook={"done_1h": 4, "done_today": 8}, macmini={"done_1h": 6, "done_today": 12})
|
||||
machines = build_machines(stats, split, [], deep_enabled=True)
|
||||
machines = build_machines(stats, [])
|
||||
macmini = _machine(machines, "macmini")
|
||||
macbook = _machine(machines, "macbook")
|
||||
|
||||
assert macmini["pending"] == 7 and macmini["failed"] == 1
|
||||
assert macmini["done_1h"] == 2 + 6 # classify + macmini 몫 (10 아님)
|
||||
assert macmini["done_today"] == 3 + 12
|
||||
assert macbook["done_1h"] == 4 and macbook["done_today"] == 8
|
||||
assert macbook["pending"] == 0 # 풀 pending 은 macmini 만
|
||||
assert macmini["pending"] == 9 and macmini["failed"] == 1
|
||||
assert macmini["processing"] == 1
|
||||
assert macmini["done_1h"] == 2 + 10 + 3
|
||||
assert macmini["done_today"] == 3 + 20 + 4
|
||||
assert macmini["stages"] == ["classify", "summarize", "deep_summary"]
|
||||
assert _machine(machines, "nas")["pending"] == 0
|
||||
|
||||
|
||||
def test_summarize_by_machine_projection():
|
||||
"""build_summarize_by_machine = split 의 done_1h/done_today 를 머신별로 투영
|
||||
(done_15m 은 제외 — 내부 state 판정 전용)."""
|
||||
from services.queue_overview import build_summarize_by_machine
|
||||
split = _split(
|
||||
macbook={"done_1h": 226, "done_today": 312, "done_15m": 60},
|
||||
macmini={"done_1h": 37, "done_today": 94, "done_15m": 9},
|
||||
)
|
||||
sbm = build_summarize_by_machine(split)
|
||||
assert sbm == {
|
||||
"macmini": {"done_1h": 37, "done_today": 94},
|
||||
"macbook": {"done_1h": 226, "done_today": 312},
|
||||
}
|
||||
assert "done_15m" not in sbm["macbook"]
|
||||
|
||||
|
||||
def test_compose_overview_includes_summarize_by_machine():
|
||||
"""compose_overview 응답 계약에 summarize_by_machine 포함 (FE 레인 분담 재료)."""
|
||||
now_kst = datetime(2026, 6, 13, 13, 0, tzinfo=KST)
|
||||
stats = {"summarize": _stage(pending=1317, done_1h=264)}
|
||||
split = _split(macbook={"done_1h": 226, "done_today": 312}, macmini={"done_1h": 37, "done_today": 94})
|
||||
ov = compose_overview(stats, split, {}, {}, [], deep_enabled=True, now_kst=now_kst)
|
||||
assert ov["summarize_by_machine"]["macbook"]["done_1h"] == 226
|
||||
assert ov["summarize_by_machine"]["macmini"]["done_today"] == 94
|
||||
|
||||
|
||||
def test_deep_disabled_deep_summary_counts_to_macmini():
|
||||
stats = {"deep_summary": _stage(pending=2, processing=1, done_1h=3, done_today=4)}
|
||||
machines = build_machines(stats, _split(), [], deep_enabled=False)
|
||||
macmini = _machine(machines, "macmini")
|
||||
macbook = _machine(machines, "macbook")
|
||||
assert macmini["pending"] == 2 and macmini["processing"] == 1
|
||||
assert macmini["done_1h"] == 3 and macmini["done_today"] == 4
|
||||
assert macbook["stages"] == [] and macbook["pending"] == 0
|
||||
assert _machine(machines, "macmini")["stages"] == ["classify", "summarize", "deep_summary"]
|
||||
|
||||
|
||||
def test_deferred_pending_always_on_macbook_card():
|
||||
"""보류(deferred_until 미래)는 summarize+deep_summary 합산으로 macbook 카드 귀속.
|
||||
deep 슬롯 유무와 무관 (보류 = 맥북 불가 신호)."""
|
||||
def test_deferred_pending_on_macmini_card():
|
||||
"""보류(deferred_until 미래)는 summarize+deep_summary 합산으로 macmini 카드 귀속
|
||||
(보류 = LLM 백오프 신호)."""
|
||||
stats = {
|
||||
"summarize": _stage(pending=5, deferred_pending=2),
|
||||
"deep_summary": _stage(pending=1, deferred_pending=1),
|
||||
}
|
||||
for deep_enabled in (True, False):
|
||||
machines = build_machines(stats, _split(), [], deep_enabled=deep_enabled)
|
||||
assert _machine(machines, "macbook")["deferred_pending"] == 3
|
||||
assert _machine(machines, "gpu")["deferred_pending"] == 0
|
||||
assert _machine(machines, "macmini")["deferred_pending"] == 0
|
||||
machines = build_machines(stats, [])
|
||||
assert _machine(machines, "macmini")["deferred_pending"] == 3
|
||||
assert _machine(machines, "nas")["deferred_pending"] == 0
|
||||
|
||||
|
||||
# ─── state 판정 ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_macbook_state_active_wins_over_deferred_while_working():
|
||||
def test_macmini_state_active_wins_over_deferred_while_working():
|
||||
"""가동 > 보류 (사용자 피드백 2026-06-11): 일하고 있으면 백오프 잔여가 있어도 '가동'.
|
||||
|
||||
보류 건수는 deferred_pending 필드가 별도로 전달 — 카드 라인이 표시.
|
||||
"""
|
||||
stats = {"summarize": _stage(pending=1, deferred_pending=1)}
|
||||
split = _split(macbook={"done_15m": 3})
|
||||
machines = build_machines(stats, split, [], deep_enabled=True)
|
||||
mb = _machine(machines, "macbook")
|
||||
assert mb["state"] == "active"
|
||||
assert mb["deferred_pending"] == 1
|
||||
stats = {"summarize": _stage(pending=1, deferred_pending=1, done_15m=3)}
|
||||
machines = build_machines(stats, [])
|
||||
mm = _machine(machines, "macmini")
|
||||
assert mm["state"] == "active"
|
||||
assert mm["deferred_pending"] == 1
|
||||
|
||||
|
||||
def test_macbook_state_deferred_only_when_not_working():
|
||||
def test_macmini_state_deferred_only_when_not_working():
|
||||
"""일이 멈춰 있고(처리 0·최근 완료 0) 백오프만 쌓인 상태에서만 '보류'."""
|
||||
stats = {"summarize": _stage(pending=1, deferred_pending=1)}
|
||||
machines = build_machines(stats, _split(), [], deep_enabled=True)
|
||||
assert _machine(machines, "macbook")["state"] == "deferred"
|
||||
machines = build_machines(stats, [])
|
||||
assert _machine(machines, "macmini")["state"] == "deferred"
|
||||
|
||||
|
||||
def test_macbook_state_active_on_recent_qwen_done():
|
||||
split = _split(macbook={"done_15m": 1})
|
||||
machines = build_machines({}, split, [], deep_enabled=True)
|
||||
assert _machine(machines, "macbook")["state"] == "active"
|
||||
|
||||
|
||||
def test_macbook_state_idle():
|
||||
machines = build_machines({}, _split(), [], deep_enabled=True)
|
||||
assert _machine(machines, "macbook")["state"] == "idle"
|
||||
|
||||
|
||||
def test_gpu_state_active_on_processing():
|
||||
stats = {"extract": _stage(processing=1)}
|
||||
machines = build_machines(stats, _split(), [], deep_enabled=True)
|
||||
assert _machine(machines, "gpu")["state"] == "active"
|
||||
|
||||
|
||||
def test_gpu_state_active_on_recent_done():
|
||||
stats = {"embed": _stage(done_15m=2)}
|
||||
machines = build_machines(stats, _split(), [], deep_enabled=True)
|
||||
assert _machine(machines, "gpu")["state"] == "active"
|
||||
|
||||
|
||||
def test_gpu_state_idle_when_old_done_only():
|
||||
stats = {"embed": _stage(done_1h=5, done_today=9)} # 15분 내 완료 없음
|
||||
machines = build_machines(stats, _split(), [], deep_enabled=True)
|
||||
assert _machine(machines, "gpu")["state"] == "idle"
|
||||
|
||||
|
||||
def test_macmini_state_not_active_on_macbook_pool_done():
|
||||
"""summarize 풀 완료가 전부 macbook 몫이면 macmini 는 active 아님 (귀속 기준)."""
|
||||
stats = {"summarize": _stage(done_15m=1)}
|
||||
split = _split(macbook={"done_15m": 1})
|
||||
machines = build_machines(stats, split, [], deep_enabled=True)
|
||||
def test_macmini_state_idle():
|
||||
machines = build_machines({}, [])
|
||||
assert _machine(machines, "macmini")["state"] == "idle"
|
||||
|
||||
|
||||
def test_nas_state_active_on_processing():
|
||||
stats = {"extract": _stage(processing=1)}
|
||||
machines = build_machines(stats, [])
|
||||
assert _machine(machines, "nas")["state"] == "active"
|
||||
|
||||
|
||||
def test_nas_state_active_on_recent_done():
|
||||
stats = {"embed": _stage(done_15m=2)}
|
||||
machines = build_machines(stats, [])
|
||||
assert _machine(machines, "nas")["state"] == "active"
|
||||
|
||||
|
||||
def test_nas_state_idle_when_old_done_only():
|
||||
stats = {"embed": _stage(done_1h=5, done_today=9)} # 15분 내 완료 없음
|
||||
machines = build_machines(stats, [])
|
||||
assert _machine(machines, "nas")["state"] == "idle"
|
||||
|
||||
|
||||
def test_macmini_state_active_on_summarize_processing():
|
||||
stats = {"summarize": _stage(processing=1)}
|
||||
machines = build_machines(stats, _split(), [], deep_enabled=True)
|
||||
machines = build_machines(stats, [])
|
||||
assert _machine(machines, "macmini")["state"] == "active"
|
||||
|
||||
|
||||
@@ -228,21 +158,18 @@ def test_current_summarize_to_macmini_max_two():
|
||||
{"stage": "summarize", "document_id": 3, "title": "문서C", "original_filename": None, "file_path": None},
|
||||
{"stage": "extract", "document_id": 4, "title": "문서D", "original_filename": None, "file_path": None},
|
||||
]
|
||||
machines = build_machines({}, _split(), rows, deep_enabled=True)
|
||||
machines = build_machines({}, rows)
|
||||
macmini = _machine(machines, "macmini")
|
||||
gpu = _machine(machines, "gpu")
|
||||
nas = _machine(machines, "nas")
|
||||
assert [c["document_id"] for c in macmini["current"]] == [1, 2] # 최대 2건
|
||||
assert macmini["current"][0] == {"document_id": 1, "title": "문서A", "stage": "summarize"}
|
||||
assert [c["document_id"] for c in gpu["current"]] == [4]
|
||||
assert _machine(machines, "macbook")["current"] == []
|
||||
assert [c["document_id"] for c in nas["current"]] == [4]
|
||||
|
||||
|
||||
def test_current_deep_summary_follows_deep_slot():
|
||||
def test_current_deep_summary_to_macmini():
|
||||
rows = [{"stage": "deep_summary", "document_id": 9, "title": "심층", "original_filename": None, "file_path": None}]
|
||||
enabled = build_machines({}, _split(), rows, deep_enabled=True)
|
||||
disabled = build_machines({}, _split(), rows, deep_enabled=False)
|
||||
assert _machine(enabled, "macbook")["current"][0]["document_id"] == 9
|
||||
assert _machine(disabled, "macmini")["current"][0]["document_id"] == 9
|
||||
machines = build_machines({}, rows)
|
||||
assert _machine(machines, "macmini")["current"][0]["document_id"] == 9
|
||||
|
||||
|
||||
def test_display_title_fallback_chain():
|
||||
@@ -344,32 +271,15 @@ def test_rows_to_stage_stats_conversion():
|
||||
assert stats["summarize"]["deferred_pending"] == 2
|
||||
|
||||
|
||||
def test_rows_to_summarize_split_conversion():
|
||||
rows = [
|
||||
(True, 4, 8, 1), # is_macbook
|
||||
(False, 6, 12, 0),
|
||||
]
|
||||
split = rows_to_summarize_split(rows)
|
||||
assert split["macbook"] == {"done_1h": 4, "done_today": 8, "done_15m": 1}
|
||||
assert split["macmini"] == {"done_1h": 6, "done_today": 12, "done_15m": 0}
|
||||
|
||||
|
||||
def test_rows_to_summarize_split_empty():
|
||||
split = rows_to_summarize_split([])
|
||||
assert split["macbook"]["done_1h"] == 0 and split["macmini"]["done_today"] == 0
|
||||
|
||||
|
||||
def test_compose_overview_contract_shape():
|
||||
"""응답 dict 의 키가 FE 계약 shape 과 정확히 일치하는지 고정."""
|
||||
out = compose_overview(
|
||||
{"summarize": _stage(pending=1)},
|
||||
_split(),
|
||||
{}, {}, [],
|
||||
deep_enabled=True,
|
||||
now_kst=datetime(2026, 6, 11, 14, 30, tzinfo=KST),
|
||||
)
|
||||
assert set(out.keys()) == {"machines", "stages", "summarize_eta", "trend_24h", "totals"}
|
||||
assert [m["key"] for m in out["machines"]] == ["gpu", "macmini", "macbook"]
|
||||
assert [m["key"] for m in out["machines"]] == ["nas", "macmini"]
|
||||
for m in out["machines"]:
|
||||
assert set(m.keys()) == {
|
||||
"key", "label", "state", "stages", "pending", "processing", "failed",
|
||||
@@ -381,7 +291,7 @@ def test_compose_overview_contract_shape():
|
||||
assert set(out["trend_24h"][0].keys()) == {"hour", "inflow", "done"}
|
||||
assert set(out["totals"].keys()) == {"pending", "processing", "failed"}
|
||||
# 머신 label 고정 (raw 모델명 노출 금지 — label 만)
|
||||
assert [m["label"] for m in out["machines"]] == ["GPU 서버", "맥미니", "맥북 M5 Max"]
|
||||
assert [m["label"] for m in out["machines"]] == ["나스", "맥미니"]
|
||||
|
||||
|
||||
# ─── build_stages (단계별 현황 — 2026-06-11 사용자 피드백: 완료 가시화) ──────
|
||||
|
||||
Reference in New Issue
Block a user