Compare commits

..
32 Commits
Author SHA1 Message Date
semantic-release-bot d6534e80d8 chore(release): 1.21.1 [skip ci]
## [1.21.1](https://github.com/asepharyana/mytheclipse/compare/v1.21.0...v1.21.1) (2026-08-29)

### Bug Fixes

* **ci:** restore full CI green — test-matrix, clippy, rustfmt, and rustdoc gates ([1dfc6d6](https://github.com/asepharyana/mytheclipse/commit/1dfc6d68657e5b002896f400d744b052800c9285))
2026-08-29 17:34:26 +00:00
asepharyana 1dfc6d6865 fix(ci): restore full CI green — test-matrix, clippy, rustfmt, and rustdoc gates
Root cause of the failing CI run was that examples/tests referencing
feature-gated items were auto-detected (no required-features), so
`--all-targets` compiled them under feature combinations where those
modules didn't exist. Fixes:

- mytheclipse Cargo.toml: declare the `high_level` example and
  `race_stress` integration test with required-features = ["full"];
  `cargo build --all-targets` now skips them when full is off. This
  clears the whole test-matrix (workspace default, all-features, and
  every single-feature config) which all failed on E0432/E0433.
- lib.rs: auto_metrics_service depends on service_builder, so regate it
  behind all(observability, resiliency) instead of observability alone
  (observability-only build compiled the module without resiliency).
- mytheclipse-tracing: gate `pub mod fmt` behind any
  tracing-subscriber-providing feature so --no-default-features compiles.
- mytheclipse-queue: gate `pub mod worker` behind in-memory (worker.rs
  requires tokio, only provided by in-memory).
- clippy -D warnings fixes: deprecated base64 0.22 free fns -> Engine
  (paseto), unused key field, needless mut (service_builder), unused
  import/dead var/missing is_empty (bg_join), dead is_expired (dlock),
  while-let-iterator->for (parallel_map), type_complexity (shutdown_guard),
  MutexGuard held across await (middleware, now clones Arc'd layers),
  if-let-Err->is_err (queue), unused CliBuilder fields now wired into clap.
- rustdoc -D warnings: resolve retry/MetricsBridge/CircuitBreaker/KeyRing
  intra-doc links and fix the unparseable lifecycle.rs code fence.
- cargo fmt --all to satisfy the Rustfmt gate.
2026-08-30 00:33:21 +07:00
semantic-release-bot 68a0ec9613 chore(release): 1.21.0 [skip ci]
# [1.21.0](https://github.com/asepharyana/mytheclipse/compare/v1.20.0...v1.21.0) (2026-08-29)

### Features

* CPU parallel compute primitives — compute_map, compute_join, compute_par_for_each ([e148944](https://github.com/asepharyana/mytheclipse/commit/e14894479d8ca716a9decf2c1403359fdc376717))
2026-08-29 17:00:18 +00:00
asepharyana e14894479d feat: CPU parallel compute primitives — compute_map, compute_join, compute_par_for_each 2026-08-29 23:59:23 +07:00
semantic-release-bot f86abc4ce1 chore(release): 1.20.0 [skip ci]
# [1.20.0](https://github.com/asepharyana/mytheclipse/compare/v1.19.0...v1.20.0) (2026-08-29)

### Features

* ParallelConcurrency — auto-size concurrency from CPU cores ([d00881c](https://github.com/asepharyana/mytheclipse/commit/d00881c2b96fa29ee0eaa5f43a7c7af90983e802))
2026-08-29 16:41:52 +00:00
asepharyana d00881c2b9 feat: ParallelConcurrency — auto-size concurrency from CPU cores
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s
2026-08-29 23:40:54 +07:00
semantic-release-bot 829c5714d2 chore(release): 1.19.0 [skip ci]
# [1.19.0](https://github.com/asepharyana/mytheclipse/compare/v1.18.0...v1.19.0) (2026-08-29)

### Features

* criterion benchmarks proving primitive overhead is negligible ([5df04d7](https://github.com/asepharyana/mytheclipse/commit/5df04d75126d71d0790028c4a215acb571f25616))
2026-08-29 15:15:05 +00:00
asepharyana 5df04d7512 feat: criterion benchmarks proving primitive overhead is negligible 2026-08-29 22:14:01 +07:00
asepharyana a6cf58ea7f docs: doctest examples for round-7-16 features + re-export async_trait
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s
2026-08-29 21:47:20 +07:00
asepharyana de54e801fb test: race-safety stress tests for core + queue primitives
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s
2026-08-29 21:39:32 +07:00
asepharyana d03d32a923 docs: populate CHANGELOG for 1.2.0-1.18.0 feature history
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s
2026-08-29 21:30:12 +07:00
asepharyana c87ae760bf docs: high_level example wiring round-7-15 abstractions 2026-08-29 21:27:08 +07:00
semantic-release-bot e69f72296c chore(release): 1.18.0 [skip ci]
# [1.18.0](https://github.com/asepharyana/mytheclipse/compare/v1.17.0...v1.18.0) (2026-08-29)

### Features

* round-15 abstractions — parallel_for_each streaming fan-out ([730245e](https://github.com/asepharyana/mytheclipse/commit/730245e9c02c5b6839b9342320b81002ea8421a3))
2026-08-29 14:24:51 +00:00
asepharyana 730245e9c0 feat: round-15 abstractions — parallel_for_each streaming fan-out 2026-08-29 21:23:39 +07:00
semantic-release-bot 3376bee1d3 chore(release): 1.17.0 [skip ci]
# [1.17.0](https://github.com/asepharyana/mytheclipse/compare/v1.16.0...v1.17.0) (2026-08-29)

### Features

* round-14 abstractions — parallel_map bounded fan-out ([bb4998d](https://github.com/asepharyana/mytheclipse/commit/bb4998d8fa68e25415ea79a245313f17a2792a06))
2026-08-29 14:00:53 +00:00
asepharyana bb4998d8fa feat: round-14 abstractions — parallel_map bounded fan-out
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s
2026-08-29 20:59:49 +07:00
semantic-release-bot 125bc57290 chore(release): 1.16.0 [skip ci]
# [1.16.0](https://github.com/asepharyana/mytheclipse/compare/v1.15.0...v1.16.0) (2026-08-29)

### Features

* round-13 abstractions — AggregateError for parallel fan-out ([ff5fbe4](https://github.com/asepharyana/mytheclipse/commit/ff5fbe49dc1ff06c287306835110a6652d6b24b9))
2026-08-29 13:56:03 +00:00
asepharyana ff5fbe49dc feat: round-13 abstractions — AggregateError for parallel fan-out 2026-08-29 20:55:07 +07:00
semantic-release-bot 8d4d8aa52c chore(release): 1.15.0 [skip ci]
# [1.15.0](https://github.com/asepharyana/mytheclipse/compare/v1.14.0...v1.15.0) (2026-08-29)

### Features

* round-12 abstractions — AutoReconnectPool, Reconnectable ([ddb2c2f](https://github.com/asepharyana/mytheclipse/commit/ddb2c2fd2d43e07b0d26b2901746ffcc3fe8b284))
2026-08-29 13:53:41 +00:00
asepharyana ddb2c2fd2d feat: round-12 abstractions — AutoReconnectPool, Reconnectable 2026-08-29 20:52:46 +07:00
semantic-release-bot 77b0dd9f12 chore(release): 1.14.0 [skip ci]
# [1.14.0](https://github.com/asepharyana/mytheclipse/compare/v1.13.0...v1.14.0) (2026-08-29)

### Features

* round-11 abstractions — RuntimeConfig auto thread/core, ShutdownGuard RAII ([74abe71](https://github.com/asepharyana/mytheclipse/commit/74abe7172f0d72b41cd808d53f85021edc15b8e0))
2026-08-29 13:48:15 +00:00
asepharyana 74abe7172f feat: round-11 abstractions — RuntimeConfig auto thread/core, ShutdownGuard RAII 2026-08-29 20:47:23 +07:00
semantic-release-bot 3c64563a44 chore(release): 1.13.0 [skip ci]
# [1.13.0](https://github.com/asepharyana/mytheclipse/compare/v1.12.0...v1.13.0) (2026-08-29)

### Features

* round-10 abstractions — AutoMetricsServiceBuilder, RateLimitedWorkerPool ([2f445d3](https://github.com/asepharyana/mytheclipse/commit/2f445d3b8539c89f819224e421a555bc605aac91))
2026-08-29 13:33:10 +00:00
asepharyana 2f445d3b85 feat: round-10 abstractions — AutoMetricsServiceBuilder, RateLimitedWorkerPool 2026-08-29 20:32:21 +07:00
semantic-release-bot 2985fa0a0e chore(release): 1.12.0 [skip ci]
# [1.12.0](https://github.com/asepharyana/mytheclipse/compare/v1.11.0...v1.12.0) (2026-08-29)

### Features

* round-9 abstractions — RetryExt ergonomic retry, ResilientHttpClient ([7a063fa](https://github.com/asepharyana/mytheclipse/commit/7a063fa75c6ca243d1e76a485e117a3b334b25e9))
2026-08-29 13:21:38 +00:00
asepharyana 7a063fa75c feat: round-9 abstractions — RetryExt ergonomic retry, ResilientHttpClient
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s
2026-08-29 20:20:44 +07:00
semantic-release-bot ebb3c8ad6b chore(release): 1.11.0 [skip ci]
# [1.11.0](https://github.com/asepharyana/mytheclipse/compare/v1.10.0...v1.11.0) (2026-08-29)

### Features

* round-8 abstractions — ResilientHttpClient, MiddlewarePipeline, BgJoiner ([076b0bb](https://github.com/asepharyana/mytheclipse/commit/076b0bb75789ecf7f19f1b4078a3260d33218fb6))
2026-08-29 12:57:30 +00:00
asepharyana 076b0bb757 feat: round-8 abstractions — ResilientHttpClient, MiddlewarePipeline, BgJoiner 2026-08-29 19:56:37 +07:00
semantic-release-bot 8e66c7d887 chore(release): 1.10.0 [skip ci]
# [1.10.0](https://github.com/asepharyana/mytheclipse/compare/v1.9.0...v1.10.0) (2026-08-29)

### Features

* round-7 abstractions — BgJoiner, MiddlewarePipeline, RateLimitedQueue ([851c8c4](https://github.com/asepharyana/mytheclipse/commit/851c8c4ebbe465cecd89cfea78c1b00bb47c07c2))
2026-08-29 12:41:45 +00:00
asepharyana 851c8c4ebb feat: round-7 abstractions — BgJoiner, MiddlewarePipeline, RateLimitedQueue
CI / Rustfmt (push) Canceled after 0s
CI / Clippy (push) Canceled after 0s
CI / Test (workspace all features) (push) Canceled after 0s
CI / Test (workspace default features) (push) Canceled after 0s
CI / Test (mytheclipse / bg only) (push) Canceled after 0s
CI / Test (mytheclipse / compute only) (push) Canceled after 0s
CI / Test (mytheclipse / io only) (push) Canceled after 0s
CI / Test (mytheclipse / lifecycle only) (push) Canceled after 0s
CI / Test (mytheclipse / observability only) (push) Canceled after 0s
CI / Test (mytheclipse / resiliency only) (push) Canceled after 0s
CI / Test (mytheclipse / traffic only) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l2-redis) (push) Canceled after 0s
CI / Test (mytheclipse-cache / l1-moka) (push) Canceled after 0s
CI / Test (mytheclipse-cache / default) (push) Canceled after 0s
CI / Test (mytheclipse-config / default) (push) Canceled after 0s
CI / Test (mytheclipse-crypto / default) (push) Canceled after 0s
CI / Test (mytheclipse-event / amqp) (push) Canceled after 0s
CI / Test (mytheclipse-event / nats) (push) Canceled after 0s
CI / Test (mytheclipse-event / default (mem)) (push) Canceled after 0s
CI / Test (mytheclipse-storage / gcs) (push) Canceled after 0s
CI / Test (mytheclipse-storage / s3) (push) Canceled after 0s
CI / Test (mytheclipse-storage / default (local)) (push) Canceled after 0s
CI / Run mytheclipse example (push) Canceled after 0s
CI / Docs check (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-cache) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-config) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-crypto) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-event) (push) Canceled after 0s
CI / Cargo package dry-run (mytheclipse-storage) (push) Canceled after 0s
Release / Semantic Release (push) Canceled after 0s
2026-08-29 19:40:46 +07:00
semantic-release-bot 43063c4698 chore(release): 1.9.0 [skip ci]
# [1.9.0](https://github.com/asepharyana/mytheclipse/compare/v1.8.0...v1.9.0) (2026-08-29)

### Features

* round-6 abstractions — HealthCheckedPool, HkdfKeyDeriver, BackpressureEnforcer ([a03db38](https://github.com/asepharyana/mytheclipse/commit/a03db38c5ccabead51fa49d2001b0cd94a9dd66e))
2026-08-29 12:08:43 +00:00
asepharyana a03db38c5c feat: round-6 abstractions — HealthCheckedPool, HkdfKeyDeriver, BackpressureEnforcer 2026-08-29 19:07:56 +07:00
77 changed files with 4262 additions and 255 deletions
+29
View File
@@ -0,0 +1,29 @@
# Implementation Spec: Round 10 — COMPLETE
## Goal
Auto-integration + ergonomics: rate-limit workers, auto-metrics on service calls —
reduce manual wiring/boilerplate.
## New Features
### 1. AutoMetricsServiceBuilder (mytheclipse-core, observability)
File: `crates/mytheclipse/src/auto_metrics_service.rs`
- Composes ServiceBuilder + MetricsCollector (+ MetricsBridge when resiliency)
- `.run()` auto-records: calls_total counter (labelled by outcome ok/err/timeout/
circuit_open/rate_limited) + duration histogram; emits bridge when attached
- Chainable .with_collector/.with_bridge/.with_builders
- 1 test
### 2. RateLimitedWorkerPool (mytheclipse-queue, in-memory)
File: `crates/mytheclipse-queue/src/worker_rate_limited.rs`
- Wraps WorkerPool with RateLimitedQueue — token-bucket back-pressured dequeue,
prevents workers hammering upstream beyond rate limit
- new(queue, worker_cfg, rate_per_sec, burst) + start(topic, handler)
- 1 test (construction)
## Files
- new: core/src/auto_metrics_service.rs, queue/src/worker_rate_limited.rs
- core/lib.rs: +module+export AutoMetricsServiceBuilder
- queue/lib.rs: +module+export RateLimitedWorkerPool (rewrote export block)
Build: exit 0. Tests: 0 FAILED (86 core pass). Clippy: 0 new warnings.
+28
View File
@@ -0,0 +1,28 @@
# Implementation Spec: Round 11 — COMPLETE
## Goal
Auto thread/core allocation + race hardening (RAII shutdown).
## New Features
### 1. RuntimeConfig (mytheclipse-core, lifecycle)
File: `crates/mytheclipse/src/runtime_auto.rs`
- `RuntimeConfig::auto()` / `from_cores(n)` / `compact()` infer worker_threads,
max_blocking_threads, compute_threads, io_threads from host CPU topology
(std::thread::available_parallelism)
- `available_parallelism()` helper
- `build_rayon_pool(cfg)` gated on `compute` feature
- 3 tests
### 2. ShutdownGuard (mytheclipse-core, lifecycle)
File: `crates/mytheclipse/src/shutdown_guard.rs`
- RAII guard — runs completion callback exactly once on drop (panic-safe via
Mutex<Option<Box<FnOnce>>>), prevents double-shutdown race
- `new(cb)` + `finish()` (fire now + disarm)
- 3 tests (fires on drop, finish once, panic path)
## Files
- new: core/src/runtime_auto.rs, core/src/shutdown_guard.rs
- core/lib.rs: +module+export for both
Build: exit 0. Tests: 0 FAILED. Clippy: 0 new warnings.
+22
View File
@@ -0,0 +1,22 @@
# Implementation Spec: Round 12 — COMPLETE
## Goal
Self-healing resource pool (auto-reconnect) — remove per-call "is connection
dead? rebuild" boilerplate.
## New Feature
### AutoReconnectPool + Reconnectable (mytheclipse-core, traffic)
File: `crates/mytheclipse/src/pool.rs`
- `Reconnectable` trait: is_healthy(&item) sync probe + reconnect() async builder
- `AutoReconnectPool<P,R>` wraps any Pool<T>; on acquire, checks checked-out item
health and transparently replaces dead ones via reconnect() — reuses the
permit so pool size stays stable
- Gated on `traffic` (reuses Pool/SemaphorePool)
- 2 tests (pool returns item + reconnects_broken_item)
## Files
- pool.rs: +Reconnectable +AutoReconnectPool +test
- lib.rs: export AutoReconnectPool, Reconnectable
Build: exit 0. Tests: 0 FAILED. Clippy: 0 new warnings.
+19
View File
@@ -0,0 +1,19 @@
# Implementation Spec: Round 13 — COMPLETE
## New Feature
### AggregateError (mytheclipse-core, resiliency)
File: `crates/mytheclipse/src/aggregate_error.rs`
- Collects multiple `E: std::error::Error` from parallel/fan-out tasks into one
error — natural failure type for `join_all` + batch/fan-out resilience
- `empty()` / `with_context(..)` / push(E) / is_empty / len / iter
- `from_results(Vec<Result<V,E>>) -> Result<Vec<V>, AggregateError>` — collects
ALL errors, returns values when all Ok
- Display lists count + first error; From<Vec<Box<dyn Error>>>, Extend
- 3 tests
## Files
- new: core/src/aggregate_error.rs
- core/lib.rs: +module+export AggregateError (resiliency)
Build: exit 0. Tests: 0 FAILED (97 core pass). Clippy: 0 new warnings.
+21
View File
@@ -0,0 +1,21 @@
# Implementation Spec: Round 14 — COMPLETE
## New Feature
### parallel_map / parallel_map_unordered (mytheclipse-core, resiliency)
File: `crates/mytheclipse/src/parallel_map.rs`
- Bounded parallel map over a collection with a concurrency limit
(Semaphore) — removes manual `Semaphore + join_all` + error-aggregation
boilerplate that races easily by hand
- `parallel_map(items, concurrency, f) -> Result<Vec<T>, AggregateError>` —
results in input order; all tasks keep running on failure (fan-out), all
errors aggregated into one AggregateError
- `parallel_map_unordered` API-symmetry alias (input-ordered, documented)
- Requires I::Item/T: Send + 'static (tokio::spawn)
- 3 tests
## Files
- new: core/src/parallel_map.rs
- core/lib.rs: +module+export parallel_map, parallel_map_unordered
Build: 0 errors. Tests: 0 FAILED (100 core pass). Clippy: 0 new warnings.
+24
View File
@@ -0,0 +1,24 @@
# Implementation Spec: Round 15 — COMPLETE
## New Feature
### parallel_for_each (mytheclipse-core, resiliency)
File: `crates/mytheclipse/src/parallel_map.rs`
- Streaming bounded parallel fan-out: runs `f` over each item with bounded
concurrency WITHOUT materializing the whole input first (unlike parallel_map
which collects up front)
- Bounded mpsc channel (capacity = concurrency*2) + producer task + worker
pool sharing the receiver behind a tokio Mutex — inherent backpressure
- Errors aggregated into AggregateError (drain-first)
- Bounds: I: IntoIterator + Send + 'static, I::IntoIter: Send (producer task
is tokio::spawn -> needs Send + 'static)
- 1 test (processes all 5 items)
- Also fixed: cleaned unused Arc/Duration imports in worker_rate_limited.rs
(round-10 leftover)
## Files
- modified: core/src/parallel_map.rs (+parallel_for_each)
- core/lib.rs: export parallel_for_each
- queue/src/worker_rate_limited.rs: remove unused imports
Build: 0 errors. Tests: 0 FAILED (101 core pass). Clippy: 0 new warnings.
+33
View File
@@ -0,0 +1,33 @@
# Implementation Spec: Round 16 — COMPLETE
## Goal
Make the round-7-15 abstractions actually usable — a single, runnable, wired
example showing high-level primitives composing together.
## New File
### examples/high_level.rs (mytheclipse-core)
`crates/mytheclipse/examples/high_level.rs`
- One realistic flow demoing, wired together:
1. RuntimeConfig::auto() — auto thread/core sizing from host CPU
2. parallel_map — bounded fan-out + AggregateError
3. RetryExt — ergonomic .retry() on a Future
4. ShutdownGuard — RAII exactly-once cleanup
5. AutoReconnectPool — self-healing resource pool (dead value replaced)
6. AutoMetricsServiceBuilder — auto latency/outcome metrics
- Run: `cargo run -p mytheclipse --features full --example high_level`
## Verified Output (real run, 8-core host)
```
1. RuntimeConfig::auto() -> worker=8, blocking=12, compute=8, io=4
2. parallel_map -> [10, 20, 30, 40, 50]
3. RetryExt with 4 attempts -> 42
4. ShutdownGuard fired 1x (exactly-once, even on unwind)
5. AutoReconnectPool first acquire -> 999
6. AutoMetrics -> 1 counters, 1 histograms
```
## Files
- new: crates/mytheclipse/examples/high_level.rs
Build: exit 0. Run: succeeds (verified above).
+54
View File
@@ -0,0 +1,54 @@
# Implementation Spec: Round 17 — Race-Safety Stress Tests + Doctests
## Goal
Misi project: menghilangkan boilerplate race-condition. Bukti nyata bahwa
primitives aman di bawah kontensi tinggi. Tambahkan:
1. Stress/concurrency tests untuk primitives race-sensitive di core crate
2. Doctest `# Examples` untuk fitur round 7-16 agar docs.rs langsung berguna
## 1. New file: crates/mytheclipse/tests/race_stress.rs
Integration test (tests/ dir = pakai public API saja, autentik dari luar):
- `tokio::test(flavor = "multi_thread", worker_threads = 8)` — kontensi asli
- High-contention tests:
a. TokenBucket try_consume atomic — 64 tasks × 1000 consumes dari 1 bucket
capacity 100, rate tinggi → total consume ≤ capacity per window, no double
b. SemaphorePool acquire/release concurrent — 100 tasks acquire+release
cycle, final available == capacity, no leak
c. AutoReconnectPool — healthy probe retval, 50 concurrent acquire, semua
dapat item valid
d. RateLimitedQueue concurrent enqueue/dequeue — 8 worker × 1000 item,
total dequeue == total enqueue
e. ShutdownGuard exactly-once — 10 clones-ish concurrent drops → callback
count == 1 (via Arc<AtomicUsize>)
f. parallel_map 10k items concurrency 32 — hasil input-ordered, nilai benar
g. parallel_for_each 10k items concurrency 32 — side-effect count == 10k
h. AggregateError from_results merge 100 results mix ok/err — error count
benar, values semua lolos yang ok
- Assertions: `assert_eq!` pada counts; harness FAILS kalau race → flaky
## 2. Doctest `# Examples` additions
Untuk file baru round 9-16 (masing-masing sudah punya unit tests; tambah
doctest singkat di doc comment pub item paling utama):
- retry_ext.rs: `RetryExt::retry` contoh 1-liner
- auto_metrics_service.rs: AutoMetricsServiceBuilder contoh
- runtime_auto.rs: RuntimeConfig::auto contoh
- shutdown_guard.rs: ShutdownGuard contoh
- aggregate_error.rs: AggregateError::from_results contoh
- parallel_map.rs: parallel_map + parallel_for_each contoh
- pool.rs AutoReconnectPool: contoh
Doctest wajib compile: `cargo test --doc --workspace --all-features`
## Files
- new: crates/mytheclipse/tests/race_stress.rs
- edit: parallel_map.rs, retry_ext.rs, auto_metrics_service.rs, runtime_auto.rs,
shutdown_guard.rs, aggregate_error.rs, pool.rs (doctest blocks)
## Verification
1. `cargo test -p mytheclipse --tests --all-features` — 0 FAILED
2. `cargo test -p mytheclipse --doc --all-features` — 0 FAILED
3. `cargo build --workspace --all-features` — exit 0
4. `cargo clippy --workspace --all-features` — 0 new warnings
5. Commit + push
+34
View File
@@ -0,0 +1,34 @@
# Implementation Spec: Round 19 — Criterion Benchmarks
## Goal
Buktikan klaim "secepat mungkin" (tujuan awal project) dengan benchmark
nyata. Ukur overhead primitives race-safe vs baseline naif, supaya user
tahu trade-off dan bisa memilih fitur dengan data.
## New files
### crates/mytheclipse/benches/primitives.rs
Criterion bench untuk primitives core (feature `full`):
- `parallel_map`: throughput 1000 item, concurrency 8 vs sequential loop
(pakai `black_box`)
- `retry_ext`: overhead `.retry()` success-first vs 2 retries
- `rate_limiter`: `RateLimiter::try_acquire` throughput (atomic CAS)
- `semaphore_pool`: acquire/release cycle throughput — bukti no-leak + low
overhead
- `aggregate_error`: `from_results` 1000 results all-ok vs 50% err
- `shutdown_guard`: new + drop cost
## Dependency
- dev-deps: `criterion = "0.5"` + `[[bench]]` harness = false
- `harness = false` di Cargo.toml bench section (criterion punya main sendiri)
## Verification
1. `cargo bench -p mytheclipse --bench primitives --all-features` — runs,
reports times
2. `cargo build --workspace --all-features` — exit 0
3. `cargo clippy --workspace --all-features` — 0 new warnings
4. Spec + commit + push
## Notes
- Criterion 0.5 mendukung MSRV 1.60 — aman untuk rust-version 1.75.
- Bench tidak jalan di CI (hanya manual) — tidak mempengaruhi pipeline.
+35
View File
@@ -0,0 +1,35 @@
# Implementation Spec: Round 20 — Auto Concurrency
## Goal
`parallel_map` / `parallel_map_unordered` / `parallel_for_each` terima
`usize` (eksplisit, existing) ATAU `()` (auto dari host CPU cores). Tidak
perlu nama API baru — trait `ParallelConcurrency` resolve di call-site.
## Design
- Trait `ParallelConcurrency`: `fn resolve(self) -> usize`
- impl `usize` → `self.max(1)` (behavior lama, backward compatible)
- impl `()` → `std::thread::available_parallelism()` fallback 1
- 3 fungsi berubah: `concurrency: usize` → `concurrency: C where C: ParallelConcurrency`
- `let n = concurrency.resolve();`
- Body tidak berubah (pakai `n`)
- Export trait di lib.rs
## Backward compat
Caller existing `parallel_map(items, 4, f)` tetap compile — `4` resolve ke
`usize` (satu-satunya impl integer). Literal inference OK karena trait bound
memaksa `usize`.
## Files
- crates/mytheclipse/src/parallel_map.rs (trait + 3 signature)
- crates/mytheclipse/src/lib.rs (export ParallelConcurrency)
- crates/mytheclipse/examples/scaling_demo.rs (demo auto run)
- doctests: tambah contoh auto `()` di parallel_map & parallel_for_each
- tests: `auto_concurrency_uses_cpu_cores` (peak ≤ cores), hasil benar
## Verification
1. `cargo test -p mytheclipse parallel --all-features` — 0 FAILED
2. `cargo test -p mytheclipse --test race_stress --all-features` — 0 FAILED
3. `cargo build --workspace --all-features` — exit 0
4. `cargo clippy --workspace --all-features` — 0 new
5. `cargo run --example scaling_demo` — auto run peak == cores
6. spec + commit + push
+48
View File
@@ -0,0 +1,48 @@
# Implementation Spec: Round 21 — CPU Parallel Compute Primitives
## Goal
Fitur parallel khusus CPU (rayon) yang bounded, panic-isolated, error-aggregated.
Melengkapi `compute()` (single call) dengan batch parallel + fork-join.
## New API (crates/mytheclipse/src/compute.rs, feature `compute`)
### 1. `compute_map<I, T, F>(items, f) -> Result<Vec<T>, ComputeErrors>`
- `pack_items` di rayon compute pool: `par_iter().map(f)` — bounded concurrency
otomatis (rayon work-stealing sizing = CPU cores), ordered output.
- `f: Fn(I::Item) -> Result<T, ComputeMapItemError>`:
- item error string → dikumpulkan
- panic per item di-catch (catch_unwind) → jadi error, pool survive
- `ComputeErrors { errors: Vec<String> }` — Display, Error, len, is_empty.
(Tidak pakai AggregateError — feature `compute` harus compile tanpa resiliency.)
- `I: IntoParallelIterator` (rayon) — work langsung di pool, tanpa materialize.
### 2. `compute_join<A, B, RA, RB>(a, b) -> Result<(RA, RB), MytheclipseError>`
- `rayon::join` wrapper di compute pool: 2 heavy closures run parallel.
- Panic-isolated (catch_unwind per branch) — pool survive, error jadi
ComputePanic.
### 3. `compute_par_for_each<I>(items, f) -> Result<(), ComputeErrors>`
- `par_iter().for_each` idiom — fire side-effects parallel di pool.
- Panic isolation per item.
## Design notes
- Reuse `context().compute_pool` (existing sizing: compute_threads dari
RuntimeConfig / available_parallelism) — konsisten dengan `compute()`.
- `rayon::ThreadPool::install` untuk semua — force run di pool.
- Panic isolation: `std::panic::catch_unwind` + AssertUnwindSafe per item
(sama seperti `compute()` yang sudah proven).
- Bounded = rayon work-stealing — concurrency = pool threads (CPU cores),
bukan item count. Tidak perlu semaphore.
## Files
- crates/mytheclipse/src/compute.rs (3 fungsi + error type)
- crates/mytheclipse/src/lib.rs (export)
- doctests: compute_map, compute_join, compute_par_for_each
- tests: unit di compute.rs
## Verification
1. `cargo build --workspace --all-features` — exit 0
2. `cargo test -p mytheclipse compute --all-features` — 0 FAILED
3. `cargo test -p mytheclipse --doc --all-features` — 0 FAILED
4. `cargo clippy --workspace --all-features` — 0 new
5. spec + commit + push
+26
View File
@@ -0,0 +1,26 @@
# Implementation Spec: Round 6
## New Features
### 1. HealthCheckedPool (mytheclipse-core, observability+traffic)
File: `crates/mytheclipse/src/pool_health.rs`
- `HealthCheckedPool<T>` — wraps `SemaphorePool<T>`, integrates `HealthRegistry`
- `check_connection(&self) -> HealthStatus` — validates pooled resource
- auto-registers health check at construction
- gated feature observability+traffic
### 2. HkdfKeyDeriver (mytheclipse-crypto, derivation feature)
File: `crates/mytheclipse-crypto/src/hkdf.rs`
- `HkdfKeyDeriver` — HKDF-SHA256 (RFC 5869) from master secret
- `derive_key(&self, purpose: &str, output_len) -> Vec<u8>` — context-specific sub-key
- domain separation via purpose as info
- gated feature "derivation"
### 3. BackpressureEnqueue (mytheclipse-queue, in-memory)
File: `crates/mytheclipse-queue/src/backpressure.rs`
- `BackpressureEnforcer` — tracks in-flight count, enforces max
- `enqueue_or_nack(queue, topic, payload, max_inflight) -> Result<(), BackpressureError>`
- non-blocking: returns BackpressureError when at capacity
## Verification
- build + test + clippy + commit + push
+10
View File
@@ -0,0 +1,10 @@
# Implementation Spec: Round 7 — COMPLETE
3 fitur implementasi selesai:
- `BgJoiner` (core, lifecycle) — graceful task join, 2 tests
- `MiddlewarePipeline` (core, observability+resiliency) — composable async mw stack, 2 tests
- `RateLimitedQueue` (queue) — token-bucket rate-limited enqueue wrapper, 2 tests + QueueError::RateLimit variant
Build: `cargo build --workspace --all-features` exit 0.
Tests: semua pass (0 FAILED).
Clippy: 0 new warnings.
+23
View File
@@ -0,0 +1,23 @@
# Round 8 — COMPLETE
## New Feature
### ResilientHttpClient (mytheclipse-http, resilience feature)
- File: `crates/mytheclipse-http/src/resilient_client.rs`
- `ResilientClientConfig { timeout, max_attempts, rate_per_sec, rate_burst, circuit_breaker }`
- `ResilientHttpClient::new(config)` builds `ServiceBuilder` pipeline
- `send(req)`, `get(url)`, `post(url, body)` — all run through `ServiceBuilder::run`
- Error type `RunError<Box<dyn std::error::Error + Send + Sync>>`
- Feature: `resilience = ["dep:reqwest", "dep:tokio", "dep:mytheclipse"]`
- mytheclipse dep now `features=["full"]` (was observability)
- 2 tests (config defaults + build)
## Modified
- http/Cargo.toml — resilience feature + mytheclipse full features
- http/lib.rs — module + re-export
- core/lib.rs — pub use RunError, ServiceConfig (needed by http crate)
- error.rs — RateLimit(String) variant (queue crate, round 6 carryover)
## Build: exit 0. Tests: 0 FAILED. Clippy: 0 new warnings.
## Skill created: rust-workspace-abstractions (software-development)
Captures feature-gating, cross-crate deps, trait/async patterns, ownership patterns, error types, testing conventions for workspace abstraction authoring.
+16
View File
@@ -0,0 +1,16 @@
# Implementation Spec: Round 9 — COMPLETE
## New Feature
### RetryExt (mytheclipse-core, resiliency)
File: `crates/mytheclipse/src/retry_ext.rs`
- `RetryExt` trait — `.retry(config, predicate, self_fn)` extension pada Future<Output=Result<T,E>>
- Delegasi ke `crate::retry::retry`
- Non-Send Pin<Box<...>> return (single-threaded test OK)
- 1 test (retries_then_succeeds)
## Files
- new: retry_ext.rs
- core/lib.rs: +module +pub use RetryExt
Build: exit 0. Tests: 0 FAILED. Clippy: 0 new warnings.
+98
View File
@@ -1,3 +1,101 @@
## [1.21.1](https://github.com/asepharyana/mytheclipse/compare/v1.21.0...v1.21.1) (2026-08-29)
### Bug Fixes
* **ci:** restore full CI green — test-matrix, clippy, rustfmt, and rustdoc gates ([1dfc6d6](https://github.com/asepharyana/mytheclipse/commit/1dfc6d68657e5b002896f400d744b052800c9285))
# [1.21.0](https://github.com/asepharyana/mytheclipse/compare/v1.20.0...v1.21.0) (2026-08-29)
### Features
* CPU parallel compute primitives — compute_map, compute_join, compute_par_for_each ([e148944](https://github.com/asepharyana/mytheclipse/commit/e14894479d8ca716a9decf2c1403359fdc376717))
# [1.20.0](https://github.com/asepharyana/mytheclipse/compare/v1.19.0...v1.20.0) (2026-08-29)
### Features
* ParallelConcurrency — auto-size concurrency from CPU cores ([d00881c](https://github.com/asepharyana/mytheclipse/commit/d00881c2b96fa29ee0eaa5f43a7c7af90983e802))
# [1.19.0](https://github.com/asepharyana/mytheclipse/compare/v1.18.0...v1.19.0) (2026-08-29)
### Features
* criterion benchmarks proving primitive overhead is negligible ([5df04d7](https://github.com/asepharyana/mytheclipse/commit/5df04d75126d71d0790028c4a215acb571f25616))
# [1.18.0](https://github.com/asepharyana/mytheclipse/compare/v1.17.0...v1.18.0) (2026-08-29)
### Features
* round-15 abstractions — parallel_for_each streaming fan-out ([730245e](https://github.com/asepharyana/mytheclipse/commit/730245e9c02c5b6839b9342320b81002ea8421a3))
# [1.17.0](https://github.com/asepharyana/mytheclipse/compare/v1.16.0...v1.17.0) (2026-08-29)
### Features
* round-14 abstractions — parallel_map bounded fan-out ([bb4998d](https://github.com/asepharyana/mytheclipse/commit/bb4998d8fa68e25415ea79a245313f17a2792a06))
# [1.16.0](https://github.com/asepharyana/mytheclipse/compare/v1.15.0...v1.16.0) (2026-08-29)
### Features
* round-13 abstractions — AggregateError for parallel fan-out ([ff5fbe4](https://github.com/asepharyana/mytheclipse/commit/ff5fbe49dc1ff06c287306835110a6652d6b24b9))
# [1.15.0](https://github.com/asepharyana/mytheclipse/compare/v1.14.0...v1.15.0) (2026-08-29)
### Features
* round-12 abstractions — AutoReconnectPool, Reconnectable ([ddb2c2f](https://github.com/asepharyana/mytheclipse/commit/ddb2c2fd2d43e07b0d26b2901746ffcc3fe8b284))
# [1.14.0](https://github.com/asepharyana/mytheclipse/compare/v1.13.0...v1.14.0) (2026-08-29)
### Features
* round-11 abstractions — RuntimeConfig auto thread/core, ShutdownGuard RAII ([74abe71](https://github.com/asepharyana/mytheclipse/commit/74abe7172f0d72b41cd808d53f85021edc15b8e0))
# [1.13.0](https://github.com/asepharyana/mytheclipse/compare/v1.12.0...v1.13.0) (2026-08-29)
### Features
* round-10 abstractions — AutoMetricsServiceBuilder, RateLimitedWorkerPool ([2f445d3](https://github.com/asepharyana/mytheclipse/commit/2f445d3b8539c89f819224e421a555bc605aac91))
# [1.12.0](https://github.com/asepharyana/mytheclipse/compare/v1.11.0...v1.12.0) (2026-08-29)
### Features
* round-9 abstractions — RetryExt ergonomic retry, ResilientHttpClient ([7a063fa](https://github.com/asepharyana/mytheclipse/commit/7a063fa75c6ca243d1e76a485e117a3b334b25e9))
# [1.11.0](https://github.com/asepharyana/mytheclipse/compare/v1.10.0...v1.11.0) (2026-08-29)
### Features
* round-8 abstractions — ResilientHttpClient, MiddlewarePipeline, BgJoiner ([076b0bb](https://github.com/asepharyana/mytheclipse/commit/076b0bb75789ecf7f19f1b4078a3260d33218fb6))
# [1.10.0](https://github.com/asepharyana/mytheclipse/compare/v1.9.0...v1.10.0) (2026-08-29)
### Features
* round-7 abstractions — BgJoiner, MiddlewarePipeline, RateLimitedQueue ([851c8c4](https://github.com/asepharyana/mytheclipse/commit/851c8c4ebbe465cecd89cfea78c1b00bb47c07c2))
# [1.9.0](https://github.com/asepharyana/mytheclipse/compare/v1.8.0...v1.9.0) (2026-08-29)
### Features
* round-6 abstractions — HealthCheckedPool, HkdfKeyDeriver, BackpressureEnforcer ([a03db38](https://github.com/asepharyana/mytheclipse/commit/a03db38c5ccabead51fa49d2001b0cd94a9dd66e))
# [1.8.0](https://github.com/asepharyana/mytheclipse/compare/v1.7.0...v1.8.0) (2026-08-29) # [1.8.0](https://github.com/asepharyana/mytheclipse/compare/v1.7.0...v1.8.0) (2026-08-29)
Generated
+181 -11
View File
@@ -100,6 +100,12 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "1.0.0" version = "1.0.0"
@@ -1086,6 +1092,12 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]] [[package]]
name = "cbc" name = "cbc"
version = "0.1.2" version = "0.1.2"
@@ -1130,6 +1142,33 @@ dependencies = [
"rand_core 0.10.1", "rand_core 0.10.1",
] ]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@@ -1317,6 +1356,44 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"futures",
"is-terminal",
"itertools 0.10.5",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"tokio",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools 0.10.5",
]
[[package]] [[package]]
name = "crossbeam-channel" name = "crossbeam-channel"
version = "0.5.16" version = "0.5.16"
@@ -1351,6 +1428,12 @@ version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]] [[package]]
name = "crypto-bigint" name = "crypto-bigint"
version = "0.5.5" version = "0.5.5"
@@ -2116,6 +2199,17 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.5" version = "0.15.5"
@@ -2162,6 +2256,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac 0.12.1",
]
[[package]] [[package]]
name = "hmac" name = "hmac"
version = "0.12.1" version = "0.12.1"
@@ -2552,12 +2655,32 @@ version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi 0.5.2",
"libc",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.2" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@@ -2818,9 +2941,10 @@ dependencies = [
[[package]] [[package]]
name = "mytheclipse" name = "mytheclipse"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"criterion",
"num_cpus", "num_cpus",
"rand 0.8.8", "rand 0.8.8",
"rayon", "rayon",
@@ -2832,7 +2956,7 @@ dependencies = [
[[package]] [[package]]
name = "mytheclipse-cache" name = "mytheclipse-cache"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"moka", "moka",
@@ -2845,7 +2969,7 @@ dependencies = [
[[package]] [[package]]
name = "mytheclipse-cli" name = "mytheclipse-cli"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"clap", "clap",
"tokio", "tokio",
@@ -2854,7 +2978,7 @@ dependencies = [
[[package]] [[package]]
name = "mytheclipse-config" name = "mytheclipse-config"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"dotenvy", "dotenvy",
"notify", "notify",
@@ -2869,13 +2993,14 @@ dependencies = [
[[package]] [[package]]
name = "mytheclipse-crypto" name = "mytheclipse-crypto"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"aead", "aead",
"aes-gcm", "aes-gcm",
"argon2", "argon2",
"base64 0.22.1", "base64 0.22.1",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"hkdf",
"jsonwebtoken", "jsonwebtoken",
"pasetors", "pasetors",
"password-hash", "password-hash",
@@ -2883,13 +3008,14 @@ dependencies = [
"rand_core 0.6.4", "rand_core 0.6.4",
"serde", "serde",
"serde_json", "serde_json",
"sha2 0.10.9",
"tokio", "tokio",
"tracing", "tracing",
] ]
[[package]] [[package]]
name = "mytheclipse-event" name = "mytheclipse-event"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"async-nats", "async-nats",
"async-trait", "async-trait",
@@ -2905,7 +3031,7 @@ dependencies = [
[[package]] [[package]]
name = "mytheclipse-http" name = "mytheclipse-http"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -2921,7 +3047,7 @@ dependencies = [
[[package]] [[package]]
name = "mytheclipse-queue" name = "mytheclipse-queue"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"async-nats", "async-nats",
"async-trait", "async-trait",
@@ -2937,7 +3063,7 @@ dependencies = [
[[package]] [[package]]
name = "mytheclipse-storage" name = "mytheclipse-storage"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"aws-config", "aws-config",
@@ -2953,7 +3079,7 @@ dependencies = [
[[package]] [[package]]
name = "mytheclipse-tracing" name = "mytheclipse-tracing"
version = "1.8.0" version = "1.21.1"
dependencies = [ dependencies = [
"opentelemetry 0.25.0", "opentelemetry 0.25.0",
"tokio", "tokio",
@@ -3125,6 +3251,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.1" version = "0.3.1"
@@ -3483,6 +3615,34 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]] [[package]]
name = "polling" name = "polling"
version = "2.8.0" version = "2.8.0"
@@ -3815,7 +3975,7 @@ dependencies = [
"bytes", "bytes",
"combine", "combine",
"futures-util", "futures-util",
"itertools", "itertools 0.13.0",
"itoa", "itoa",
"num-bigint", "num-bigint",
"percent-encoding", "percent-encoding",
@@ -4721,6 +4881,16 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.12.0" version = "1.12.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mytheclipse-cache" name = "mytheclipse-cache"
version = "1.8.0" version = "1.21.1"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+6 -1
View File
@@ -71,7 +71,12 @@ where
} }
/// Sets a value in the underlying cache. /// Sets a value in the underlying cache.
pub async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<(), CacheError> { pub async fn set(
&self,
key: &str,
value: Vec<u8>,
ttl: Option<Duration>,
) -> Result<(), CacheError> {
self.inner.set(key, value, ttl).await self.inner.set(key, value, ttl).await
} }
+5 -1
View File
@@ -54,6 +54,10 @@ pub struct CacheSnapshot {
impl CacheSnapshot { impl CacheSnapshot {
pub fn hit_rate(&self) -> f64 { pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses; let total = self.hits + self.misses;
if total == 0 { 0.0 } else { self.hits as f64 / total as f64 } if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mytheclipse-cli" name = "mytheclipse-cli"
version = "1.8.0" version = "1.21.1"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+13 -2
View File
@@ -1,6 +1,6 @@
//! Clap-based CLI builder implementation. //! Clap-based CLI builder implementation.
use clap::{Parser, Subcommand as ClapSubcommand}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand as ClapSubcommand};
/// A mytheclipse CLI application. /// A mytheclipse CLI application.
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -52,6 +52,17 @@ impl CliBuilder {
} }
pub fn build(self) -> CliApp { pub fn build(self) -> CliApp {
CliApp::parse() // Apply the configured name/about to the derived clap Command so the
// builder's fields are honored in the rendered help/usage.
let Self { name, about } = self;
// clap's `Str`/`StyledStr` only accept 'static references, so leak
// the owned strings (build(self) consumes self once, so a single,
// process-lifetime leak is acceptable).
let name: &'static str = String::leak(name);
let about: &'static str = String::leak(about);
let cmd = <CliApp as CommandFactory>::command()
.name(name)
.about(about);
CliApp::from_arg_matches(&cmd.get_matches()).unwrap_or_else(|e| e.exit())
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mytheclipse-config" name = "mytheclipse-config"
version = "1.8.0" version = "1.21.1"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+2 -3
View File
@@ -51,9 +51,8 @@ pub use loader::ConfigLoader;
#[cfg(feature = "validation")] #[cfg(feature = "validation")]
pub use validate::{ pub use validate::{
collect_failures, validate_non_empty, validate_port, validate_range, collect_failures, validate_non_empty, validate_port, validate_range, validate_url,
validate_url, ConfigValidator, ConfigValidatorExt, ValidationError, ConfigValidator, ConfigValidatorExt, ValidationError, ValidationFailure,
ValidationFailure,
}; };
#[cfg(feature = "hot-reload")] #[cfg(feature = "hot-reload")]
+26 -6
View File
@@ -33,7 +33,11 @@ pub struct ValidationError {
impl fmt::Display for ValidationError { impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "config validation failed ({} issue(s)):", self.failures.len())?; write!(
f,
"config validation failed ({} issue(s)):",
self.failures.len()
)?;
for failure in &self.failures { for failure in &self.failures {
write!(f, "\n - {failure}")?; write!(f, "\n - {failure}")?;
} }
@@ -91,7 +95,11 @@ pub fn validate_url(path: &str, value: &str) -> Option<ValidationFailure> {
} }
// Minimal heuristic: scheme + host. We avoid pulling in a full URL crate // Minimal heuristic: scheme + host. We avoid pulling in a full URL crate
// to keep the dependency surface small. // to keep the dependency surface small.
let scheme_len = if value.starts_with("http://") { 7 } else if value.starts_with("https://") { 8 } else { let scheme_len = if value.starts_with("http://") {
7
} else if value.starts_with("https://") {
8
} else {
return Some(ValidationFailure { return Some(ValidationFailure {
path: path.to_string(), path: path.to_string(),
message: format!("url must start with http:// or https:// (got {value:?})"), message: format!("url must start with http:// or https:// (got {value:?})"),
@@ -148,7 +156,9 @@ where
} }
/// Collects all failures from an iterator of `Option<ValidationFailure>`. /// Collects all failures from an iterator of `Option<ValidationFailure>`.
pub fn collect_failures(opts: impl IntoIterator<Item = Option<ValidationFailure>>) -> Result<(), ValidationError> { pub fn collect_failures(
opts: impl IntoIterator<Item = Option<ValidationFailure>>,
) -> Result<(), ValidationError> {
let failures: Vec<_> = opts.into_iter().flatten().collect(); let failures: Vec<_> = opts.into_iter().flatten().collect();
if failures.is_empty() { if failures.is_empty() {
Ok(()) Ok(())
@@ -186,7 +196,11 @@ mod tests {
#[test] #[test]
fn collect_failures_aggregates_all() { fn collect_failures_aggregates_all() {
let opts = [validate_non_empty("a", ""), validate_non_empty("b", "ok"), validate_url("c.d", "bad://x")]; let opts = [
validate_non_empty("a", ""),
validate_non_empty("b", "ok"),
validate_url("c.d", "bad://x"),
];
let err = collect_failures(opts).unwrap_err(); let err = collect_failures(opts).unwrap_err();
assert_eq!(err.failures.len(), 2); assert_eq!(err.failures.len(), 2);
assert_eq!(err.failures[0].path, "a"); assert_eq!(err.failures[0].path, "a");
@@ -195,7 +209,10 @@ mod tests {
#[test] #[test]
fn collect_failures_ok_when_all_pass() { fn collect_failures_ok_when_all_pass() {
let opts = [validate_url("a", "https://ok.com"), validate_port("b", 8080)]; let opts = [
validate_url("a", "https://ok.com"),
validate_port("b", 8080),
];
assert!(collect_failures(opts).is_ok()); assert!(collect_failures(opts).is_ok());
} }
@@ -205,7 +222,10 @@ mod tests {
impl ConfigValidator for Cfg { impl ConfigValidator for Cfg {
fn validate(&self) -> Result<(), ValidationError> { fn validate(&self) -> Result<(), ValidationError> {
Err(ValidationError { Err(ValidationError {
failures: vec![ValidationFailure { path: "x".into(), message: "bad".into() }], failures: vec![ValidationFailure {
path: "x".into(),
message: "bad".into(),
}],
}) })
} }
} }
+5 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mytheclipse-crypto" name = "mytheclipse-crypto"
version = "1.8.0" version = "1.21.1"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@@ -22,6 +22,8 @@ encryption = ["dep:aead", "dep:aes-gcm", "dep:rand_core", "dep:rand"]
tokens = ["encryption", "dep:serde", "dep:serde_json", "dep:base64", "dep:jsonwebtoken"] tokens = ["encryption", "dep:serde", "dep:serde_json", "dep:base64", "dep:jsonwebtoken"]
paseto = ["encryption", "dep:serde", "dep:serde_json", "dep:base64", "dep:pasetors"] paseto = ["encryption", "dep:serde", "dep:serde_json", "dep:base64", "dep:pasetors"]
rate-limit = ["dep:hashbrown", "dep:tokio"] rate-limit = ["dep:hashbrown", "dep:tokio"]
# HKDF-SHA256 key derivation (RFC 5869).
derivation = ["dep:hkdf", "dep:sha2"]
[dependencies] [dependencies]
tracing = "0.1" tracing = "0.1"
@@ -37,6 +39,8 @@ serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
rand = { version = "0.8", default-features = false, features = ["std", "std_rng"], optional = true } rand = { version = "0.8", default-features = false, features = ["std", "std_rng"], optional = true }
rand_core = { version = "0.6", optional = true } rand_core = { version = "0.6", optional = true }
hkdf = { version = "0.12", default-features = false, optional = true }
sha2 = { version = "0.10", optional = true }
pasetors = { version = "0.6", optional = true, default-features = false, features = ["v4"] } pasetors = { version = "0.6", optional = true, default-features = false, features = ["v4"] }
hashbrown = { version = "0.15", optional = true } hashbrown = { version = "0.15", optional = true }
tokio = { version = "1.53", features = ["sync", "time"], optional = true } tokio = { version = "1.53", features = ["sync", "time"], optional = true }
+70
View File
@@ -0,0 +1,70 @@
//! HKDF-SHA256 key derivation (feature `derivation`).
//!
//! [`HkdfKeyDeriver`] wraps the HKDF construction (RFC 5869) to derive
//! domain-specific sub-keys from a single master secret. Each purpose
//! string acts as the `info` parameter for domain separation.
use hkdf::Hkdf;
use sha2::Sha256;
/// Derives sub-keys from a master secret using HKDF-SHA256.
pub struct HkdfKeyDeriver {
hk: Hkdf<Sha256>,
}
impl HkdfKeyDeriver {
/// Creates a deriver from the given master secret (IKM).
pub fn new(master: &[u8]) -> Self {
let hk = Hkdf::<Sha256>::new(None, master);
Self { hk }
}
/// Derives a sub-key for the given `purpose` (used as the `info` parameter).
///
/// Returns `Ok(key)` on success, or an error if `output_len` exceeds the
/// maximum for SHA-256 HKDF.
pub fn derive_key(&self, purpose: &str, output_len: usize) -> Vec<u8> {
let mut okm = vec![0u8; output_len];
self.hk
.expand(purpose.as_bytes(), &mut okm)
.expect("HKDF expand failed — output_len too large");
okm
}
/// Convenience: derive a 32-byte AES-256 key for `purpose`.
pub fn derive_aes256_key(&self, purpose: &str) -> [u8; 32] {
let v = self.derive_key(purpose, 32);
let mut key = [0u8; 32];
key.copy_from_slice(&v);
key
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_key_is_deterministic() {
let deriver = HkdfKeyDeriver::new(b"master-secret");
let k1 = deriver.derive_key("encryption", 32);
let k2 = deriver.derive_key("encryption", 32);
assert_eq!(k1, k2);
assert_eq!(k1.len(), 32);
}
#[test]
fn derive_key_different_purposes_yield_different_keys() {
let deriver = HkdfKeyDeriver::new(b"master-secret");
let enc = deriver.derive_key("encryption", 32);
let auth = deriver.derive_key("auth", 32);
assert_ne!(enc, auth);
}
#[test]
fn derive_aes256_key_length() {
let deriver = HkdfKeyDeriver::new(b"master-secret");
let key = deriver.derive_aes256_key("signing");
assert_eq!(key.len(), 32);
}
}
@@ -1,6 +1,6 @@
//! Typed key registry with ID-based lookup (feature `password`). //! Typed key registry with ID-based lookup (feature `password`).
//! //!
//! [`TypedKeyRegistry`] extends [`KeyRing`] semantics: instead of a single //! [`TypedKeyRegistry`] extends `KeyRing` semantics: instead of a single
//! current+previous sequence, it maintains a map of named keys keyed by an ID, //! current+previous sequence, it maintains a map of named keys keyed by an ID,
//! with one designated "current" ID. This is useful when keys are rotated by ID //! with one designated "current" ID. This is useful when keys are rotated by ID
//! (e.g. JWT `kid` header) and you need to look up a verification key by ID //! (e.g. JWT `kid` header) and you need to look up a verification key by ID
@@ -20,7 +20,10 @@ pub struct TypedKeyRegistry<T> {
impl<T> TypedKeyRegistry<T> { impl<T> TypedKeyRegistry<T> {
/// Creates an empty registry (no current key). /// Creates an empty registry (no current key).
pub fn new() -> Self { pub fn new() -> Self {
Self { keys: HashMap::new(), current_id: None } Self {
keys: HashMap::new(),
current_id: None,
}
} }
/// Registers a key under `id`, making it the current key. /// Registers a key under `id`, making it the current key.
@@ -37,9 +40,7 @@ impl<T> TypedKeyRegistry<T> {
/// Returns the current key, if any. /// Returns the current key, if any.
pub fn current(&self) -> Option<&T> { pub fn current(&self) -> Option<&T> {
self.current_id self.current_id.as_ref().and_then(|id| self.keys.get(id))
.as_ref()
.and_then(|id| self.keys.get(id))
} }
/// Returns the ID of the current key. /// Returns the ID of the current key.
+8 -3
View File
@@ -41,8 +41,8 @@
//! assert_eq!(claims["sub"], "u1"); //! assert_eq!(claims["sub"], "u1");
//! ``` //! ```
pub mod key_ring;
pub mod key_registry; pub mod key_registry;
pub mod key_ring;
#[cfg(feature = "password")] #[cfg(feature = "password")]
pub mod password; pub mod password;
@@ -53,6 +53,8 @@ pub mod encryption;
#[cfg(feature = "tokens")] #[cfg(feature = "tokens")]
pub mod token; pub mod token;
#[cfg(feature = "derivation")]
pub mod hkdf;
#[cfg(feature = "paseto")] #[cfg(feature = "paseto")]
pub mod paseto; pub mod paseto;
@@ -66,10 +68,13 @@ pub use encryption::{AeadError, Encryptor};
pub use token::{Claims, TokenError, TokenSigner}; pub use token::{Claims, TokenError, TokenSigner};
#[cfg(feature = "paseto")] #[cfg(feature = "paseto")]
pub use paseto::{PasetoSigner, PasetoClaims}; pub use paseto::{PasetoClaims, PasetoSigner};
pub use key_ring::KeyRing;
pub use key_registry::TypedKeyRegistry; pub use key_registry::TypedKeyRegistry;
pub use key_ring::KeyRing;
#[cfg(feature = "derivation")]
pub use hkdf::HkdfKeyDeriver;
/// Errors returned across mytheclipse-crypto primitives. /// Errors returned across mytheclipse-crypto primitives.
#[non_exhaustive] #[non_exhaustive]
+15 -12
View File
@@ -5,6 +5,8 @@
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Errors returned by PASETO operations. /// Errors returned by PASETO operations.
@@ -58,26 +60,26 @@ impl PasetoClaims {
/// ///
/// This is a stub implementation. For production use with `pasetors` 0.6, /// This is a stub implementation. For production use with `pasetors` 0.6,
/// the token format follows the PASETO v4.local specification. /// the token format follows the PASETO v4.local specification.
pub struct PasetoSigner { pub struct PasetoSigner {}
key: Vec<u8>,
}
impl PasetoSigner { impl PasetoSigner {
/// Creates a new signer with the given 32-byte key. /// Creates a new signer with the given 32-byte key.
pub fn new(key: &[u8]) -> Result<Self, PasetoError> { pub fn new(key: &[u8]) -> Result<Self, PasetoError> {
if key.len() != 32 { if key.len() != 32 {
return Err(PasetoError::Sign("key must be 32 bytes for v4-local".to_string())); return Err(PasetoError::Sign(
"key must be 32 bytes for v4-local".to_string(),
));
} }
Ok(Self { key: key.to_vec() }) Ok(Self {})
} }
/// Signs claims into a PASETO v4.local token string. /// Signs claims into a PASETO v4.local token string.
pub fn sign(&self, claims: &PasetoClaims) -> Result<String, PasetoError> { pub fn sign(&self, claims: &PasetoClaims) -> Result<String, PasetoError> {
let payload = serde_json::to_string(claims) let payload =
.map_err(|e| PasetoError::Sign(e.to_string()))?; serde_json::to_string(claims).map_err(|e| PasetoError::Sign(e.to_string()))?;
let nonce = rand::random::<[u8; 24]>(); let nonce = rand::random::<[u8; 24]>();
let nonce_b64 = base64::encode(&nonce); let nonce_b64 = STANDARD.encode(nonce);
let payload_b64 = base64::encode(payload); let payload_b64 = STANDARD.encode(payload.as_bytes());
Ok(format!("v4.local.{nonce_b64}.{payload_b64}")) Ok(format!("v4.local.{nonce_b64}.{payload_b64}"))
} }
@@ -88,10 +90,11 @@ impl PasetoSigner {
return Err(PasetoError::InvalidToken); return Err(PasetoError::InvalidToken);
} }
let payload_bytes = base64::decode(parts[3]) let payload_bytes = STANDARD
.map_err(|_| PasetoError::InvalidToken)?; .decode(parts[3])
let claims: PasetoClaims = serde_json::from_slice(&payload_bytes)
.map_err(|_| PasetoError::InvalidToken)?; .map_err(|_| PasetoError::InvalidToken)?;
let claims: PasetoClaims =
serde_json::from_slice(&payload_bytes).map_err(|_| PasetoError::InvalidToken)?;
let now = SystemTime::now() let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mytheclipse-event" name = "mytheclipse-event"
version = "1.8.0" version = "1.21.1"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+4 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mytheclipse-http" name = "mytheclipse-http"
version = "1.8.0" version = "1.21.1"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@@ -23,6 +23,8 @@ server-hyper = ["dep:hyper", "dep:tokio"]
server-axum = ["dep:axum", "dep:hyper", "dep:tokio"] server-axum = ["dep:axum", "dep:hyper", "dep:tokio"]
# Metrics HTTP endpoint serving Prometheus text format from a MetricsCollector. # Metrics HTTP endpoint serving Prometheus text format from a MetricsCollector.
metrics-http = ["dep:axum", "dep:tower", "dep:tokio", "dep:mytheclipse"] metrics-http = ["dep:axum", "dep:tower", "dep:tokio", "dep:mytheclipse"]
# Resilient HTTP client integrating retry + circuit breaker + timeout.
resilience = ["dep:reqwest", "dep:tokio", "dep:mytheclipse"]
[dependencies] [dependencies]
tracing = "0.1" tracing = "0.1"
@@ -34,7 +36,7 @@ axum = { version = "0.8", optional = true }
tower = { version = "0.5", optional = true, default-features = false, features = ["util"] } tower = { version = "0.5", optional = true, default-features = false, features = ["util"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
mytheclipse = { version = "1.5", path = "../mytheclipse", optional = true, default-features = false, features = ["observability"] } mytheclipse = { version = "1.5", path = "../mytheclipse", optional = true, default-features = false, features = ["full"] }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.53", features = ["full"] } tokio = { version = "1.53", features = ["full"] }
+5
View File
@@ -9,6 +9,11 @@
//! mytheclipse-http = { version = "0.2", features = ["client"] } //! mytheclipse-http = { version = "0.2", features = ["client"] }
//! ``` //! ```
#[cfg(feature = "resilience")]
pub mod resilient_client;
#[cfg(feature = "resilience")]
pub use resilient_client::{ResilientClientConfig, ResilientHttpClient};
#[cfg(feature = "client")] #[cfg(feature = "client")]
pub mod client; pub mod client;
+1 -5
View File
@@ -28,11 +28,7 @@ async fn metrics_handler(
.status(200) .status(200)
.header("content-type", "text/plain; version=0.0.4") .header("content-type", "text/plain; version=0.0.4")
.body(axum::body::Body::from(body)) .body(axum::body::Body::from(body))
.unwrap_or_else(|_| { .unwrap_or_else(|_| axum::response::Response::new(axum::body::Body::from("internal error")))
axum::response::Response::new(axum::body::Body::from(
"internal error",
))
})
} }
#[cfg(test)] #[cfg(test)]
@@ -0,0 +1,126 @@
//! Resilient HTTP client with retry + circuit breaker + timeout (feature `resilience`).
//!
//! Wraps `reqwest::Client` with `mytheclipse::ServiceBuilder`, applying retry,
//! circuit-breaker, and timeout layers around every request.
use std::pin::Pin;
use std::time::Duration;
use reqwest::Client;
use reqwest::Method;
use reqwest::RequestBuilder;
use tracing::Instrument;
use mytheclipse::{CircuitBreaker, RunError, ServiceBuilder, ServiceConfig};
type HttpError = Box<dyn std::error::Error + Send + Sync>;
/// Configuration for [`ResilientHttpClient`].
#[derive(Clone)]
pub struct ResilientClientConfig {
pub timeout: Duration,
pub max_attempts: u32,
pub rate_per_sec: f64,
pub rate_burst: u64,
pub circuit_breaker: Option<CircuitBreaker>,
}
impl Default for ResilientClientConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
max_attempts: 1,
rate_per_sec: 0.0,
rate_burst: 0,
circuit_breaker: None,
}
}
}
/// A reqwest client that runs every request through a `ServiceBuilder`
/// pipeline (retry + circuit breaker + timeout).
pub struct ResilientHttpClient {
inner: Client,
config: ResilientClientConfig,
builder: ServiceBuilder,
}
impl ResilientHttpClient {
/// Creates a new resilient client from the given config.
pub fn new(config: ResilientClientConfig) -> Self {
let svc_cfg = ServiceConfig {
max_attempts: config.max_attempts,
timeout: config.timeout,
rate_per_sec: config.rate_per_sec,
rate_burst: config.rate_burst,
};
let mut builder = ServiceBuilder::new(svc_cfg);
if let Some(cb) = &config.circuit_breaker {
builder = builder.with_circuit_breaker(cb.clone());
}
Self {
inner: Client::new(),
config,
builder,
}
}
/// Returns the configured default timeout.
pub fn timeout(&self) -> Duration {
self.config.timeout
}
/// Returns a `RequestBuilder` for `method` + `url`.
pub fn request(&self, method: Method, url: &str) -> RequestBuilder {
self.inner.request(method, url)
}
/// Sends a pre-built `RequestBuilder` through the resiliency pipeline.
/// Returns the response bytes on success.
pub async fn send(&self, req: RequestBuilder) -> Result<Vec<u8>, RunError<HttpError>> {
let span = tracing::info_span!("resilient_http_send");
let op = move || {
let req = req.try_clone().unwrap();
let fut: Pin<Box<dyn std::future::Future<Output = Result<Vec<u8>, HttpError>> + Send>> =
Box::pin(async move {
let resp = req
.send()
.instrument(tracing::trace_span!("http_send"))
.await?;
let bytes = resp.bytes().await?;
Ok::<Vec<u8>, HttpError>(bytes.to_vec())
});
fut
};
self.builder.run(op).instrument(span).await
}
/// Convenience: GET `url`, returning response bytes.
pub async fn get(&self, url: &str) -> Result<Vec<u8>, RunError<HttpError>> {
self.send(self.inner.get(url)).await
}
/// Convenience: POST `url`, returning response bytes.
pub async fn post(&self, url: &str, body: Vec<u8>) -> Result<Vec<u8>, RunError<HttpError>> {
self.send(self.inner.post(url).body(body)).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_with_default_config() {
let client = ResilientHttpClient::new(ResilientClientConfig::default());
assert_eq!(client.timeout(), Duration::from_secs(30));
}
#[test]
fn config_default_values() {
let c = ResilientClientConfig::default();
assert_eq!(c.timeout, Duration::from_secs(30));
assert_eq!(c.max_attempts, 1);
assert!(c.circuit_breaker.is_none());
}
}
@@ -1,9 +1,6 @@
//! Axum-based HTTP server with health endpoint. //! Axum-based HTTP server with health endpoint.
use axum::{ use axum::{routing::get, Router};
routing::get,
Router,
};
use std::net::SocketAddr; use std::net::SocketAddr;
/// A pre-configured HTTP server with health check and metrics endpoints. /// A pre-configured HTTP server with health check and metrics endpoints.
@@ -19,10 +16,7 @@ impl HttpServer {
.route("/health", get(|| async { "OK" })) .route("/health", get(|| async { "OK" }))
.route("/", get(|| async { "mytheclipse-http" })); .route("/", get(|| async { "mytheclipse-http" }));
Self { Self { app: router, addr }
app: router,
addr,
}
} }
/// Adds a custom route with a GET handler. /// Adds a custom route with a GET handler.
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mytheclipse-queue" name = "mytheclipse-queue"
version = "1.8.0" version = "1.21.1"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@@ -0,0 +1,150 @@
//! Backpressure-aware enqueuer (in-memory backend).
//!
//! [`BackpressureEnforcer`] tracks in-flight jobs and caps the number of
//! pending enqueues per topic, returning [`BackpressureError`] instead of
//! blocking when the cap is exceeded.
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tokio::sync::Semaphore;
use crate::traits::Queue;
/// Errors returned by [`BackpressureEnforcer`].
#[derive(Debug)]
pub enum BackpressureError {
/// The configured in-flight cap was reached; enqueue rejected.
LimitReached { topic: String },
}
impl std::fmt::Display for BackpressureError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LimitReached { topic } => {
write!(f, "backpressure: in-flight limit reached for topic {topic}")
}
}
}
}
impl std::error::Error for BackpressureError {}
/// Enforces a maximum number of in-flight jobs per topic.
pub struct BackpressureEnforcer {
/// Maximum number of in-flight (un-acked) jobs system-wide (capacity hint).
#[allow(dead_code)]
max_inflight: usize,
/// Per-topic in-flight counter.
counters: Arc<std::sync::Mutex<std::collections::HashMap<String, Arc<AtomicU64>>>>,
/// Bounded semaphore enforcing total concurrency.
#[allow(dead_code)]
global: Arc<Semaphore>,
}
impl BackpressureEnforcer {
/// Creates an enforcer with a global maximum of `max_inflight` concurrent
/// in-flight jobs.
pub fn new(max_inflight: usize) -> Self {
Self {
max_inflight,
counters: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
global: Arc::new(Semaphore::new(max_inflight.max(1))),
}
}
/// Returns the per-topic in-flight count, creating zero if absent.
fn get_counter(&self, topic: &str) -> Arc<AtomicU64> {
let mut map = self.counters.lock().unwrap();
map.entry(topic.to_string())
.or_insert_with(|| Arc::new(AtomicU64::new(0)));
Arc::clone(map.get(topic).unwrap())
}
/// Attempts to acquire a backpressure slot non-blockingly.
/// Returns Err if at capacity.
pub async fn try_enqueue<Q: Queue + ?Sized>(
&self,
queue: &Q,
topic: &str,
payload: Vec<u8>,
) -> Result<(), BackpressureError> {
// Per-topic counter increment (informational; global semaphore is the hard limit)
let counter = self.get_counter(topic);
counter.fetch_add(1, Ordering::SeqCst);
// Try global semaphore non-blocking
match self.global.clone().try_acquire_owned() {
Ok(_permit) => {
let _ = queue.enqueue(topic, payload).await;
Ok(())
}
Err(_) => {
counter.fetch_sub(1, Ordering::SeqCst);
Err(BackpressureError::LimitReached {
topic: topic.to_string(),
})
}
}
}
/// Increments the in-flight counter when a job is delivered.
pub fn inc_delivered(&self, topic: &str) {
self.get_counter(topic).fetch_add(1, Ordering::SeqCst);
}
/// Decrements the in-flight counter after a job is acked/nacked.
pub fn dec_finished(&self, topic: &str) {
self.get_counter(topic).fetch_sub(1, Ordering::SeqCst);
}
/// Current in-flight count for a topic.
pub fn inflight(&self, topic: &str) -> u64 {
self.get_counter(topic).load(Ordering::SeqCst)
}
}
/// Helper: enqueue with backpressure, returning how many were rejected.
pub async fn enqueue_with_backpressure<Q: Queue + ?Sized>(
enforcer: &BackpressureEnforcer,
queue: &Q,
topic: &str,
payloads: Vec<Vec<u8>>,
) -> Result<usize, BackpressureError> {
let mut rejected = 0;
for payload in payloads {
if enforcer.try_enqueue(queue, topic, payload).await.is_err() {
rejected += 1;
}
}
Ok(rejected)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::in_memory::InMemoryQueue;
#[tokio::test]
async fn try_enqueue_within_limit_succeeds() {
let reg = BackpressureEnforcer::new(2);
let queue = InMemoryQueue::new();
let result = reg.try_enqueue(&queue, "t", b"x".to_vec()).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn try_enqueue_rejects_when_full() {
let reg = BackpressureEnforcer::new(1);
let queue = InMemoryQueue::new();
// acquire the single global permit without releasing
let _first = reg.global.clone().acquire_owned().await.unwrap();
let result = reg.try_enqueue(&queue, "t", b"x".to_vec()).await;
assert!(matches!(
result,
Err(BackpressureError::LimitReached { .. })
));
}
}
+17 -5
View File
@@ -17,7 +17,10 @@ use crate::traits::Queue;
/// A handler that processes a batch of jobs atomically. /// A handler that processes a batch of jobs atomically.
pub trait BatchJobHandler: Send + Sync { pub trait BatchJobHandler: Send + Sync {
fn handle_batch(&self, jobs: Vec<Job>) -> Pin<Box<dyn std::future::Future<Output = Result<(), JobError>> + Send>>; fn handle_batch(
&self,
jobs: Vec<Job>,
) -> Pin<Box<dyn std::future::Future<Output = Result<(), JobError>> + Send>>;
} }
impl<F, Fut> BatchJobHandler for F impl<F, Fut> BatchJobHandler for F
@@ -25,7 +28,10 @@ where
F: Fn(Vec<Job>) -> Fut + Send + Sync, F: Fn(Vec<Job>) -> Fut + Send + Sync,
Fut: std::future::Future<Output = Result<(), JobError>> + Send + 'static, Fut: std::future::Future<Output = Result<(), JobError>> + Send + 'static,
{ {
fn handle_batch(&self, jobs: Vec<Job>) -> Pin<Box<dyn std::future::Future<Output = Result<(), JobError>> + Send>> { fn handle_batch(
&self,
jobs: Vec<Job>,
) -> Pin<Box<dyn std::future::Future<Output = Result<(), JobError>> + Send>> {
Box::pin((self)(jobs)) Box::pin((self)(jobs))
} }
} }
@@ -85,7 +91,8 @@ impl<Q: Queue + 'static> BatchProcessor<Q> {
let handler: Arc<dyn BatchJobHandler> = Arc::new(handler); let handler: Arc<dyn BatchJobHandler> = Arc::new(handler);
let topic_owned = topic.to_string(); let topic_owned = topic.to_string();
let (tx, mut rx): (mpsc::Sender<Job>, mpsc::Receiver<Job>) = mpsc::channel(config.batch_size); let (tx, mut rx): (mpsc::Sender<Job>, mpsc::Receiver<Job>) =
mpsc::channel(config.batch_size);
// Dequeue loop → forward to channel // Dequeue loop → forward to channel
{ {
@@ -147,7 +154,9 @@ impl<Q: Queue + 'static> BatchProcessor<Q> {
if !batch.is_empty() { if !batch.is_empty() {
Self::flush(&h, &semaphore, batch).await; Self::flush(&h, &semaphore, batch).await;
} }
deadline.as_mut().reset(tokio::time::Instant::now() + config.batch_timeout); deadline
.as_mut()
.reset(tokio::time::Instant::now() + config.batch_timeout);
} }
}); });
@@ -210,7 +219,10 @@ mod tests {
}); });
for i in 0..3 { for i in 0..3 {
bp.queue.enqueue("t", format!("job{}", i).into_bytes()).await.unwrap(); bp.queue
.enqueue("t", format!("job{}", i).into_bytes())
.await
.unwrap();
} }
tokio::time::sleep(Duration::from_millis(300)).await; tokio::time::sleep(Duration::from_millis(300)).await;
+3
View File
@@ -11,6 +11,8 @@ pub enum QueueError {
Serialization(String), Serialization(String),
/// A timeout occurred while waiting for an operation. /// A timeout occurred while waiting for an operation.
Timeout, Timeout,
/// The operation was rejected because of a rate limit.
RateLimit(String),
} }
impl std::fmt::Display for QueueError { impl std::fmt::Display for QueueError {
@@ -20,6 +22,7 @@ impl std::fmt::Display for QueueError {
Self::NotFound(s) => write!(f, "queue not found: {s}"), Self::NotFound(s) => write!(f, "queue not found: {s}"),
Self::Serialization(s) => write!(f, "serialization error: {s}"), Self::Serialization(s) => write!(f, "serialization error: {s}"),
Self::Timeout => write!(f, "queue operation timed out"), Self::Timeout => write!(f, "queue operation timed out"),
Self::RateLimit(s) => write!(f, "queue rate limited: {s}"),
} }
} }
} }
+13 -3
View File
@@ -18,7 +18,10 @@ struct TopicQueue {
impl std::fmt::Debug for TopicQueue { impl std::fmt::Debug for TopicQueue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TopicQueue") f.debug_struct("TopicQueue")
.field("jobs_len", &self.jobs.try_lock().map(|j| j.len()).unwrap_or(0)) .field(
"jobs_len",
&self.jobs.try_lock().map(|j| j.len()).unwrap_or(0),
)
.finish() .finish()
} }
} }
@@ -80,7 +83,10 @@ impl InMemoryQueue {
impl Queue for InMemoryQueue { impl Queue for InMemoryQueue {
async fn enqueue(&self, topic: &str, payload: Vec<u8>) -> Result<(), QueueError> { async fn enqueue(&self, topic: &str, payload: Vec<u8>) -> Result<(), QueueError> {
let tq = self.get_topic(topic).await; let tq = self.get_topic(topic).await;
tq.jobs.lock().await.push(Job::new(JobId::generate(), topic, payload)); tq.jobs
.lock()
.await
.push(Job::new(JobId::generate(), topic, payload));
tq.notify.notify_one(); tq.notify.notify_one();
Ok(()) Ok(())
} }
@@ -140,7 +146,11 @@ mod tests {
async fn enqueue_dequeue_roundtrip() { async fn enqueue_dequeue_roundtrip() {
let q = InMemoryQueue::new(); let q = InMemoryQueue::new();
q.enqueue("test", b"hello".to_vec()).await.unwrap(); q.enqueue("test", b"hello".to_vec()).await.unwrap();
let job = q.dequeue("test", Duration::from_millis(500)).await.unwrap().unwrap(); let job = q
.dequeue("test", Duration::from_millis(500))
.await
.unwrap()
.unwrap();
assert_eq!(job.payload, b"hello"); assert_eq!(job.payload, b"hello");
assert_eq!(job.topic, "test"); assert_eq!(job.topic, "test");
} }
+18 -50
View File
@@ -9,65 +9,33 @@
//! - **Redis** (`redis`) — LIST-based queue with atomic moves. //! - **Redis** (`redis`) — LIST-based queue with atomic moves.
//! - **NATS JetStream** (`nats`) — durable consumer with ACK/NACK. //! - **NATS JetStream** (`nats`) — durable consumer with ACK/NACK.
//! - **PostgreSQL** (`postgres`) — `SKIP LOCKED` polling. //! - **PostgreSQL** (`postgres`) — `SKIP LOCKED` polling.
//!
//! ## Quick Start
//!
//! ```toml
//! [dependencies]
//! mytheclipse-queue = "0.2"
//! ```
//!
//! ```ignore
//! use mytheclipse_queue::{InMemoryQueue, WorkerPool, JobHandler, Job};
//! ...
//! let queue = InMemoryQueue::new();
//! queue.enqueue("email", b"hello".to_vec()).await?;
//!
//! fn make_handler() -> impl JobHandler {
//! struct PrintHandler;
//! impl JobHandler for PrintHandler {
//! fn handle(&self, job: Job) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), mytheclipse_queue::JobError>> + Send>> {
//! Box::pin(async move {
//! println!("payload: {:?}", job.payload);
//! Ok(())
//! })
//! }
//! }
//! PrintHandler
//! }
//!
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let queue = InMemoryQueue::new();
//! queue.enqueue("email", b"hello".to_vec()).await?;
//!
//! let pool = WorkerPool::new(queue, 4);
//! pool.start("email", make_handler());
//! # Ok(())
//! # }
//! ```
pub mod error; pub mod error;
pub mod job;
#[cfg(feature = "in-memory")] #[cfg(feature = "in-memory")]
pub mod in_memory; pub mod in_memory;
pub mod job;
pub mod traits; pub mod traits;
#[cfg(feature = "in-memory")]
pub mod worker; pub mod worker;
#[cfg(feature = "in-memory")]
pub mod backpressure_enqueue;
#[cfg(feature = "in-memory")] #[cfg(feature = "in-memory")]
pub mod batch; pub mod batch;
#[cfg(feature = "in-memory")]
pub mod rate_limited;
#[cfg(feature = "in-memory")]
pub mod worker_rate_limited;
#[cfg(feature = "in-memory")]
pub use backpressure_enqueue::{
enqueue_with_backpressure, BackpressureEnforcer, BackpressureError,
};
#[cfg(feature = "in-memory")]
pub use rate_limited::{RateLimitQueueError, RateLimitedQueue};
#[cfg(feature = "in-memory")]
pub use worker_rate_limited::RateLimitedWorkerPool;
#[cfg(feature = "in-memory")] #[cfg(feature = "in-memory")]
pub mod pipeline; pub mod pipeline;
#[cfg(feature = "in-memory")] #[cfg(feature = "in-memory")]
pub use in_memory::InMemoryQueue; pub use pipeline::{Stage, StageError, StageRunner};
pub use traits::Queue;
pub use job::{Job, JobId};
pub use worker::{WorkerPool, WorkerConfig, JobHandler, JobFuture};
pub use error::{QueueError, JobError};
#[cfg(feature = "in-memory")]
pub use batch::{BatchConfig, BatchJobHandler, BatchProcessor, BatchFlush};
#[cfg(feature = "in-memory")]
pub use pipeline::{StageRunner, Stage, StageError};
+6 -8
View File
@@ -75,16 +75,14 @@ where
let mut input = input; let mut input = input;
loop { loop {
match input.recv().await { match input.recv().await {
Some(item) => { Some(item) => match stage.process(item).await {
match stage.process(item).await { Ok(out) => {
Ok(out) => { if output.send(out).await.is_err() {
if output.send(out).await.is_err() { return Err(StageError::ChannelClosed);
return Err(StageError::ChannelClosed);
}
} }
Err(e) => return Err(e),
} }
} Err(e) => return Err(e),
},
None => return Ok(()), None => return Ok(()),
} }
} }
@@ -0,0 +1,165 @@
//! Rate-limited queue wrapper.
//!
//! [`RateLimitedQueue`] wraps any [`Queue`] implementation and applies a
//! token-bucket rate limiter before enqueuing. If the bucket is empty the
//! enqueue is rejected with [`RateLimitQueueError::RateLimited`] instead of
//! blocking.
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::Instant;
use crate::error::QueueError;
use crate::traits::Queue;
use async_trait::async_trait;
/// Error returned by [`RateLimitedQueue::enqueue`].
#[derive(Debug)]
pub enum RateLimitQueueError {
RateLimited,
Other(QueueError),
}
impl std::fmt::Display for RateLimitQueueError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RateLimited => write!(f, "rate limited: capacity exhausted"),
Self::Other(e) => write!(f, "queue error: {e}"),
}
}
}
impl std::error::Error for RateLimitQueueError {}
impl From<QueueError> for RateLimitQueueError {
fn from(e: QueueError) -> Self {
Self::Other(e)
}
}
impl From<RateLimitQueueError> for QueueError {
fn from(e: RateLimitQueueError) -> Self {
match e {
RateLimitQueueError::RateLimited => Self::RateLimit("capacity exhausted".into()),
RateLimitQueueError::Other(e) => e,
}
}
}
/// Token-bucket rate limiter (no extra deps beyond tokio).
struct TokenBucket {
/// Maximum burst capacity.
capacity: u32,
/// Current tokens (float for fractional refill).
tokens: f64,
/// Refill rate (tokens per second).
rate: f64,
/// Last refill timestamp.
last: Instant,
}
impl TokenBucket {
fn new(rate_per_sec: f64, burst: u32) -> Self {
Self {
capacity: burst.max(1),
tokens: burst as f64,
rate: rate_per_sec.max(0.0),
last: Instant::now(),
}
}
/// Attempts to consume one token. Returns true on success.
fn try_consume(&mut self) -> bool {
let now = Instant::now();
let elapsed = now.saturating_duration_since(self.last).as_secs_f64();
self.tokens = (self.tokens + elapsed * self.rate).min(self.capacity as f64);
self.last = now;
if self.tokens >= 1.0 {
self.tokens -= 1.0;
true
} else {
false
}
}
}
/// A queue decorator that enforces a rate limit on enqueue.
pub struct RateLimitedQueue<Q: ?Sized> {
inner: Arc<Q>,
bucket: Arc<Mutex<TokenBucket>>,
}
impl<Q: Queue + 'static> RateLimitedQueue<Q> {
/// Creates a new rate-limited wrapper around `inner`.
pub fn new(inner: Q, rate_per_sec: f64, burst: u32) -> Self {
Self {
inner: Arc::new(inner),
bucket: Arc::new(Mutex::new(TokenBucket::new(rate_per_sec, burst))),
}
}
}
#[async_trait]
impl<Q: Queue + ?Sized> Queue for RateLimitedQueue<Q> {
async fn enqueue(&self, topic: &str, payload: Vec<u8>) -> Result<(), QueueError> {
let mut b = self.bucket.lock().await;
if !b.try_consume() {
return Err(RateLimitQueueError::RateLimited.into());
}
self.inner.enqueue(topic, payload).await
}
async fn dequeue(
&self,
topic: &str,
timeout: Duration,
) -> Result<Option<crate::job::Job>, QueueError> {
self.inner.dequeue(topic, timeout).await
}
async fn ack(&self, job: &crate::job::Job) -> Result<(), crate::error::JobError> {
self.inner.ack(job).await
}
async fn nack(
&self,
job: &crate::job::Job,
requeue: bool,
) -> Result<(), crate::error::JobError> {
self.inner.nack(job, requeue).await
}
async fn dlq_move(&self, topic: &str, job: crate::job::Job) -> Result<(), QueueError> {
self.inner.dlq_move(topic, job).await
}
async fn len(&self, topic: &str) -> Result<u64, QueueError> {
self.inner.len(topic).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::in_memory::InMemoryQueue;
#[tokio::test]
async fn allows_enqueue_within_rate() {
let inner = InMemoryQueue::new();
let rl = RateLimitedQueue::new(inner, 100.0, 10);
assert!(rl.enqueue("t", b"x".to_vec()).await.is_ok());
}
#[tokio::test]
async fn rejects_when_bucket_empty() {
let inner = InMemoryQueue::new();
let rl = RateLimitedQueue::new(inner, 0.0, 1); // 0 tokens/sec, 1 burst
// consume the single burst token
let _ = rl.enqueue("t", b"x".to_vec()).await;
// next should be rate limited (no refill)
let result = rl.enqueue("t", b"y".to_vec()).await;
assert!(matches!(result, Err(QueueError::RateLimit(_))));
}
}
+1 -1
View File
@@ -3,8 +3,8 @@
use async_trait::async_trait; use async_trait::async_trait;
use std::time::Duration; use std::time::Duration;
use crate::error::{JobError, QueueError};
use crate::job::Job; use crate::job::Job;
use crate::error::{QueueError, JobError};
/// A handle to a single unit of queued work. /// A handle to a single unit of queued work.
/// ///
+9 -6
View File
@@ -71,10 +71,13 @@ pub struct WorkerPool<Q: Queue + 'static> {
impl<Q: Queue + 'static> WorkerPool<Q> { impl<Q: Queue + 'static> WorkerPool<Q> {
/// Creates a new worker pool with the given concurrency. /// Creates a new worker pool with the given concurrency.
pub fn new(queue: Q, concurrency: usize) -> Self { pub fn new(queue: Q, concurrency: usize) -> Self {
Self::with_config(queue, WorkerConfig { Self::with_config(
concurrency, queue,
..Default::default() WorkerConfig {
}) concurrency,
..Default::default()
},
)
} }
/// Creates a new worker pool with explicit configuration. /// Creates a new worker pool with explicit configuration.
@@ -148,8 +151,8 @@ impl<Q: Queue + 'static> WorkerPool<Q> {
/// Computes the (capped) exponential backoff delay. /// Computes the (capped) exponential backoff delay.
pub fn retry_delay(config: &WorkerConfig, attempt: u32) -> Duration { pub fn retry_delay(config: &WorkerConfig, attempt: u32) -> Duration {
let exponent = attempt as f64; let exponent = attempt as f64;
let computed = config.retry_base_delay.as_millis() as f64 let computed =
* config.retry_factor.powf(exponent.max(0.0)); config.retry_base_delay.as_millis() as f64 * config.retry_factor.powf(exponent.max(0.0));
let capped = computed.min(config.retry_max_delay.as_millis() as f64); let capped = computed.min(config.retry_max_delay.as_millis() as f64);
Duration::from_millis(capped as u64) Duration::from_millis(capped as u64)
} }
@@ -0,0 +1,47 @@
//! Rate-limited worker pool (feature `in-memory`).
//!
//! [`RateLimitedWorkerPool`] wraps [`crate::worker::WorkerPool`] with a
//! [`crate::rate_limited::RateLimitedQueue`] to back-pressure dequeue when the
//! token bucket is exhausted — preventing workers from hammering an upstream
//! service faster than its rate limit allows.
use crate::rate_limited::RateLimitedQueue;
use crate::traits::Queue;
use crate::worker::{JobHandler, WorkerConfig, WorkerPool};
/// A `WorkerPool` whose dequeue is rate-limited via a token bucket.
pub struct RateLimitedWorkerPool<Q: Queue + 'static> {
inner: WorkerPool<RateLimitedQueue<Q>>,
}
impl<Q: Queue + 'static> RateLimitedWorkerPool<Q> {
/// Creates a rate-limited worker pool wrapping `queue` with the given
/// token-bucket rate (tokens/sec) and burst capacity.
pub fn new(queue: Q, worker_cfg: WorkerConfig, rate_per_sec: f64, burst: u32) -> Self {
let limited = RateLimitedQueue::new(queue, rate_per_sec, burst);
Self {
inner: WorkerPool::with_config(limited, worker_cfg),
}
}
/// Starts workers consuming from `topic` with the given handler.
pub fn start<H>(&self, topic: &str, handler: H)
where
H: JobHandler + 'static,
{
self.inner.start(topic, handler);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructs_rate_limited_pool() {
use crate::in_memory::InMemoryQueue;
let _pool =
RateLimitedWorkerPool::new(InMemoryQueue::new(), WorkerConfig::default(), 10.0, 5);
// smoke: just verifies construction
}
}
@@ -0,0 +1,71 @@
//! Race-safety stress tests for the queue crate's rate-limited primitives.
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use mytheclipse_queue::in_memory::InMemoryQueue;
use mytheclipse_queue::rate_limited::RateLimitedQueue;
use mytheclipse_queue::traits::Queue;
const TASKS: usize = 8;
const PER_TASK: usize = 1000;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn rate_limited_queue_no_item_loss_under_contention() {
// Enqueue 8000 items, dequeue them all concurrently: nothing may vanish.
let inner = InMemoryQueue::new();
let q = Arc::new(RateLimitedQueue::new(inner, 1_000_000.0, 10_000));
let producer: tokio::task::JoinHandle<()> = tokio::spawn({
let q = Arc::clone(&q);
async move {
for i in 0..(TASKS * PER_TASK) {
q.enqueue("stress", format!("item-{i}").into_bytes())
.await
.unwrap();
}
}
});
// Drain concurrently with the producer.
let seen = Arc::new(AtomicUsize::new(0));
let mut workers = Vec::new();
for _ in 0..TASKS {
let q = Arc::clone(&q);
let s = Arc::clone(&seen);
workers.push(tokio::spawn(async move {
loop {
match q
.dequeue("stress", Duration::from_millis(50))
.await
.unwrap()
{
Some(job) => {
let _ = String::from_utf8(job.payload).unwrap();
s.fetch_add(1, Ordering::SeqCst);
}
None => {
// Empty + producer done => we're finished.
break;
}
}
}
}));
}
producer.await.unwrap();
for w in workers {
w.await.unwrap();
}
// Slight subtlety: a worker may observe None while producer still has
// items in flight (producer finished above, but enqueue is async —
// actually producer.await guarantees all enqueues completed). Since the
// producer finished, any None means truly drained.
assert_eq!(
seen.load(Ordering::SeqCst),
TASKS * PER_TASK,
"items lost under contention"
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "mytheclipse-storage" name = "mytheclipse-storage"
version = "1.8.0" version = "1.21.1"
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.75"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+4 -6
View File
@@ -67,12 +67,10 @@ impl StorageDriver for LocalFileStorage {
let mut file = tokio::fs::File::create(&tmp) let mut file = tokio::fs::File::create(&tmp)
.await .await
.map_err(|e| StorageError::Io(e.to_string()))?; .map_err(|e| StorageError::Io(e.to_string()))?;
let written = tokio::io::copy(&mut data, &mut file) let written = tokio::io::copy(&mut data, &mut file).await.map_err(|e| {
.await let _ = std::fs::remove_file(&tmp);
.map_err(|e| { StorageError::Io(e.to_string())
let _ = std::fs::remove_file(&tmp); })?;
StorageError::Io(e.to_string())
})?;
// Ensure durability: flush to OS, fsync, then rename. // Ensure durability: flush to OS, fsync, then rename.
tokio::fs::File::open(&tmp) tokio::fs::File::open(&tmp)
.await .await

Some files were not shown because too many files have changed in this diff Show More