tradein/proxy_pool: защита «последнего узла выделенной affinity» не считает узлы 'any' и прячет свободный узел от всех источников #3299

Open
opened 2026-08-31 11:09:12 +00:00 by lekss361 · 0 comments
Owner

Свободный здоровый узел с выделенной provider_affinity не выдаётся ни одному источнику — ни «своему», ни чужому, — если он единственный с этой привязкой. Даже когда «свой» источник прекрасно обслуживается узлами 'any'.

Замер (прод, 31.08.2026, в транзакции с ROLLBACK)

Изолирована одна переменная: узел 12 здоров и свободен в обоих плечах, меняется только привязка.

плечо привязка узла 12 acquire('domclick') заход 1 заход 2 (fallback)
А avito ничего ничего
Б any 12

И контрольный вопрос, ради которого защита существует:

остался бы avito с прокси, отдай мы 12-й домклику? → узел 13

Да, остался бы. Защита сработала там, где защищать было не от чего.

Причина

app/services/proxy_pool.py, второй заход acquire() (~строки 354-376):

AND ( sp.provider_affinity = 'any'
      OR EXISTS (
          SELECT 1 FROM scrape_proxies AS other
          WHERE other.provider_affinity = sp.provider_affinity   -- ← только ТА ЖЕ привязка
            AND other.enabled AND other.id <> sp.id
            AND NOT EXISTS (... бан по own affinity ...)
      ) )

EXISTS ищет другой узел с той же выделенной привязкой. Но узлы 'any' обслуживают выделенный источник наравне: основной запрос отбирает provider_affinity IN (:provider, 'any') (там же, ~строка 310). Значит правильный вопрос — «останется ли у источника sp.provider_affinity хоть один пригодный кандидат, если этот узел уйдёт», и в него обязаны входить 'any'-узлы. Сейчас вопрос сужен до «есть ли у этой привязки ВТОРОЙ выделенный узел», а такого почти никогда нет: выделенная привязка по смыслу штучная.

Итог обратный замыслу: единственный выделенный узел становится невыдаваемым вообще никому. Чужим — защита не пускает; своим — он и так проходил бы первым заходом, но если своих запросов нет, узел просто простаивает.

Цена

30.08 этим был убит добор карточек Домклика. Узел 12 стоял provider_affinity='avito', был здоров и свободен, а acquire('domclick') возвращал пусто — прогоны 5449-5459 легли подряд с «пул прокси пуст». Обошли руками, переведя узел на 'any'; настоящая причина — это условие.

