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
@@ -0,0 +1,92 @@
|
||||
"""Два процесса воркера на одном брокере и Redis (#110): у каждого своё имя и своя персональная очередь,
|
||||
задача без url в открытое окно всегда попадает к воркеру, который держит это окно."""
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
import main
|
||||
from db.journal import Journal
|
||||
from tasks.broker import COMMON_QUEUE, RUN_TASK, personal_queue
|
||||
from worker import windows
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_workers(env, monkeypatch):
|
||||
"""Два пула вместо пула харнесса; общую очередь разбирают по очереди, как два consumer'а RabbitMQ."""
|
||||
env.redis.delete("robotex:worker:w1")
|
||||
pools = [windows.WindowPool(env.store, None, slots=2, journal=Journal(env.db)) for _ in range(2)]
|
||||
for pool in pools:
|
||||
env.call(pool.register)
|
||||
turn = itertools.cycle(pools)
|
||||
delivered = []
|
||||
|
||||
async def dispatch(message):
|
||||
if message.queue_name == COMMON_QUEUE:
|
||||
pool = next(turn)
|
||||
else:
|
||||
pool = next(p for p in pools if personal_queue(p.worker_id) == message.queue_name)
|
||||
delivered.append((message.args[1], pool.worker_id))
|
||||
if message.actor_name == RUN_TASK:
|
||||
await pool.handle(*message.args)
|
||||
else:
|
||||
await pool.close(*message.args)
|
||||
await pool.report()
|
||||
|
||||
monkeypatch.setattr(main, "dispatch", dispatch)
|
||||
yield pools, delivered
|
||||
for pool in pools:
|
||||
env.call(pool.close_all)
|
||||
|
||||
|
||||
def run(env, session_id=None, **body):
|
||||
env.client.cookies.clear()
|
||||
if session_id:
|
||||
env.client.cookies.set("session_id", session_id)
|
||||
resp = env.client.post("/v2/tasks", json={"actions": [], "browser_idle_timeout": 5, **body})
|
||||
assert resp.status_code == 202, resp.text
|
||||
task = env.client.get(f"/v2/tasks/{resp.json()['task_id']}").json()
|
||||
return resp.cookies.get("session_id") or session_id, task
|
||||
|
||||
|
||||
def test_workers_get_distinct_names(env, two_workers):
|
||||
pools, _ = two_workers
|
||||
assert [p.worker_id for p in pools] == ["worker-1", "worker-2"]
|
||||
workers = env.client.get("/health").json()["workers"]
|
||||
assert [w["worker_id"] for w in workers] == ["worker-1", "worker-2"]
|
||||
assert len({w["instance"] for w in workers}) == 2
|
||||
|
||||
|
||||
def test_task_without_url_goes_to_window_owner(env, two_workers):
|
||||
pools, delivered = two_workers
|
||||
# окна двух сессий открываются на разных воркерах
|
||||
sid_a, task = run(env, url="https://example.com/a")
|
||||
assert task["result"]["status"] == "ok"
|
||||
sid_b, task = run(env, url="https://example.com/b")
|
||||
assert task["result"]["status"] == "ok"
|
||||
owner = {sid: wid for sid, wid in delivered}
|
||||
assert sorted(owner.values()) == ["worker-1", "worker-2"]
|
||||
|
||||
# следующие задачи без url — всегда в окно своей сессии
|
||||
for _ in range(3):
|
||||
for sid in (sid_a, sid_b):
|
||||
_, task = run(env, sid)
|
||||
assert task["status"] == "done" and task["result"]["status"] == "ok", task
|
||||
assert delivered[-1] == (sid, owner[sid])
|
||||
assert all(len(p.windows) == 1 for p in pools)
|
||||
|
||||
|
||||
def test_worker_stops_when_name_is_taken(env, two_workers):
|
||||
"""Имя истекло (Redis был недоступен) и досталось другому процессу — старый владелец останавливается."""
|
||||
pools, _ = two_workers
|
||||
stopped = []
|
||||
pools[0].on_lost = lambda: stopped.append(True)
|
||||
env.redis.delete("robotex:worker:worker-1")
|
||||
newcomer = windows.WindowPool(env.store, None, slots=1)
|
||||
assert env.call(newcomer.register) == "worker-1"
|
||||
|
||||
env.call(pools[0].report)
|
||||
env.call(pools[0].report)
|
||||
assert stopped == [True] and pools[0].lost
|
||||
# при остановке старый владелец не снимает чужой heartbeat
|
||||
env.call(pools[0].unregister)
|
||||
assert env.client.get("/health").json()["workers"][0]["instance"] == newcomer.instance
|
||||
Reference in new issue
Block a user