SPB Git

spb/ai-atlas

Public

Worker sticks to one job kind while any remain; embeddings only queued when no extraction is pending (no model thrash on llm-api)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 12, 2026) parent 021595d Browse files

2 changed files +13 −6

modified src/aiatlas/services/jobs.py +9 −4
@@ -39,13 +39,15 @@ async def enqueue(conn: AsyncConnection, kind: str, payload: dict[str, Any], *,
39 return row["id"] if row else None 39 return row["id"] if row else None
40 40
41 41
42−async def claim_next(conn: AsyncConnection, worker: str, kinds: list[str] | None = None) -> dict[str, Any] | None: 42+async def claim_next(conn: AsyncConnection, worker: str, kinds: list[str] | None = None, *, sticky_kind: str | None = None) -> dict[str, Any] | None:
43+ """Claim the next job. `sticky_kind` keeps a worker on the kind it just ran while any remain: LLM jobs of one kind share a loaded
44+ model, and alternating kinds (extraction ↔ embeddings) makes the inference server swap models every job."""
43 kind_filter = "and kind = any(cast(:kinds as text[]))" if kinds else "" 45 kind_filter = "and kind = any(cast(:kinds as text[]))" if kinds else ""
44 row = await fetch_one(conn, f"""with next as ( 46 row = await fetch_one(conn, f"""with next as (
45 select id from jobs where status = 'queued' and run_after <= now() {kind_filter} 47 select id from jobs where status = 'queued' and run_after <= now() {kind_filter}
46− order by priority, run_after limit 1 for update skip locked) 48+ order by case when kind = cast(:sticky as text) then 0 else 1 end, priority, run_after limit 1 for update skip locked)
47 update jobs j set status = 'running', locked_by = :w, locked_at = now(), started_at = now(), attempts = attempts + 1 49 update jobs j set status = 'running', locked_by = :w, locked_at = now(), started_at = now(), attempts = attempts + 1
48− from next where j.id = next.id returning j.*""", w=worker, kinds=kinds or []) 50+ from next where j.id = next.id returning j.*""", w=worker, kinds=kinds or [], sticky=sticky_kind or "")
49 return row 51 return row
50 52
51 53
@@ -98,13 +100,16 @@ async def run_worker(*, concurrency: int | None = None, kinds: list[str] | None
98 sem.release() 100 sem.release()
99 101
100 tasks: set[asyncio.Task[None]] = set() 102 tasks: set[asyncio.Task[None]] = set()
103+ last_kind: str | None = None
101 while not stop.is_set(): 104 while not stop.is_set():
102 await sem.acquire() # hold a slot BEFORE claiming, so concurrency bounds claims too 105 await sem.acquire() # hold a slot BEFORE claiming, so concurrency bounds claims too
103 if stop.is_set(): 106 if stop.is_set():
104 sem.release() 107 sem.release()
105 break 108 break
106 async with transaction() as conn: 109 async with transaction() as conn:
107− job = await claim_next(conn, worker, kinds) 110+ job = await claim_next(conn, worker, kinds, sticky_kind=last_kind)
111+ if job is not None:
112+ last_kind = job["kind"]
108 if job is None: 113 if job is None:
109 sem.release() 114 sem.release()
110 try: 115 try:
modified src/aiatlas/services/scheduler.py +4 −2
@@ -12,7 +12,7 @@ from apscheduler.triggers.cron import CronTrigger
12 12
13 from aiatlas.config import settings 13 from aiatlas.config import settings
14 from aiatlas.connectors import get, registry 14 from aiatlas.connectors import get, registry
15−from aiatlas.db import execute, fetch_all, transaction 15+from aiatlas.db import execute, fetch_all, fetch_one, transaction
16 from aiatlas.services import cache 16 from aiatlas.services import cache
17 from aiatlas.services.jobs import enqueue, requeue_stale, run_worker 17 from aiatlas.services.jobs import enqueue, requeue_stale, run_worker
18 18
@@ -84,7 +84,9 @@ async def hourly() -> None:
84 n = await requeue_stale(conn) 84 n = await requeue_stale(conn)
85 if n: 85 if n:
86 log.info("requeued stale jobs", extra={"n": n}) 86 log.info("requeued stale jobs", extra={"n": n})
87− if settings.llm_available: 87+ async with transaction() as conn:
88+ extraction_pending = await fetch_one(conn, "select 1 from jobs where kind = 'llm_extract' and status in ('queued','running') limit 1")
89+ if settings.llm_available and not extraction_pending: # never interleave embeddings with extraction: the server would swap models
88 from aiatlas.services.embeddings import pending_entity_ids 90 from aiatlas.services.embeddings import pending_entity_ids
89 91
90 ids = await pending_entity_ids(limit=400) 92 ids = await pending_entity_ids(limit=400)
91 93
Menu

Theme