fix(ptica): ключ синглтон-лока kn-свипа зависит от множества, а не от порядка (#2464) #2970

Merged
bot-backend merged 1 commit from fix/2464-lock-key-order into main 2026-08-20 10:34:31 +00:00
2 changed files with 73 additions and 1 deletions

View file

@ -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}"

View file

@ -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(":*")