# Прокси-пул Читать перед правкой `Core/Proxies/**` и `Infrastructure/Proxies/**`. ## Добавить источник 1. Реализовать `IProxySource` (или `IMutableProxySource`, если список редактируемый). 2. Зарегистрировать в `AddAvParserProxies()`. Порядок регистрации = порядок слияния; свой список идёт последним, чтобы пользовательский адрес перебивал фидовый. ## Инварианты - **`ProxyPool` переиспользует существующие `ProxyEntry` по `Endpoint.Key`** — иначе перезагрузка списка стирала бы статистику, а публичные фиды переиздаются каждые несколько минут. - **Проба обязана ходить по https.** Обычный HTTP через прокси — это форвард GET с абсолютным URI, а https — `CONNECT`-туннель, и свободные прокси сплошь и рядом умеют первое и отказывают во втором («The proxy tunnel request to proxy … failed with status code 400»). Все собираемые адреса идут по https, поэтому прокси без CONNECT мертва *для нас*, и проверять надо ровно то, чем пользуемся. Раньше умолчание было http с обоснованием «не отбраковывать тех, кто просто не умеет CONNECT» — это и наполняло пул живыми на бумаге. Старое значение в `settings.json` подменяется на https в `AppSettings.ToProxyOptions()`; введённое пользователем не трогается. - **Выдаются только подтверждённо живые, пока такие есть.** Сортировки «живые вперёд» не хватало: её видит только липкая стратегия, берущая голову списка, а round-robin и взвешенный выбор тянут из всего набора — то есть из пары тысяч непроверенных адресов, и почти каждый запрос платил полный connect-таймаут, чтобы это выяснить. Теперь «живых N» на странице и то, куда реально ходит сбор, — одно и то же число. - **Фолбэк на всё доступное, когда живых нет.** Непроверенная ≠ мёртвая: на холодном пуле строгий фильтр не выдал бы ничего и сбор просто встал бы, не попробовав. Ответившая на этом пути прокси сама себя переводит в живые. - **Карантин остаётся отдельным механизмом от `Health`.** `Health` — «что видели в последний раз»; исключать навсегда всё когда-либо упавшее значит потерять прокси после первой осечки. Уже было багом, ловит `A_failing_proxy_is_quarantined_and_comes_back_later`. - **`LiveCount` считает только `Alive` и не в карантине.** На нём висит гейт сбора, поэтому «доступна» (окно истекло) и «живая» намеренно расходятся: гейт не должен открываться от одного лишь истечения окна. - **Проба не трогает `SuccessCount`/`FailureCount`** — они про реальные запросы, а свип по паре тысяч адресов перезаписал бы всё, на чём держится взвешенный выбор. Провал пробы даёт карантин через `RecordProbe(..., quarantineOnFailure:)`. - **Лиза без вердикта нейтральна**: отменённая операция — не вина прокси, иначе каждый Cancel карантинил бы здоровые адреса. - **Вердикт лизы — про транспорт, а не про ресурс.** 404 и 429 — успех прокси; иначе пул карантинил бы рабочие адреса с частотой мёртвых ссылок, а на лимит отвечал бы сменой прокси, то есть обходом лимита. - **Прогрев обрывается по достижении цели**, а не проходит список до конца: `WarmUpAsync` линкует CTS и гасит остаток на `MinimumLiveProxies`. Порядок кандидатов — `WarmUpOrder()`, публичный ровно ради проверки без прогона проб. - **Остаток дочищает `TopUpAsync` в фоне.** Прогрев по построению оставляет тысячи адресов в `Unknown` — это читается как «проверка не запускалась», и, что хуже, сбор получает пул из десяти прокси вместо сотен рабочих. Фоновый проход запускает `ProxyPoolLoader` после прогрева, с четвертью `ProbeConcurrency`: пользовательский свип — это всплеск, за которым смотрят, а этот идёт минутами и, возможно, параллельно с закачкой. - **`TopUpAsync` трогает только `Unknown`.** Перепроверять мёртвых — это и есть свип; перепроверять живых значит посадить в карантин прокси, которой прямо сейчас пользуется прогон. - **`TopUpAsync` не делает `_strategy.Reset()`**, в отличие от свипа: сброс уронил бы липкий выбор посреди сбора, причём ради записей, которых в этом выборе и нет. `Changed` он поднимает раз в `ChangeInterval` проб — иначе страница молчит несколько минут. - **Фоновый проход не идёт при `ProxyHealthCheck.Lazy`**: «проверять в момент выдачи» — это ровно просьба не свипать. - **`SaveStateAsync` сериализован семафором.** Писателей теперь двое (фоновый проход по завершении и выход из приложения), а запись идёт через один и тот же временный файл с переименованием. - **`RestoreState` не выставляет `Health = Alive`** — «работала вчера» живёт в `WasAliveOnLastRun` и влияет только на порядок прогрева. Иначе пул отрапортует живыми тех, с кем не разговаривал: прогрев сочтёт цель достигнутой, а гейт сбора откроется по данным недельной давности. Ловит `A_remembered_proxy_is_not_reported_live_until_it_answers_again`. - **`RestoreState` не восстанавливает карантин**: окно — стенные часы, между запусками могли пройти сутки, и перенос сажал бы прокси за давно истёкшее. - **Сохраняются только `HasEverAnswered`** = `SuccessCount > 0 || IsBelievedAlive` («believed» — вердикт этой сессии, а без него прошлой). Прогрев обрывается рано, поэтому большинство запомненных заканчивают сессию непроверенными — строгое `Health == Alive` стёрло бы накопленный список за пару запусков. - **`Select` и `Next` зарезервированы для CA1716** — метод стратегии называется `Pick`. ## Статистика на «Обзоре» `DashboardViewModel` держит сводку пула и обновляет её по событию `IProxyPool.Changed` с throttle 250 мс — событие приходит на каждый исход лизы и на каждую пробу. «Живых» на карточке — это `LiveCount`, то же число, что читает гейт сбора; `RefreshProxyStats()` публичен, чтобы тесты не ждали throttle.