Gate network parsers on a working proxy and remember what worked

The pool now warms up from what the previous run learned instead of starting
cold every launch. Startup probes the remembered proxies first, stops as soon
as ProxyMinimumLive of them answer, and writes the survivors to
proxies.state.json after the warm-up and again on shutdown. Only proxies that
ever answered are stored: the feed republishes a few thousand dead addresses
every five minutes, and "was dead an hour ago" says almost nothing.

Remembered state is a hint, not a verdict. A restored proxy sorts first in the
warm-up queue but is not counted live until it answers in this session -
otherwise a launch a week later would report live proxies it had never spoken
to, the warm-up would skip the very entries it exists to re-check, and the
parser gate would open on week-old evidence.

That gate is the other half: a parser declaring RequiresNetwork will not run
while the pool has nothing live. The Parse page disables the run button and
shows a banner that leads to the Proxies page. Parsers that work on pasted text
are never gated - they have nothing to route, and blocking them would make the
app useless whenever the public lists are down. Two new settings cover the
escape hatch and the target: "allow network parsers without a proxy" and how
many live proxies to find at startup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-08-13 19:11:51 +03:00
co-authored by Claude Opus 5
parent 85656e70b0
commit 44fb0d3a5f
31 changed files with 1349 additions and 41 deletions
+31
View File
@@ -106,6 +106,37 @@ dotnet csharpier check .
- **Лиза без вердикта нейтральна.** Отменённая операция — не вина прокси; считать это отказом
значит карантинить здоровые прокси на каждый Cancel.
- **`Select` и `Next` — зарезервированные слова для CA1716.** Метод стратегии называется `Pick`.
- **`LiveCount` считает только `Alive` и не в карантине.** На нём висит гейт парсера, поэтому
«доступна» (карантин истёк) и «живая» здесь намеренно расходятся: гейт не должен открываться
от одного лишь истечения окна.
- **Прогрев обрывается по достижении цели, а не проходит список до конца.** `WarmUpAsync` линкует
CTS и гасит остаток, как только набралось `MinimumLiveProxies`. Порядок кандидатов —
`WarmUpOrder()`, он публичный ровно затем, чтобы порядок проверялся без прогона проб.
- **`RestoreState` не выставляет `Health = Alive`.** «Работала вчера» живёт в отдельном
`WasAliveOnLastRun` и влияет только на порядок прогрева. Если восстанавливать как `Alive`, пул
отрапортует живыми тех, с кем не разговаривал: прогрев сочтёт цель достигнутой и не проверит
никого, а гейт парсера откроется по данным недельной давности. Ловит
`A_remembered_proxy_is_not_reported_live_until_it_answers_again`.
- **`RestoreState` не восстанавливает карантин.** Окно — стенные часы, между запусками могли пройти
сутки; перенос окна сажал бы прокси за то, что давно истекло.
- **Сохраняются только `HasEverAnswered`** = `SuccessCount > 0 || IsBelievedAlive`, где
«believed» = вердикт этой сессии, а без него — прошлой. Мёртвые в фиде исчисляются тысячами и
переиздаются каждые пять минут. Важен именно перенос: прогрев обрывается рано, поэтому
большинство запомненных заканчивают сессию непроверенными — строгое `Health == Alive` стирало бы
накопленный список за пару запусков.
## Гейт парсера
`IParser.RequiresNetwork` — дефолтная реализация возвращает `false`, поэтому добавление парсера
остаётся однострочным. Парсер, который куда-то ходит, **обязан** её переопределить, иначе поедет
напрямую в обход настройки.
Гейт живёт в `ParseViewModel.RefreshProxyGate()` и складывается из трёх условий: парсер сетевой,
`AllowDirectConnection` выключен, `LiveCount == 0`. Он пересчитывается по событию пула (с
throttle 250 мс — пул дёргается на каждый исход лизы), при смене парсера и при смене настроек.
`ParseView.axaml` держит баннер под `x:Name="ProxyGateBanner"`; `ParseViewTests` рендерит его
по-настоящему, потому что мёртвый биндинг `IsVisible` не ломает ни одного VM-теста.
## Добавить строку в UI