From fd2fc945a7df601b054365f6337290333abe716e Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 20 Aug 2026 15:15:05 +0500 Subject: [PATCH] =?UTF-8?q?fix(ptica):=20=D0=BA=D0=BB=D1=8E=D1=87=20=D1=81?= =?UTF-8?q?=D0=B8=D0=BD=D0=B3=D0=BB=D1=82=D0=BE=D0=BD-=D0=BB=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=20kn-=D1=81=D0=B2=D0=B8=D0=BF=D0=B0=20=D0=B7=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=D1=82=20=D0=BE=D1=82=20=D0=BC=D0=BD=D0=BE?= =?UTF-8?q?=D0=B6=D0=B5=D1=81=D1=82=D0=B2=D0=B0,=20=D0=B0=20=D0=BD=D0=B5?= =?UTF-8?q?=20=D0=BE=D1=82=20=D0=BF=D0=BE=D1=80=D1=8F=D0=B4=D0=BA=D0=B0=20?= =?UTF-8?q?(#2464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit devs_key = ",".join(developers) — список джойнился как пришёл, а приходит он прямо из тела запроса (`developers` в admin_scrape, порядок произвольный). Один и тот же набор в другом порядке давал ДРУГОЙ ключ: синглтон молча переставал быть синглтоном, два свипа шли параллельно по одним и тем же разработчикам. Второе следствие того же корня: force_release_lock строит ключ этой же функцией. Оператор, снимающий залипший лок и перечисливший разработчиков в ином порядке, молча не снимал ничего. sorted(set(...)) закрывает оба случая: порядок и повторы ('A','A' ≡ 'A'). Про переход: формат ключа меняется, поэтому лок, удерживаемый ПРЯМО СЕЙЧАС под старым ключом, осиротеет и истечёт по TTL. Практического риска нет — kn-свип не отрабатывает с 28.06 (DOM.РФ отдаёт страницу блокировки, см. #2443). Тесты сравнивают ЗНАЧЕНИЯ ключей, поэтому красное на origin/main — про разошедшиеся ключи, а не про отсутствующую функцию: один набор в двух порядках → два разных ключа → падает повтор в списке меняет ключ → падает разные наборы остаются разными — контроль, зелёный с обеих сторон регион остаётся в ключе — контроль, зелёный с обеих сторон None/[] по-прежнему дают '*' — контроль, зелёный с обеих сторон Первый контроль не для симметрии: ловит «починку» через огрубление ключа (отбросить developers вовсе) — тогда свипы по разным разработчикам блокировали бы друг друга. Прогоны: -k "scrape_kn or lock" — 114 passed rc=0. Co-Authored-By: Claude Opus 5 --- backend/app/workers/tasks/scrape_kn.py | 15 ++++- .../tests/services/test_2464_kn_lock_key.py | 59 +++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 backend/tests/services/test_2464_kn_lock_key.py diff --git a/backend/app/workers/tasks/scrape_kn.py b/backend/app/workers/tasks/scrape_kn.py index d243c047..0e7af756 100644 --- a/backend/app/workers/tasks/scrape_kn.py +++ b/backend/app/workers/tasks/scrape_kn.py @@ -40,7 +40,20 @@ _RELEASE_LOCK_LUA = ( def _lock_key(region_code: int, developers: list[str] | None) -> str: - devs_key = ",".join(developers) if developers else "*" + """Ключ синглтон-лока. Зависит от МНОЖЕСТВА разработчиков, не от их порядка. + + #2464: раньше список джойнился как пришёл, а приходит он прямо из тела запроса + (`developers` в admin_scrape). Один и тот же набор, поданный в другом порядке, + давал ДРУГОЙ ключ — и синглтон молча переставал быть синглтоном: два свипа шли + параллельно по одним и тем же разработчикам. + + Второе следствие того же: `force_release_lock` строит ключ этой же функцией. + Оператор, снимающий залипший лок и перечисливший разработчиков в ином порядке, + молча не снимал ничего. + + sorted(set(...)) закрывает оба: порядок и повторы ('A','A' ≡ 'A'). + """ + devs_key = ",".join(sorted(set(developers))) if developers else "*" return f"scrape:kn:lock:{region_code}:{devs_key}" diff --git a/backend/tests/services/test_2464_kn_lock_key.py b/backend/tests/services/test_2464_kn_lock_key.py new file mode 100644 index 00000000..24c0bbef --- /dev/null +++ b/backend/tests/services/test_2464_kn_lock_key.py @@ -0,0 +1,59 @@ +"""Ключ синглтон-лока kn-свипа зависит от множества, а не от порядка (#2464). + +`developers` приходит прямо из тела запроса (`admin_scrape`), порядок произвольный. +Раньше список джойнился как пришёл: один и тот же набор в другом порядке давал ДРУГОЙ +ключ — синглтон молча переставал быть синглтоном, два свипа шли параллельно по одним и +тем же разработчикам. + +Второе следствие того же корня: `force_release_lock` строит ключ этой же функцией. +Оператор, снимающий залипший лок и перечисливший разработчиков в ином порядке, молча не +снимал ничего. + +Тесты сравнивают ЗНАЧЕНИЯ ключей — на origin/main они не совпадают, то есть красное +означает «ключи разошлись», а не «функции нет». +""" + +from __future__ import annotations + +import os + +os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") + + +def _key(region: int, devs: list[str] | None) -> str: + from app.workers.tasks.scrape_kn import _lock_key + + return _lock_key(region, devs) + + +def test_same_set_different_order_gives_same_key() -> None: + """Порядок не должен создавать второй лок для того же набора.""" + a = _key(66, ["6208_0", "1234_5", "9999_1"]) + b = _key(66, ["9999_1", "6208_0", "1234_5"]) + assert a == b, f"один набор дал два ключа:\n {a}\n {b}" + + +def test_duplicates_do_not_change_the_key() -> None: + """Повтор в списке — тот же набор, тот же лок.""" + assert _key(66, ["6208_0", "6208_0"]) == _key(66, ["6208_0"]) + + +def test_different_sets_stay_separate() -> None: + """Контроль: разные наборы обязаны остаться разными локами. + + Ловит «починку» через огрубление ключа (напр. отбросить developers вовсе) — + тогда свипы по разным разработчикам блокировали бы друг друга. + """ + assert _key(66, ["A"]) != _key(66, ["B"]) + assert _key(66, ["A"]) != _key(66, ["A", "B"]) + + +def test_region_still_separates_locks() -> None: + """Контроль: регион остаётся частью ключа.""" + assert _key(66, ["A"]) != _key(74, ["A"]) + + +def test_empty_and_none_mean_all_developers() -> None: + """Контроль: прежняя семантика '*' для пустого/None сохранена.""" + assert _key(66, None).endswith(":*") + assert _key(66, []).endswith(":*") -- 2.45.3