f60d6e52fc
worker-pool-policy §B 1B 영역 완료. 1A scaffold (mig 270~274 + 503 stub) 위에:
- mig 275/276: worker_jobs (status CHECK + user_id=owner) + pending partial index
- create_laptop_worker_bot_token + require_worker_user dependency (voice-memo 동형)
- /internal/worker/{register,heartbeat,claim,result,drain} 5 endpoint 실 구현
- /claim FOR UPDATE SKIP LOCKED + 204 body 0
- /result 소유권 검증 (worker_id 매칭, 404) + failed 재시도 (attempts/max)
- explicit failure 시 request.result 무시 (DB result NULL 유지)
- 테스트 22 항목 7 파일
policy §B.2 5 invariant 보존: voice-memo wrapper 변경 0, drain advisory,
result raw JSONB, ProcessingQueue 무변경, 운영 자동 분기 변경 0.
활용처 (recap context + /jobs/recap + payload 100KB guard) = Registry-1C 영역.
stale recovery / 노트북 client / canonical promote = Notebook-Pilot-1 영역.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
3.3 KiB
Python
77 lines
3.3 KiB
Python
"""worker_capabilities + worker_heartbeats + worker_jobs 테이블 ORM.
|
|
|
|
1A scaffold (mig 270~274) + 1B 활성화 (mig 275~276). 1B = WorkerJob 신규 + 5 endpoint 실 구현.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import BigInteger, DateTime, ForeignKey, SmallInteger, Text
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from core.database import Base
|
|
|
|
|
|
class WorkerCapability(Base):
|
|
__tablename__ = "worker_capabilities"
|
|
|
|
worker_id: Mapped[str] = mapped_column(Text, primary_key=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
BigInteger, ForeignKey("users.id"), nullable=False
|
|
)
|
|
device_label: Mapped[str] = mapped_column(Text, nullable=False)
|
|
worker_class: Mapped[str] = mapped_column(Text, nullable=False)
|
|
tier: Mapped[str] = mapped_column(Text, nullable=False)
|
|
capabilities: Mapped[list] = mapped_column(JSONB, default=list, nullable=False)
|
|
models_loaded: Mapped[list] = mapped_column(JSONB, default=list, nullable=False)
|
|
endpoint: Mapped[str | None] = mapped_column(Text)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=datetime.now, nullable=False
|
|
)
|
|
last_registered_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=datetime.now, nullable=False
|
|
)
|
|
|
|
|
|
class WorkerHeartbeat(Base):
|
|
__tablename__ = "worker_heartbeats"
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
worker_id: Mapped[str] = mapped_column(
|
|
Text, ForeignKey("worker_capabilities.worker_id"), nullable=False
|
|
)
|
|
heartbeat_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=datetime.now, nullable=False
|
|
)
|
|
status: Mapped[str] = mapped_column(Text, nullable=False)
|
|
current_job_id: Mapped[int | None] = mapped_column(BigInteger)
|
|
battery: Mapped[str | None] = mapped_column(Text)
|
|
thermal: Mapped[str | None] = mapped_column(Text)
|
|
raw_payload: Mapped[dict] = mapped_column(JSONB, default=dict, nullable=False)
|
|
|
|
|
|
class WorkerJob(Base):
|
|
# user_id = job owner user_id (실 사용자). worker bot 아님. worker 인증은 worker_id+JWT 별도.
|
|
# result = raw JSONB only (policy §B.2 invariant 3 — canonical promote = Notebook-Pilot-1).
|
|
__tablename__ = "worker_jobs"
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
BigInteger, ForeignKey("users.id"), nullable=False
|
|
)
|
|
job_type: Mapped[str] = mapped_column(Text, nullable=False)
|
|
status: Mapped[str] = mapped_column(Text, nullable=False, default="pending")
|
|
worker_id: Mapped[str | None] = mapped_column(
|
|
Text, ForeignKey("worker_capabilities.worker_id")
|
|
)
|
|
payload: Mapped[dict] = mapped_column(JSONB, default=dict, nullable=False)
|
|
result: Mapped[dict | None] = mapped_column(JSONB)
|
|
error_message: Mapped[str | None] = mapped_column(Text)
|
|
attempts: Mapped[int] = mapped_column(SmallInteger, default=0, nullable=False)
|
|
max_attempts: Mapped[int] = mapped_column(SmallInteger, default=3, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=datetime.now, nullable=False
|
|
)
|
|
claimed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|