fix: имя воркера — свободное worker-N из Redis, а не hostname (#110)
Два процесса на одном хосте получали один WORKER_ID и делили персональную очередь: задачу без url в открытое окно забирал процесс без этого окна. Теперь при старте воркер занимает наименьшее свободное имя worker-N (SET NX на heartbeat-ключ), heartbeat продлевает его только при своём instance; если имя занял другой процесс — воркер останавливается. Имена и персональные очереди переиспользуются. WORKER_ID из env убран, в /health — instance, host, pid. make dev при выходе гасит и api, и worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f7d60563d
commit
502b6e1003
8 files changed
+236
-23
No files matched your search
+17
-9
@@ -8,7 +8,7 @@ dramatiq, а не из event loop.
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import signal
|
||||
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
logging.basicConfig(
|
||||
@@ -30,7 +30,6 @@ from worker.windows import Outcome, WindowPool # noqa: E402
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
WORKER_ID = os.getenv("WORKER_ID") or socket.gethostname()
|
||||
# сколько окон Chrome одновременно — и выполняющих задачу, и удерживаемых; ≈ числу ядер (docs/Deployment.md)
|
||||
BROWSER_SLOTS = int(os.getenv("BROWSER_SLOTS", "4"))
|
||||
# через сколько задача без свободного слота снова попадёт в очередь
|
||||
@@ -68,14 +67,23 @@ async def _cleanup_loop(pool: WindowPool) -> None:
|
||||
await asyncio.sleep(PROFILE_CLEANUP_INTERVAL)
|
||||
|
||||
|
||||
async def _start() -> None:
|
||||
def _stop_self() -> None:
|
||||
"""Имя воркера занял другой процесс: штатная остановка, как по Ctrl+C. dramatiq при выходе дочернего
|
||||
процесса останавливается целиком, Docker (restart: unless-stopped) запустит воркер заново с новым именем."""
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
|
||||
|
||||
async def _start() -> str:
|
||||
"""Пул окон и имя воркера. Имя (worker-N) — не из env, а свободное в Redis (#110): два процесса на одном
|
||||
хосте не делят персональную очередь, а имена и очереди переиспользуются после перезапусков."""
|
||||
global _pool
|
||||
db = connect_db()
|
||||
await db.init()
|
||||
_pool = WindowPool(task_store.TaskStore(task_store.connect()), WORKER_ID, BROWSER_SLOTS, Journal(db))
|
||||
await _pool.report()
|
||||
_pool = WindowPool(task_store.TaskStore(task_store.connect()), None, BROWSER_SLOTS, Journal(db), on_lost=_stop_self)
|
||||
worker_id = await _pool.register()
|
||||
_background.extend([asyncio.create_task(_heartbeat_loop(_pool)), asyncio.create_task(_cleanup_loop(_pool))])
|
||||
log.info("worker %s: %d browser slots, queues %s, %s", WORKER_ID, BROWSER_SLOTS, COMMON_QUEUE, personal_queue(WORKER_ID))
|
||||
log.info("worker %s: %d browser slots, queues %s, %s", worker_id, BROWSER_SLOTS, COMMON_QUEUE, personal_queue(worker_id))
|
||||
return worker_id
|
||||
|
||||
|
||||
async def _stop() -> None:
|
||||
@@ -83,7 +91,7 @@ async def _stop() -> None:
|
||||
task.cancel()
|
||||
_background.clear()
|
||||
await _pool.close_all()
|
||||
await _pool.store.remove_worker(WORKER_ID)
|
||||
await _pool.unregister()
|
||||
if _pool.journal.db:
|
||||
await _pool.journal.db.close()
|
||||
|
||||
@@ -93,14 +101,14 @@ class Lifecycle(Middleware):
|
||||
добавлен в брокер раньше), а окна закрываются до его остановки."""
|
||||
|
||||
def before_worker_boot(self, broker, worker):
|
||||
_run(_start())
|
||||
# персональная очередь объявляется до запуска потоков воркера: dramatiq подпишется и на неё
|
||||
broker.declare_queue(personal_queue(_run(_start())))
|
||||
|
||||
def before_worker_shutdown(self, broker, worker):
|
||||
_run(_stop())
|
||||
|
||||
|
||||
broker.add_middleware(Lifecycle())
|
||||
broker.declare_queue(personal_queue(WORKER_ID))
|
||||
|
||||
|
||||
@dramatiq.actor(actor_name=RUN_TASK, queue_name=COMMON_QUEUE, max_retries=0, time_limit=TASK_TIME_LIMIT_MS)
|
||||
|
||||
Reference in new issue
Block a user