- Introduced a rule editor in the gallery for users to define actions when a media item is found again, allowing for better management of media rules. - Updated `GalleryViewModel` to handle rule actions, including saving, editing, and removing rules, with appropriate UI bindings. - Enhanced UI components in `GalleryView.axaml` to support rule editing, including action selection and source management. - Added localization strings for new rule-related features in both English and Russian. - Improved unit tests to cover new rule management functionalities, ensuring robust behavior during rule creation and editing. These changes significantly enhance the user experience by providing a more interactive and flexible way to manage media items in the gallery.
82 lines
9.3 KiB
Markdown
82 lines
9.3 KiB
Markdown
# Прокси-пул
|
||
|
||
Читать перед правкой `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» на странице и то, куда реально ходит сбор, —
|
||
одно и то же число.
|
||
- **`AcquireAsync(excludedCountries)` — для правила «попробовать из другой страны».** Записи, у
|
||
которых фид не назвал страну, остаются в игре: неизвестная страна — не совпадение, и отбрасывать их
|
||
значит сужать пул из-за пустого поля.
|
||
- **Фолбэк на всё доступное, когда живых нет.** Непроверенная ≠ мёртвая: на холодном пуле строгий
|
||
фильтр не выдал бы ничего и сбор просто встал бы, не попробовав. Ответившая на этом пути прокси
|
||
сама себя переводит в живые.
|
||
- **Карантин остаётся отдельным механизмом от `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.
|