refactor: migrate infra to mytheclipse crates (retry, cache, event, ratelimit, config)
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.
This commit is contained in:
asepharyana
2026-08-30 18:53:00 +07:00
parent 6ebff90d5b
commit c7ffd29e79
14 changed files with 703 additions and 564 deletions
@@ -0,0 +1,155 @@
# 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)`
Generated
+253 -272
View File
@@ -79,12 +79,6 @@ dependencies = [
"derive_arbitrary", "derive_arbitrary",
] ]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@@ -272,20 +266,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "backoff"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
dependencies = [
"futures-core",
"getrandom 0.2.17",
"instant",
"pin-project-lite",
"rand 0.8.5",
"tokio",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.22.1" version = "0.22.1"
@@ -312,6 +292,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.0" version = "2.11.0"
@@ -508,61 +494,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "config"
version = "0.15.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c"
dependencies = [
"async-trait",
"convert_case",
"json5",
"pathdiff",
"ron",
"rust-ini",
"serde-untagged",
"serde_core",
"serde_json",
"toml",
"winnow",
"yaml-rust2",
]
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.9.6" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.17",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -622,6 +559,15 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crossbeam-channel"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.6" version = "0.8.6"
@@ -656,12 +602,6 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@@ -825,15 +765,6 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
@@ -885,17 +816,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-serde"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec"
dependencies = [
"serde",
"serde_core",
"typeid",
]
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.14" version = "0.3.14"
@@ -934,6 +854,16 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.9" version = "0.1.9"
@@ -998,6 +928,15 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "funty" name = "funty"
version = "2.0.0" version = "2.0.0"
@@ -1605,12 +1544,23 @@ dependencies = [
] ]
[[package]] [[package]]
name = "instant" name = "inotify"
version = "0.1.13" version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [ dependencies = [
"cfg-if", "bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
dependencies = [
"libc",
] ]
[[package]] [[package]]
@@ -1667,14 +1617,23 @@ dependencies = [
] ]
[[package]] [[package]]
name = "json5" name = "kqueue"
version = "0.4.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea"
dependencies = [ dependencies = [
"pest", "kqueue-sys",
"pest_derive", "libc",
"serde", ]
[[package]]
name = "kqueue-sys"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
dependencies = [
"bitflags 2.11.0",
"libc",
] ]
[[package]] [[package]]
@@ -1710,7 +1669,7 @@ version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.0",
"libc", "libc",
"plain", "plain",
"redox_syscall 0.7.3", "redox_syscall 0.7.3",
@@ -1833,6 +1792,18 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.0" version = "1.2.0"
@@ -1861,6 +1832,61 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "mytheclipse"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62ec67baf3bde7c129eefa282ca89768f3975ce60dcba8c5f7adb8e045d404aa"
dependencies = [
"async-trait",
"num_cpus",
"rand 0.8.5",
"thiserror 2.0.18",
"tokio",
"tracing",
]
[[package]]
name = "mytheclipse-cache"
version = "1.21.1"
dependencies = [
"async-trait",
"redis",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]]
name = "mytheclipse-config"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09ce745aab668bcd51971d5f297525ff2d9e0534ff252b7b473433b4e855c8d"
dependencies = [
"dotenvy",
"notify",
"serde",
"serde_json",
"serde_yaml",
"tokio",
"toml",
"tracing",
]
[[package]]
name = "mytheclipse-event"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef3f6657e41861ba79425f9c7e9442628f3e70a454da25d392d4bfc37326210b"
dependencies = [
"async-trait",
"serde",
"serde_json",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.18" version = "0.2.18"
@@ -1884,6 +1910,25 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.11.0",
"crossbeam-channel",
"filetime",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio 0.8.11",
"walkdir",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@@ -1977,7 +2022,7 @@ version = "0.10.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.0",
"cfg-if", "cfg-if",
"foreign-types", "foreign-types",
"libc", "libc",
@@ -2090,16 +2135,6 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list",
"hashbrown 0.14.5",
]
[[package]] [[package]]
name = "ouroboros" name = "ouroboros"
version = "0.18.5" version = "0.18.5"
@@ -2153,12 +2188,6 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "0.7.0"
@@ -2174,49 +2203,6 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662"
dependencies = [
"memchr",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "pest_meta"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220"
dependencies = [
"pest",
"sha2",
]
[[package]] [[package]]
name = "pgvector" name = "pgvector"
version = "0.4.1" version = "0.4.1"
@@ -2390,7 +2376,7 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [ dependencies = [
"toml_edit", "toml_edit 0.25.8+spec-1.1.0",
] ]
[[package]] [[package]]
@@ -2617,7 +2603,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.0",
] ]
[[package]] [[package]]
@@ -2626,7 +2612,7 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.0",
] ]
[[package]] [[package]]
@@ -2754,20 +2740,6 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "ron"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32"
dependencies = [
"bitflags",
"once_cell",
"serde",
"serde_derive",
"typeid",
"unicode-ident",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.10" version = "0.9.10"
@@ -2822,16 +2794,6 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "rust-ini"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]] [[package]]
name = "rust_decimal" name = "rust_decimal"
version = "1.41.0" version = "1.41.0"
@@ -2870,7 +2832,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@@ -2981,15 +2943,17 @@ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"axum 0.8.8", "axum 0.8.8",
"backoff",
"chrono", "chrono",
"config",
"dashmap", "dashmap",
"deadpool-redis", "deadpool-redis",
"dotenvy", "dotenvy",
"flate2", "flate2",
"futures", "futures",
"http", "http",
"mytheclipse",
"mytheclipse-cache",
"mytheclipse-config",
"mytheclipse-event",
"opentelemetry", "opentelemetry",
"opentelemetry-otlp", "opentelemetry-otlp",
"opentelemetry_sdk", "opentelemetry_sdk",
@@ -3114,7 +3078,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.0",
"core-foundation 0.10.1", "core-foundation 0.10.1",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -3137,7 +3101,7 @@ version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.0",
"cssparser", "cssparser",
"derive_more", "derive_more",
"log", "log",
@@ -3166,18 +3130,6 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde-untagged"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
dependencies = [
"erased-serde",
"serde",
"serde_core",
"typeid",
]
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.228" version = "1.0.228"
@@ -3224,11 +3176,11 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.1.0" version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [ dependencies = [
"serde_core", "serde",
] ]
[[package]] [[package]]
@@ -3243,6 +3195,19 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.13.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "servo_arc" name = "servo_arc"
version = "0.4.3" version = "0.4.3"
@@ -3488,7 +3453,7 @@ dependencies = [
"atoi", "atoi",
"base64", "base64",
"bigdecimal", "bigdecimal",
"bitflags", "bitflags 2.11.0",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono", "chrono",
@@ -3535,7 +3500,7 @@ dependencies = [
"atoi", "atoi",
"base64", "base64",
"bigdecimal", "bigdecimal",
"bitflags", "bitflags 2.11.0",
"byteorder", "byteorder",
"chrono", "chrono",
"crc", "crc",
@@ -3677,6 +3642,17 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "syn"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "sync_wrapper" name = "sync_wrapper"
version = "1.0.2" version = "1.0.2"
@@ -3703,7 +3679,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.0",
"core-foundation 0.9.4", "core-foundation 0.9.4",
"system-configuration-sys", "system-configuration-sys",
] ]
@@ -3828,15 +3804,6 @@ dependencies = [
"time-core", "time-core",
] ]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.2" version = "0.8.2"
@@ -3864,13 +3831,13 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.50.0" version = "1.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio 1.2.0",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
@@ -3881,13 +3848,13 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "2.6.1" version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.117", "syn 3.0.4",
] ]
[[package]] [[package]]
@@ -3948,15 +3915,23 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "1.1.0+spec-1.1.0" version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [ dependencies = [
"serde_core", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime 0.6.11",
"toml_parser", "toml_edit 0.22.27",
"winnow", ]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
] ]
[[package]] [[package]]
@@ -3968,6 +3943,20 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap 2.13.0",
"serde",
"serde_spanned",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.15",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.25.8+spec-1.1.0" version = "0.25.8+spec-1.1.0"
@@ -3975,9 +3964,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c"
dependencies = [ dependencies = [
"indexmap 2.13.0", "indexmap 2.13.0",
"toml_datetime", "toml_datetime 1.1.0+spec-1.1.0",
"toml_parser", "toml_parser",
"winnow", "winnow 1.0.0",
] ]
[[package]] [[package]]
@@ -3986,9 +3975,15 @@ version = "1.1.0+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
dependencies = [ dependencies = [
"winnow", "winnow 1.0.0",
] ]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "tonic" name = "tonic"
version = "0.12.3" version = "0.12.3"
@@ -4062,7 +4057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"bitflags", "bitflags 2.11.0",
"bytes", "bytes",
"futures-core", "futures-core",
"futures-util", "futures-util",
@@ -4181,24 +4176,12 @@ dependencies = [
"utf-8", "utf-8",
] ]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.19.0" version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.9.0" version = "2.9.0"
@@ -4232,12 +4215,6 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.2" version = "0.2.2"
@@ -4250,6 +4227,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@@ -4504,7 +4487,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.11.0",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"indexmap 2.13.0", "indexmap 2.13.0",
"semver", "semver",
@@ -4787,6 +4770,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.0" version = "1.0.0"
@@ -4854,7 +4846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags", "bitflags 2.11.0",
"indexmap 2.13.0", "indexmap 2.13.0",
"log", "log",
"serde", "serde",
@@ -4899,17 +4891,6 @@ dependencies = [
"tap", "tap",
] ]
[[package]]
name = "yaml-rust2"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9"
dependencies = [
"arraydeque",
"encoding_rs",
"hashlink",
]
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "1.0.1" version = "1.0.1"
+5 -2
View File
@@ -13,6 +13,11 @@ default-run = "scraper"
# Dependensi yang dibutuhkan saat aplikasi berjalan # Dependensi yang dibutuhkan saat aplikasi berjalan
[dependencies] [dependencies]
mytheclipse = { version = "1.20", features = ["resiliency", "traffic"] }
mytheclipse-cache = { path = "/home/code/mytheclipse/crates/mytheclipse-cache", features = ["l2-redis", "cache-aside"] }
mytheclipse-config = { version = "1.20" }
mytheclipse-event = { version = "1.20" }
axum = { version = "0.8.8", features = ["ws", "multipart", "macros"] } axum = { version = "0.8.8", features = ["ws", "multipart", "macros"] }
tokio = { version = "1.49.0", features = ["full"] } tokio = { version = "1.49.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
@@ -36,7 +41,6 @@ urlencoding = "2.1"
url = "2.5.8" url = "2.5.8"
tower-http = { version = "0.6.8", features = ["fs", "cors", "compression-gzip", "compression-br", "compression-zstd"] } tower-http = { version = "0.6.8", features = ["fs", "cors", "compression-gzip", "compression-br", "compression-zstd"] }
backoff = { version = "0.4", features = ["futures", "tokio"] }
dashmap = "6.1" dashmap = "6.1"
deadpool-redis = { version = "0.22.1", features = ["serde"] } deadpool-redis = { version = "0.22.1", features = ["serde"] }
rayon = "1.11" rayon = "1.11"
@@ -44,7 +48,6 @@ scraper = "0.25.0"
flate2 = "1.1" flate2 = "1.1"
redis = { version = "0.32.7", features = ["tokio-rustls-comp", "safe_iterators"] } redis = { version = "0.32.7", features = ["tokio-rustls-comp", "safe_iterators"] }
thiserror = "2.0.18" thiserror = "2.0.18"
config = { version = "0.15.19", features = ["toml"] }
utoipa = { version = "5.0", features = ["axum_extras"] } utoipa = { version = "5.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0", features = ["axum"] } utoipa-swagger-ui = { version = "9.0", features = ["axum"] }
+1 -1
View File
@@ -77,7 +77,7 @@ impl Application {
// App State components // App State components
let db_arc = Arc::new(db); let db_arc = Arc::new(db);
let event_bus = Arc::new(crate::events::bus::EventBus::new()); let event_bus = Arc::new(crate::events::bus::new_event_bus());
let redis_pool = crate::infrastructure::cache::redis_pool::redis_pool() let redis_pool = crate::infrastructure::cache::redis_pool::redis_pool()
.map_err(|e| anyhow::anyhow!("Failed to init Redis pool: {}", e))?; .map_err(|e| anyhow::anyhow!("Failed to init Redis pool: {}", e))?;
+36 -18
View File
@@ -5,7 +5,7 @@
//! - Fails fast at startup if required variables are missing //! - Fails fast at startup if required variables are missing
//! - Supports hierarchical configuration (default -> environment-specific) //! - Supports hierarchical configuration (default -> environment-specific)
use config::{Config, ConfigError, Environment, File}; use mytheclipse_config::ConfigError;
use serde::Deserialize; use serde::Deserialize;
use std::env; use std::env;
use std::sync::LazyLock; use std::sync::LazyLock;
@@ -220,6 +220,8 @@ impl AppConfig {
/// 2. `config/{environment}.toml` /// 2. `config/{environment}.toml`
/// 3. `config/default.toml` /// 3. `config/default.toml`
pub fn load() -> Result<Self, ConfigError> { pub fn load() -> Result<Self, ConfigError> {
use mytheclipse_config::ConfigLoader;
// Load .env file first // Load .env file first
if let Err(e) = dotenvy::dotenv() { if let Err(e) = dotenvy::dotenv() {
tracing::debug!("Could not load .env file: {}", e); tracing::debug!("Could not load .env file: {}", e);
@@ -227,24 +229,40 @@ impl AppConfig {
let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "development".into()); let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "development".into());
let config = Config::builder() // Start with default config file (optional — many deploys are env-only)
// Start with default config file let mut loader = ConfigLoader::<AppConfig>::new()
.add_source(File::with_name("config/default").required(false)) .load_dotenv(std::path::Path::new(".env"))
// Layer on environment-specific values .unwrap_or_else(|_| ConfigLoader::<AppConfig>::new());
.add_source(File::with_name(&format!("config/{}", run_mode)).required(false))
// Add environment variables (with APP_ prefix)
.add_source(
Environment::with_prefix("APP")
.separator("__")
.try_parsing(true),
)
// Map legacy env vars to new config structure
.set_override_option("database_url", env::var("DATABASE_URL").ok())?
.set_override_option("jwt_secret", env::var("JWT_SECRET").ok())?
.set_override_option("redis_url", env::var("REDIS_URL").ok())?
.build()?;
config.try_deserialize() let default_path = std::path::Path::new("config/default.toml");
if default_path.exists() {
loader = loader.merge_file(default_path)?;
}
// Layer on environment-specific values (optional)
let env_path_str = format!("config/{}.toml", run_mode);
let env_path = std::path::Path::new(&env_path_str);
if env_path.exists() {
loader = loader.merge_file(env_path)?;
}
// Add environment variables (with APP_ prefix)
loader = loader.merge_env("APP");
// Map legacy env vars to new config structure.
// ConfigLoader merges at leaf level; legacy vars override the merged
// value directly (highest priority after APP_*).
let mut value = loader.peek().clone();
if let Ok(v) = env::var("DATABASE_URL") {
value["database_url"] = serde_json::Value::String(v);
}
if let Ok(v) = env::var("JWT_SECRET") {
value["jwt_secret"] = serde_json::Value::String(v);
}
if let Ok(v) = env::var("REDIS_URL") {
value["redis_url"] = serde_json::Value::String(v);
}
ConfigLoader::<AppConfig>::new().merge_value(value).build()
} }
/// Check if running in production mode /// Check if running in production mode
+18 -120
View File
@@ -1,146 +1,44 @@
//! Event bus implementation. //! Event bus — delegated to `mytheclipse_event`.
//!
//! The scraper uses `TypedEventBus<InMemoryEventBus>` from the mytheclipse
//! event crate (JSON-typed pub/sub over an in-process broadcast bus).
//! Domain events are plain serde structs; `mytheclipse_event::Event` is a
//! blanket trait, so every `Serialize + DeserializeOwned + Send + Sync + 'static`
//! type is automatically an event — no manual impl needed.
use async_trait::async_trait; use mytheclipse_event::{InMemoryEventBus, TypedEventBus};
use std::{any::TypeId, collections::HashMap, sync::Arc}; /// Event payload marker — re-exported so domain types can reference it.
use tokio::sync::{broadcast, RwLock}; pub use mytheclipse_event::Event;
use tracing::{debug, info};
/// Trait for events that can be published. /// The scraper's application event bus.
pub trait Event: Clone + Send + Sync + 'static { pub type EventBus = TypedEventBus<InMemoryEventBus>;
/// Event name for logging/debugging.
const NAME: &'static str;
}
/// Trait for event handlers. /// Build a new in-memory event bus.
#[async_trait] pub fn new_event_bus() -> EventBus {
pub trait EventHandler<E: Event>: Send + Sync { TypedEventBus::new(InMemoryEventBus::default())
async fn handle(&self, event: E);
}
/// The event bus for publishing and subscribing to events.
pub struct EventBus {
channels: RwLock<HashMap<TypeId, Box<dyn std::any::Any + Send + Sync>>>,
}
impl EventBus {
/// Create a new event bus.
pub fn new() -> Self {
Self {
channels: RwLock::new(HashMap::new()),
}
}
/// Publish an event to all subscribers.
pub async fn publish<E: Event>(&self, event: E) {
let type_id = TypeId::of::<E>();
let channels = self.channels.read().await;
if let Some(sender) = channels.get(&type_id) {
if let Some(tx) = sender.downcast_ref::<broadcast::Sender<E>>() {
let _ = tx.send(event);
debug!("Published event: {}", E::NAME);
}
}
}
/// Subscribe to events of a specific type.
/// Returns a receiver that can be used to receive events.
pub async fn subscribe<E: Event>(&self) -> broadcast::Receiver<E> {
let type_id = TypeId::of::<E>();
// Check if channel exists
{
let channels = self.channels.read().await;
if let Some(sender) = channels.get(&type_id) {
if let Some(tx) = sender.downcast_ref::<broadcast::Sender<E>>() {
return tx.subscribe();
}
}
}
// Create new channel
let (tx, rx) = broadcast::channel::<E>(100);
{
let mut channels = self.channels.write().await;
channels.insert(type_id, Box::new(tx));
}
// Re-get the receiver from the stored sender
let channels = self.channels.read().await;
if let Some(sender) = channels.get(&type_id) {
if let Some(tx) = sender.downcast_ref::<broadcast::Sender<E>>() {
return tx.subscribe();
}
}
rx
}
/// Register a handler for a specific event type.
/// The handler will be called whenever an event of that type is published.
pub async fn on<E: Event, H: EventHandler<E> + 'static>(&self, handler: H) {
let mut rx = self.subscribe::<E>().await;
let handler = Arc::new(handler);
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(event) => {
handler.handle(event).await;
}
Err(broadcast::error::RecvError::Closed) => break,
Err(broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!("Event handler lagged by {} events", n);
}
}
}
});
info!("Registered handler for event: {}", E::NAME);
}
}
impl Default for EventBus {
fn default() -> Self {
Self::new()
}
} }
// Common events // Common events
/// User registered event. /// User registered event.
#[derive(Clone, Debug)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct UserRegistered { pub struct UserRegistered {
pub user_id: String, pub user_id: String,
pub email: String, pub email: String,
pub name: String, pub name: String,
} }
impl Event for UserRegistered {
const NAME: &'static str = "user.registered";
}
/// User logged in event. /// User logged in event.
#[derive(Clone, Debug)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct UserLoggedIn { pub struct UserLoggedIn {
pub user_id: String, pub user_id: String,
pub ip_address: Option<String>, pub ip_address: Option<String>,
} }
impl Event for UserLoggedIn {
const NAME: &'static str = "user.logged_in";
}
/// Order created event. /// Order created event.
#[derive(Clone, Debug)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct OrderCreated { pub struct OrderCreated {
pub order_id: String, pub order_id: String,
pub user_id: String, pub user_id: String,
pub total: f64, pub total: f64,
} }
impl Event for OrderCreated {
const NAME: &'static str = "order.created";
}
+1
View File
@@ -1,3 +1,4 @@
pub mod mytheclipse;
pub mod redis; pub mod redis;
pub mod redis_pool; pub mod redis_pool;
+54
View File
@@ -0,0 +1,54 @@
//! Bridge between the deadpool Redis pool and `mytheclipse_cache::RedisCache`.
//!
//! The scraper keeps its deadpool `Pool` (connection lifecycle, recycling) and
//! hands each checked-out connection to mytheclipse's `RedisCache` (which
//! implements the library `Cache` trait). Because both sides now share the
//! same `redis` crate version, a `deadpool_redis::Connection` can be converted
//! directly into a `redis::aio::MultiplexedConnection` via `take()`.
use std::sync::LazyLock;
use mytheclipse_cache::{Cache, CacheError, RedisCache};
use tokio::sync::OnceCell;
use crate::infrastructure::cache::redis_pool::redis_pool;
/// Lazily-initialised shared `RedisCache` built from the deadpool pool.
///
/// The multiplexed connection is cheaply cloneable (Arc-backed), so the whole
/// process shares one logical connection while deadpool manages recycling.
static REDIS_CACHE: LazyLock<OnceCell<RedisCache>> = LazyLock::new(OnceCell::new);
/// Return a handle to the shared mytheclipse `RedisCache`, initialising it on
/// first use from the deadpool pool.
pub async fn redis_cache() -> Result<&'static RedisCache, CacheError> {
let cell = &*REDIS_CACHE;
cell.get_or_try_init(|| async {
let pool = redis_pool().map_err(CacheError::Io)?;
let conn = pool
.get()
.await
.map_err(|e| CacheError::Io(e.to_string()))?;
let mux = deadpool_redis::Connection::take(conn);
Ok(RedisCache::new(mux))
})
.await
}
/// Convenience wrappers so callers can use the mytheclipse `Cache` methods
/// directly without importing the trait twice.
pub async fn get(key: &str) -> Result<Option<Vec<u8>>, CacheError> {
redis_cache().await?.get(key).await
}
pub async fn set(
key: &str,
value: Vec<u8>,
ttl: Option<std::time::Duration>,
) -> Result<(), CacheError> {
redis_cache().await?.set(key, value, ttl).await
}
pub async fn invalidate(key: &str) -> Result<(), CacheError> {
redis_cache().await?.invalidate(key).await
}
+42 -56
View File
@@ -1,70 +1,59 @@
//! Redis caching helpers. //! Redis caching helpers — typed wrapper over `mytheclipse_cache`.
//!
//! The underlying byte-cache is `mytheclipse_cache::RedisCache` (which
//! implements the library `Cache` trait). This module keeps the ergonomic
//! typed JSON surface the use cases rely on (`get_or_set`, `set_with_ttl`)
//! while delegating the actual Redis commands to the library.
use deadpool_redis::redis::AsyncCommands; use mytheclipse_cache::{Cache as CacheTrait, CacheError};
use deadpool_redis::Pool;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use tracing::{debug, error}; use tracing::debug;
/// Default cache TTL in seconds (5 minutes). /// Default cache TTL in seconds (5 minutes).
pub const DEFAULT_CACHE_TTL: u64 = 300; pub const DEFAULT_CACHE_TTL: u64 = 300;
/// Cache helper for Redis operations. /// Typed JSON cache helper over the shared mytheclipse `RedisCache`.
pub struct Cache<'a> { pub struct Cache<'a> {
pool: &'a Pool, _marker: std::marker::PhantomData<&'a ()>,
} }
impl<'a> Cache<'a> { impl<'a> Cache<'a> {
pub fn new(pool: &'a Pool) -> Self { pub fn new(_pool: &'a deadpool_redis::Pool) -> Self {
Self { pool } Self {
_marker: std::marker::PhantomData,
}
}
async fn cache(&self) -> Result<&'static mytheclipse_cache::RedisCache, CacheError> {
super::mytheclipse::redis_cache().await
} }
pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> { pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
let mut conn = match self.pool.get().await { match self.cache().await {
Ok(c) => c, Ok(cache) => match CacheTrait::get(cache, key).await {
Ok(Some(bytes)) => serde_json::from_slice(&bytes).ok(),
Ok(None) => None,
Err(e) => { Err(e) => {
error!("Cache: failed to get connection: {}", e); debug!("Cache: get error for {}: {}", key, e);
return None; None
}
},
Err(e) => {
debug!("Cache: unavailable for {}: {}", key, e);
None
} }
};
let cached: Option<String> = conn.get(key).await.ok()?;
if cached.is_some() {
debug!("Cache hit: {}", key);
} else {
debug!("Cache miss: {}", key);
} }
cached.and_then(|json| serde_json::from_str(&json).ok())
} }
pub async fn mget<T: DeserializeOwned>(&self, keys: &[String]) -> Vec<Option<T>> { pub async fn mget<T: DeserializeOwned>(&self, keys: &[String]) -> Vec<Option<T>> {
if keys.is_empty() { if keys.is_empty() {
return Vec::new(); return Vec::new();
} }
let mut out = Vec::with_capacity(keys.len());
let mut conn = match self.pool.get().await { for k in keys {
Ok(c) => c, out.push(self.get::<T>(k).await);
Err(e) => {
error!("Cache: failed to get connection: {}", e);
return std::iter::repeat_with(|| None).take(keys.len()).collect();
} }
}; out
use deadpool_redis::redis::cmd;
let cached_values: Vec<Option<String>> =
match cmd("MGET").arg(keys).query_async(&mut conn).await {
Ok(v) => v,
Err(e) => {
error!("Cache: failed to mget values: {}", e);
return std::iter::repeat_with(|| None).take(keys.len()).collect();
}
};
cached_values
.into_iter()
.map(|opt_s| opt_s.and_then(|json| serde_json::from_str(&json).ok()))
.collect()
} }
pub async fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<(), String> { pub async fn set<T: Serialize>(&self, key: &str, value: &T) -> Result<(), String> {
@@ -77,9 +66,10 @@ impl<'a> Cache<'a> {
value: &T, value: &T,
ttl_secs: u64, ttl_secs: u64,
) -> Result<(), String> { ) -> Result<(), String> {
let mut conn = self.pool.get().await.map_err(|e| e.to_string())?; let json = serde_json::to_vec(value).map_err(|e| e.to_string())?;
let json = serde_json::to_string(value).map_err(|e| e.to_string())?; let ttl = std::time::Duration::from_secs(ttl_secs);
conn.set_ex::<_, _, ()>(key, json, ttl_secs) let cache = self.cache().await.map_err(|e| e.to_string())?;
CacheTrait::set(cache, key, json, Some(ttl))
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
debug!("Cache: set key {} with TTL {}s", key, ttl_secs); debug!("Cache: set key {} with TTL {}s", key, ttl_secs);
@@ -87,18 +77,14 @@ impl<'a> Cache<'a> {
} }
pub async fn delete(&self, key: &str) -> Result<(), String> { pub async fn delete(&self, key: &str) -> Result<(), String> {
let mut conn = self.pool.get().await.map_err(|e| e.to_string())?; let cache = self.cache().await.map_err(|e| e.to_string())?;
conn.del::<_, ()>(key).await.map_err(|e| e.to_string())?; CacheTrait::invalidate(cache, key)
debug!("Cache: deleted key {}", key); .await
Ok(()) .map_err(|e| e.to_string())
} }
pub async fn exists(&self, key: &str) -> bool { pub async fn exists(&self, key: &str) -> bool {
let mut conn = match self.pool.get().await { self.get::<serde_json::Value>(key).await.is_some()
Ok(c) => c,
Err(_) => return false,
};
conn.exists::<_, bool>(key).await.unwrap_or(false)
} }
/// Get or set: returns cached value or computes and caches new value. /// Get or set: returns cached value or computes and caches new value.
+3 -7
View File
@@ -1,7 +1,6 @@
//! Otakudesu anime scraping repository. //! Otakudesu anime scraping repository.
use async_trait::async_trait; use async_trait::async_trait;
use backoff::future::retry;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::domain::entity::anime::{ use crate::domain::entity::anime::{
@@ -13,7 +12,7 @@ use crate::domain::repository::ScrapingRepository;
use crate::infrastructure::repository::parsers::otakudesu_parser; use crate::infrastructure::repository::parsers::otakudesu_parser;
use crate::infrastructure::scraping::html_fetcher::fetch_html_with_retry; use crate::infrastructure::scraping::html_fetcher::fetch_html_with_retry;
use crate::infrastructure::scraping::proxy_fetch::fetch_with_proxy; use crate::infrastructure::scraping::proxy_fetch::fetch_with_proxy;
use crate::infrastructure::scraping::retry::{default_backoff, transient}; use crate::infrastructure::scraping::retry::{default_backoff, retry, retry_all};
const OTAKUDESU_BASE_URL: &str = "https://otakudesu.cloud"; const OTAKUDESU_BASE_URL: &str = "https://otakudesu.cloud";
@@ -208,14 +207,11 @@ impl OtakudesuRepository {
} }
Err(e) => { Err(e) => {
warn!("Failed to fetch URL: {}, error: {:?}", url_owned, e); warn!("Failed to fetch URL: {}, error: {:?}", url_owned, e);
Err(transient(ScrapingError::Http(format!( Err(ScrapingError::Http(format!("Proxy fetch failed: {}", e)))
"Proxy fetch failed: {}",
e
))))
} }
} }
}; };
retry(backoff, fetch_op) retry(backoff, retry_all, fetch_op)
.await .await
.map_err(|e| ScrapingError::Http(e.to_string())) .map_err(|e| ScrapingError::Http(e.to_string()))
} }
+3 -4
View File
@@ -4,8 +4,7 @@
use crate::domain::error::ScrapingError; use crate::domain::error::ScrapingError;
use crate::infrastructure::scraping::proxy_fetch::fetch_with_proxy; use crate::infrastructure::scraping::proxy_fetch::fetch_with_proxy;
use crate::infrastructure::scraping::retry::{default_backoff, transient}; use crate::infrastructure::scraping::retry::{default_backoff, retry, retry_all};
use backoff::future::retry;
use regex::Regex; use regex::Regex;
use scraper::{ElementRef, Html, Selector}; use scraper::{ElementRef, Html, Selector};
use std::sync::LazyLock; use std::sync::LazyLock;
@@ -23,12 +22,12 @@ pub async fn fetch_html_with_retry(url: &str) -> Result<String, ScrapingError> {
} }
Err(e) => { Err(e) => {
warn!("Failed to fetch: {}, error: {:?}", url, e); warn!("Failed to fetch: {}, error: {:?}", url, e);
Err(transient(e)) Err(e)
} }
} }
}; };
retry(backoff, fetch_operation) retry(backoff, retry_all, fetch_operation)
.await .await
.map_err(|e| ScrapingError::Http(e.to_string())) .map_err(|e| ScrapingError::Http(e.to_string()))
} }
+14 -12
View File
@@ -2,12 +2,11 @@
// Updated for sync Redis API, reqwest API changes, and concurrency optimization. // Updated for sync Redis API, reqwest API changes, and concurrency optimization.
use dashmap::DashMap; use dashmap::DashMap;
use redis::AsyncCommands;
use std::sync::LazyLock; use std::sync::LazyLock;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tracing::{debug, error, warn}; use tracing::{debug, error, warn};
use crate::infrastructure::cache::redis_pool::get_redis_conn; use crate::infrastructure::cache::mytheclipse;
use crate::infrastructure::utils::cache_ttl::CACHE_TTL_VERY_SHORT; use crate::infrastructure::utils::cache_ttl::CACHE_TTL_VERY_SHORT;
use crate::infrastructure::utils::http::common_headers; use crate::infrastructure::utils::http::common_headers;
use crate::infrastructure::utils::http::is_internet_baik_block_page; use crate::infrastructure::utils::http::is_internet_baik_block_page;
@@ -52,13 +51,13 @@ fn get_fetch_cache_key(slug: &str) -> String {
} }
async fn get_cached_fetch(slug: &str) -> Result<Option<FetchResult>, AppError> { async fn get_cached_fetch(slug: &str) -> Result<Option<FetchResult>, AppError> {
let mut conn = get_redis_conn().await?;
let key = get_fetch_cache_key(slug); let key = get_fetch_cache_key(slug);
let bytes = mytheclipse::get(&key)
.await
.map_err(|e| AppError::Internal(format!("Cache get failed for {}: {}", slug, e)))?;
let cached: Option<String> = conn.get(&key).await?; if let Some(bytes) = bytes {
match serde_json::from_slice::<FetchResult>(&bytes) {
if let Some(cached_str) = cached {
match serde_json::from_str::<FetchResult>(&cached_str) {
Ok(parsed) => { Ok(parsed) => {
debug!("[fetchWithProxy] Returning cached response for {}", slug); debug!("[fetchWithProxy] Returning cached response for {}", slug);
Ok(Some(parsed)) Ok(Some(parsed))
@@ -71,14 +70,17 @@ async fn get_cached_fetch(slug: &str) -> Result<Option<FetchResult>, AppError> {
} }
async fn set_cached_fetch(slug: &str, value: &FetchResult) -> Result<(), AppError> { async fn set_cached_fetch(slug: &str, value: &FetchResult) -> Result<(), AppError> {
let mut conn = get_redis_conn().await?;
let key = get_fetch_cache_key(slug); let key = get_fetch_cache_key(slug);
let json_string = serde_json::to_string(value)?; let json = serde_json::to_vec(value)?;
// Use standardized TTL // Use standardized TTL
conn.set_ex::<_, _, ()>(&key, &json_string, CACHE_TTL_VERY_SHORT) mytheclipse::set(
.await?; &key,
Ok(()) json,
Some(std::time::Duration::from_secs(CACHE_TTL_VERY_SHORT)),
)
.await
.map_err(|e| AppError::Internal(format!("Cache set failed for {}: {}", slug, e)))
} }
// --- REDIS CACHE WRAPPER END --- // --- REDIS CACHE WRAPPER END ---
+98 -38
View File
@@ -1,16 +1,21 @@
//! HTTP retry utilities with exponential backoff. //! HTTP retry utilities — delegated to mytheclipse `retry` primitives.
//!
//! The scraper uses mytheclipse's retry machinery (`RetryConfig` +
//! `mytheclipse::retry`). These helpers keep the old backoff-style call
//! sites ergonomic while delegating the actual backoff/sleep/jitter logic
//! to the library.
use backoff::ExponentialBackoff; use mytheclipse::{JitterKind, RetryConfig};
use std::time::Duration; use std::time::Duration;
/// Default retry configuration for HTTP requests. /// Default retry configuration for HTTP requests.
pub fn default_backoff() -> ExponentialBackoff { pub fn default_backoff() -> RetryConfig {
ExponentialBackoff { RetryConfig {
initial_interval: Duration::from_millis(500), max_attempts: 4,
max_interval: Duration::from_secs(10), base_delay: Duration::from_millis(500),
multiplier: 2.0, max_delay: Duration::from_secs(10),
max_elapsed_time: Some(Duration::from_secs(30)), factor: 2.0,
..Default::default() jitter: JitterKind::Full,
} }
} }
@@ -20,47 +25,102 @@ pub fn custom_backoff(
max_secs: u64, max_secs: u64,
multiplier: f64, multiplier: f64,
max_elapsed_secs: u64, max_elapsed_secs: u64,
) -> ExponentialBackoff { ) -> RetryConfig {
ExponentialBackoff { RetryConfig {
initial_interval: Duration::from_millis(initial_ms), max_attempts: compute_attempts(initial_ms, max_secs, multiplier, max_elapsed_secs).max(1),
max_interval: Duration::from_secs(max_secs), base_delay: Duration::from_millis(initial_ms),
multiplier, max_delay: Duration::from_secs(max_secs),
max_elapsed_time: Some(Duration::from_secs(max_elapsed_secs)), factor: multiplier,
..Default::default() jitter: JitterKind::Full,
} }
} }
/// Estimate the number of attempts that fit in `max_elapsed_secs` given the
/// exponential backoff curve: solve `sum(base * factor^i) ≈ max_elapsed`.
fn compute_attempts(initial_ms: u64, max_secs: u64, multiplier: f64, max_elapsed_secs: u64) -> u32 {
if initial_ms == 0 || multiplier <= 1.0 {
return 1;
}
let mut elapsed_ms = 0u64;
let mut attempt = 0u64;
let max_ms = max_secs.saturating_mul(1000);
let budget_ms = max_elapsed_secs.saturating_mul(1000);
let mut delay_ms = initial_ms;
while elapsed_ms < budget_ms {
elapsed_ms = elapsed_ms.saturating_add(delay_ms);
attempt += 1;
delay_ms = ((delay_ms as f64) * multiplier).min(max_ms as f64) as u64;
}
attempt as u32
}
/// Quick backoff for fast retries (3 attempts, 100ms initial). /// Quick backoff for fast retries (3 attempts, 100ms initial).
pub fn quick_backoff() -> ExponentialBackoff { pub fn quick_backoff() -> RetryConfig {
ExponentialBackoff { RetryConfig {
initial_interval: Duration::from_millis(100), max_attempts: 3,
max_interval: Duration::from_secs(1), base_delay: Duration::from_millis(100),
multiplier: 2.0, max_delay: Duration::from_secs(1),
max_elapsed_time: Some(Duration::from_secs(5)), factor: 2.0,
..Default::default() jitter: JitterKind::Full,
} }
} }
/// Slow backoff for long operations (10 attempts, 1s initial). /// Slow backoff for long operations (10 attempts, 1s initial).
pub fn slow_backoff() -> ExponentialBackoff { pub fn slow_backoff() -> RetryConfig {
ExponentialBackoff { RetryConfig {
initial_interval: Duration::from_secs(1), max_attempts: 10,
max_interval: Duration::from_secs(30), base_delay: Duration::from_secs(1),
multiplier: 2.0, max_delay: Duration::from_secs(30),
max_elapsed_time: Some(Duration::from_secs(120)), factor: 2.0,
..Default::default() jitter: JitterKind::Full,
} }
} }
/// Make an error transient (will be retried). /// Re-export mytheclipse retry for convenience.
pub fn transient<E>(err: E) -> backoff::Error<E> { pub use mytheclipse::retry;
backoff::Error::transient(err)
/// Re-export transient/permanent helpers for API compatibility.
///
/// The legacy `backoff` crate distinguished transient vs permanent errors at
/// the error-type level; mytheclipse uses a retry predicate instead. All
/// scraped-HTTP failures are transient by nature (network/5xx), so both
/// helpers return the error unchanged and every call site retries everything
/// (`|_| true`). `permanent` is kept as a no-op alias for source
/// compatibility.
pub fn transient<E>(e: E) -> E {
e
} }
/// Make an error permanent (will NOT be retried). /// No-op alias for source compatibility (see [`transient`]).
pub fn permanent<E>(err: E) -> backoff::Error<E> { pub fn permanent<E>(e: E) -> E {
backoff::Error::permanent(err) e
} }
// Re-export retry function for convenience /// Predicate used by all scraper retry loops: retry every error.
pub use backoff::future::retry; pub fn retry_all<E>(_: &E) -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn backoff_configs_build() {
assert_eq!(default_backoff().max_attempts, 4);
assert_eq!(quick_backoff().max_attempts, 3);
assert_eq!(slow_backoff().max_attempts, 10);
}
#[test]
fn compute_attempts_curve() {
// 100ms base, 2x, 1s max, 5s budget → roughly 6 attempts.
let n = compute_attempts(100, 1, 2.0, 5);
assert!(n >= 4 && n <= 8, "got {n}");
}
#[test]
fn retry_all_retries() {
assert!(retry_all::<std::io::Error>(&std::io::Error::other("x")));
}
}
+19 -33
View File
@@ -1,4 +1,8 @@
//! Rate limiting middleware. //! Rate limiting middleware — backed by `mytheclipse::RateLimiter`.
//!
//! The limiter is a token bucket from the mytheclipse core crate. The
//! middleware keeps the same axum shape (State<Arc<RateLimiter>>) and
//! `check()` semantics, but delegates token accounting to the library.
use axum::{ use axum::{
extract::Request, extract::Request,
@@ -7,42 +11,24 @@ use axum::{
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Json, Json,
}; };
use std::sync::atomic::{AtomicU64, Ordering}; use mytheclipse::RateLimiter;
use std::sync::{Arc, Mutex}; use std::sync::Arc;
use std::time::Instant;
use crate::presentation::dto::common::ApiResponse; use crate::presentation::dto::common::ApiResponse;
/// Simple in-memory rate limiter. /// Convenience alias so callers don't need the mytheclipse import.
pub struct RateLimiter { pub type AppRateLimiter = RateLimiter;
max_requests: u64,
window_secs: u64, /// Build a rate limiter (rate = requests/sec, burst = max burst capacity).
counter: AtomicU64, pub fn new_rate_limiter(rate_per_sec: f64, burst: u64) -> Arc<RateLimiter> {
window_start: Mutex<Instant>, Arc::new(RateLimiter::new(rate_per_sec, burst))
} }
impl RateLimiter { /// Compatibility constructor matching the old (max_requests, window_secs) API.
pub fn new(max_requests: u64, window_secs: u64) -> Arc<Self> { /// Converts a fixed window into an equivalent token-bucket rate.
Arc::new(Self { pub fn new_window_rate_limiter(max_requests: u64, window_secs: u64) -> Arc<RateLimiter> {
max_requests, let rate_per_sec = max_requests as f64 / window_secs.max(1) as f64;
window_secs, new_rate_limiter(rate_per_sec, max_requests)
counter: AtomicU64::new(0),
window_start: Mutex::new(Instant::now()),
})
}
pub fn check(&self) -> bool {
let Ok(mut window_guard) = self.window_start.lock() else {
return false;
};
let window = &mut *window_guard;
if window.elapsed().as_secs() >= self.window_secs {
*window = Instant::now();
self.counter.store(0, Ordering::SeqCst);
}
let count = self.counter.fetch_add(1, Ordering::SeqCst);
count < self.max_requests
}
} }
pub async fn rate_limit_middleware( pub async fn rate_limit_middleware(
@@ -50,7 +36,7 @@ pub async fn rate_limit_middleware(
request: Request, request: Request,
next: Next, next: Next,
) -> Response { ) -> Response {
if state.check() { if state.try_acquire().is_ok() {
next.run(request).await next.run(request).await
} else { } else {
( (