from sqlalchemy import Column, Index, SmallInteger, String, Text, DateTime from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.orm import declarative_base from sqlalchemy.sql import text from common.enums import DBType # 모든 ORM 모델의 베이스. insert 시 isinstance 체크에도 사용된다. MAIN_BASE = declarative_base() class job(MAIN_BASE): """작업 큐. PostgreSQL 을 '제대로' 큐로 쓴다 — 원자적 CAS claim + lease 소유권 + dead-letter. 코드값(status/type)은 SMALLINT 정수 코드(common.enums 매핑), 시각은 전 구간 TIMESTAMPTZ, 무 FK. """ @staticmethod def DBType(): return DBType.MAIN.value __tablename__ = "job" job_id = Column(UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()")) job_type = Column(SmallInteger, nullable=False) # JobType status = Column(SmallInteger, nullable=False, server_default=text("1")) # JobStatus (1=PENDING) priority = Column(SmallInteger, nullable=False, server_default=text("100")) # 낮을수록 우선 payload = Column(JSONB, nullable=False, server_default=text("'{}'::jsonb")) # 잡 입력 result = Column(JSONB, nullable=True) # 잡 출력(완료 시) dedupe_key = Column(String(200), nullable=True) # 활성 중복 방지 키(부분 유니크) attempts = Column(SmallInteger, nullable=False, server_default=text("0")) # 시도 횟수(claim 시 +1) max_attempts = Column(SmallInteger, nullable=False, server_default=text("3")) run_after = Column(DateTime(timezone=True), nullable=False, server_default=text("now()")) # 이 시각 이후에만 claim(백오프) lease_until = Column(DateTime(timezone=True), nullable=True) # 소유권 임대 만료(reaper 회수 기준) worker_id = Column(String(80), nullable=True) # 현재 점유 워커 run_started_at = Column(DateTime(timezone=True), nullable=True) # RUNNING 진입 시각(할당시각과 분리) last_error = Column(Text, nullable=True) created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("now()")) updated_at = Column(DateTime(timezone=True), nullable=False, server_default=text("now()"), onupdate=text("now()")) __table_args__ = ( # claim 정렬/필터용: PENDING 중 run_after 지난 것을 priority·생성순으로 Index("ix_job_claim", "status", "run_after", "priority", "created_at"), # reaper: 만료된 RUNNING lease 회수용 Index("ix_job_lease", "status", "lease_until"), # 활성 중복 방지: 같은 dedupe_key 는 PENDING/RUNNING 중 하나만 존재 가능 Index( "uq_job_dedupe_active", "dedupe_key", unique=True, postgresql_where=text("status IN (1, 2) AND dedupe_key IS NOT NULL"), ), ) class search_negative(MAIN_BASE): """네거티브 캐시 — '검색해도 없더라'를 TTL 동안 기억해 재검색 낭비를 막는다. until 이 지나면 자동 무효(재도전 허용 — 나중에 입고될 수 있으므로).""" @staticmethod def DBType(): return DBType.MAIN.value __tablename__ = "search_negative" key = Column(String(300), primary_key=True) # 보통 product_code(없으면 query) until = Column(DateTime(timezone=True), nullable=False) # 이 시각까지 not_found 로 간주 reason = Column(String(200), nullable=True) # 종료 사유 메모(관측) created_at = Column(DateTime(timezone=True), nullable=False, server_default=text("now()"))