Пул дефицитен (#2638), поэтому цена прямая: узел, за который заплачено, простаивает.

Правка

Заменить «есть ли другой узел ТОЙ ЖЕ привязки» на «останется ли у sp.provider_affinity хоть один пригодный кандидат без этого узла», с тем же предикатом пригодности, что в основном запросе:

OR EXISTS (
    SELECT 1 FROM scrape_proxies AS other
    WHERE other.provider_affinity IN (sp.provider_affinity, 'any')   -- ← ключевая правка
      AND other.enabled
      AND other.consecutive_fails < :max_fails
      AND other.id <> sp.id
      AND NOT EXISTS (SELECT 1 FROM scrape_proxy_source_bans b2
                      WHERE b2.proxy_id = other.id
                        AND b2.source = sp.provider_affinity
                        AND b2.banned_until > now())
)

Заодно два расхождения с основным запросом, которые стоит устранить в том же заходе: other не проверяется на consecutive_fails (нездоровый узел засчитывается за backup) и на leased_by (тут спорно — арендованный узел вернётся, так что, возможно, и не надо; решить осознанно и записать почему).

Приёмка

  • Тест ровно на замеренный расклад: один узел с выделенной привязкой + один 'any'-узел → чужой источник получает выделенный узел, «свой» остаётся с 'any'.
  • Тест на исходный смысл защиты (не сломать #2600): выделенная привязка с ЕДИНСТВЕННЫМ узлом и никаких 'any'-узлов → fallback его не забирает.
  • Тест: 'any'-кандидат, забаненный источником выделенной привязки, за backup не считается.

Смежное

  • #2600 — откуда взялась защита; замысел верный, предикат узкий.
  • #2638 — дефицит узлов; этот баг его усугубляет, выводя оплаченный узел из оборота.
  • #3288, #3297 — про Авито: там отказ площадки не опознаётся и узел не банится адресно, а уходит в глобальный карантин. Вместе с этим тикетом — три разных способа потерять живой узел.
Свободный здоровый узел с выделенной `provider_affinity` не выдаётся ни одному источнику — ни «своему», ни чужому, — если он единственный с этой привязкой. Даже когда «свой» источник прекрасно обслуживается узлами `'any'`. ## Замер (прод, 31.08.2026, в транзакции с ROLLBACK) Изолирована одна переменная: узел 12 здоров и свободен в обоих плечах, меняется **только** привязка. | плечо | привязка узла 12 | `acquire('domclick')` заход 1 | заход 2 (fallback) | |---|---|---|---| | А | `avito` | ничего | **ничего** | | Б | `any` | **12** | — | И контрольный вопрос, ради которого защита существует: ``` остался бы avito с прокси, отдай мы 12-й домклику? → узел 13 ``` Да, остался бы. Защита сработала там, где защищать было не от чего. ## Причина `app/services/proxy_pool.py`, второй заход `acquire()` (~строки 354-376): ```sql AND ( sp.provider_affinity = 'any' OR EXISTS ( SELECT 1 FROM scrape_proxies AS other WHERE other.provider_affinity = sp.provider_affinity -- ← только ТА ЖЕ привязка AND other.enabled AND other.id <> sp.id AND NOT EXISTS (... бан по own affinity ...) ) ) ``` `EXISTS` ищет **другой узел с той же выделенной привязкой**. Но узлы `'any'` обслуживают выделенный источник наравне: основной запрос отбирает `provider_affinity IN (:provider, 'any')` (там же, ~строка 310). Значит правильный вопрос — «останется ли у источника `sp.provider_affinity` хоть один пригодный кандидат, если этот узел уйдёт», и в него обязаны входить `'any'`-узлы. Сейчас вопрос сужен до «есть ли у этой привязки ВТОРОЙ выделенный узел», а такого почти никогда нет: выделенная привязка по смыслу штучная. Итог обратный замыслу: единственный выделенный узел становится невыдаваемым **вообще никому**. Чужим — защита не пускает; своим — он и так проходил бы первым заходом, но если своих запросов нет, узел просто простаивает. ## Цена 30.08 этим был убит добор карточек Домклика. Узел 12 стоял `provider_affinity='avito'`, был здоров и свободен, а `acquire('domclick')` возвращал пусто — прогоны 5449-5459 легли подряд с «пул прокси пуст». Обошли руками, переведя узел на `'any'`; настоящая причина — это условие. Пул дефицитен (#2638), поэтому цена прямая: узел, за который заплачено, простаивает. ## Правка Заменить «есть ли другой узел ТОЙ ЖЕ привязки» на «останется ли у `sp.provider_affinity` хоть один пригодный кандидат без этого узла», с тем же предикатом пригодности, что в основном запросе: ```sql OR EXISTS ( SELECT 1 FROM scrape_proxies AS other WHERE other.provider_affinity IN (sp.provider_affinity, 'any') -- ← ключевая правка AND other.enabled AND other.consecutive_fails < :max_fails AND other.id <> sp.id AND NOT EXISTS (SELECT 1 FROM scrape_proxy_source_bans b2 WHERE b2.proxy_id = other.id AND b2.source = sp.provider_affinity AND b2.banned_until > now()) ) ``` Заодно два расхождения с основным запросом, которые стоит устранить в том же заходе: `other` не проверяется на `consecutive_fails` (нездоровый узел засчитывается за backup) и на `leased_by` (тут спорно — арендованный узел вернётся, так что, возможно, и не надо; решить осознанно и записать почему). ## Приёмка - Тест ровно на замеренный расклад: один узел с выделенной привязкой + один `'any'`-узел → чужой источник получает выделенный узел, «свой» остаётся с `'any'`. - Тест на исходный смысл защиты (не сломать #2600): выделенная привязка с ЕДИНСТВЕННЫМ узлом и **никаких** `'any'`-узлов → fallback его не забирает. - Тест: `'any'`-кандидат, забаненный источником выделенной привязки, за backup не считается. ## Смежное - #2600 — откуда взялась защита; замысел верный, предикат узкий. - #2638 — дефицит узлов; этот баг его усугубляет, выводя оплаченный узел из оборота. - #3288, #3297 — про Авито: там отказ площадки не опознаётся и узел не банится адресно, а уходит в глобальный карантин. Вместе с этим тикетом — три разных способа потерять живой узел.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3299
No description provided.