Перейти к содержимому

F-07 — ProxyPool (Phase 1 in-memory MVP)

Контекст

После F-06 (BaseCollector + WB collectors) — proxy.py был minimal random.choice без state. F-07 spec из X-collectors-proxy §3.2-3.4: sticky per-(account, rate_bucket) + weighted rotation + health checks + per-error-type cooldown.

Plan: 2 дня. Actual: ~1h (~16x speedup, single session).

Что сделано

Phase 1 in-memory implementation в apps/api/src/razmakh_api/etl/proxy.py:

  1. Proxy dataclass: url, weight (1-10), last_used_at, consecutive_failures, last_error, cooldown_until. Identity = url (eq/hash на url).

  2. ProxyPool class:

    • pick(sticky_key) / pick_proxy(sticky_key) — sticky via SHA256(key) mod len(sorted) + weighted random fallback
    • mark_success — reset state + weight+1
    • mark_failure(error_type) — per-type penalty:
      • rate_limit_429 → weight-2, cooldown 60s
      • connection_error → weight-3, cooldown 300s
      • http_5xx → weight-1, no cooldown (WB issue, не IP)
      • other → weight-1
    • Consecutive failures escalation: 3+ failures → 10 min extended cooldown
    • health_check(proxy) — async HEAD https://httpbin.org/ip через proxy, 5s timeout
    • health_check_all() — parallel asyncio.gather
    • Backward compat F-06: from_env(), from_string(), pick() -> str | None
  3. WBClient integration (apps/api/src/razmakh_api/etl/wb/client.py):

    • New args: pool: ProxyPool | None, sticky_key: str | None
    • При pool active: per-request fresh httpx.AsyncClient(proxy=pool.pick_proxy(sticky_key).url)
      • mark_success / mark_failure outcome reporting
    • При pool=None (legacy): single shared client с фиксированным proxy=str (F-06 contract)
    • Graceful fallback: если все proxies в cooldown → log warning + try direct request
  4. Collectors sticky_key (НЕ модифицировал логику кроме sticky_key):

    • wb.statistics.salesf"{ctx.marketplace_account_id}:wb.statistics"
    • wb.common.seller_infof"{ctx.marketplace_account_id}:wb.common"
    • Bucket separation per X-collectors-proxy §3.4 spec
  5. Tests (apps/api/tests/test_proxy.py) — 36 unit tests:

    • Construction: dedup, whitespace strip, default weights
    • from_env: backward compat F-06, blank lines, returns str URL
    • Sticky pick: consistency 10/10 same key, different keys distribute, balanced over 400 keys × 4 proxies
    • Cooldown handling: sticky fall through, all-in-cooldown returns None
    • Weighted random: equal distribution, higher weight picked more
    • mark_failure: per-error-type weight + cooldown, escalation 3+ consecutive
    • mark_success: resets state + weight+1, cap at 10
    • Health check via respx: 200→success, 429→rate_limit, 5xx→http_5xx, ConnectError→connection_error, parallel all

Что заимствовано из X-collectors-proxy

Spec §Что взято в F-07
§3.2 architectureConcept of pool с per-proxy state (weight, health_status, cooldown)
§3.3 health checkHEAD к external endpoint (ipify заменён на httpbin.org/ip — simpler, no JSON parse)
§3.4 stickyPer-(account, rate_bucket) deterministic binding via hash
§3.4 weightedrandom.choices с weights param
§3.4 outcomePer-status-code penalty + cooldown (429 / connection / 5xx)

Что deferred Phase 1.5+ (НЕ сделано сейчас, документировано в plan)

  1. etl.proxy_pool table (PG schema) — Phase 1.5 multi-worker shared state. Текущая реализация: in-memory dict per process. Достаточно для Phase 1 (single uvicorn worker).
  2. Redis-backed pool state — Phase 1.5+ (multi-worker shared limiter + pool, нужен Redis stack).
  3. Cron health monitoring (ipify 30s, WB ping 5min) — F-10 observability. Текущая реализация: health_check() доступен но не auto-scheduled.
  4. 14-day proxy rotation — Phase 1.5+ infra automation.
  5. 10 RU-IP proxies provisioned (Tailscale exit nodes / IPRoyal) — Block 1 prerequisites (provisioning task, не code task).
  6. secret_ref format для proxy credentials — Block 1 prep (когда real proxies появятся, они в Infisical как razmakh://prod/_internal/proxy/<id>/url).
  7. Prometheus metrics exporter (active/banned count, latency p95) — F-10.
  8. Alert rules > 50% unhealthy → page on-call — F-10.
  9. Multi-provider failover (Netnik Tailscale + IPRoyal + ProxyMesh) — Phase 2+.
  10. Per-region geo routing — Phase 2+ (SaaS multi-region).

Lesson learned (главный вывод)

В контексте Phase 1 proxy pool — делай in-memory с state в Proxy dataclass + ProxyPool dict, sticky через deterministic hash, weighted через random.choices. Это покрывает 80% spec из X-collectors-proxy §3.2-3.4 без PG dependency. PG-backed etl.proxy_pool + Redis-backed shared state defer на Phase 1.5+ когда появится multi-worker requirement.

Anti-pattern: писать PG migration + Redis client в Phase 1 когда single uvicorn worker — overengineering, удлиняет F-07 с 1h до 1-2 дней без real value.

В каких будущих ситуациях пригодится

  • Phase 1.5 Block 1.5.1 (multi-worker scaling) — migrate state в Redis (нужна Redis в stack)
  • F-10 observability — добавить Prometheus exporters + Grafana dashboard для pool state
  • Block 1 ETL onboarding — provision 10 RU-IP proxies + secret_ref в Infisical
  • Phase 2 multi-region SaaS — per-region geo routing layer над текущим pool

Что подтверждено

  • 36/36 unit tests pass (respx для httpbin mock, no real network)
  • mypy strict clean (10 etl source files)
  • ruff clean (Cyrillic комментарии OK per per-file-ignores RUF001/002/003)
  • 88/88 full suite pass (нет F-06 regressions: 36 proxy + 15 infisical + 26 manifest + 11 secret_ref)

Process notes

Research consulted перед implementation (per f06-skipped-research lesson):

  • Прочитал X-collectors-proxy.md §3.2-3.4 spec полностью
  • Прочитал C-wb-api-inventory.md §rate limits (token-level → sticky binding rationale)
  • Прочитал lessons/f06-skipped-research-halturность-2026-05-17.md (правило permanent)
  • Прочитал estimates-recalibration template для tracking

Scope discipline: НЕ добавил etl.proxy_pool PG migration (deferred Phase 1.5). НЕ переписал runner / base.py / rate_limiter.py. Только sticky_key в 2 collectors.

Backward compat: F-06 tests pass — ProxyPool.from_env().pick() всё ещё работает, collectors могут вызывать WBClient(token=..., proxy=str) напрямую.

Связанные lessons

  • [[lessons/f06-skipped-research-halturность-2026-05-17]] — что research consultation важна
  • [[lessons/estimates-recalibration-2026-05-17]] — 16x speedup на pure-code задачах подтверждён ещё раз
  • [[research/X-collectors-proxy]] — full spec (часть приземлена сейчас, часть Phase 1.5+)