63457e6afc
주제(study_topic) 메타를 발행 레이어에 실어 viewer 가 주제/회차 단위 퀴즈를 구성하게 한다(현재 topic 이름 미발행이라 불가). plan study-viewer-port S-1. - publish_projection: KIND_TOPIC + project_topic(topic_id·name·exam_round_size). 회차는 미발행 = viewer 가 pub_content(study_question) 의 exam_name/exam_round 로 파생(추가 발행 불요). topic_id = project_question.topic_id 와 동일 DS 식별자라 viewer 문항→주제 상관 키(pub_id 는 opaque 라 상관 키 아님). - publish_enqueue: enqueue_topic_publish + backfill_publish_topics(bounded page, deleted_at IS NULL). 멱등 = 워커 (payload_hash, deleted) 디둡. - study_topics 저작훅(전부 study_publish_enabled 게이트): create(flush→enqueue→ commit) / update(재투영, payload 무변경은 디둡이 rev 안 올림=churn 0) / delete(tombstone, raw DELETE 금지·워커 경유). - scripts/backfill_publish_topics.py: 기존 주제 1회 outbox 적재(overflow 가드). 워커·/published/feed 는 kind-generic(무변경, 실측). flag on 환경 배포 시 주제 발행 시작 → S-3 viewer 수용(generic upsert·kind-filtered read) 선행 전제, 게이트 PASS 됨. 백필 실행·배포순서 cutover 는 deploy 게이트(소프트락)라 본 슬라이스 미포함. py_compile PASS · project_topic payload 단위검증. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.5 KiB
Python
70 lines
2.5 KiB
Python
"""S-1 초기 백필 — 기존 active study_topics 를 발행 outbox 에 1회 적재.
|
|
|
|
publish_outbox 에만 적재한다(멱등: 발행 워커의 (payload_hash, deleted) 디둡이
|
|
중복 enqueue 를 no-op 으로 흡수). study_publish_enabled=True 일 때 발행 워커가
|
|
1분 주기로 drain → published 에 rev 부여 → viewer pull-sync.
|
|
|
|
주제 수는 개인 학습툴이라 소량 — bounded page 사실상 1페이지지만 PAGE 도달 시
|
|
overflow 가드로 페이징 누락을 경보(silent truncation 금지).
|
|
|
|
실행 (GPU 서버):
|
|
docker exec hyungi_document_server-fastapi-1 python /app/scripts/backfill_publish_topics.py
|
|
# dry-run(적재 없이 카운트만):
|
|
docker exec hyungi_document_server-fastapi-1 python /app/scripts/backfill_publish_topics.py --dry-run
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
|
|
# fastapi 컨테이너 WORKDIR=/app — `from models...` import 가능하게 path 추가.
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
from sqlalchemy import func, select
|
|
|
|
from core.config import settings
|
|
from core.database import async_session
|
|
from models.study_topic import StudyTopic
|
|
from services.study.publish_enqueue import backfill_publish_topics
|
|
|
|
# 개인 학습툴 주제 수 대비 넉넉. 도달 시 overflow 가드가 경보.
|
|
PAGE = 5000
|
|
|
|
|
|
async def run(dry_run: bool) -> None:
|
|
async with async_session() as session:
|
|
active = (
|
|
await session.execute(
|
|
select(func.count())
|
|
.select_from(StudyTopic)
|
|
.where(StudyTopic.deleted_at.is_(None))
|
|
)
|
|
).scalar() or 0
|
|
|
|
print(f"[info] study_publish_enabled={settings.study_publish_enabled} "
|
|
f"(False 면 적재는 되나 워커가 drain 안 함)")
|
|
print(f"[info] active 주제 {active}건")
|
|
if dry_run:
|
|
print("[dry-run] 적재 안 함. 실제 실행은 --dry-run 제거.")
|
|
return
|
|
|
|
async with async_session() as session:
|
|
n = await backfill_publish_topics(session, after_id=0, limit=PAGE)
|
|
await session.commit()
|
|
|
|
print(f"\n[ok] outbox 적재 {n}건 — 발행 워커가 drain(flag on 시) 하며 rev 부여.")
|
|
if n >= PAGE:
|
|
print(f"[warn] PAGE({PAGE}) 도달 — 주제가 더 있을 수 있음. after_id 페이징 추가 필요.")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="S-1 pub_topics 초기 백필")
|
|
parser.add_argument("--dry-run", action="store_true", default=False)
|
|
args = parser.parse_args()
|
|
asyncio.run(run(args.dry_run))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|