Deploy Scraper / build-and-deploy (push) Canceled after 0s
Replace hand-rolled infrastructure with the custom mytheclipse library: - retry (src/infrastructure/scraping/retry.rs): backoff crate -> mytheclipse RetryConfig + retry() with retry_all predicate; same call-site helpers. - cache (src/infrastructure/cache): deadpool raw AsyncCommands -> mytheclipse_cache::RedisCache bridge (src/infrastructure/cache/mytheclipse.rs), typed JSON wrapper keeps the Cache<'a> API used by use cases. - events (src/events/bus.rs): custom broadcast pub/sub -> mytheclipse_event InMemoryEventBus + TypedEventBus alias. - ratelimit (src/presentation/middleware/ratelimit.rs): hand-rolled window -> mytheclipse::RateLimiter token bucket. - config (src/config/mod.rs): config crate -> mytheclipse_config ConfigLoader, preserving env-only fallback (config files optional). No route/API/Redis-key changes. mytheclipse-cache path-dep points at local source (redis 0.32 aligned with deadpool). Removed backoff/config deps.
155 lines
8.6 KiB
Markdown
155 lines
8.6 KiB
Markdown
# Spec: Migrate scraper infra to mytheclipse crates (Round 1)
|
|
|
|
Date: 2026-08-30
|
|
Repo: /home/code/scraper (github.com/asepharyana/scraper)
|
|
Goal: Replace hand-rolled infrastructure in the scraper with the user's
|
|
custom `mytheclipse` library crates where there is a clear 1:1 mapping.
|
|
|
|
## Library crates (from /home/code/mytheclipse, all v1.20.0)
|
|
|
|
- `mytheclipse` — core: retry (retry/RetryConfig/JitterKind/RetryError),
|
|
ratelimit (RateLimiter/RateLimitError), timeouts
|
|
(timeout), concurrency primitives, ServiceBuilder,
|
|
runtime_auto (available_parallelism), spawn_io/spawn_bg.
|
|
- `mytheclipse-cache` — Cache trait, MemoryCache, RedisCache (l2-redis),
|
|
CacheAside (cache-aside), CacheError.
|
|
- `mytheclipse-config`— ConfigLoader<T> (file + env merge, hot-reload, validation).
|
|
- `mytheclipse-event` — EventBus trait, InMemoryEventBus, TypedEventBus (byte/JSON pub/sub).
|
|
- `mytheclipse-crypto`— (NOT used in this round — scraper has no crypto/JWT usage.)
|
|
|
|
## Current state (baseline)
|
|
|
|
- Cargo.toml has NO mytheclipse crates. Uses directly:
|
|
- `backoff` — retry with exponential backoff (hand-rolled wrapper in
|
|
src/infrastructure/scraping/retry.rs)
|
|
- `deadpool-redis` — Redis pool + raw `redis::AsyncCommands` in
|
|
src/infrastructure/cache/{redis_pool.rs,redis.rs}
|
|
- `dashmap` — request coalescing in proxy_fetch.rs (IN_FLIGHT map)
|
|
- `rayon` — komik_parser parallel map
|
|
- `config` crate — config loading in src/config/mod.rs
|
|
- `opentelemetry*` — metrics in src/observability/metrics.rs
|
|
- custom EventBus in src/events/bus.rs (unused by any handler)
|
|
- custom RateLimiter in src/presentation/middleware/ratelimit.rs (unused by router)
|
|
- Baseline: `cargo check` currently clean (verified in background).
|
|
|
|
## Replacement mapping (behavior-preserving)
|
|
|
|
| # | Hand-rolled | mytheclipse replacement | Files touched |
|
|
|---|---|---|---|
|
|
| 1 | backoff + retry.rs wrapper | `mytheclipse::retry` + `RetryConfig` | src/infrastructure/scraping/retry.rs, parsing_utils.rs, otakudesu.rs |
|
|
| 2 | deadpool-redis pool in redis_pool.rs | Keep pool, wrap conn with `RedisCache` (mytheclipse-cache) | redis_pool.rs + new bridge, redis.rs, proxy_fetch.rs |
|
|
| 3 | Cache helper (redis.rs) | `CacheAside` + typed JSON serde over `RedisCache` | redis.rs, application/*/use_cases.rs |
|
|
| 4 | custom EventBus (events/bus.rs) | `InMemoryEventBus`/`TypedEventBus` (mytheclipse-event) | events/bus.rs → re-export, state.rs, bootstrap |
|
|
| 5 | custom RateLimiter middleware | `mytheclipse::RateLimiter` (token bucket) | presentation/middleware/ratelimit.rs |
|
|
| 6 | `config` crate loader in config/mod.rs | `mytheclipse-config` ConfigLoader<T> | src/config/mod.rs |
|
|
| 7 | HTTP client wrapper (http_client.rs) | `mytheclipse-http` HttpClient (optional) | http_client.rs (SKIP this round — retry semantics differ; reqwest needs headers/UA control) |
|
|
| 8 | OTel metrics (metrics.rs) | Keep opentelemetry (mytheclipse-tracing has no metrics exporter; avoid behavior change) | SKIP this round |
|
|
| 9 | rayon in komik_parser | `mytheclipse::compute::compute_par_for_each` (feature compute) | komik_parser.rs (SKIP this round — parser correctness risk; rayon works) |
|
|
|
|
## Scope decision (this round)
|
|
|
|
Implement items #1–#6. Skip #7–#9 with rationale:
|
|
- #7 mytheclipse-http client is a thin reqwest wrapper without header/UA control
|
|
needed by common_headers(); converting scrapers through it changes fetch
|
|
semantics (returns bytes, loses status/content-type) — not behavior-preserving.
|
|
- #8 mytheclipse-tracing has no metrics exporter; opentelemetry stays.
|
|
- #9 parser logic (2k+ LOC) is out of scope for infra migration; rayon stays.
|
|
|
|
## Detail per item
|
|
|
|
### 1. retry.rs → mytheclipse::retry
|
|
- Replace `backoff::ExponentialBackoff` with `mytheclipse::{retry, RetryConfig, JitterKind}`.
|
|
- Keep signature-compatible helpers so call sites barely change:
|
|
`default_backoff() -> RetryConfig`, `quick_backoff()`, `slow_backoff()`,
|
|
`custom_backoff(...)`.
|
|
- `transient/permanent` helpers: mytheclipse retry uses a `predicate` closure
|
|
`Fn(&E) -> bool`. Replace transient/permanent with a retryable predicate
|
|
(retry on any error except a marker). To preserve "transient = retry,
|
|
permanent = stop", use a wrapper type or predicate returning true for all
|
|
errors, and treat 4xx-style permanent errors by converting callers to return
|
|
a `Permanent` variant.
|
|
- Keep the `retry` fn name re-exported for minimal call-site churn.
|
|
|
|
### 2. Redis: mytheclipse-cache RedisCache + bridge
|
|
- Add `mytheclipse-cache = { version = "1.20", features = ["l2-redis", "cache-aside"] }`.
|
|
- Keep deadpool pool (mytheclipse has no pool); obtain
|
|
`redis::aio::MultiplexedConnection` from deadpool conn (deadpool_redis::Connection
|
|
derefs to `&mut redis::aio::ConnectionLike` — need to convert via `into_multiplexed()`).
|
|
- New bridge file `src/infrastructure/cache/mytheclipse.rs`:
|
|
`pub fn redis_cache() -> &'static mytheclipse_cache::RedisCache` building
|
|
from the pool lazily (LazyLock).
|
|
|
|
### 3. Cache helper → CacheAside
|
|
- Rewrite `src/infrastructure/cache/redis.rs` as a thin typed wrapper over
|
|
`RedisCache` implementing `get/get_or_set/set/set_with_ttl/delete/exists`
|
|
with serde_json, so use_cases keep the same ergonomic API (minimal churn)
|
|
but delegate to mytheclipse `Cache` trait underneath.
|
|
- `get_or_set` becomes CacheAside-equivalent (read-through).
|
|
|
|
### 4. events/bus.rs → mytheclipse-event
|
|
- Replace custom Event/EventHandler/pub-sub with
|
|
`TypedEventBus<InMemoryEventBus>`.
|
|
- Define domain events as serde structs implementing `mytheclipse_event::Event`
|
|
(which is a blanket trait on Serialize+DeserializeOwned types).
|
|
- `EventBus` type alias: `pub type EventBus = TypedEventBus<InMemoryEventBus>`.
|
|
- state.rs + bootstrap keep `Arc<EventBus>`; `new()` -> `EventBus::new(InMemoryEventBus::default())`.
|
|
- Publish/subscribe usage sites (none currently) adapt if any.
|
|
|
|
### 5. Ratelimit middleware → mytheclipse::RateLimiter
|
|
- Keep middleware shape (axum State) but inner limiter becomes
|
|
`mytheclipse::RateLimiter` token bucket: `RateLimiter::new(rate_per_sec, burst)`.
|
|
- `check()` -> `try_acquire()` returns Result<_, RateLimitError>.
|
|
|
|
### 6. config/mod.rs → mytheclipse-config
|
|
- Replace `config::Config::builder()` with `mytheclipse_config::ConfigLoader<AppConfig>`.
|
|
- Sources: load_dotenv (env feature) → merge_file(config/default.toml) →
|
|
merge_file(config/{RUN_MODE}.toml) → merge_env (APP__ prefix + legacy env
|
|
var mapping preserved).
|
|
- Keep `CONFIG` LazyLock and the same `AppConfig` struct (`Deserialize` +
|
|
`mytheclipse_config::Config` blanket trait).
|
|
|
|
## Schema/type changes
|
|
- `deadpool_redis::Pool` still in AppState (no change) — RedisCache wraps conns.
|
|
- `AppState.event_bus` becomes `Arc<TypedEventBus<InMemoryEventBus>>`.
|
|
- Error mapping: add `From<mytheclipse_cache::CacheError>` for AppError +
|
|
DomainError? Keep the existing string-based paths; add explicit From impls
|
|
where needed.
|
|
|
|
## Backend surface
|
|
- No HTTP route changes. No API contract changes.
|
|
- Redis cache keys/TTLs unchanged. Event topics unchanged (none active).
|
|
|
|
## Frontend surface
|
|
- None (backend-only service).
|
|
|
|
## Verification steps
|
|
1. `cargo check` — exit 0
|
|
2. `cargo test` — all pass
|
|
3. `cargo clippy -- -D warnings` — 0 warnings (project lint gate)
|
|
4. `cargo fmt --check` — clean
|
|
5. `cargo build --release` — succeeds
|
|
6. Smoke: boot server briefly if feasible (needs Redis/DB; else compile-only + unit tests)
|
|
|
|
## Risk register
|
|
- deadpool Connection → MultiplexedConnection conversion API differences
|
|
(0.22 vs 0.27 redis). Verify at compile time; fallback = keep raw conn in
|
|
Cache impl.
|
|
- `mytheclipse::retry` predicate-based vs backoff transient/permanent — call
|
|
sites that relied on permanent-stop need review (repository fetch_html uses
|
|
`transient()` on everything — safe to retry-all).
|
|
- ConfigLoader merge_env nesting: existing env var mapping uses `APP__` prefix
|
|
+ legacy names. Must keep exact names (APP_DATABASE_URL etc.) — verify with
|
|
`peek()` / unit test on the real struct.
|
|
- EventBus trait object: `Arc<TypedEventBus<InMemoryEventBus>>` is concrete —
|
|
no trait object; AppState carries the concrete type to avoid dyn issues.
|
|
|
|
## Migration order (batches, each verified)
|
|
Batch A: Cargo.toml deps + retry.rs rewrite + parsing_utils/otakudesu call sites
|
|
Batch B: redis_pool bridge + Cache rewrite (redis.rs) + proxy_fetch cache calls
|
|
Batch C: events/bus.rs + state.rs + bootstrap
|
|
Batch D: ratelimit middleware + router wiring
|
|
Batch E: config/mod.rs ConfigLoader
|
|
Batch F: full verification + commit
|
|
|
|
Each batch: cargo check + cargo test must stay green. Commit once at end with
|
|
message: `refactor: migrate infra to mytheclipse crates (retry, cache, event, ratelimit, config)` |