"""Два процесса воркера на одном брокере и 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