├── .github ├── dependabot.yml └── workflows │ ├── CI.yml │ └── audit.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── documentation ├── simple_flow.puml └── transitions │ ├── only_cache.puml │ ├── stale.puml │ └── upstream.puml ├── examples ├── Cargo.toml ├── examples │ ├── actix_web.rs │ ├── async_backend.rs │ ├── cacheable_response.rs │ ├── debug.rs │ ├── metrics.rs │ ├── sync_backend.rs │ └── tower.rs └── release.toml ├── hitbox-actix ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── src │ ├── actor.rs │ ├── builder.rs │ ├── handlers.rs │ ├── lib.rs │ ├── messages.rs │ └── runtime.rs └── tests │ ├── test_cache_key.rs │ ├── test_mock_backend.rs │ ├── test_proxy_actor.rs │ └── test_redis_backend.rs ├── hitbox-backend ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── backend.rs │ ├── lib.rs │ ├── response.rs │ ├── serializer.rs │ └── value.rs ├── hitbox-derive ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── cacheable_macro.rs │ ├── cacheable_response_macro.rs │ ├── container.rs │ └── lib.rs ├── hitbox-redis ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── src │ ├── actor.rs │ ├── error.rs │ └── lib.rs └── tests │ └── integration_test.rs ├── hitbox-tokio ├── Cargo.toml └── src │ ├── cache.rs │ ├── lib.rs │ └── runtime.rs ├── hitbox-tower ├── Cargo.toml └── src │ ├── lib.rs │ └── runtime.rs ├── hitbox ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── src │ ├── cache.rs │ ├── dev │ │ ├── mock_adapter.rs │ │ ├── mock_backend.rs │ │ └── mod.rs │ ├── error.rs │ ├── lib.rs │ ├── metrics.rs │ ├── response.rs │ ├── runtime │ │ ├── adapter.rs │ │ └── mod.rs │ ├── settings.rs │ ├── states │ │ ├── cache_policy │ │ │ ├── base.rs │ │ │ ├── cacheable.rs │ │ │ ├── mod.rs │ │ │ └── non_cacheable.rs │ │ ├── cache_polled │ │ │ ├── actual.rs │ │ │ ├── base.rs │ │ │ ├── error.rs │ │ │ ├── missed.rs │ │ │ ├── mod.rs │ │ │ └── stale.rs │ │ ├── cache_updated │ │ │ ├── base.rs │ │ │ └── mod.rs │ │ ├── finish │ │ │ ├── base.rs │ │ │ └── mod.rs │ │ ├── initial │ │ │ ├── base.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── upstream_polled │ │ │ ├── base.rs │ │ │ ├── error.rs │ │ │ ├── error_with_stale.rs │ │ │ ├── mod.rs │ │ │ └── successful.rs │ ├── transition_groups │ │ ├── mod.rs │ │ ├── only_cache.rs │ │ ├── stale.rs │ │ └── upstream.rs │ └── value.rs └── tests │ ├── cache_key.rs │ ├── cacheable_derive.rs │ ├── cacheable_response_derive.rs │ ├── metrics.rs │ ├── mod.rs │ ├── response.rs │ ├── settings.rs │ ├── states │ ├── cache_policy.rs │ ├── cache_polled.rs │ ├── mod.rs │ └── upstream_polled.rs │ └── transitions │ ├── cache_disabled.rs │ ├── cache_enabled.rs │ ├── mod.rs │ └── stale.rs └── release.toml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: {} 8 | 9 | jobs: 10 | check: 11 | # Run `cargo check` first to ensure that the pushed code at least compiles. 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | rust: [1.60.0, stable] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: ${{ matrix.rust }} 21 | profile: minimal 22 | override: true 23 | - name: Check 24 | uses: actions-rs/cargo@v1 25 | with: 26 | command: check 27 | args: --all --bins --tests --benches 28 | 29 | test: 30 | needs: check 31 | runs-on: ubuntu-latest 32 | strategy: 33 | matrix: 34 | rust: [1.60.0, stable, beta] 35 | redis-version: [6] 36 | steps: 37 | - uses: actions/checkout@v3 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | toolchain: ${{ matrix.rust }} 41 | profile: minimal 42 | override: true 43 | # Starts Redis server needed by hitbox-redis for integration tests. 44 | - name: Start Redis 45 | uses: supercharge/redis-github-action@1.4.0 46 | with: 47 | redis-version: ${{ matrix.redis-version }} 48 | - name: Run tests 49 | uses: actions-rs/cargo@v1 50 | with: 51 | command: test 52 | args: --workspace --all-features -- --test-threads=1 53 | 54 | - name: Generate coverage file 55 | if: > 56 | matrix.rust == 'stable' 57 | run: | 58 | cargo install cargo-tarpaulin 59 | cargo tarpaulin --out Xml --verbose --workspace --all-features --ignore-tests -- --test-threads=1 60 | 61 | - name: Upload to Codecov 62 | if: > 63 | matrix.rust == 'stable' 64 | uses: codecov/codecov-action@v3 65 | with: 66 | file: cobertura.xml 67 | 68 | clippy: 69 | # Check for any warnings. This is informational and thus is allowed to fail. 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v3 73 | - uses: actions-rs/toolchain@v1 74 | with: 75 | toolchain: stable 76 | components: clippy 77 | profile: minimal 78 | - name: Clippy 79 | uses: actions-rs/clippy-check@v1 80 | with: 81 | token: ${{ secrets.GITHUB_TOKEN }} 82 | args: --workspace --all-features --bins --examples --tests --benches -- -D warnings 83 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | on: 3 | push: 4 | paths: 5 | - '**/Cargo.toml' 6 | - '**/Cargo.lock' 7 | jobs: 8 | security_audit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions-rs/audit-check@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Added by cargo 2 | # 3 | #already existing elements are commented out 4 | 5 | /target 6 | **/*.rs.bk 7 | Cargo.lock 8 | **/target 9 | tarpaulin-report.html 10 | .idea 11 | .DS_Store 12 | .vscode 13 | 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "hitbox", 5 | "hitbox-tokio", 6 | "hitbox-actix", 7 | "hitbox-backend", 8 | "hitbox-derive", 9 | "hitbox-redis", 10 | "hitbox-tower", 11 | "examples", 12 | ] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Makc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hitbox 2 | 3 | [![Build status](https://github.com/hit-box/hitbox/actions/workflows/CI.yml/badge.svg)](https://github.com/hit-box/hitbox/actions?query=workflow) 4 | [![Coverage Status](https://codecov.io/gh/hit-box/hitbox/branch/master/graph/badge.svg?token=tgAm8OBLkY)](https://codecov.io/gh/hit-box/hitbox) 5 | 6 | Hitbox is an asynchronous caching framework supporting multiple backends and suitable 7 | for distributed and for single-machine applications. 8 | 9 | ## Framework integrations 10 | - [x] [Actix](https://github.com/hit-box/hitbox/tree/master/hitbox-actix) 11 | - [ ] Actix-Web 12 | 13 | ## Features 14 | - [x] Automatic cache key generation. 15 | - [x] Multiple cache backend implementations: 16 | - [x] Stale cache mechanics. 17 | - [ ] Cache locks for [dogpile effect] preventions. 18 | - [ ] Distributed cache locks. 19 | - [ ] Detailed metrics out of the box. 20 | 21 | ## Backend implementations 22 | - [x] [Redis](https://github.com/hit-box/hitbox/tree/master/hitbox-backend) 23 | - [ ] In-memory backend 24 | 25 | ## Feature flags 26 | * derive - Support for [Cacheable] trait derive macros. 27 | * metrics - Support for metrics. 28 | 29 | ## Restrictions 30 | Default cache key implementation based on serde_qs crate 31 | and have some [restrictions](https://docs.rs/serde_qs/latest/serde_qs/#supported-types). 32 | 33 | ## Documentation 34 | * [API Documentation](https://docs.rs/hitbox/) 35 | * [Examples](https://github.com/hit-box/hitbox/tree/master/examples/examples) 36 | 37 | ## Example 38 | 39 | Dependencies: 40 | 41 | ```toml 42 | [dependencies] 43 | hitbox = "0.1" 44 | ``` 45 | 46 | Code: 47 | 48 | First, you should derive [Cacheable] trait for your struct or enum: 49 | 50 | ```rust 51 | use hitbox::prelude::*; 52 | use serde::{Deserialize, Serialize}; 53 | 54 | #[derive(Cacheable, Serialize)] // With features=["derive"] 55 | struct Ping { 56 | id: i32, 57 | } 58 | ``` 59 | Or implement that trait manually: 60 | 61 | ```rust 62 | use hitbox::{Cacheable, CacheError}; 63 | struct Ping { id: i32 } 64 | impl Cacheable for Ping { 65 | fn cache_key(&self) -> Result { 66 | Ok(format!("{}::{}", self.cache_key_prefix(), self.id)) 67 | } 68 | 69 | fn cache_key_prefix(&self) -> String { "Ping".to_owned() } 70 | } 71 | ``` 72 | 73 | [Cacheable]: https://docs.rs/hitbox/latest/hitbox/cache/trait.Cacheable.html 74 | [CacheableResponse]: https://docs.rs/hitbox/latest/hitbox/response/trait.CacheableResponse.html 75 | [Backend]: https://docs.rs/hitbox/latest/hitbox/dev/trait.Backend.html 76 | [RedisBackend]: https://docs.rs/hitbox-redis/latest/hitbox_redis/struct.RedisBackend.html 77 | [hitbox-actix]: https://docs.rs/hitbox-actix/latest/hitbox_actix/ 78 | [dogpile effect]: https://www.sobstel.org/blog/preventing-dogpile-effect/ 79 | -------------------------------------------------------------------------------- /documentation/simple_flow.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 'https://plantuml.com/sequence-diagram 3 | 4 | participant GetUsers << (M,#ADD1B2) >> 5 | participant "QueryCache" << (M,#ADD1B2) >> 6 | participant CacheActor 7 | participant CacheBackend 8 | participant DatabaseActor 9 | 10 | == First request == 11 | 12 | autonumber 1 13 | 14 | GetUsers -> "QueryCache": into_cache() 15 | activate "QueryCache" 16 | "QueryCache" --> CacheActor: send() 17 | activate CacheActor 18 | CacheActor --> CacheBackend: send() 19 | activate CacheBackend 20 | CacheActor <-- CacheBackend: 21 | deactivate CacheBackend 22 | note right: No cached data found 23 | CacheActor --> DatabaseActor: send() 24 | activate DatabaseActor 25 | CacheActor <-- DatabaseActor 26 | deactivate DatabaseActor 27 | note right: Return data from database 28 | CacheActor --> CacheBackend: update_cache() 29 | activate CacheBackend 30 | CacheActor <-- CacheBackend 31 | deactivate CacheBackend 32 | CacheActor -> GetUsers: response 33 | deactivate CacheActor 34 | deactivate "QueryCache" 35 | 36 | == Second request == 37 | 38 | autonumber 1 39 | 40 | GetUsers -> "QueryCache": into_cache() 41 | activate "QueryCache" 42 | "QueryCache" --> CacheActor: send() 43 | activate CacheActor 44 | CacheActor --> CacheBackend: send() 45 | activate CacheBackend 46 | CacheActor <-- CacheBackend: 47 | deactivate CacheBackend 48 | note right: Return cached data 49 | CacheActor -> GetUsers: response 50 | deactivate CacheActor 51 | deactivate "QueryCache" 52 | @enduml 53 | -------------------------------------------------------------------------------- /documentation/transitions/only_cache.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 'https://plantuml.com/state-diagram 3 | 4 | scale 700 width 5 | [*] --> Initial 6 | 7 | Initial --> CachePolled::Actual 8 | Initial --> CachePolled::Stale 9 | Initial --> CachePolled::Miss 10 | Initial --> CachePolled::Error 11 | 12 | CachePolled::Actual --> Finish 13 | CachePolled::Stale --> Finish 14 | 15 | CachePolled::Miss --> UpstreamPolled::Successful 16 | CachePolled::Miss --> UpstreamPolled::Error 17 | 18 | CachePolled::Miss --> UpstreamPolled::Successful 19 | UpstreamPolled::Successful --> CachePolicyChecked::Cacheable 20 | UpstreamPolled::Successful --> CachePolicyChecked::NonCacheable 21 | 22 | CachePolled::Error --> UpstreamPolled::Successful 23 | CachePolled::Error --> UpstreamPolled::Error 24 | 25 | CachePolicyChecked::Cacheable --> CacheUpdated 26 | CachePolicyChecked::NonCacheable --> Finish 27 | UpstreamPolled::Error --> Finish 28 | 29 | CacheUpdated --> Finish 30 | 31 | Finish --> [*] 32 | 33 | @enduml -------------------------------------------------------------------------------- /documentation/transitions/stale.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 'https://plantuml.com/state-diagram 3 | 4 | scale 700 width 5 | [*] --> Initial 6 | 7 | Initial --> CachePolled::Actual 8 | Initial --> CachePolled::Stale 9 | Initial --> CachePolled::Miss 10 | Initial --> CachePolled::Error 11 | 12 | CachePolled::Actual --> Finish 13 | 14 | CachePolled::Stale --> UpstreamPolledStaleRetrieved::Successful 15 | CachePolled::Stale --> UpstreamPolledStaleRetrieved::Error 16 | 17 | UpstreamPolledStaleRetrieved::Successful --> CachePolicyChecked::Cacheable 18 | UpstreamPolledStaleRetrieved::Successful --> CachePolicyChecked::NonCacheable 19 | 20 | CachePolicyChecked::Cacheable --> CacheUpdated 21 | CacheUpdated --> Finish 22 | CachePolicyChecked::NonCacheable --> Finish 23 | 24 | UpstreamPolledStaleRetrieved::Error --> Finish 25 | 26 | CachePolled::Miss --> UpstreamPolled::Successful 27 | UpstreamPolled::Successful --> CachePolicyChecked::Cacheable 28 | UpstreamPolled::Successful --> CachePolicyChecked::NonCacheable 29 | 30 | CachePolled::Miss --> UpstreamPolled::Error 31 | 32 | CachePolled::Error --> UpstreamPolled::Successful 33 | CachePolled::Error --> UpstreamPolled::Error 34 | 35 | UpstreamPolled::Successful --> CacheUpdated 36 | UpstreamPolled::Error --> Finish 37 | 38 | CacheUpdated --> Finish 39 | 40 | Finish --> [*] 41 | 42 | @enduml -------------------------------------------------------------------------------- /documentation/transitions/upstream.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 'https://plantuml.com/state-diagram 3 | 4 | scale 700 width 5 | [*] --> Initial 6 | 7 | Initial --> UpstreamPolled::Successful 8 | Initial --> UpstreamPolled::Error 9 | 10 | UpstreamPolled::Successful --> Finish 11 | UpstreamPolled::Error --> Finish 12 | 13 | Finish --> [*] 14 | 15 | @enduml -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hitbox-examples" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2021" 6 | 7 | [features] 8 | default = [] 9 | 10 | [dev-dependencies] 11 | hitbox = { path = "../hitbox", features = ["derive", "metrics"] } 12 | hitbox-actix = { path = "../hitbox-actix", features = ["redis"]} 13 | actix = "0.13" 14 | log = "0.4" 15 | actix-rt = "2" 16 | serde_json = "1" 17 | serde_qs = { version = "0.10" } 18 | serde = { version = "1", features = ["derive"] } 19 | chrono = { version = "0.4", features = ["serde"] } 20 | thiserror = "1" 21 | prometheus = { version = "0.13" } 22 | env_logger = "0.9" 23 | actix_derive = "0.6" 24 | actix-web = "4.0" 25 | tracing = "0.1" 26 | tracing-subscriber = "0.3" 27 | metrics-exporter-prometheus = "0.11" 28 | 29 | hyper = { version = "0.14", features = ["full"] } 30 | tokio = { version = "1", features = ["full"] } 31 | tower = { version = "0.4", features = ["full"] } 32 | tower-http = { version = "0.3", features = ["full"] } 33 | http = "0.2" 34 | -------------------------------------------------------------------------------- /examples/examples/actix_web.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use actix_derive::Message; 3 | use actix_web::{web, App, HttpResponse, HttpServer, Responder}; 4 | use hitbox_actix::prelude::*; 5 | use serde::Serialize; 6 | 7 | fn fibonacci(n: u8) -> u64 { 8 | match n { 9 | 0 => 1, 10 | 1 => 1, 11 | _ => fibonacci(n - 1) + fibonacci(n - 2), 12 | } 13 | } 14 | 15 | struct FibonacciActor; 16 | 17 | impl Actor for FibonacciActor { 18 | type Context = SyncContext; 19 | } 20 | 21 | #[derive(Message, Cacheable, Serialize)] 22 | #[rtype(result = "u64")] 23 | struct GetNumber { 24 | number: u8, 25 | } 26 | 27 | impl Handler for FibonacciActor { 28 | type Result = ::Result; 29 | 30 | fn handle(&mut self, msg: GetNumber, _ctx: &mut Self::Context) -> Self::Result { 31 | fibonacci(msg.number) 32 | } 33 | } 34 | 35 | async fn index( 36 | n: web::Path, 37 | fib: web::Data>, 38 | cache: web::Data>, 39 | ) -> impl Responder { 40 | let query = GetNumber { 41 | number: n.into_inner(), 42 | }; 43 | let number = cache.send(query.into_cache(&fib)).await.unwrap().unwrap(); 44 | HttpResponse::Ok().body(format!("Generate Fibonacci number {}", number)) 45 | } 46 | 47 | #[actix::main] 48 | async fn main() -> std::io::Result<()> { 49 | env_logger::builder() 50 | .filter_level(log::LevelFilter::Debug) 51 | .init(); 52 | 53 | let fib = { SyncArbiter::start(3, move || FibonacciActor) }; 54 | let cache = Cache::new().await.unwrap().start(); 55 | 56 | HttpServer::new(move || { 57 | App::new() 58 | .app_data(fib.clone()) 59 | .app_data(cache.clone()) 60 | .route("/fibonacci/{num}", web::get().to(index)) 61 | }) 62 | .bind("127.0.0.1:8080")? 63 | .run() 64 | .await 65 | } 66 | -------------------------------------------------------------------------------- /examples/examples/async_backend.rs: -------------------------------------------------------------------------------- 1 | /*use actix::prelude::*; 2 | use hitbox::dev::{Backend, BackendError, Delete, DeleteStatus, Get, Lock, LockStatus, Set}; 3 | use hitbox_actix::prelude::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | struct UpstreamActor; 7 | 8 | impl Actor for UpstreamActor { 9 | type Context = Context; 10 | } 11 | 12 | #[derive(MessageResponse, Deserialize, Serialize, Debug)] 13 | struct Pong(i32); 14 | 15 | #[derive(Message, Cacheable, Serialize)] 16 | #[rtype(result = "Result")] 17 | struct Ping { 18 | pub id: i32, 19 | } 20 | 21 | impl Handler for UpstreamActor { 22 | type Result = ResponseFuture<::Result>; 23 | 24 | fn handle(&mut self, msg: Ping, _ctx: &mut Self::Context) -> Self::Result { 25 | Box::pin(async move { 26 | actix_rt::time::sleep(core::time::Duration::from_secs(3)).await; 27 | Ok(Pong(msg.id)) 28 | }) 29 | } 30 | } 31 | 32 | struct DummyBackend; 33 | 34 | impl Actor for DummyBackend { 35 | type Context = Context; 36 | } 37 | 38 | impl Backend for DummyBackend { 39 | type Actor = Self; 40 | type Context = Context; 41 | } 42 | 43 | impl Handler for DummyBackend { 44 | type Result = ResponseFuture>, BackendError>>; 45 | 46 | fn handle(&mut self, _msg: Get, _: &mut Self::Context) -> Self::Result { 47 | log::warn!("Dummy backend GET"); 48 | let fut = async move { Ok(None) }; 49 | Box::pin(fut) 50 | } 51 | } 52 | 53 | impl Handler for DummyBackend { 54 | type Result = Result; 55 | 56 | fn handle(&mut self, _msg: Set, _: &mut Self::Context) -> Self::Result { 57 | log::warn!("Dummy backend SET"); 58 | Ok("42".to_owned()) 59 | } 60 | } 61 | 62 | impl Handler for DummyBackend { 63 | type Result = ResponseFuture>; 64 | 65 | fn handle(&mut self, _msg: Delete, _: &mut Self::Context) -> Self::Result { 66 | log::warn!("Dummy backend Delete"); 67 | let fut = async move { Ok(DeleteStatus::Missing) }; 68 | Box::pin(fut) 69 | } 70 | } 71 | 72 | impl Handler for DummyBackend { 73 | type Result = ResponseFuture>; 74 | 75 | fn handle(&mut self, _msg: Lock, _: &mut Self::Context) -> Self::Result { 76 | log::warn!("Dummy backend Lock"); 77 | let fut = async move { Ok(LockStatus::Acquired) }; 78 | Box::pin(fut) 79 | } 80 | } 81 | 82 | #[actix::main] 83 | async fn main() -> Result<(), CacheError> { 84 | env_logger::builder() 85 | .filter_level(log::LevelFilter::Debug) 86 | .init(); 87 | 88 | let dummy_backend = DummyBackend.start(); 89 | 90 | let cache = CacheActor::builder().finish(dummy_backend).start(); 91 | let upstream = UpstreamActor.start(); 92 | 93 | let msg = Ping { id: 42 }; 94 | let _ = cache.send(msg.into_cache(&upstream)).await??; 95 | 96 | Ok(()) 97 | }*/ 98 | 99 | fn main() { 100 | 101 | } 102 | -------------------------------------------------------------------------------- /examples/examples/cacheable_response.rs: -------------------------------------------------------------------------------- 1 | /*use actix::prelude::*; 2 | use actix_derive::{Message, MessageResponse}; 3 | use hitbox_actix::prelude::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug)] 7 | struct UpstreamActor; 8 | 9 | #[derive(Debug)] 10 | struct Error; 11 | 12 | impl Actor for UpstreamActor { 13 | type Context = Context; 14 | } 15 | 16 | #[derive(MessageResponse, Deserialize, Serialize, Debug)] 17 | struct Pong(i32); 18 | 19 | #[derive(Message, Cacheable, Serialize)] 20 | #[rtype(result = "Result")] 21 | struct Ping { 22 | id: i32, 23 | } 24 | 25 | impl Handler for UpstreamActor { 26 | type Result = ResponseFuture<::Result>; 27 | 28 | fn handle(&mut self, msg: Ping, _ctx: &mut Self::Context) -> Self::Result { 29 | println!("Handler::Ping"); 30 | Box::pin(async move { 31 | actix_rt::time::sleep(core::time::Duration::from_secs(3)).await; 32 | Ok(Pong(msg.id)) 33 | }) 34 | } 35 | } 36 | 37 | use tracing_subscriber::EnvFilter; 38 | 39 | #[actix::main] 40 | async fn main() -> Result<(), CacheError> { 41 | let filter = EnvFilter::new("hitbox=trace"); 42 | tracing_subscriber::fmt() 43 | .with_max_level(tracing::Level::TRACE) 44 | .with_env_filter(filter) 45 | .init(); 46 | 47 | let backend = RedisBackend::new().await.unwrap().start(); 48 | 49 | let cache = Cache::builder() 50 | .with_stale() 51 | .without_lock() 52 | .finish(backend) 53 | .start(); 54 | let upstream = UpstreamActor.start(); 55 | 56 | let msg = Ping { id: 42 }; 57 | let res = cache.send(msg.into_cache(&upstream)).await??; 58 | println!("{:#?}", res); 59 | Ok(()) 60 | }*/ 61 | 62 | fn main() { 63 | 64 | } 65 | -------------------------------------------------------------------------------- /examples/examples/debug.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use actix_derive::{Message, MessageResponse}; 3 | use hitbox_actix::prelude::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | #[derive(Debug)] 7 | struct UpstreamActor; 8 | 9 | impl Actor for UpstreamActor { 10 | type Context = Context; 11 | } 12 | 13 | enum CacheableResult { 14 | Cacheable(T), 15 | NoneCacheable(U), 16 | } 17 | 18 | trait CacheableResponse { 19 | fn cache(&self) -> CacheableResult<&T, &E>; 20 | } 21 | 22 | impl CacheableResponse for i32 { 23 | fn cache(&self) -> CacheableResult<&i32, &E> { 24 | CacheableResult::Cacheable(self) 25 | } 26 | } 27 | 28 | impl CacheableResponse for Result { 29 | fn cache(&self) -> CacheableResult<&T, &E> { 30 | match self { 31 | Ok(value) => CacheableResult::Cacheable(value), 32 | Err(value) => CacheableResult::NoneCacheable(value), 33 | } 34 | } 35 | } 36 | 37 | #[derive(MessageResponse, Deserialize, Serialize, Debug)] 38 | struct Pong(i32); 39 | 40 | #[derive(Debug)] 41 | struct PongError {} 42 | 43 | #[derive(Message, Cacheable, Serialize)] 44 | #[rtype(result = "Result")] 45 | struct Ping { 46 | id: i32, 47 | } 48 | 49 | impl Handler for UpstreamActor { 50 | type Result = ResponseFuture<::Result>; 51 | 52 | fn handle(&mut self, msg: Ping, _ctx: &mut Self::Context) -> Self::Result { 53 | Box::pin(async move { 54 | actix_rt::time::sleep(core::time::Duration::from_secs(3)).await; 55 | Ok(Pong(msg.id)) 56 | }) 57 | } 58 | } 59 | 60 | #[derive(Debug)] 61 | enum Error { 62 | Actix(actix::MailboxError), 63 | Cache(hitbox::CacheError), 64 | Msg(PongError), 65 | } 66 | 67 | impl From for Error { 68 | fn from(err: actix::MailboxError) -> Error { 69 | Error::Actix(err) 70 | } 71 | } 72 | 73 | impl From for Error { 74 | fn from(err: hitbox::CacheError) -> Error { 75 | Error::Cache(err) 76 | } 77 | } 78 | 79 | impl From for Error { 80 | fn from(err: PongError) -> Error { 81 | Error::Msg(err) 82 | } 83 | } 84 | 85 | #[actix::main] 86 | async fn main() -> Result<(), Error> { 87 | env_logger::builder() 88 | .filter_level(log::LevelFilter::Trace) 89 | .init(); 90 | 91 | let cache = Cache::new().await?.start(); 92 | let upstream = UpstreamActor.start(); 93 | 94 | let msg = Ping { id: 42 }; 95 | let _ = cache.send(msg.into_cache(&upstream)).await???; 96 | Ok(()) 97 | } 98 | -------------------------------------------------------------------------------- /examples/examples/metrics.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use actix_derive::MessageResponse; 3 | use hitbox::{hitbox_serializer, CachePolicy, Cacheable, CacheableResponse}; 4 | use hitbox_actix::{Cache, CacheError, IntoCache}; 5 | use metrics_exporter_prometheus::PrometheusBuilder; 6 | 7 | #[derive(Default)] 8 | struct UpstreamActor; 9 | 10 | impl Actor for UpstreamActor { 11 | type Context = Context; 12 | } 13 | 14 | #[derive(Message, Cacheable, serde::Serialize)] 15 | #[rtype(result = "Pong")] 16 | struct Ping { 17 | number: u8, 18 | } 19 | 20 | impl Ping { 21 | fn new(number: u8) -> Self { 22 | Self { number } 23 | } 24 | } 25 | 26 | #[derive(Debug, serde::Serialize, serde::Deserialize, MessageResponse, CacheableResponse)] 27 | struct Pong { 28 | number: u8, 29 | } 30 | 31 | impl Handler for UpstreamActor { 32 | type Result = MessageResult; 33 | 34 | fn handle(&mut self, msg: Ping, _: &mut Self::Context) -> Self::Result { 35 | MessageResult(Pong { number: msg.number }) 36 | } 37 | } 38 | 39 | #[actix::main] 40 | async fn main() { 41 | let upstream = UpstreamActor::default().start(); 42 | let cache = Cache::new().await.unwrap().start(); 43 | let recorder = PrometheusBuilder::new() 44 | .install_recorder() 45 | .expect("failed to install recorder"); 46 | 47 | let _pong_0 = cache 48 | .send(Ping::new(0).into_cache(&upstream)) 49 | .await 50 | .expect("actix mailbox timeout/closed") 51 | .expect("cache actor error"); 52 | 53 | let _again_pong_0 = cache 54 | .send(Ping::new(0).into_cache(&upstream)) 55 | .await 56 | .expect("actix mailbox timeout/closed") 57 | .expect("cache actor error"); 58 | 59 | let _pong_1 = cache 60 | .send(Ping::new(1).into_cache(&upstream)) 61 | .await 62 | .expect("actix mailbox timeout/closed") 63 | .expect("cache actor error"); 64 | 65 | println!("{}", recorder.render()); 66 | } 67 | -------------------------------------------------------------------------------- /examples/examples/sync_backend.rs: -------------------------------------------------------------------------------- 1 | /*use actix::prelude::*; 2 | use hitbox::dev::{Backend, BackendError, Delete, DeleteStatus, Get, Lock, LockStatus, Set}; 3 | use hitbox_actix::prelude::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | struct UpstreamActor; 7 | 8 | impl Actor for UpstreamActor { 9 | type Context = Context; 10 | } 11 | 12 | #[derive(MessageResponse, Deserialize, Serialize, Debug)] 13 | struct Pong(i32); 14 | 15 | impl Cacheable for Ping { 16 | fn cache_key(&self) -> Result { 17 | Ok(format!("{}::{}", self.cache_key_prefix(), self.id)) 18 | } 19 | fn cache_key_prefix(&self) -> String { 20 | "Pong".to_owned() 21 | } 22 | } 23 | 24 | #[derive(Message)] 25 | #[rtype(result = "Result")] 26 | struct Ping { 27 | pub id: i32, 28 | } 29 | 30 | impl Handler for UpstreamActor { 31 | type Result = ResponseFuture<::Result>; 32 | 33 | fn handle(&mut self, msg: Ping, _ctx: &mut Self::Context) -> Self::Result { 34 | Box::pin(async move { 35 | actix_rt::time::sleep(core::time::Duration::from_secs(3)).await; 36 | Ok(Pong(msg.id)) 37 | }) 38 | } 39 | } 40 | 41 | struct DummySyncBackend; 42 | 43 | impl Actor for DummySyncBackend { 44 | type Context = SyncContext; 45 | } 46 | 47 | impl Backend for DummySyncBackend { 48 | type Actor = Self; 49 | type Context = SyncContext; 50 | } 51 | 52 | impl Handler for DummySyncBackend { 53 | type Result = Result>, BackendError>; 54 | 55 | fn handle(&mut self, _msg: Get, _: &mut Self::Context) -> Self::Result { 56 | log::warn!("Dummy sync backend GET"); 57 | Ok(None) 58 | } 59 | } 60 | 61 | impl Handler for DummySyncBackend { 62 | type Result = Result; 63 | 64 | fn handle(&mut self, _msg: Set, _: &mut Self::Context) -> Self::Result { 65 | log::warn!("Dummy sync backend SET"); 66 | Ok("42".to_owned()) 67 | } 68 | } 69 | 70 | impl Handler for DummySyncBackend { 71 | type Result = Result; 72 | 73 | fn handle(&mut self, _msg: Delete, _: &mut Self::Context) -> Self::Result { 74 | log::warn!("Dummy sync backend Delete"); 75 | Ok(DeleteStatus::Missing) 76 | } 77 | } 78 | 79 | impl Handler for DummySyncBackend { 80 | type Result = Result; 81 | 82 | fn handle(&mut self, _msg: Lock, _: &mut Self::Context) -> Self::Result { 83 | log::warn!("Dummy sync backend Lock"); 84 | Ok(LockStatus::Acquired) 85 | } 86 | } 87 | 88 | #[actix::main] 89 | async fn main() -> Result<(), CacheError> { 90 | env_logger::builder() 91 | .filter_level(log::LevelFilter::Debug) 92 | .init(); 93 | 94 | let dummy_sync_backend = { SyncArbiter::start(3, move || DummySyncBackend) }; 95 | 96 | let cache = CacheActor::builder().finish(dummy_sync_backend).start(); 97 | let upstream = UpstreamActor.start(); 98 | 99 | let msg = Ping { id: 42 }; 100 | let _ = cache.send(msg.into_cache(&upstream)).await??; 101 | 102 | Ok(()) 103 | }*/ 104 | 105 | fn main() { 106 | 107 | } 108 | -------------------------------------------------------------------------------- /examples/examples/tower.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::Infallible, net::SocketAddr}; 2 | use hyper::{Body, Server}; 3 | 4 | use tower::make::Shared; 5 | use http::{Request, Response}; 6 | 7 | async fn handle(_: Request) -> Result, Infallible> { 8 | Ok(Response::new("Hello, World!".into())) 9 | } 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | let service = tower::ServiceBuilder::new() 14 | .layer(tower_http::trace::TraceLayer::new_for_http()) 15 | .service_fn(handle); 16 | 17 | let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 18 | Server::bind(&addr) 19 | .serve(Shared::new(service)) 20 | .await 21 | .expect("server error"); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /examples/release.toml: -------------------------------------------------------------------------------- 1 | disable-publish = true 2 | disable-push = true 3 | disable-tag = true 4 | pre-release-replacements = [] 5 | -------------------------------------------------------------------------------- /hitbox-actix/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.0] - 2021-05-29 10 | ### Added 11 | - Initial release 12 | -------------------------------------------------------------------------------- /hitbox-actix/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hitbox-actix" 3 | version = "0.1.0" 4 | authors = ["Belousow Makc ", "Andrey Ermilov "] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "Asynchronous caching framework for Actix." 8 | readme = "README.md" 9 | repository = "https://github.com/hit-box/hitbox/" 10 | categories = ["caching", "asynchronous"] 11 | keywords = ["cache", "actix", "async", "cache-backend", "hitbox"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | hitbox = { path = "../hitbox", version = "0.1.0" } 17 | hitbox-backend = { path = "../hitbox-backend", version = "0.1.0" } 18 | hitbox-redis = { path = "../hitbox-redis", version = "0.1.0", optional = true } 19 | actix = { version = "0.13" } 20 | serde = { version = "1", features = ["derive"] } 21 | tracing = "0.1" 22 | serde_json = "1.0.64" 23 | async-trait = "0.1.52" 24 | 25 | [features] 26 | default = ["redis", "derive"] 27 | 28 | redis = ["hitbox-redis"] 29 | derive = ["hitbox/derive"] 30 | 31 | [package.metadata.docs.rs] 32 | all-features = true 33 | rustdoc-args = ["--cfg", "docsrs"] 34 | -------------------------------------------------------------------------------- /hitbox-actix/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Makc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /hitbox-actix/README.md: -------------------------------------------------------------------------------- 1 | # hitbox-actix 2 | 3 | [![Build status](https://github.com/hit-box/hitbox/actions/workflows/CI.yml/badge.svg)](https://github.com/hit-box/hitbox/actions?query=workflow) 4 | [![Coverage Status](https://codecov.io/gh/hit-box/hitbox/branch/master/graph/badge.svg?token=tgAm8OBLkY)](https://codecov.io/gh/hit-box/hitbox) 5 | 6 | Hitbox-Actix is an asynchronous caching framework for [Actix] actor framework. 7 | It's designed for distributed and for single-machine applications. 8 | 9 | ## Features 10 | - [x] Automatic cache key generation. 11 | - [x] Multiple cache backend implementations. 12 | - [x] Stale cache mechanics. 13 | - [ ] Cache locks for [dogpile effect] preventions. 14 | - [ ] Distributed cache locks. 15 | - [ ] Detailed metrics out of the box. 16 | 17 | ## Backend implementations 18 | - [x] [Redis](https://github.com/hit-box/hitbox/tree/master/hitbox-redis) 19 | - [ ] In-memory backend 20 | 21 | ## Feature flags 22 | * derive - Support for [Cacheable] trait derive macros. 23 | * redis - Support for default redis backend. 24 | 25 | ## Restrictions 26 | Default cache key implementation based on serde_qs crate 27 | and have some [restrictions](https://docs.rs/serde_qs/latest/serde_qs/#supported-types). 28 | 29 | ## Documentation 30 | * [API Documentation](https://docs.rs/hitbox-actix/) 31 | * [Examples](https://github.com/hit-box/hitbox/tree/master/examples/examples) 32 | 33 | ### Flow diagrams: 34 | 35 | [![Simple flow](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/hit-box/hitbox/master/documentation/simple_flow.puml)](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/hit-box/hitbox/master/documentation/simple_flow.puml) 36 | 37 | ## Example 38 | 39 | ### Dependencies: 40 | 41 | ```toml 42 | [dependencies] 43 | hitbox_actix = "0.1" 44 | ``` 45 | 46 | ### Code: 47 | 48 | First, you should derive [Cacheable] trait for your actix [Message]: 49 | 50 | ```rust 51 | use actix::prelude::*; 52 | use actix_derive::{Message, MessageResponse}; 53 | use hitbox_actix::prelude::*; 54 | use serde::{Deserialize, Serialize}; 55 | 56 | #[derive(Message, Cacheable, Serialize)] 57 | #[rtype(result = "Result")] 58 | struct Ping { 59 | id: i32, 60 | } 61 | 62 | #[derive(MessageResponse, Deserialize, Serialize, Debug)] 63 | struct Pong(i32); 64 | 65 | #[derive(Debug)] 66 | struct Error; 67 | ``` 68 | 69 | Next step is declare Upstream actor and implement actix Handler for Ping: 70 | 71 | ```rust 72 | #[derive(Debug)] 73 | struct UpstreamActor; 74 | 75 | impl Actor for UpstreamActor { 76 | type Context = Context; 77 | } 78 | 79 | impl Handler for UpstreamActor { 80 | type Result = ResponseFuture<::Result>; 81 | 82 | fn handle(&mut self, msg: Ping, _ctx: &mut Self::Context) -> Self::Result { 83 | println!("Handler::Ping"); 84 | Box::pin(async move { 85 | actix_rt::time::sleep(core::time::Duration::from_secs(3)).await; 86 | Ok(Pong(msg.id)) 87 | }) 88 | } 89 | } 90 | ``` 91 | The last step is initialize and start CacheActor and UpstreamActor: 92 | 93 | ```rust 94 | use tracing_subscriber::EnvFilter; 95 | 96 | #[actix_rt::main] 97 | async fn main() -> Result<(), CacheError> { 98 | let filter = EnvFilter::new("hitbox=trace"); 99 | tracing_subscriber::fmt() 100 | .with_max_level(tracing::Level::TRACE) 101 | .with_env_filter(filter) 102 | .init(); 103 | 104 | let backend = RedisBackend::new() 105 | .await? 106 | .start(); 107 | 108 | let cache = Cache::builder() 109 | .with_stale() 110 | .finish(backend) 111 | .start(); 112 | let upstream = UpstreamActor.start(); 113 | 114 | /// And send `Ping` message into cache actor 115 | let msg = Ping { id: 42 }; 116 | let res = cache.send(msg.into_cache(&upstream)).await??; 117 | println!("{:#?}", res); 118 | Ok(()) 119 | } 120 | ``` 121 | 122 | [Cacheable]: https://docs.rs/hitbox/latest/hitbox/cache/trait.Cacheable.html 123 | [CacheableResponse]: https://docs.rs/hitbox/latest/hitbox/response/trait.CacheableResponse.html 124 | [Backend]: https://docs.rs/hitbox-backend/latest/hitbox_backend/trait.Backend.html 125 | [RedisBackend]: https://docs.rs/hitbox-redis/latest/hitbox_redis/struct.RedisBackend.html 126 | [dogpile effect]: https://www.sobstel.org/blog/preventing-dogpile-effect/ 127 | [Message]: https://docs.rs/actix/latest/actix/trait.Message.html 128 | 129 | [Actix]: https://github.com/actix/actix/ 130 | -------------------------------------------------------------------------------- /hitbox-actix/src/actor.rs: -------------------------------------------------------------------------------- 1 | //! Cache actor and Builder. 2 | use crate::builder::CacheBuilder; 3 | use actix::prelude::*; 4 | use hitbox::settings::CacheSettings; 5 | use hitbox::CacheError; 6 | use hitbox_backend::CacheBackend; 7 | use hitbox_redis::RedisBackend; 8 | use tracing::{debug, info}; 9 | use std::sync::Arc; 10 | 11 | /// Actix actor implements cache logic. 12 | /// 13 | /// This actor implement only `Handler`. 14 | /// Where [QueryCache](crate::QueryCache) - Actix message with two fields: 15 | /// * Generic actix message for sending to upstream actor. 16 | /// * Address of upstream actor 17 | /// 18 | /// # Example 19 | /// ```rust 20 | /// use actix::prelude::*; 21 | /// use hitbox_actix::{Cache, RedisBackend, CacheError}; 22 | /// 23 | /// #[actix::main] 24 | /// async fn main() -> Result<(), CacheError> { 25 | /// let cache = Cache::new().await?.start(); 26 | /// Ok(()) 27 | /// } 28 | /// ``` 29 | pub struct CacheActor 30 | where 31 | B: CacheBackend, 32 | { 33 | pub(crate) settings: CacheSettings, 34 | pub(crate) backend: Arc, 35 | } 36 | 37 | impl CacheActor 38 | where 39 | B: CacheBackend, 40 | { 41 | /// Initialize new Cache actor with default [`hitbox_redis::RedisBackend`]. 42 | #[allow(clippy::new_ret_no_self)] 43 | pub async fn new() -> Result, CacheError> { 44 | let backend = RedisBackend::new()?; 45 | Ok(CacheBuilder::default().finish(backend)) 46 | } 47 | 48 | /// Creates new [CacheBuilder] instance for Cache actor configuration. 49 | pub fn builder() -> CacheBuilder { 50 | CacheBuilder::default() 51 | } 52 | } 53 | 54 | impl Actor for CacheActor 55 | where 56 | B: CacheBackend + Unpin + 'static, 57 | { 58 | type Context = Context; 59 | 60 | fn started(&mut self, _: &mut Self::Context) { 61 | info!("Cache actor started"); 62 | debug!("Cache enabled: {:?}", self.settings); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /hitbox-actix/src/builder.rs: -------------------------------------------------------------------------------- 1 | //! CacheActor builder patter implementation. 2 | use crate::CacheActor; 3 | use hitbox::settings::{CacheSettings, Status}; 4 | use hitbox_backend::CacheBackend; 5 | use std::{marker::PhantomData, sync::Arc}; 6 | 7 | /// Cache actor configurator. 8 | /// 9 | /// # Example 10 | /// ```rust 11 | /// use actix::prelude::*; 12 | /// use hitbox_actix::{Cache, RedisBackend, CacheError}; 13 | /// 14 | /// #[actix::main] 15 | /// async fn main() -> Result<(), Box> { 16 | /// let backend = RedisBackend::new()?; 17 | /// let cache = Cache::builder() 18 | /// .enable() 19 | /// .finish(backend) 20 | /// .start(); 21 | /// Ok(()) 22 | /// } 23 | /// ``` 24 | pub struct CacheBuilder 25 | where 26 | B: CacheBackend, 27 | { 28 | settings: CacheSettings, 29 | _p: PhantomData, 30 | } 31 | 32 | impl Default for CacheBuilder 33 | where 34 | B: CacheBackend, 35 | { 36 | fn default() -> Self { 37 | CacheBuilder { 38 | settings: CacheSettings { 39 | cache: Status::Enabled, 40 | stale: Status::Enabled, 41 | lock: Status::Disabled, 42 | }, 43 | _p: PhantomData::default(), 44 | } 45 | } 46 | } 47 | 48 | impl CacheBuilder 49 | where 50 | B: CacheBackend, 51 | { 52 | /// Enable interaction with cache backend. (Default value). 53 | pub fn enable(mut self) -> Self { 54 | self.settings.cache = Status::Enabled; 55 | self 56 | } 57 | 58 | /// Disable interaction with cache backend. 59 | /// 60 | /// All messages sent to disabled Cache actor passed directly to an upstream actor. 61 | pub fn disable(mut self) -> Self { 62 | self.settings.cache = Status::Disabled; 63 | self 64 | } 65 | 66 | /// Enable stale cache mechanics. (Default value). 67 | /// 68 | /// If [CacheActor] receives a stale value, it does not return it immediately. 69 | /// It polls data from upstream, and if the upstream returned an error, 70 | /// the [CacheActor] returns a stale value. If no error occurred in the upstream, 71 | /// then a fresh value is stored in the cache and returned. 72 | pub fn with_stale(mut self) -> Self { 73 | self.settings.stale = Status::Enabled; 74 | self 75 | } 76 | 77 | /// Disable stale cache mechanics. 78 | pub fn without_stale(mut self) -> Self { 79 | self.settings.stale = Status::Disabled; 80 | self 81 | } 82 | 83 | /// Enable cache lock mechanics. 84 | /// 85 | /// Prevents multiple upstream requests for the same cache key in case of cache data is missing. 86 | /// Only the first request will produce an upstream request. 87 | /// The remaining requests wait for a first upstream response and return updated data. 88 | /// If `with_stale` is enabled the remaining requests don't wait for an upstream response 89 | /// and return stale cache data if it exists. 90 | pub fn with_lock(mut self) -> Self { 91 | self.settings.lock = Status::Enabled; 92 | self 93 | } 94 | 95 | /// Disable cache lock mechanics. (Default value). 96 | pub fn without_lock(mut self) -> Self { 97 | self.settings.lock = Status::Disabled; 98 | self 99 | } 100 | 101 | /// Instantiate new [Cache] instance with current configuration and passed backend. 102 | /// 103 | /// Backend is an [Addr] of actix [Actor] which implements [Backend] trait: 104 | /// 105 | /// [Cache]: crate::Cache 106 | /// [Backend]: hitbox_backend::Backend 107 | /// [Addr]: https://docs.rs/actix/latest/actix/prelude/struct.Addr.html 108 | /// [Actor]: https://docs.rs/actix/latest/actix/prelude/trait.Actor.html 109 | /// [Messages]: https://docs.rs/actix/latest/actix/prelude/trait.Message.html 110 | /// [Handler]: https://docs.rs/actix/latest/actix/prelude/trait.Handler.html 111 | pub fn finish(self, backend: B) -> CacheActor { 112 | CacheActor { 113 | settings: self.settings, 114 | backend: Arc::new(backend), 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /hitbox-actix/src/handlers.rs: -------------------------------------------------------------------------------- 1 | //! Actix Handler implementation. 2 | 3 | use crate::{ActixAdapter, CacheActor, QueryCache}; 4 | use actix::{ 5 | dev::{MessageResponse, ResponseFuture, ToEnvelope}, 6 | prelude::*, 7 | }; 8 | use hitbox::states::initial::Initial; 9 | use hitbox::{CacheError, Cacheable}; 10 | use hitbox_backend::{CacheBackend, CacheableResponse}; 11 | use serde::{de::DeserializeOwned, Serialize}; 12 | 13 | impl Handler> for CacheActor 14 | where 15 | B: CacheBackend + Unpin + 'static + Send + Sync, 16 | A: Actor + Handler + Send, 17 | M: Message + Cacheable + Send + 'static + Sync, 18 | M::Result: MessageResponse + CacheableResponse + std::fmt::Debug + Send + Sync, 19 | <::Result as CacheableResponse>::Cached: Serialize + DeserializeOwned, 20 | ::Context: ToEnvelope, 21 | { 22 | type Result = ResponseFuture::Result, CacheError>>; 23 | 24 | fn handle(&mut self, msg: QueryCache, _: &mut Self::Context) -> Self::Result { 25 | let adapter_result = ActixAdapter::new(msg, self.backend.clone()); 26 | let settings = self.settings.clone(); 27 | Box::pin(async move { 28 | let initial_state = Initial::new(settings, adapter_result?); 29 | initial_state.transitions().await 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /hitbox-actix/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | #![cfg_attr(docsrs, feature(doc_cfg))] 3 | //! # Hitbox-Actix 4 | //! 5 | //! [![Build status](https://github.com/hit-box/hitbox/actions/workflows/CI.yml/badge.svg)](https://github.com/hit-box/hitbox/actions?query=workflow) 6 | //! [![Coverage Status](https://codecov.io/gh/hit-box/hitbox/branch/master/graph/badge.svg?token=tgAm8OBLkY)](https://codecov.io/gh/hit-box/hitbox) 7 | //! 8 | //! Hitbox-Actix is an asynchronous caching framework for [Actix] actor framework. 9 | //! It's designed for distributed and for single-machine applications. 10 | //! 11 | //! ## Features 12 | //! - [x] Automatic cache key generation. 13 | //! - [x] Multiple cache backend implementations. 14 | //! - [x] Stale cache mechanics. 15 | //! - [ ] Cache locks for [dogpile effect] preventions. 16 | //! - [ ] Distributed cache locks. 17 | //! - [ ] Detailed metrics out of the box. 18 | //! 19 | //! ## Backend implementations: 20 | //! - [x] [Redis](https://github.com/hit-box/hitbox/tree/master/hitbox-backend) 21 | //! - [ ] In-memory backend 22 | //! 23 | //! ## Feature flags 24 | //! * derive - Support for [Cacheable] trait derive macros. 25 | //! * redis - Support for default redis backend. 26 | //! 27 | //! ## Restrictions 28 | //! Default cache key implementation based on serde_qs crate 29 | //! and have some [restrictions](https://docs.rs/serde_qs/latest/serde_qs/#supported-types). 30 | //! 31 | //! ## Documentation 32 | //! * [API Documentation](https://docs.rs/hitbox_acitx/) 33 | //! * [Examples](https://github.com/hit-box/hitbox/tree/master/examples/examples) 34 | //! 35 | //! ### Flow diagrams: 36 | //! [![Simple flow](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/hit-box/hitbox/master/documentation/simple_flow.puml)](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/hit-box/hitbox/master/documentation/simple_flow.puml) 37 | //! 38 | //! ## Example 39 | //! 40 | //! ### Dependencies: 41 | //! 42 | //! ```toml 43 | //! [dependencies] 44 | //! hitbox_actix = "0.1" 45 | //! ``` 46 | //! 47 | //! ### Code: 48 | //! 49 | //! First, you should derive [Cacheable] trait for your actix [Message]: 50 | //! 51 | //! ```rust,ignore 52 | //! use actix::prelude::*; 53 | //! use actix_derive::{Message, MessageResponse}; 54 | //! use hitbox_actix::prelude::*; 55 | //! use serde::{Deserialize, Serialize}; 56 | //! 57 | //! #[derive(Message, Cacheable, Serialize)] 58 | //! #[rtype(result = "Result")] 59 | //! struct Ping { 60 | //! id: i32, 61 | //! } 62 | //! 63 | //! #[derive(MessageResponse, Deserialize, Serialize, Debug)] 64 | //! struct Pong(i32); 65 | //! 66 | //! #[derive(Debug)] 67 | //! struct Error; 68 | //! ``` 69 | //! 70 | //! Next step is declare Upstream actor and implement actix Handler for Ping: 71 | //! 72 | //! ```rust,ignore 73 | //! #[derive(Debug)] 74 | //! struct UpstreamActor; 75 | //! 76 | //! impl Actor for UpstreamActor { 77 | //! type Context = Context; 78 | //! } 79 | //! 80 | //! impl Handler for UpstreamActor { 81 | //! type Result = ResponseFuture<::Result>; 82 | //! 83 | //! fn handle(&mut self, msg: Ping, _ctx: &mut Self::Context) -> Self::Result { 84 | //! println!("Handler::Ping"); 85 | //! Box::pin(async move { 86 | //! actix_rt::time::sleep(core::time::Duration::from_secs(3)).await; 87 | //! Ok(Pong(msg.id)) 88 | //! }) 89 | //! } 90 | //! } 91 | //! ``` 92 | //! The last step is initialize and start CacheActor and UpstreamActor: 93 | //! 94 | //! ```rust,ignore 95 | //! use tracing_subscriber::EnvFilter; 96 | //! 97 | //! #[actix_rt::main] 98 | //! async fn main() -> Result<(), CacheError> { 99 | //! let filter = EnvFilter::new("hitbox=trace"); 100 | //! tracing_subscriber::fmt() 101 | //! .with_max_level(tracing::Level::TRACE) 102 | //! .with_env_filter(filter) 103 | //! .init(); 104 | //! 105 | //! let backend = RedisBackend::new() 106 | //! .await? 107 | //! .start(); 108 | //! 109 | //! let cache = Cache::builder() 110 | //! .with_stale() 111 | //! .finish(backend) 112 | //! .start(); 113 | //! let upstream = UpstreamActor.start(); 114 | //! 115 | //! /// And send `Ping` message into cache actor 116 | //! let msg = Ping { id: 42 }; 117 | //! let res = cache.send(msg.into_cache(&upstream)).await??; 118 | //! println!("{:#?}", res); 119 | //! Ok(()) 120 | //! } 121 | //! ``` 122 | //! 123 | //! [Cacheable]: hitbox::Cacheable 124 | //! [CacheableResponse]: hitbox::CacheableResponse 125 | //! [Backend]: hitbox_backend::Backend 126 | //! [RedisBackend]: hitbox_redis::RedisActor 127 | //! [dogpile effect]: https://www.sobstel.org/blog/preventing-dogpile-effect/ 128 | //! [Message]: actix::Message 129 | //! [Actix]: https://github.com/actix/actix/ 130 | 131 | pub mod actor; 132 | pub mod builder; 133 | pub mod handlers; 134 | pub mod messages; 135 | pub mod runtime; 136 | 137 | pub use actor::CacheActor; 138 | pub use builder::CacheBuilder; 139 | pub use hitbox::{CacheError, Cacheable}; 140 | pub use messages::{IntoCache, QueryCache}; 141 | pub use runtime::ActixAdapter; 142 | 143 | #[cfg(feature = "redis")] 144 | #[cfg_attr(docsrs, doc(cfg(feature = "redis")))] 145 | pub use hitbox_redis::RedisBackend; 146 | 147 | /// Default type alias with RedisBackend. 148 | /// You can disable it or define it manually in your code. 149 | #[cfg(feature = "redis")] 150 | #[cfg_attr(docsrs, doc(cfg(feature = "redis")))] 151 | pub type Cache = CacheActor; 152 | 153 | /// Prelude for hitbox_actix. 154 | pub mod prelude { 155 | #[cfg(feature = "redis")] 156 | #[cfg_attr(docsrs, doc(cfg(feature = "redis")))] 157 | pub use crate::{Cache, RedisBackend}; 158 | pub use crate::{CacheActor, CacheBuilder, CacheError, Cacheable, IntoCache, QueryCache}; 159 | pub use hitbox::hitbox_serializer; 160 | } 161 | -------------------------------------------------------------------------------- /hitbox-actix/src/messages.rs: -------------------------------------------------------------------------------- 1 | //! QueryCache message declaration and converting. 2 | use actix::{dev::MessageResponse, prelude::*}; 3 | use hitbox::{CacheError, Cacheable}; 4 | 5 | /// Trait describes coversion from any [actix::Message] into QueryCache message. 6 | pub trait IntoCache: Cacheable { 7 | /// Helper method to convert Message into [QueryCache] message. 8 | /// 9 | /// # Examples 10 | /// ``` 11 | /// use actix::prelude::*; 12 | /// use hitbox_actix::prelude::*; 13 | /// use serde::Serialize; 14 | /// 15 | /// struct Upstream; 16 | /// 17 | /// impl Actor for Upstream { 18 | /// type Context = Context; 19 | /// } 20 | /// 21 | /// #[derive(Cacheable, Serialize, Message, Debug, PartialEq)] 22 | /// #[rtype(result = "()")] 23 | /// struct QueryNothing { 24 | /// id: Option, 25 | /// } 26 | /// 27 | /// #[actix::main] 28 | /// async fn main() { 29 | /// let upstream = Upstream.start(); 30 | /// let query = QueryNothing { id: Some(1) } 31 | /// .into_cache(&upstream); 32 | /// } 33 | /// ``` 34 | fn into_cache(self, upstream: &Addr) -> QueryCache 35 | where 36 | A: Actor, 37 | Self: Message + Send + Sized, 38 | Self::Result: MessageResponse + Send + 'static, 39 | { 40 | QueryCache { 41 | upstream: upstream.clone(), 42 | message: self, 43 | } 44 | } 45 | } 46 | 47 | impl IntoCache for M {} 48 | 49 | /// Intermediate actix message which handled by Cache actor. 50 | /// 51 | /// This message a product of upstream message and upstream actor address. 52 | /// In other words, QueryCache is a struct that includes base message with user data 53 | /// and address of an actor that is a recipient of this message. 54 | /// You can only send QueryCache messages to Cache actor. 55 | pub struct QueryCache 56 | where 57 | M: Message + Cacheable + Send, 58 | M::Result: MessageResponse + Send, 59 | A: Actor, 60 | { 61 | pub(crate) upstream: Addr, 62 | pub(crate) message: M, 63 | } 64 | 65 | impl QueryCache 66 | where 67 | M: Message + Cacheable + Send, 68 | M::Result: MessageResponse + Send, 69 | A: Actor, 70 | { 71 | /// Returns upstream actor type name or . 72 | pub(crate) fn upstream_name(&self) -> &'static str { 73 | std::any::type_name::() 74 | .rsplit("::") 75 | .next() 76 | .unwrap_or("") 77 | } 78 | 79 | /// Returns final cache key. 80 | /// 81 | /// This method compose final cache key from Cacheable::cache_key 82 | /// and Upstream actor type name. 83 | pub fn cache_key(&self) -> Result { 84 | Ok(format!( 85 | "{}::{}", 86 | self.upstream_name(), 87 | self.message.cache_key()? 88 | )) 89 | } 90 | } 91 | 92 | impl Message for QueryCache 93 | where 94 | A: Actor, 95 | M: Message + Cacheable + Send, 96 | M::Result: MessageResponse + Send, 97 | { 98 | type Result = Result<::Result, CacheError>; 99 | } 100 | -------------------------------------------------------------------------------- /hitbox-actix/src/runtime.rs: -------------------------------------------------------------------------------- 1 | //! [hitbox::runtime::RuntimeAdapter] implementation for Actix runtime. 2 | use actix::dev::{MessageResponse, ToEnvelope}; 3 | use actix::{Actor, Handler, Message}; 4 | use async_trait::async_trait; 5 | use serde::de::DeserializeOwned; 6 | use serde::Serialize; 7 | use std::borrow::Cow; 8 | use tracing::warn; 9 | 10 | use hitbox::runtime::{AdapterResult, EvictionPolicy, RuntimeAdapter, TtlSettings}; 11 | use hitbox::{CacheError, CacheState, Cacheable, CacheableResponse, CachedValue}; 12 | use hitbox_backend::CacheBackend; 13 | 14 | use crate::QueryCache; 15 | 16 | use std::sync::Arc; 17 | 18 | /// [`RuntimeAdapter`] for Actix runtime. 19 | pub struct ActixAdapter 20 | where 21 | A: Actor + Handler, 22 | M: Message + Cacheable + Send + Sync, 23 | M::Result: MessageResponse + Send, 24 | B: CacheBackend, 25 | { 26 | message: Option>, 27 | cache_key: String, 28 | cache_ttl: u32, 29 | cache_stale_ttl: u32, 30 | backend: Arc, 31 | } 32 | 33 | impl ActixAdapter 34 | where 35 | A: Actor + Handler, 36 | M: Message + Cacheable + Send + Sync, 37 | M::Result: MessageResponse + Send, 38 | B: CacheBackend, 39 | { 40 | /// Creates new instance of Actix runtime adapter. 41 | pub fn new(message: QueryCache, backend: Arc) -> Result { 42 | let cache_key = message.cache_key()?; 43 | let cache_stale_ttl = message.message.cache_stale_ttl(); 44 | let cache_ttl = message.message.cache_ttl(); 45 | Ok(Self { 46 | message: Some(message), 47 | backend, 48 | cache_key, 49 | cache_ttl, 50 | cache_stale_ttl, 51 | }) 52 | } 53 | } 54 | 55 | #[async_trait] 56 | impl RuntimeAdapter for ActixAdapter 57 | where 58 | A: Actor + Handler, 59 | A::Context: ToEnvelope, 60 | M: Message + Cacheable + Send + 'static + Sync, 61 | M::Result: MessageResponse + Send, 62 | B: CacheBackend + 'static + Send + Sync, 63 | T: CacheableResponse + 'static + Sync, 64 | U: DeserializeOwned + Serialize, 65 | Self: Send + Sync, 66 | { 67 | type UpstreamResult = T; 68 | 69 | async fn poll_upstream(&mut self) -> AdapterResult { 70 | let message = self.message.take(); 71 | let message = message.ok_or_else(|| { 72 | CacheError::CacheKeyGenerationError("Message already sent to upstream".to_owned()) 73 | })?; 74 | message 75 | .upstream 76 | .send(message.message) 77 | .await 78 | .map_err(|err| CacheError::UpstreamError(Box::new(err))) 79 | } 80 | 81 | async fn poll_cache(&self) -> AdapterResult> { 82 | let backend = self.backend.clone(); 83 | let cache_key = self.cache_key.clone(); 84 | backend 85 | .get(cache_key) 86 | .await 87 | .map(CacheState::from) 88 | .map_err(CacheError::from) 89 | } 90 | 91 | async fn update_cache<'a>( 92 | &self, 93 | cached_value: &'a CachedValue, 94 | ) -> AdapterResult<()> { 95 | let ttl = self.cache_ttl; 96 | let backend = self.backend.clone(); 97 | let cache_key = self.cache_key.clone(); 98 | backend 99 | .set(cache_key, cached_value, Some(ttl)) 100 | .await 101 | .map_err(|err| { 102 | warn!("Updating cache error {}", err); 103 | CacheError::from(err) 104 | }) 105 | } 106 | 107 | fn eviction_settings(&self) -> EvictionPolicy { 108 | let ttl_settings = TtlSettings { 109 | ttl: self.cache_ttl, 110 | stale_ttl: self.cache_stale_ttl, 111 | }; 112 | EvictionPolicy::Ttl(ttl_settings) 113 | } 114 | 115 | fn upstream_name(&self) -> Cow<'static, str> { 116 | std::any::type_name::() 117 | .rsplit("::") 118 | .next() 119 | .unwrap_or("Unknown upstream") 120 | .into() 121 | } 122 | 123 | fn message_name(&self) -> Cow<'static, str> { 124 | self.cache_key.clone().into() 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /hitbox-actix/tests/test_cache_key.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use hitbox_actix::prelude::*; 3 | use serde::Serialize; 4 | 5 | #[derive(Cacheable, Serialize, Message)] 6 | #[rtype(result = "String")] 7 | struct Message { 8 | id: i32, 9 | alias: String, 10 | } 11 | 12 | struct Upstream; 13 | 14 | impl Actor for Upstream { 15 | type Context = Context; 16 | } 17 | 18 | impl Handler for Upstream { 19 | type Result = ResponseFuture; 20 | 21 | fn handle(&mut self, _msg: Message, _: &mut Self::Context) -> Self::Result { 22 | Box::pin(async { "Upstream".to_owned() }) 23 | } 24 | } 25 | 26 | struct NextUpstream; 27 | 28 | impl Actor for NextUpstream { 29 | type Context = Context; 30 | } 31 | 32 | impl Handler for NextUpstream { 33 | type Result = ResponseFuture; 34 | 35 | fn handle(&mut self, _msg: Message, _: &mut Self::Context) -> Self::Result { 36 | Box::pin(async { "NextUpstream".to_owned() }) 37 | } 38 | } 39 | 40 | #[actix::test] 41 | async fn test_final_cache_key() { 42 | let upstream = Upstream.start(); 43 | let next_upstream = NextUpstream.start(); 44 | let message = Message { 45 | id: 42, 46 | alias: "test".to_owned(), 47 | } 48 | .into_cache(&upstream); 49 | assert_eq!( 50 | message.cache_key().unwrap().as_str(), 51 | "Upstream::Message::v0::id=42&alias=test" 52 | ); 53 | let message = Message { 54 | id: 28, 55 | alias: "cow level".to_owned(), 56 | } 57 | .into_cache(&next_upstream); 58 | assert_eq!( 59 | message.cache_key().unwrap().as_str(), 60 | "NextUpstream::Message::v0::id=28&alias=cow+level" 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /hitbox-actix/tests/test_mock_backend.rs: -------------------------------------------------------------------------------- 1 | /*use actix::prelude::*; 2 | use hitbox::dev::{ 3 | mock_backend::backend::{GetMessages, MockBackend, MockMessage}, 4 | Get, 5 | }; 6 | use hitbox::{CacheError, Cacheable}; 7 | use hitbox::{CachePolicy, CacheableResponse}; 8 | use hitbox_actix::prelude::*; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | struct UpstreamActor; 12 | 13 | impl Actor for UpstreamActor { 14 | type Context = Context; 15 | } 16 | 17 | #[derive(MessageResponse, CacheableResponse, Deserialize, Serialize, Debug)] 18 | struct Pong { 19 | id: i32, 20 | } 21 | 22 | #[derive(Message, Serialize)] 23 | #[rtype(result = "Pong")] 24 | struct Ping { 25 | pub id: i32, 26 | } 27 | 28 | impl Cacheable for Ping { 29 | fn cache_key(&self) -> Result { 30 | Ok(format!("{}::{}", self.cache_key_prefix(), self.id)) 31 | } 32 | fn cache_key_prefix(&self) -> String { 33 | "Ping".to_owned() 34 | } 35 | } 36 | 37 | impl Handler for UpstreamActor { 38 | type Result = ::Result; 39 | 40 | fn handle(&mut self, msg: Ping, _ctx: &mut Self::Context) -> Self::Result { 41 | Pong { id: msg.id } 42 | } 43 | } 44 | 45 | #[actix::test] 46 | async fn test_mock_backend() { 47 | let backend = MockBackend::new().start(); 48 | let cache = CacheActor::builder().finish(backend.clone()).start(); 49 | let upstream = UpstreamActor.start(); 50 | let msg = Ping { id: 42 }; 51 | cache 52 | .send(msg.into_cache(&upstream)) 53 | .await 54 | .unwrap() 55 | .unwrap(); 56 | let messages = backend.send(GetMessages).await.unwrap().0; 57 | assert_eq!( 58 | messages[..1], 59 | [MockMessage::Get(Get { 60 | key: "UpstreamActor::Ping::42".to_owned() 61 | }),] 62 | ); 63 | }*/ 64 | -------------------------------------------------------------------------------- /hitbox-actix/tests/test_proxy_actor.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use hitbox_actix::{Cache, CacheError, Cacheable, IntoCache}; 3 | use tracing::info; 4 | 5 | pub struct Upstream; 6 | 7 | impl Actor for Upstream { 8 | type Context = Context; 9 | 10 | fn started(&mut self, _: &mut Self::Context) { 11 | info!("Cache actor started"); 12 | } 13 | } 14 | 15 | #[derive(Message)] 16 | #[rtype(result = "Result")] 17 | pub struct Ping; 18 | 19 | impl Cacheable for Ping { 20 | fn cache_key(&self) -> Result { 21 | Ok(self.cache_key_prefix()) 22 | } 23 | fn cache_key_prefix(&self) -> String { 24 | "Ping".to_owned() 25 | } 26 | } 27 | 28 | impl Handler for Upstream { 29 | type Result = ResponseFuture>; 30 | 31 | fn handle(&mut self, _msg: Ping, _: &mut Self::Context) -> Self::Result { 32 | Box::pin(async { Ok(42) }) 33 | } 34 | } 35 | 36 | #[derive(Message)] 37 | #[rtype(result = "i32")] 38 | pub struct Pong; 39 | 40 | impl Cacheable for Pong { 41 | fn cache_key(&self) -> Result { 42 | Ok(self.cache_key_prefix()) 43 | } 44 | fn cache_key_prefix(&self) -> String { 45 | "Pong".to_owned() 46 | } 47 | } 48 | 49 | impl Handler for Upstream { 50 | type Result = i32; 51 | 52 | fn handle(&mut self, _msg: Pong, _: &mut Self::Context) -> Self::Result { 53 | 42 54 | } 55 | } 56 | 57 | struct SyncUpstream; 58 | 59 | impl Actor for SyncUpstream { 60 | type Context = SyncContext; 61 | } 62 | 63 | impl Handler for SyncUpstream { 64 | type Result = i32; 65 | 66 | fn handle(&mut self, _msg: Pong, _: &mut Self::Context) -> Self::Result { 67 | 42 68 | } 69 | } 70 | 71 | impl Handler for SyncUpstream { 72 | type Result = Result; 73 | 74 | fn handle(&mut self, _msg: Ping, _: &mut Self::Context) -> Self::Result { 75 | Ok(42) 76 | } 77 | } 78 | 79 | #[actix::test] 80 | async fn test_async_proxy() { 81 | let cache = Cache::new().await.unwrap().start(); 82 | let upstream = Upstream {}.start(); 83 | let res = cache.send(Ping {}.into_cache(&upstream)).await.unwrap(); 84 | assert_eq!(res.unwrap(), Ok(42)); 85 | let res = cache.send(Pong {}.into_cache(&upstream)).await.unwrap(); 86 | assert_eq!(res.unwrap(), 42); 87 | } 88 | 89 | #[actix::test] 90 | async fn test_sync_proxy() { 91 | let upstream = SyncArbiter::start(10, move || SyncUpstream {}); 92 | let cache = Cache::new().await.unwrap().start(); 93 | let res = cache.send(Pong {}.into_cache(&upstream)).await.unwrap(); 94 | assert_eq!(res.unwrap(), 42); 95 | let res = cache.send(Ping {}.into_cache(&upstream)).await.unwrap(); 96 | assert_eq!(res.unwrap(), Ok(42)); 97 | } 98 | -------------------------------------------------------------------------------- /hitbox-actix/tests/test_redis_backend.rs: -------------------------------------------------------------------------------- 1 | use actix::prelude::*; 2 | use hitbox::{dev::CacheBackend, CacheError, CachePolicy, Cacheable, CacheableResponse}; 3 | use hitbox_actix::prelude::*; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | struct UpstreamActor; 7 | 8 | impl Actor for UpstreamActor { 9 | type Context = Context; 10 | } 11 | 12 | #[derive(MessageResponse, CacheableResponse, Deserialize, Serialize, Debug, PartialEq)] 13 | struct Pong { 14 | id: i32, 15 | } 16 | 17 | #[derive(Message, Serialize)] 18 | #[rtype(result = "Pong")] 19 | struct Ping { 20 | pub id: i32, 21 | } 22 | 23 | impl Cacheable for Ping { 24 | fn cache_key(&self) -> Result { 25 | Ok(format!("{}::{}", self.cache_key_prefix(), self.id)) 26 | } 27 | fn cache_key_prefix(&self) -> String { 28 | "Ping".to_owned() 29 | } 30 | } 31 | 32 | impl Handler for UpstreamActor { 33 | type Result = ::Result; 34 | 35 | fn handle(&mut self, msg: Ping, _ctx: &mut Self::Context) -> Self::Result { 36 | Pong { id: msg.id } 37 | } 38 | } 39 | 40 | #[actix::test] 41 | async fn test_mock_backend() { 42 | let backend = RedisBackend::new().unwrap(); 43 | let cache = CacheActor::builder().finish(backend).start(); 44 | let upstream = UpstreamActor.start(); 45 | let msg = Ping { id: 42 }; 46 | cache 47 | .send(msg.into_cache(&upstream)) 48 | .await 49 | .unwrap() 50 | .unwrap(); 51 | 52 | let backend = RedisBackend::new().unwrap(); 53 | let res: Pong = backend 54 | .get("UpstreamActor::Ping::42".to_owned()) 55 | .await 56 | .unwrap() 57 | .unwrap() 58 | .into_inner(); 59 | assert_eq!(res, Pong { id: 42 }); 60 | 61 | } 62 | -------------------------------------------------------------------------------- /hitbox-backend/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.0] - 2021-05-29 10 | ### Added 11 | - Initial release 12 | 13 | -------------------------------------------------------------------------------- /hitbox-backend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hitbox-backend" 3 | version = "0.1.0" 4 | authors = ["Belousow Makc ", "Andrey Ermilov "] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "Backend trait for asynchronous caching framework in Rust." 8 | readme = "README.md" 9 | repository = "https://github.com/hit-box/hitbox/" 10 | categories = ["caching", "asynchronous"] 11 | keywords = ["cache", "actix", "async", "cache-backend", "hitbox"] 12 | 13 | [dependencies] 14 | async-trait = "0.1" 15 | chrono = { version = "0.4", features = ["serde"] } 16 | serde = { version = "1", features = ["derive"] } 17 | serde_json = "1" 18 | actix = "0.13" 19 | thiserror = "1" 20 | -------------------------------------------------------------------------------- /hitbox-backend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Makc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /hitbox-backend/README.md: -------------------------------------------------------------------------------- 1 | # hitbox-backend 2 | 3 | [![Build status](https://github.com/hit-box/hitbox/actions/workflows/CI.yml/badge.svg)](https://github.com/hit-box/hitbox/actions?query=workflow) 4 | 5 | [Hitbox] is an asynchronous caching framework supporting multiple backends and suitable 6 | for distributed and for single-machine applications. 7 | 8 | Hitbox Backend is the core primitive for Hitbox. 9 | Trait [Backend] representing the functions required to interact with cache backend. 10 | If you want to implement your own backend, you in the right place. 11 | 12 | ## Examples 13 | * [Async backend](https://github.com/hit-box/hitbox/blob/master/examples/examples/async_backend.rs) 14 | * [Sync backend](https://github.com/hit-box/hitbox/blob/master/examples/examples/sync_backend.rs) 15 | 16 | [Backend]: https://docs.rs/hitbox-backend/latest/hitbox_backend/trait.Backend.html 17 | [Hitbox]: https://github.com/hit-box/hitbox 18 | -------------------------------------------------------------------------------- /hitbox-backend/src/backend.rs: -------------------------------------------------------------------------------- 1 | use crate::{BackendError, CacheableResponse, CachedValue, DeleteStatus}; 2 | use async_trait::async_trait; 3 | 4 | pub type BackendResult = Result; 5 | 6 | #[async_trait] 7 | pub trait CacheBackend { 8 | async fn get(&self, key: String) -> BackendResult>> 9 | where 10 | T: CacheableResponse, 11 | ::Cached: serde::de::DeserializeOwned; 12 | async fn set( 13 | &self, 14 | key: String, 15 | value: &CachedValue, 16 | ttl: Option, 17 | ) -> BackendResult<()> 18 | where 19 | T: CacheableResponse + Sync, 20 | ::Cached: serde::de::DeserializeOwned; 21 | async fn delete(&self, key: String) -> BackendResult; 22 | async fn start(&self) -> BackendResult<()>; 23 | } 24 | -------------------------------------------------------------------------------- /hitbox-backend/src/lib.rs: -------------------------------------------------------------------------------- 1 | // #![warn(missing_docs)] 2 | //! Traits and struct messages for hitbox backend interaction. 3 | //! 4 | //! If you want implement your own backend, you in the right place. 5 | use actix::dev::ToEnvelope; 6 | use actix::prelude::*; 7 | use serializer::SerializerError; 8 | use thiserror::Error; 9 | 10 | mod value; 11 | pub mod serializer; 12 | mod backend; 13 | mod response; 14 | 15 | pub use value::{CachedValue, TtlSettings, EvictionPolicy}; 16 | pub use response::{CacheableResponse, CachePolicy}; 17 | pub use backend::{CacheBackend, BackendResult}; 18 | 19 | 20 | /// Define the behavior needed of an cache layer to work with cache backend. 21 | /// 22 | /// Ultimately the implementing type must be an Actix `Actor` and it must implement handlers for a 23 | /// specific set of message types: 24 | /// 25 | /// * [Get] 26 | /// * [Set] 27 | /// * [Lock] 28 | /// * [Delete] 29 | /// 30 | /// [Get]: crate::Get 31 | /// [Set]: crate::Set 32 | /// [Delete]: crate::Delete 33 | /// [Lock]: crate::Lock 34 | pub trait Backend 35 | where 36 | Self: Actor + Handler + Handler + Handler + Handler, 37 | { 38 | /// Type of backend actor bound. 39 | type Actor: Actor::Context> 40 | + Handler 41 | + Handler 42 | + Handler 43 | + Handler; 44 | /// Type for backend Actor context. 45 | type Context: ActorContext 46 | + ToEnvelope 47 | + ToEnvelope 48 | + ToEnvelope 49 | + ToEnvelope; 50 | } 51 | 52 | /// Proxy Error describes general groups of errors in backend interaction process. 53 | #[derive(Debug, Error)] 54 | pub enum BackendError { 55 | /// Internal backend error, state or computation error. 56 | /// 57 | /// Any error not bounded with network interaction. 58 | #[error(transparent)] 59 | InternalError(Box), 60 | /// Network interaction error. 61 | #[error(transparent)] 62 | ConnectionError(Box), 63 | /// Serializing\Deserializing data error. 64 | #[error(transparent)] 65 | SerializerError(#[from] SerializerError), 66 | } 67 | 68 | /// Actix message requests cache backend value by key. 69 | #[derive(Message, Debug, Clone, PartialEq, Eq)] 70 | #[rtype(result = "Result>, BackendError>")] 71 | pub struct Get { 72 | /// Key of cache backend record. 73 | pub key: String, 74 | } 75 | 76 | /// Actix message writes cache backend value by key. 77 | #[derive(Message, Debug, Clone, PartialEq, Eq)] 78 | #[rtype(result = "Result")] 79 | pub struct Set { 80 | /// Key of cache backend record. 81 | pub key: String, 82 | /// Data for sorage by cache key. 83 | pub value: Vec, 84 | /// Optional value of time-to-live for cache record. 85 | pub ttl: Option, 86 | } 87 | 88 | /// Status of deleting result. 89 | #[derive(Debug, PartialEq, Eq)] 90 | pub enum DeleteStatus { 91 | /// Record successfully deleted. 92 | Deleted(u32), 93 | /// Record already missing. 94 | Missing, 95 | } 96 | 97 | /// Actix message delete record in backend by key. 98 | #[derive(Message, Debug, Clone, PartialEq, Eq)] 99 | #[rtype(result = "Result")] 100 | pub struct Delete { 101 | /// Key of cache backend record for deleting 102 | pub key: String, 103 | } 104 | 105 | /// Actix message creates lock in cache backend. 106 | #[derive(Message, Debug, Clone, PartialEq, Eq)] 107 | #[rtype(result = "Result")] 108 | pub struct Lock { 109 | /// Key of cache backend record for lock. 110 | pub key: String, 111 | /// Time-to-live for cache key lock record. 112 | pub ttl: u32, 113 | } 114 | 115 | /// Enum for representing status of Lock object in backend. 116 | #[derive(Debug, PartialEq, Eq)] 117 | pub enum LockStatus { 118 | /// Lock successfully created and acquired. 119 | Acquired, 120 | /// Lock object already acquired (locked). 121 | Locked, 122 | } 123 | -------------------------------------------------------------------------------- /hitbox-backend/src/response.rs: -------------------------------------------------------------------------------- 1 | //! Trait and datatypes that describes which data should be store in cache. 2 | //! 3 | //! For more detailed information and examples please see [CacheableResponse 4 | //! documentation](trait.CacheableResponse.html). 5 | use serde::{de::DeserializeOwned, Serialize}; 6 | 7 | #[cfg(feature = "derive")] 8 | pub use hitbox_derive::CacheableResponse; 9 | 10 | /// This trait determines which types should be cached or not. 11 | pub enum CachePolicy { 12 | /// This variant should be stored in cache backend 13 | Cacheable(T), 14 | /// This variant shouldn't be stored in the cache backend. 15 | NonCacheable(U), 16 | } 17 | 18 | /// Thit is one of the basic trait which determines should data store in cache backend or not. 19 | /// 20 | /// For primitive types and for user-defined types (with derive macro) 21 | /// cache_policy returns CachePolicy::Cached variant. 22 | /// 23 | /// For `Result` cache_policy method return `CachePolicy::Cacheable(T)` only for data included into 24 | /// `Ok(T)` variant. 25 | /// 26 | /// `Option` is the same with Result: for `Some(T)` returns `CachedPolicy::Cacheable(T)`. `None` are 27 | /// `NonCacheable` by default. 28 | /// 29 | /// ## User defined types: 30 | /// If you want decribe custom caching rules for your own types (for example Enum) you should 31 | /// implement `CacheableResponse` for that type: 32 | /// 33 | /// ```rust,ignore 34 | /// use hitbox::{CacheableResponse, CachePolicy}; 35 | /// 36 | /// enum HttpResponse { 37 | /// Ok(String), 38 | /// Unauthorized(i32), 39 | /// } 40 | /// 41 | /// impl CacheableResponse for HttpResponse { 42 | /// type Cached = String; 43 | /// fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 44 | /// match self { 45 | /// HttpResponse::Ok(body) => CachePolicy::Cacheable(body), 46 | /// _ => CachePolicy::NonCacheable(()), 47 | /// } 48 | /// } 49 | /// fn into_cache_policy(self) -> CachePolicy { 50 | /// match self { 51 | /// HttpResponse::Ok(body) => CachePolicy::Cacheable(body), 52 | /// _ => CachePolicy::NonCacheable(self), 53 | /// } 54 | /// } 55 | /// fn from_cached(cached: Self::Cached) -> Self { 56 | /// HttpResponse::Ok(cached) 57 | /// } 58 | /// } 59 | /// ``` 60 | /// In that case only `HttpResponse::Ok` variant will be saved into the cache backend. 61 | /// And all `String`s from the cache backend will be treated as `HttpReponse::Ok(String)` variant. 62 | pub trait CacheableResponse 63 | where 64 | Self: Sized, 65 | Self::Cached: Serialize, 66 | { 67 | /// Describes what type will be stored into the cache backend. 68 | type Cached; 69 | /// Returns cache policy for current type with borrowed data. 70 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()>; 71 | /// Returns cache policy for current type with owned data. 72 | fn into_cache_policy(self) -> CachePolicy; 73 | /// Describes how previously cached data will be transformed into the original type. 74 | fn from_cached(cached: Self::Cached) -> Self; 75 | } 76 | 77 | // There are several CacheableResponse implementations for the most common types. 78 | 79 | /// Implementation `CacheableResponse` for `Result` type. 80 | /// We store to cache only `Ok` variant. 81 | impl CacheableResponse for Result 82 | where 83 | I: Serialize + DeserializeOwned, 84 | { 85 | type Cached = I; 86 | fn into_cache_policy(self) -> CachePolicy { 87 | match self { 88 | Ok(value) => CachePolicy::Cacheable(value), 89 | Err(_) => CachePolicy::NonCacheable(self), 90 | } 91 | } 92 | fn from_cached(cached: Self::Cached) -> Self { 93 | Ok(cached) 94 | } 95 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 96 | match self { 97 | Ok(value) => CachePolicy::Cacheable(value), 98 | Err(_) => CachePolicy::NonCacheable(()), 99 | } 100 | } 101 | } 102 | 103 | /// Implementation `CacheableResponse` for `Option` type. 104 | /// We store to cache only `Some` variant. 105 | impl CacheableResponse for Option 106 | where 107 | I: Serialize + DeserializeOwned, 108 | { 109 | type Cached = I; 110 | fn into_cache_policy(self) -> CachePolicy { 111 | match self { 112 | Some(value) => CachePolicy::Cacheable(value), 113 | None => CachePolicy::NonCacheable(self), 114 | } 115 | } 116 | fn from_cached(cached: Self::Cached) -> Self { 117 | Some(cached) 118 | } 119 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 120 | match self { 121 | Some(value) => CachePolicy::Cacheable(value), 122 | None => CachePolicy::NonCacheable(()), 123 | } 124 | } 125 | } 126 | 127 | /// Implementation `CacheableResponse` for primitive types. 128 | macro_rules! CACHEABLE_RESPONSE_IMPL { 129 | ($type:ty) => { 130 | impl CacheableResponse for $type { 131 | type Cached = $type; 132 | fn into_cache_policy(self) -> CachePolicy { 133 | CachePolicy::Cacheable(self) 134 | } 135 | fn from_cached(cached: Self::Cached) -> Self { 136 | cached 137 | } 138 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 139 | CachePolicy::Cacheable(self) 140 | } 141 | } 142 | }; 143 | } 144 | 145 | CACHEABLE_RESPONSE_IMPL!(()); 146 | CACHEABLE_RESPONSE_IMPL!(u8); 147 | CACHEABLE_RESPONSE_IMPL!(u16); 148 | CACHEABLE_RESPONSE_IMPL!(u32); 149 | CACHEABLE_RESPONSE_IMPL!(u64); 150 | CACHEABLE_RESPONSE_IMPL!(usize); 151 | CACHEABLE_RESPONSE_IMPL!(i8); 152 | CACHEABLE_RESPONSE_IMPL!(i16); 153 | CACHEABLE_RESPONSE_IMPL!(i32); 154 | CACHEABLE_RESPONSE_IMPL!(i64); 155 | CACHEABLE_RESPONSE_IMPL!(isize); 156 | CACHEABLE_RESPONSE_IMPL!(f32); 157 | CACHEABLE_RESPONSE_IMPL!(f64); 158 | CACHEABLE_RESPONSE_IMPL!(String); 159 | CACHEABLE_RESPONSE_IMPL!(&'static str); 160 | CACHEABLE_RESPONSE_IMPL!(bool); 161 | -------------------------------------------------------------------------------- /hitbox-backend/src/serializer.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{CachePolicy, CacheableResponse, CachedValue}; 4 | use chrono::{DateTime, Utc}; 5 | use serde::{Deserialize, Serialize, de::DeserializeOwned}; 6 | use thiserror::Error; 7 | 8 | #[derive(Error, Debug)] 9 | pub enum SerializerError { 10 | #[error(transparent)] 11 | Serialize(Box), 12 | #[error(transparent)] 13 | Deserialize(Box), 14 | } 15 | 16 | pub trait Serializer { 17 | type Raw; 18 | 19 | fn deserialize(data: Self::Raw) -> Result, SerializerError> 20 | where 21 | U: DeserializeOwned, 22 | T: CacheableResponse; 23 | fn serialize(value: &CachedValue) -> Result, SerializerError> 24 | where 25 | T: CacheableResponse, 26 | U: Serialize; 27 | } 28 | 29 | #[derive(Deserialize, Serialize)] 30 | struct SerializableCachedValue { 31 | data: U, 32 | expired: DateTime, 33 | } 34 | 35 | impl SerializableCachedValue { 36 | pub fn new(data: U, expired: DateTime) -> Self { 37 | SerializableCachedValue { data, expired } 38 | } 39 | } 40 | 41 | impl From> for Option> 42 | where 43 | T: CacheableResponse, 44 | { 45 | fn from(value: CachedValue) -> Self { 46 | match value.data.into_cache_policy() { 47 | CachePolicy::Cacheable(data) => Some(SerializableCachedValue::new(data, value.expired)), 48 | CachePolicy::NonCacheable(_) => None, 49 | } 50 | } 51 | } 52 | 53 | impl<'a, T, U> From<&'a CachedValue> for Option> 54 | where 55 | T: CacheableResponse, 56 | { 57 | fn from(value: &'a CachedValue) -> Self { 58 | match value.data.cache_policy() { 59 | CachePolicy::Cacheable(data) => Some(SerializableCachedValue::new(data, value.expired)), 60 | CachePolicy::NonCacheable(_) => None, 61 | } 62 | } 63 | } 64 | 65 | impl From> for CachedValue 66 | where 67 | T: CacheableResponse, 68 | { 69 | fn from(value: SerializableCachedValue) -> Self { 70 | CachedValue::new(T::from_cached(value.data), value.expired) 71 | } 72 | } 73 | 74 | #[derive(Default)] 75 | pub struct JsonSerializer> { 76 | _raw: PhantomData, 77 | } 78 | 79 | impl Serializer for JsonSerializer> { 80 | type Raw = Vec; 81 | 82 | fn deserialize(data: Self::Raw) -> Result, SerializerError> 83 | where 84 | U: DeserializeOwned, 85 | T: CacheableResponse, 86 | { 87 | let deserialized: SerializableCachedValue = serde_json::from_slice(&data) 88 | .map_err(|err| SerializerError::Deserialize(Box::new(err)))?; 89 | Ok(CachedValue::from(deserialized)) 90 | } 91 | 92 | fn serialize(value: &CachedValue) -> Result, SerializerError> 93 | where 94 | T: CacheableResponse, 95 | U: Serialize, 96 | { 97 | let serializable_value: Option> = value.into(); 98 | match serializable_value { 99 | Some(value) => Ok(Some(serde_json::to_vec(&value).map_err(|err| SerializerError::Serialize(Box::new(err)))?)), 100 | None => Ok(None), 101 | } 102 | } 103 | } 104 | 105 | impl Serializer for JsonSerializer { 106 | type Raw = String; 107 | 108 | fn deserialize(data: Self::Raw) -> Result, SerializerError> 109 | where 110 | U: DeserializeOwned, 111 | T: CacheableResponse, 112 | { 113 | let deserialized: SerializableCachedValue = serde_json::from_str(&data) 114 | .map_err(|err| SerializerError::Deserialize(Box::new(err)))?; 115 | Ok(CachedValue::from(deserialized)) 116 | } 117 | 118 | fn serialize(value: &CachedValue) -> Result, SerializerError> 119 | where 120 | T: CacheableResponse, 121 | U: Serialize, 122 | { 123 | let serializable_value: Option> = value.into(); 124 | match serializable_value { 125 | Some(value) => Ok(Some(serde_json::to_string(&value).map_err(|err| SerializerError::Serialize(Box::new(err)))?)), 126 | None => Ok(None), 127 | } 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | mod test { 133 | use super::*; 134 | use crate::CacheableResponse; 135 | 136 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 137 | struct Test { 138 | a: i32, 139 | b: String, 140 | } 141 | 142 | impl CacheableResponse for Test { 143 | type Cached = Self; 144 | 145 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 146 | CachePolicy::Cacheable(self) 147 | } 148 | 149 | fn from_cached(cached: Self::Cached) -> Self { 150 | cached 151 | } 152 | 153 | fn into_cache_policy(self) -> CachePolicy { 154 | CachePolicy::Cacheable(self) 155 | } 156 | } 157 | 158 | impl Test { 159 | pub fn new() -> Self { 160 | Self { 161 | a: 42, 162 | b: "nope".to_owned(), 163 | } 164 | } 165 | } 166 | 167 | #[test] 168 | fn test_json_bytes_serializer() { 169 | let value = CachedValue::new(Test::new(), Utc::now()); 170 | let raw = ::serialize(&value).unwrap().unwrap(); 171 | assert_eq!(value, ::deserialize(raw).unwrap()); 172 | } 173 | 174 | #[test] 175 | fn test_json_string_serializer() { 176 | let value = CachedValue::new(Test::new(), Utc::now()); 177 | let raw = JsonSerializer::::serialize(&value).unwrap().unwrap(); 178 | dbg!(&raw); 179 | assert_eq!(value, JsonSerializer::::deserialize(raw).unwrap()); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /hitbox-backend/src/value.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | 3 | #[cfg_attr(test, derive(Clone, PartialEq, Eq, Debug))] 4 | pub struct CachedValue { 5 | pub data: T, 6 | pub expired: DateTime, 7 | } 8 | 9 | impl CachedValue { 10 | pub fn new(data: T, expired: DateTime) -> Self { 11 | CachedValue { data, expired } 12 | } 13 | 14 | pub fn into_inner(self) -> T { 15 | self.data 16 | } 17 | } 18 | 19 | /// TTL eviction settings. 20 | /// 21 | /// More information you cat see in [`crate::Cacheable`] trait implementation. 22 | pub struct TtlSettings { 23 | /// Describe current cached data TTL value. 24 | /// 25 | /// More information you can see in [`crate::Cacheable::cache_ttl`]. 26 | pub ttl: u32, 27 | 28 | /// Describe current cached data stale TTL value. 29 | /// 30 | /// More information you can see in [`crate::Cacheable::cache_stale_ttl`]. 31 | pub stale_ttl: u32, 32 | } 33 | 34 | /// Cached data eviction policy settings. 35 | pub enum EvictionPolicy { 36 | /// Eviction by TTL settings. 37 | Ttl(TtlSettings), 38 | } 39 | 40 | impl From<(T, EvictionPolicy)> for CachedValue { 41 | fn from(model: (T, EvictionPolicy)) -> Self { 42 | let (data, eviction_policy) = model; 43 | match eviction_policy { 44 | EvictionPolicy::Ttl(settings) => { 45 | let duration = chrono::Duration::seconds(settings.stale_ttl as i64); 46 | let expired = chrono::Utc::now() + duration; 47 | Self { data, expired } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /hitbox-derive/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.0] - 2021-05-29 10 | ### Added 11 | - Initial release 12 | 13 | -------------------------------------------------------------------------------- /hitbox-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hitbox-derive" 3 | version = "0.1.0" 4 | authors = ["Belousow Makc ", "Andrey Ermilov "] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "Derive macros for asynchronous caching framework in Rust." 8 | readme = "README.md" 9 | repository = "https://github.com/hit-box/hitbox/" 10 | categories = ["caching", "asynchronous"] 11 | keywords = ["cache", "actix", "async", "cache-backend", "hitbox"] 12 | 13 | [dependencies] 14 | syn = { version = "1", features = ["derive"] } 15 | quote = "1" 16 | serde = "1" 17 | proc-macro2 = "1" 18 | 19 | [lib] 20 | name = "hitbox_derive" 21 | proc-macro = true 22 | -------------------------------------------------------------------------------- /hitbox-derive/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 hitbox project contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /hitbox-derive/README.md: -------------------------------------------------------------------------------- 1 | # hitbox-derive 2 | 3 | Hitbox is an asynchronous caching framework supporting multiple backends and suitable for distributed and for single-machine applications. 4 | 5 | hitbox-derive is Cacheable and CacheableResponse trait derive macro implementation. 6 | -------------------------------------------------------------------------------- /hitbox-derive/src/cacheable_macro.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | use crate::container::Container; 5 | /// Implementing Cacheable trait. 6 | /// 7 | /// Uses `serde_qs` crate to create a unique cache key. 8 | /// Default implementation of methods `cache_ttl`, `cache_stale_ttl` and `cache_version` 9 | /// are used if macros of the same name are not used. 10 | pub fn impl_macro(ast: &syn::DeriveInput) -> syn::Result { 11 | let name = &ast.ident; 12 | let message_type = format!("{name}"); 13 | let attrs = Container::from_ast(ast)?; 14 | 15 | let cache_key_implement = quote! { 16 | fn cache_key(&self) -> Result { 17 | hitbox_serializer::to_string(self) 18 | .map(|key| format!("{}::v{}::{}", self.cache_key_prefix(), self.cache_version(), key)) 19 | .map_err(|error| CacheError::CacheKeyGenerationError(error.to_string())) 20 | } 21 | }; 22 | 23 | let cache_key_prefix_implement = quote! { 24 | fn cache_key_prefix(&self) -> String { 25 | #message_type.to_owned() 26 | } 27 | }; 28 | 29 | let cache_ttl_implement = match attrs.cache_ttl { 30 | Some(cache_ttl) => quote! { 31 | fn cache_ttl(&self) -> u32 { 32 | #cache_ttl 33 | } 34 | }, 35 | None => proc_macro2::TokenStream::new(), 36 | }; 37 | 38 | let cache_stale_ttl_implement = match attrs.cache_stale_ttl { 39 | Some(cache_stale_ttl) => quote! { 40 | fn cache_stale_ttl(&self) -> u32 { 41 | #cache_stale_ttl 42 | } 43 | }, 44 | None => proc_macro2::TokenStream::new(), 45 | }; 46 | 47 | let cache_version_implement = match attrs.cache_version { 48 | Some(cache_version) => quote! { 49 | fn cache_version(&self) -> u32 { 50 | #cache_version 51 | } 52 | }, 53 | None => proc_macro2::TokenStream::new(), 54 | }; 55 | 56 | Ok(quote! { 57 | impl Cacheable for #name { 58 | #cache_key_implement 59 | #cache_key_prefix_implement 60 | #cache_ttl_implement 61 | #cache_stale_ttl_implement 62 | #cache_version_implement 63 | } 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /hitbox-derive/src/cacheable_response_macro.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | 3 | use quote::quote; 4 | 5 | /// Implementing CacheableResponse trait. 6 | pub fn impl_macro(ast: &syn::DeriveInput) -> TokenStream { 7 | let name = &ast.ident; 8 | 9 | let gen = quote! { 10 | impl CacheableResponse for #name { 11 | type Cached = #name; 12 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 13 | CachePolicy::Cacheable(self) 14 | } 15 | fn into_cache_policy(self) -> CachePolicy { 16 | CachePolicy::Cacheable(self) 17 | } 18 | fn from_cached(cached: Self::Cached) -> Self { 19 | cached 20 | } 21 | } 22 | }; 23 | gen.into() 24 | } 25 | -------------------------------------------------------------------------------- /hitbox-derive/src/container.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | 3 | const CACHE_TTL: &str = "cache_ttl"; 4 | const CACHE_STALE_TTL: &str = "cache_stale_ttl"; 5 | const CACHE_VERSION: &str = "cache_version"; 6 | 7 | fn parse_lit_to_u32(lit: &syn::Lit, attr_name: &str) -> syn::Result { 8 | match lit { 9 | syn::Lit::Int(lit) => lit 10 | .base10_parse::() 11 | .map_err(|e| syn::Error::new_spanned(lit, e)), 12 | _ => Err(syn::Error::new_spanned( 13 | lit, 14 | format!("Expected hitbox {attr_name} attribute should be u32"), 15 | )), 16 | } 17 | } 18 | 19 | pub struct Container { 20 | pub cache_ttl: Option, 21 | pub cache_stale_ttl: Option, 22 | pub cache_version: Option, 23 | } 24 | 25 | impl Container { 26 | pub fn from_ast(input: &syn::DeriveInput) -> syn::Result { 27 | let mut ttl = None; 28 | let mut stale_ttl = None; 29 | let mut version = None; 30 | 31 | let items = input 32 | .attrs 33 | .iter() 34 | .map(|attr| { 35 | if !attr.path.is_ident("hitbox") { 36 | return Ok(Vec::new()); 37 | } 38 | match attr.parse_meta() { 39 | Ok(syn::Meta::List(meta)) => Ok(meta.nested.into_iter().collect()), 40 | Ok(other) => Err(syn::Error::new_spanned(other, "expected #[hitbox(...)]")), 41 | Err(err) => Err(err), 42 | } 43 | }) 44 | .collect::, _>>()? 45 | .into_iter() 46 | .flatten(); 47 | 48 | for meta_item in items { 49 | match &meta_item { 50 | // Parse `#[hitbox(cache_ttl = 42)]` 51 | syn::NestedMeta::Meta(syn::Meta::NameValue(m)) if m.path.is_ident(CACHE_TTL) => { 52 | ttl = Some(parse_lit_to_u32(&m.lit, CACHE_TTL)?); 53 | } 54 | 55 | // Parse `#[hitbox(cache_stale_ttl = 42)]` 56 | syn::NestedMeta::Meta(syn::Meta::NameValue(m)) 57 | if m.path.is_ident(CACHE_STALE_TTL) => 58 | { 59 | stale_ttl = Some(parse_lit_to_u32(&m.lit, CACHE_STALE_TTL)?); 60 | } 61 | 62 | // Parse `#[hitbox(cache_version = 42)]` 63 | syn::NestedMeta::Meta(syn::Meta::NameValue(m)) 64 | if m.path.is_ident(CACHE_VERSION) => 65 | { 66 | version = Some(parse_lit_to_u32(&m.lit, CACHE_VERSION)?); 67 | } 68 | 69 | // Throw error on unknown attribute 70 | syn::NestedMeta::Meta(m) => { 71 | let path = m.path().into_token_stream().to_string().replace(' ', ""); 72 | return Err(syn::Error::new_spanned( 73 | m.path(), 74 | format!("Unknown hitbox container attribute `{path}`"), 75 | )); 76 | } 77 | 78 | // Throw error on other lit types 79 | lit => { 80 | return Err(syn::Error::new_spanned( 81 | lit, 82 | "Unexpected literal in hitbox container attribute", 83 | )); 84 | } 85 | } 86 | } 87 | 88 | Ok(Container { 89 | cache_ttl: ttl, 90 | cache_stale_ttl: stale_ttl, 91 | cache_version: version, 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /hitbox-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | //! This crate provides default implementations for Cacheable and CacheableResponse derive macros. 3 | //! 4 | //! You can see an example of Cacheable derive macro below: 5 | //! ```edition2018,ignore 6 | //! use hitbox::cache::Cacheable; 7 | //! use hitbox::error::CacheError; 8 | //! use serde::Serialize; 9 | //! 10 | //! #[derive(Cacheable, Serialize)] 11 | //! #[hitbox(cache_ttl=120, cache_stale_ttl=100, cache_version=100)] 12 | //! struct Message { 13 | //! field: i32, 14 | //! }; 15 | //! let message = Message { field: 42 }; 16 | //! assert_eq!(message.cache_message_key().unwrap(), "Message::v100::field=42".to_string()); 17 | //! ``` 18 | //! 19 | //! CacheableResponse example: 20 | //! ```edition2018,ignore 21 | //! use hitbox::response::CacheableResponse; 22 | //! use serde::Serialize; 23 | //! 24 | //! #[derive(CacheableResponse, Serialize)] 25 | //! pub enum MyResult { 26 | //! OptionOne(i32), 27 | //! OptionTwo(String), 28 | //! } 29 | //! ``` 30 | use proc_macro::TokenStream; 31 | 32 | mod cacheable_macro; 33 | mod cacheable_response_macro; 34 | mod container; 35 | 36 | /// Derive Cacheable macro implementation. 37 | #[proc_macro_derive(Cacheable, attributes(hitbox))] 38 | pub fn cacheable_macro_derive(input: TokenStream) -> TokenStream { 39 | let ast = syn::parse_macro_input!(input as syn::DeriveInput); 40 | cacheable_macro::impl_macro(&ast) 41 | .unwrap_or_else(|err| err.to_compile_error()) 42 | .into() 43 | } 44 | 45 | /// Derive CacheableResponse macro implementation. 46 | #[proc_macro_derive(CacheableResponse)] 47 | pub fn cacheable_response_macro_derive(input: TokenStream) -> TokenStream { 48 | let ast = syn::parse_macro_input!(input as syn::DeriveInput); 49 | cacheable_response_macro::impl_macro(&ast) 50 | } 51 | -------------------------------------------------------------------------------- /hitbox-redis/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.0] - 2021-05-29 10 | ### Added 11 | - Initial release 12 | 13 | -------------------------------------------------------------------------------- /hitbox-redis/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hitbox-redis" 3 | version = "0.1.0" 4 | authors = ["Belousow Makc ", "Andrey Ermilov "] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "Hitbox redis backend." 8 | readme = "README.md" 9 | repository = "https://github.com/hit-box/hitbox/" 10 | categories = ["caching", "asynchronous"] 11 | keywords = ["cache", "async", "cache-backend", "hitbox", "redis"] 12 | 13 | [dependencies] 14 | hitbox-backend = { path = "../hitbox-backend", version = "0.1.0" } 15 | actix = "0.13" 16 | log = "0.4" 17 | redis = { version = "0.21", features = ["tokio-comp", "connection-manager"] } 18 | actix_derive = "0.6" 19 | actix-rt = "2" 20 | thiserror = "1" 21 | async-trait = "0.1.52" 22 | serde = "1.0.136" 23 | tokio = "1.17.0" 24 | tracing = { version = "0.1.32", default-features = false } 25 | 26 | [dev-dependencies] 27 | chrono = "0.4.19" 28 | env_logger = "0.9.0" 29 | test-log = { version = "0.2.8", features = ["trace"] } 30 | tokio = { version = "1", features = ["time", "macros", "test-util", "rt-multi-thread"] } 31 | tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt"] } 32 | -------------------------------------------------------------------------------- /hitbox-redis/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Makc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /hitbox-redis/README.md: -------------------------------------------------------------------------------- 1 | # hitbox-redis 2 | 3 | Hitbox is an asynchronous caching framework supporting multiple backends and suitable for distributed and for single-machine applications. 4 | 5 | hitbox-redis is Cache [Backend] implementation for Redis. 6 | 7 | This crate uses [redis-rs] as base library for asynchronous interaction with redis nodes. 8 | It uses one [MultiplexedConnection] for better connection utilisation. 9 | 10 | ## Example backend usage with hitbox_actix 11 | 12 | ```rust 13 | use actix::prelude::*; 14 | use hitbox_actix::prelude::*; 15 | 16 | #[actix::main] 17 | async fn main() -> Result<(), CacheError> { 18 | let backend = RedisBackend::new() 19 | .await? 20 | .start(); 21 | 22 | let cache = Cache::builder() 23 | .finish(backend) 24 | .start(); 25 | Ok(()) 26 | } 27 | ``` 28 | 29 | [MultiplexedConnection]: https://docs.rs/redis/latest/redis/aio/struct.MultiplexedConnection.html 30 | [Backend]: https://docs.rs/hitbox-backend/latest/hitbox_backend/trait.Backend.html 31 | [redis-rs]: https://docs.rs/redis/ 32 | -------------------------------------------------------------------------------- /hitbox-redis/src/actor.rs: -------------------------------------------------------------------------------- 1 | //! Redis backend actor implementation. 2 | use crate::error::Error; 3 | use async_trait::async_trait; 4 | use hitbox_backend::{ 5 | serializer::{JsonSerializer, Serializer}, 6 | BackendError, BackendResult, CacheBackend, CacheableResponse, CachedValue, 7 | DeleteStatus, 8 | }; 9 | use redis::{aio::ConnectionManager, Client}; 10 | use tokio::sync::OnceCell; 11 | use tracing::trace; 12 | 13 | /// Redis cache backend based on redis-rs crate. 14 | /// 15 | /// This actor provides redis as storage [Backend] for hitbox. 16 | /// Its use one [MultiplexedConnection] for asynchronous network interaction. 17 | /// 18 | /// [MultiplexedConnection]: redis::aio::MultiplexedConnection 19 | /// [Backend]: hitbox_backend::Backend 20 | pub struct RedisBackend { 21 | client: Client, 22 | connection: OnceCell, 23 | } 24 | 25 | impl RedisBackend { 26 | /// Create new backend instance with default settings. 27 | /// 28 | /// # Examples 29 | /// ``` 30 | /// use hitbox_redis::RedisBackend; 31 | /// 32 | /// #[tokio::main] 33 | /// async fn main() { 34 | /// let backend = RedisBackend::new(); 35 | /// } 36 | /// ``` 37 | pub fn new() -> Result { 38 | Ok(Self::builder().build()?) 39 | } 40 | 41 | /// Creates new RedisBackend builder with default settings. 42 | pub fn builder() -> RedisBackendBuilder { 43 | RedisBackendBuilder::default() 44 | } 45 | 46 | /// Create lazy connection to redis via [ConnectionManager](redis::aio::ConnectionManager) 47 | pub async fn connection(&self) -> Result<&ConnectionManager, BackendError> { 48 | trace!("Get connection manager"); 49 | let manager = self 50 | .connection 51 | .get_or_try_init(|| { 52 | trace!("Initialize new redis connection manager"); 53 | self.client.get_tokio_connection_manager() 54 | }) 55 | .await 56 | .map_err(Error::from)?; 57 | Ok(manager) 58 | } 59 | } 60 | 61 | /// Part of builder pattern implemetation for RedisBackend actor. 62 | pub struct RedisBackendBuilder { 63 | connection_info: String, 64 | } 65 | 66 | impl Default for RedisBackendBuilder { 67 | fn default() -> Self { 68 | Self { 69 | connection_info: "redis://127.0.0.1/".to_owned(), 70 | } 71 | } 72 | } 73 | 74 | impl RedisBackendBuilder { 75 | /// Set connection info (host, port, database, etc.) for RedisBackend actor. 76 | pub fn server(mut self, connection_info: String) -> Self { 77 | self.connection_info = connection_info; 78 | self 79 | } 80 | 81 | /// Create new instance of Redis backend with passed settings. 82 | pub fn build(self) -> Result { 83 | Ok(RedisBackend { 84 | client: Client::open(self.connection_info)?, 85 | connection: OnceCell::new(), 86 | }) 87 | } 88 | } 89 | 90 | // /// Implementation of Actix Handler for Lock message. 91 | // impl Handler for RedisBackend { 92 | // type Result = ResponseFuture>; 93 | 94 | // fn handle(&mut self, msg: Lock, _: &mut Self::Context) -> Self::Result { 95 | // debug!("Redis Lock: {}", msg.key); 96 | // let mut con = self.connection.clone(); 97 | // Box::pin(async move { 98 | // redis::cmd("SET") 99 | // .arg(format!("lock::{}", msg.key)) 100 | // .arg("") 101 | // .arg("NX") 102 | // .arg("EX") 103 | // .arg(msg.ttl) 104 | // .query_async(&mut con) 105 | // .await 106 | // .map(|res: Option| -> LockStatus { 107 | // if res.is_some() { 108 | // LockStatus::Acquired 109 | // } else { 110 | // LockStatus::Locked 111 | // } 112 | // }) 113 | // .map_err(Error::from) 114 | // .map_err(BackendError::from) 115 | // }) 116 | // } 117 | // } 118 | 119 | #[async_trait] 120 | impl CacheBackend for RedisBackend { 121 | async fn get(&self, key: String) -> BackendResult>> 122 | where 123 | T: CacheableResponse, 124 | ::Cached: serde::de::DeserializeOwned, 125 | { 126 | let mut con = self.connection().await?.clone(); 127 | let result: Option> = redis::cmd("GET") 128 | .arg(key) 129 | .query_async(&mut con) 130 | .await 131 | .map_err(Error::from) 132 | .map_err(BackendError::from)?; 133 | result 134 | .map(|value| JsonSerializer::>::deserialize(value).map_err(BackendError::from)) 135 | .transpose() 136 | } 137 | 138 | async fn delete(&self, key: String) -> BackendResult { 139 | let mut con = self.connection().await?.clone(); 140 | redis::cmd("DEL") 141 | .arg(key) 142 | .query_async(&mut con) 143 | .await 144 | .map(|res| { 145 | if res > 0 { 146 | DeleteStatus::Deleted(res) 147 | } else { 148 | DeleteStatus::Missing 149 | } 150 | }) 151 | .map_err(Error::from) 152 | .map_err(BackendError::from) 153 | } 154 | 155 | async fn set( 156 | &self, 157 | key: String, 158 | value: &CachedValue, 159 | ttl: Option, 160 | ) -> BackendResult<()> 161 | where 162 | T: CacheableResponse, 163 | ::Cached: serde::de::DeserializeOwned, 164 | { 165 | let mut con = self.connection().await?.clone(); 166 | let mut request = redis::cmd("SET"); 167 | let serialized_value = 168 | JsonSerializer::>::serialize(value).map_err(BackendError::from)?; 169 | request.arg(key).arg(serialized_value); 170 | if let Some(ttl) = ttl { 171 | request.arg("EX").arg(ttl); 172 | }; 173 | request 174 | .query_async(&mut con) 175 | .await 176 | .map_err(Error::from) 177 | .map_err(BackendError::from) 178 | } 179 | 180 | async fn start(&self) -> BackendResult<()> { 181 | self.connection().await?; 182 | Ok(()) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /hitbox-redis/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error decplaration and transformation into [BackendError]. 2 | //! 3 | //! [BackendError]: hitbox_backend::BackendError 4 | use hitbox_backend::BackendError; 5 | use redis::RedisError; 6 | 7 | /// Redis backend error declaration. 8 | /// 9 | /// Simply, it's just a wrapper for [redis::RedisError]. 10 | /// 11 | /// [redis::RedisError]: redis::RedisError 12 | #[derive(Debug, thiserror::Error)] 13 | pub enum Error { 14 | /// Wrapper for all kinds redis-rs errors. 15 | #[error("Redis backend error: {0}")] 16 | Redis(#[from] RedisError), 17 | } 18 | 19 | impl From for BackendError { 20 | fn from(error: Error) -> Self { 21 | Self::InternalError(Box::new(error)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /hitbox-redis/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | //! hitbox [Backend] implementation for Redis. 3 | //! 4 | //! This crate uses [redis-rs] as base library for asynchronous interaction with redis nodes. 5 | //! It use one [MultiplexedConnection] for better connection utilisation. 6 | //! 7 | //! [MultiplexedConnection]: redis::aio::MultiplexedConnection 8 | //! [Backend]: hitbox_backend::Backend 9 | //! [redis-rs]: redis-rs::aio 10 | pub mod actor; 11 | pub mod error; 12 | 13 | #[doc(inline)] 14 | pub use crate::actor::{RedisBackend, RedisBackendBuilder}; 15 | -------------------------------------------------------------------------------- /hitbox-redis/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use hitbox_backend::{DeleteStatus, CacheBackend, CacheableResponse, CachePolicy, CachedValue}; 2 | use hitbox_redis::{error::Error, RedisBackend}; 3 | use serde::{Serialize, Deserialize}; 4 | use chrono::Utc; 5 | use test_log::test; 6 | 7 | #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] 8 | struct Test { 9 | a: i32, 10 | b: String, 11 | } 12 | 13 | impl CacheableResponse for Test { 14 | type Cached = Self; 15 | 16 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 17 | CachePolicy::Cacheable(self) 18 | } 19 | 20 | fn from_cached(cached: Self::Cached) -> Self { 21 | cached 22 | } 23 | 24 | fn into_cache_policy(self) -> CachePolicy { 25 | CachePolicy::Cacheable(self) 26 | } 27 | } 28 | 29 | impl Test { 30 | pub fn new() -> Self { 31 | Self { 32 | a: 42, 33 | b: "nope".to_owned(), 34 | } 35 | } 36 | } 37 | 38 | #[test(tokio::test)] 39 | async fn test_rw() -> Result<(), Error> { 40 | tokio::time::pause(); 41 | let backend = RedisBackend::new().unwrap(); 42 | backend.start().await.unwrap(); 43 | let key = "test_key".to_owned(); 44 | let inner = Test::new(); 45 | let value = CachedValue::new(inner.clone(), Utc::now()); 46 | let res = backend.set(key.clone(), &value, None).await; 47 | assert!(res.is_ok()); 48 | let res = backend.get::(key.clone()).await.unwrap(); 49 | assert_eq!(res.unwrap().into_inner(), CachedValue::new(inner, Utc::now()).into_inner()); 50 | let res = backend.delete(key.clone()).await.unwrap(); 51 | assert_eq!(res, DeleteStatus::Deleted(1)); 52 | Ok(()) 53 | } 54 | 55 | // #[actix_rt::test] 56 | // async fn test_set_expired() -> Result<(), Error> { 57 | // let addr = RedisBackend::new().await?.start(); 58 | // let message = Set { 59 | // key: "key_expired".to_owned(), 60 | // value: b"value".to_vec(), 61 | // ttl: Some(1), 62 | // }; 63 | // let res = addr.send(message.clone()).await.unwrap().unwrap(); 64 | // assert_eq!(res, "OK"); 65 | 66 | // let res = addr 67 | // .send(Get { 68 | // key: message.key.clone(), 69 | // }) 70 | // .await; 71 | // assert_eq!(res.unwrap().unwrap(), Some(message.value)); 72 | 73 | // sleep(Duration::from_secs(1)).await; 74 | 75 | // let res = addr 76 | // .send(Get { 77 | // key: message.key.clone(), 78 | // }) 79 | // .await; 80 | // assert_eq!(res.unwrap().unwrap(), None); 81 | // Ok(()) 82 | // } 83 | 84 | // #[actix_rt::test] 85 | // async fn test_delete() -> Result<(), Error> { 86 | // let addr = RedisBackend::new().await?.start(); 87 | // let message = Set { 88 | // key: "another_key".to_owned(), 89 | // value: b"value".to_vec(), 90 | // ttl: Some(1), 91 | // }; 92 | // let res = addr.send(message.clone()).await.unwrap().unwrap(); 93 | // assert_eq!(res, "OK"); 94 | 95 | // let res = addr 96 | // .send(Delete { 97 | // key: message.key.clone(), 98 | // }) 99 | // .await 100 | // .unwrap() 101 | // .unwrap(); 102 | // assert_eq!(res, DeleteStatus::Deleted(1)); 103 | 104 | // sleep(Duration::from_secs(1)).await; 105 | 106 | // let res = addr 107 | // .send(Delete { 108 | // key: message.key.clone(), 109 | // }) 110 | // .await 111 | // .unwrap() 112 | // .unwrap(); 113 | // assert_eq!(res, DeleteStatus::Missing); 114 | // Ok(()) 115 | // } 116 | 117 | // #[actix_rt::test] 118 | // async fn test_lock() -> Result<(), Error> { 119 | // let addr = RedisBackend::new().await?.start(); 120 | // let message = Lock { 121 | // key: "lock_key".to_owned(), 122 | // ttl: 1, 123 | // }; 124 | // let res = addr.send(message.clone()).await.unwrap().unwrap(); 125 | // assert_eq!(res, LockStatus::Acquired); 126 | 127 | // let res = addr.send(message.clone()).await.unwrap().unwrap(); 128 | // assert_eq!(res, LockStatus::Locked); 129 | 130 | // sleep(Duration::from_secs(1)).await; 131 | 132 | // let res = addr.send(message.clone()).await.unwrap().unwrap(); 133 | // assert_eq!(res, LockStatus::Acquired); 134 | // Ok(()) 135 | // } 136 | -------------------------------------------------------------------------------- /hitbox-tokio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hitbox-tokio" 3 | version = "0.1.0" 4 | authors = ["Belousow Makc ", "Andrey Ermilov "] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "Hitbox cache framework tokio integration." 8 | readme = "README.md" 9 | repository = "https://github.com/hit-box/hitbox/" 10 | categories = ["caching", "asynchronous"] 11 | keywords = ["cache", "async", "cache-backend", "hitbox", "tokio"] 12 | 13 | [dependencies] 14 | hitbox = { path = "../hitbox", version = "0.1.0" } 15 | hitbox-backend = { path = "../hitbox-backend", version = "0.1.0" } 16 | hitbox-derive = { path = "../hitbox-derive", version = "0.1.0", optional = true } 17 | hitbox-redis = { path = "../hitbox-redis", version = "0.1.0"} 18 | tokio = { version = "1.17.0", features = ["macros"] } 19 | async-trait = "0.1.52" 20 | tracing = { version = "0.1.32", default-features = false } 21 | serde = "1.0.136" 22 | 23 | [features] 24 | default = [] 25 | derive = ["hitbox-derive"] 26 | 27 | [dev-dependencies] 28 | env_logger = "0.9.0" 29 | test-log = { version = "0.2.8", features = ["trace"] } 30 | tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt"] } 31 | # metrics = ["prometheus", "lazy_static"] 32 | -------------------------------------------------------------------------------- /hitbox-tokio/src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | use hitbox::{ 4 | settings::{CacheSettings, Status}, 5 | states::initial::Initial, 6 | CacheableResponse, CacheError, Cacheable, 7 | }; 8 | use hitbox_backend::{BackendError, CacheBackend}; 9 | use hitbox_redis::RedisBackend; 10 | use serde::de::DeserializeOwned; 11 | 12 | use crate::FutureAdapter; 13 | 14 | pub enum CacheServiceState { 15 | Running, 16 | Stopped, 17 | } 18 | 19 | pub struct Cache { 20 | #[allow(dead_code)] 21 | state: CacheServiceState, 22 | #[allow(dead_code)] 23 | backend: B, 24 | } 25 | 26 | impl Cache 27 | where 28 | B: CacheBackend 29 | { 30 | #[allow(dead_code)] 31 | fn new() -> Result, BackendError> { 32 | Ok(::builder()?.build()) 33 | } 34 | 35 | #[allow(dead_code)] 36 | fn builder() -> Result, BackendError> { 37 | Ok(CacheBuilder { 38 | backend: Some(RedisBackend::new()?), 39 | }) 40 | } 41 | 42 | #[allow(dead_code)] 43 | async fn start(&self) -> Result<(), CacheError> { 44 | Ok(self.backend.start().await?) 45 | } 46 | 47 | #[allow(dead_code)] 48 | async fn process(&self, upstream: F, request: Req) -> Result 49 | where 50 | Req: Cacheable + Send + Sync, 51 | F: Fn(Req) -> ResFuture + Send + Sync, 52 | ResFuture: Future + Send + Sync, 53 | Res: Send + Sync + CacheableResponse + std::fmt::Debug, 54 | ::Cached: DeserializeOwned, 55 | B: CacheBackend + Send + Sync, 56 | { 57 | let adapter = FutureAdapter::new(upstream, request, &self.backend)?; 58 | // let settings = self.settings.clone(); 59 | let settings = CacheSettings { 60 | cache: Status::Enabled, 61 | lock: Status::Disabled, 62 | stale: Status::Disabled, 63 | }; 64 | let initial_state = Initial::new(settings, adapter); 65 | initial_state.transitions().await 66 | } 67 | } 68 | 69 | pub struct CacheBuilder { 70 | backend: Option, 71 | } 72 | 73 | impl CacheBuilder { 74 | // TODO: allowed temporary, remove after the implementation of the feature 75 | #[allow(dead_code)] 76 | fn backend(backend: B) -> CacheBuilder { 77 | CacheBuilder { 78 | backend: Some(backend), 79 | } 80 | } 81 | 82 | fn build(self) -> Cache { 83 | Cache { 84 | state: CacheServiceState::Stopped, 85 | backend: self.backend.unwrap(), 86 | } 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | use hitbox::{CacheError, Cacheable}; 94 | use test_log::test; 95 | 96 | struct Message(i32); 97 | 98 | impl Cacheable for Message { 99 | fn cache_key(&self) -> Result { 100 | Ok("Message".to_owned()) 101 | } 102 | fn cache_key_prefix(&self) -> String { 103 | "Message".to_owned() 104 | } 105 | fn cache_ttl(&self) -> u32 { 106 | 20 107 | } 108 | } 109 | 110 | async fn upstream_fn(message: Message) -> i32 { 111 | message.0 112 | } 113 | 114 | #[test(tokio::test)] 115 | async fn test_cache_process() { 116 | let cache = ::new().unwrap(); 117 | cache.start().await.unwrap(); 118 | let response = cache.process(upstream_fn, Message(42)).await.unwrap(); 119 | dbg!(response); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /hitbox-tokio/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use cache::{Cache, CacheServiceState}; 2 | pub use runtime::FutureAdapter; 3 | mod cache; 4 | mod runtime; 5 | -------------------------------------------------------------------------------- /hitbox-tokio/src/runtime.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, future::Future, marker::PhantomData}; 2 | 3 | use async_trait::async_trait; 4 | 5 | use hitbox::{ 6 | runtime::{AdapterResult, RuntimeAdapter}, 7 | CacheError, CacheState, Cacheable, CacheableResponse, 8 | }; 9 | use hitbox_backend::CacheBackend; 10 | use serde::de::DeserializeOwned; 11 | 12 | pub struct FutureAdapter<'b, In, Out, U, B> 13 | where 14 | In: Cacheable, 15 | { 16 | _response: PhantomData, 17 | backend: &'b B, 18 | upstream: U, 19 | request: Option, 20 | cache_key: String, 21 | cache_ttl: u32, 22 | cache_stale_ttl: u32, 23 | } 24 | 25 | impl<'b, In, Out, U, B> FutureAdapter<'b, In, Out, U, B> 26 | where 27 | In: Cacheable, 28 | { 29 | pub fn new(upstream: U, request: In, backend: &'b B) -> Result { 30 | Ok(Self { 31 | cache_key: request.cache_key()?, 32 | cache_ttl: request.cache_ttl(), 33 | cache_stale_ttl: request.cache_stale_ttl(), 34 | request: Some(request), 35 | upstream, 36 | backend, 37 | _response: PhantomData::default(), 38 | }) 39 | } 40 | } 41 | 42 | #[async_trait] 43 | impl crate::runtime::RuntimeAdapter 44 | for FutureAdapter<'b, In, Out, U, B> 45 | where 46 | Out: CacheableResponse + Send + Sync, 47 | ::Cached: DeserializeOwned, 48 | U: Send + Sync + Fn(In) -> ResFuture, 49 | ResFuture: Future + Send, 50 | In: Cacheable + Send + Sync, 51 | B: CacheBackend + Send + Sync, 52 | { 53 | type UpstreamResult = Out; 54 | async fn update_cache<'a>( 55 | &self, 56 | cached_value: &'a hitbox_backend::CachedValue, 57 | ) -> crate::runtime::AdapterResult<()> { 58 | Ok(self 59 | .backend 60 | .set(self.cache_key.clone(), cached_value, Some(self.cache_ttl)) 61 | .await?) 62 | } 63 | 64 | async fn poll_cache(&self) -> crate::runtime::AdapterResult> { 65 | Ok(self.backend.get(self.cache_key.clone()).await?.into()) 66 | } 67 | 68 | async fn poll_upstream(&mut self) -> crate::runtime::AdapterResult { 69 | let request = self.request.take(); 70 | let request = request.ok_or_else(|| { 71 | CacheError::CacheKeyGenerationError("Request already sent to upstream".to_owned()) 72 | })?; 73 | Ok((self.upstream)(request).await) 74 | } 75 | 76 | fn eviction_settings(&self) -> hitbox_backend::EvictionPolicy { 77 | let ttl_settings = hitbox_backend::TtlSettings { 78 | ttl: self.cache_ttl, 79 | stale_ttl: self.cache_stale_ttl, 80 | }; 81 | hitbox_backend::EvictionPolicy::Ttl(ttl_settings) 82 | } 83 | 84 | fn upstream_name(&self) -> Cow<'static, str> { 85 | std::any::type_name::().into() 86 | } 87 | 88 | fn message_name(&self) -> Cow<'static, str> { 89 | self.cache_key.clone().into() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /hitbox-tower/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "hitbox-tower" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | tower = "0.4" 8 | 9 | 10 | -------------------------------------------------------------------------------- /hitbox-tower/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn it_works() { 5 | let result = 2 + 2; 6 | assert_eq!(result, 4); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /hitbox-tower/src/runtime.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, marker::PhantomData}; 2 | 3 | use async_trait::async_trait; 4 | 5 | use hitbox::{ 6 | runtime::{AdapterResult, RuntimeAdapter}, 7 | CacheError, CacheState, CacheableResponse, Cacheable, 8 | }; 9 | use hitbox_backend::CacheBackend; 10 | use serde::de::DeserializeOwned; 11 | 12 | pub struct TowerAdapter<'b, Request, Response, Service, B> 13 | where 14 | Request: Cacheable, 15 | { 16 | _response: PhantomData, 17 | backend: &'b B, 18 | upstream: Service, 19 | request: Option, 20 | cache_key: String, 21 | cache_ttl: u32, 22 | cache_stale_ttl: u32, 23 | } 24 | 25 | impl<'b, Request, Response, Service, B> FutureAdapter<'b, Request, Response, Service, B> 26 | where 27 | Request: Cacheable, 28 | { 29 | pub fn new(upstream: U, request: Request, backend: &'b B) -> Result { 30 | Ok(Self { 31 | cache_key: request.cache_key()?, 32 | cache_ttl: request.cache_ttl(), 33 | cache_stale_ttl: request.cache_stale_ttl(), 34 | request: Some(request), 35 | upstream, 36 | backend, 37 | _response: PhantomData::default(), 38 | }) 39 | } 40 | } 41 | 42 | #[async_trait] 43 | impl crate::runtime::RuntimeAdapter for FutureAdapter<'b, Request, Response, U, B> 44 | where 45 | Request: Cacheable + Send + Sync, 46 | { 47 | type UpstreamResult = Response; 48 | async fn update_cache<'a>( 49 | &self, 50 | cached_value: &'a hitbox_backend::CachedValue, 51 | ) -> crate::runtime::AdapterResult<()> { 52 | Ok(self 53 | .backend 54 | .set(self.cache_key.clone(), cached_value, Some(self.cache_ttl)) 55 | .await?) 56 | } 57 | 58 | async fn poll_cache(&self) -> crate::runtime::AdapterResult> { 59 | Ok(self.backend.get(self.cache_key.clone()).await?.into()) 60 | } 61 | 62 | async fn poll_upstream(&mut self) -> crate::runtime::AdapterResult { 63 | let request = self.request.take(); 64 | let request = request.ok_or_else(|| { 65 | CacheError::CacheKeyGenerationError("Request already sent to upstream".to_owned()) 66 | })?; 67 | Ok(self.upstream.call(request).await) 68 | } 69 | 70 | fn eviction_settings(&self) -> hitbox_backend::EvictionPolicy { 71 | let ttl_settings = hitbox_backend::TtlSettings { 72 | ttl: self.cache_ttl, 73 | stale_ttl: self.cache_stale_ttl, 74 | }; 75 | hitbox_backend::EvictionPolicy::Ttl(ttl_settings) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /hitbox/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.0] - 2021-05-29 10 | ### Added 11 | - Initial release 12 | -------------------------------------------------------------------------------- /hitbox/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hitbox" 3 | version = "0.1.0" 4 | authors = ["Belousow Makc ", "Andrey Ermilov "] 5 | license = "MIT" 6 | edition = "2021" 7 | description = "Asynchronous caching framework." 8 | readme = "README.md" 9 | repository = "https://github.com/hit-box/hitbox/" 10 | categories = ["caching", "asynchronous"] 11 | keywords = ["cache", "actix", "async", "cache-backend", "hitbox"] 12 | 13 | [dependencies] 14 | actix = "0.13" 15 | hitbox-backend = { path = "../hitbox-backend", version = "0.1.0" } 16 | hitbox-derive = { path = "../hitbox-derive", version = "0.1.0", optional = true } 17 | serde_json = "1" 18 | serde_qs = { version = "0.10", optional = true } 19 | serde = { version = "1", features = ["derive"] } 20 | chrono = { version = "0.4", features = ["serde"] } 21 | thiserror = "1" 22 | metrics = { version = "0.20", optional = true } 23 | lazy_static = { version = "1", optional = true } 24 | tracing = "0.1" 25 | tokio = { version = "1.17.0", features = ["macros"] } 26 | async-trait = "0.1.52" 27 | 28 | [dev-dependencies] 29 | actix_derive = "0.6" 30 | actix-web = "4" 31 | tokio = { version = "1", features = ["macros", "test-util"] } 32 | metrics-util = "0.14" 33 | 34 | [features] 35 | default = [] 36 | 37 | derive = ["hitbox-derive", "serde_qs", "actix/macros"] 38 | metrics = ["dep:metrics", "lazy_static"] 39 | 40 | [package.metadata.docs.rs] 41 | all-features = true 42 | rustdoc-args = ["--cfg", "docsrs"] 43 | -------------------------------------------------------------------------------- /hitbox/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Makc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /hitbox/README.md: -------------------------------------------------------------------------------- 1 | # hitbox 2 | 3 | [![Build status](https://github.com/hit-box/hitbox/actions/workflows/CI.yml/badge.svg)](https://github.com/hit-box/hitbox/actions?query=workflow) 4 | [![Coverage Status](https://codecov.io/gh/hit-box/hitbox/branch/master/graph/badge.svg?token=tgAm8OBLkY)](https://codecov.io/gh/hit-box/hitbox) 5 | 6 | Hitbox is an asynchronous caching framework supporting multiple backends and suitable 7 | for distributed and for single-machine applications. 8 | 9 | ## Framework integrations 10 | - [x] [Actix](https://github.com/hit-box/hitbox/tree/master/hitbox-actix) 11 | - [ ] Actix-Web 12 | 13 | ## Features 14 | - [x] Automatic cache key generation. 15 | - [x] Multiple cache backend implementations: 16 | - [x] Stale cache mechanics. 17 | - [ ] Cache locks for [dogpile effect] preventions. 18 | - [ ] Distributed cache locks. 19 | - [ ] Detailed metrics out of the box. 20 | 21 | ## Backend implementations 22 | - [x] [Redis](https://github.com/hit-box/hitbox/tree/master/hitbox-backend) 23 | - [ ] In-memory backend 24 | 25 | ## Feature flags 26 | * derive - Support for [Cacheable] trait derive macros. 27 | * metrics - Support for metrics. 28 | 29 | ## Restrictions 30 | Default cache key implementation based on serde_qs crate 31 | and have some [restrictions](https://docs.rs/serde_qs/latest/serde_qs/#supported-types). 32 | 33 | ## Documentation 34 | * [API Documentation](https://docs.rs/hitbox/) 35 | * [Examples](https://github.com/hit-box/hitbox/tree/master/examples/examples) 36 | 37 | ## Example 38 | 39 | Dependencies: 40 | 41 | ```toml 42 | [dependencies] 43 | hitbox = "0.1" 44 | ``` 45 | 46 | Code: 47 | 48 | > **_NOTE:_** Default cache key implementation based on serde_qs crate 49 | > and have some [restrictions](https://docs.rs/serde_qs/latest/serde_qs/#supported-types). 50 | 51 | First, you should derive [Cacheable] trait for your struct or enum: 52 | 53 | ```rust 54 | use hitbox::prelude::*; 55 | use serde::{Deserialize, Serialize}; 56 | 57 | #[derive(Cacheable, Serialize)] // With features=["derive"] 58 | struct Ping { 59 | id: i32, 60 | } 61 | ``` 62 | Or implement that trait manually: 63 | 64 | ```rust 65 | use hitbox::{Cacheable, CacheError}; 66 | struct Ping { id: i32 } 67 | impl Cacheable for Ping { 68 | fn cache_key(&self) -> Result { 69 | Ok(format!("{}::{}", self.cache_key_prefix(), self.id)) 70 | } 71 | 72 | fn cache_key_prefix(&self) -> String { "Ping".to_owned() } 73 | } 74 | ``` 75 | 76 | [Cacheable]: https://docs.rs/hitbox/latest/hitbox/cache/trait.Cacheable.html 77 | [CacheableResponse]: https://docs.rs/hitbox/latest/hitbox/response/trait.CacheableResponse.html 78 | [Backend]: https://docs.rs/hitbox/latest/hitbox/dev/trait.Backend.html 79 | [RedisBackend]: https://docs.rs/hitbox-redis/latest/hitbox_redis/struct.RedisBackend.html 80 | [hitbox-actix]: https://docs.rs/hitbox-actix/latest/hitbox_actix/ 81 | [dogpile effect]: https://www.sobstel.org/blog/preventing-dogpile-effect/ 82 | -------------------------------------------------------------------------------- /hitbox/src/cache.rs: -------------------------------------------------------------------------------- 1 | //! Cacheable trait and implementation of cache logic. 2 | 3 | use crate::CacheError; 4 | #[cfg(feature = "derive")] 5 | #[cfg_attr(docsrs, doc(cfg(feature = "derive")))] 6 | pub use hitbox_derive::Cacheable; 7 | 8 | /// Trait describes cache configuration per type that implements this trait. 9 | pub trait Cacheable { 10 | /// Method should return unique identifier for struct object. 11 | /// 12 | /// In cache storage it may prepends with cache version and Upstream name. 13 | /// 14 | /// # Examples 15 | /// 16 | /// ``` 17 | /// use hitbox::cache::Cacheable; 18 | /// use hitbox::CacheError; 19 | /// 20 | /// struct QueryNothing { 21 | /// id: Option, 22 | /// } 23 | /// 24 | /// impl Cacheable for QueryNothing { 25 | /// fn cache_key(&self) -> Result { 26 | /// let key = format!("{}::id::{}", self.cache_key_prefix(), self.id.map_or_else( 27 | /// || "None".to_owned(), |id| id.to_string()) 28 | /// ); 29 | /// Ok(key) 30 | /// } 31 | /// fn cache_key_prefix(&self) -> String { "database::QueryNothing".to_owned() } 32 | /// } 33 | /// 34 | /// let query = QueryNothing { id: Some(1) }; 35 | /// assert_eq!(query.cache_key().unwrap(), "database::QueryNothing::id::1"); 36 | /// let query = QueryNothing { id: None }; 37 | /// assert_eq!(query.cache_key().unwrap(), "database::QueryNothing::id::None"); 38 | /// ``` 39 | fn cache_key(&self) -> Result; 40 | 41 | /// Method return cache key prefix based on message type. 42 | fn cache_key_prefix(&self) -> String; 43 | 44 | /// Describe time-to-live (ttl) value for cache storage in seconds. 45 | /// 46 | /// After that time value will be removed from cache storage. 47 | fn cache_ttl(&self) -> u32 { 48 | 60 49 | } 50 | 51 | /// Describe expire\stale timeout value for cache storage in seconds. 52 | /// 53 | /// After that time cached value marked as stale. 54 | /// 55 | /// ```ignore 56 | /// |__cache_is_valid__|__cache_is_stale__| -> time 57 | /// ^ ^ 58 | /// stale_ttl ttl (cache evicted) 59 | /// ``` 60 | fn cache_stale_ttl(&self) -> u32 { 61 | let ttl = self.cache_ttl(); 62 | let stale_time = 5; 63 | if ttl >= stale_time { 64 | ttl - stale_time 65 | } else { 66 | 0 67 | } 68 | } 69 | 70 | /// Describe current cache version for this type. 71 | fn cache_version(&self) -> u32 { 72 | 0 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod tests { 78 | use super::*; 79 | 80 | struct Message(i32); 81 | 82 | impl Cacheable for Message { 83 | fn cache_key(&self) -> Result { 84 | Ok("Message".to_owned()) 85 | } 86 | fn cache_key_prefix(&self) -> String { 87 | "Message".to_owned() 88 | } 89 | fn cache_ttl(&self) -> u32 { 90 | 2 91 | } 92 | } 93 | 94 | #[test] 95 | fn test_cache_stale_ttl_subtract_overflow() { 96 | let a = Message(42); 97 | assert_eq!(0, a.cache_stale_ttl()); 98 | } 99 | 100 | #[allow(dead_code)] 101 | async fn upstream_fn(message: Message) -> i32 { 102 | message.0 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /hitbox/src/dev/mock_adapter.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use crate::error::CacheError; 3 | use crate::runtime::{AdapterResult, EvictionPolicy, RuntimeAdapter, TtlSettings}; 4 | use crate::value::{CacheState, CachedValue}; 5 | use crate::CacheableResponse; 6 | use chrono::{DateTime, Utc}; 7 | use std::borrow::Cow; 8 | 9 | #[derive(Clone, Debug)] 10 | /// Settings for builder. 11 | enum MockUpstreamState { 12 | Ok(T), 13 | Error, 14 | } 15 | 16 | #[derive(Clone, Debug)] 17 | /// Settings for builder. 18 | enum MockCacheState { 19 | Actual(T), 20 | Stale((T, DateTime)), 21 | Miss, 22 | Error, 23 | } 24 | 25 | #[derive(Clone, Debug)] 26 | /// Mock for Adapter. 27 | pub struct MockAdapter 28 | where 29 | T: Clone, 30 | { 31 | /// Upstream state. 32 | upstream_state: MockUpstreamState, 33 | /// Cache state. 34 | cache_state: MockCacheState, 35 | } 36 | 37 | impl MockAdapter 38 | where 39 | T: Clone, 40 | { 41 | /// Return builder. 42 | pub fn build() -> MockAdapterBuilder { 43 | MockAdapterBuilder { 44 | upstream_state: MockUpstreamState::Error, 45 | cache_state: MockCacheState::Error, 46 | } 47 | } 48 | } 49 | 50 | /// Implement builder pattern. 51 | pub struct MockAdapterBuilder 52 | where 53 | T: Clone, 54 | { 55 | /// Upstream state. 56 | upstream_state: MockUpstreamState, 57 | /// Cache state. 58 | cache_state: MockCacheState, 59 | } 60 | 61 | impl MockAdapterBuilder 62 | where 63 | T: Clone, 64 | { 65 | pub fn with_upstream_value(self, value: T) -> Self { 66 | MockAdapterBuilder { 67 | upstream_state: MockUpstreamState::Ok(value), 68 | ..self 69 | } 70 | } 71 | pub fn with_upstream_error(self) -> Self { 72 | MockAdapterBuilder { 73 | upstream_state: MockUpstreamState::Error, 74 | ..self 75 | } 76 | } 77 | pub fn with_cache_actual(self, value: T) -> Self { 78 | MockAdapterBuilder { 79 | cache_state: MockCacheState::Actual(value), 80 | ..self 81 | } 82 | } 83 | pub fn with_cache_stale(self, value: T, expired: DateTime) -> Self { 84 | MockAdapterBuilder { 85 | cache_state: MockCacheState::Stale((value, expired)), 86 | ..self 87 | } 88 | } 89 | pub fn with_cache_miss(self) -> Self { 90 | MockAdapterBuilder { 91 | cache_state: MockCacheState::Miss, 92 | ..self 93 | } 94 | } 95 | pub fn with_cache_error(self) -> Self { 96 | MockAdapterBuilder { 97 | cache_state: MockCacheState::Error, 98 | ..self 99 | } 100 | } 101 | pub fn finish(self) -> MockAdapter { 102 | MockAdapter { 103 | upstream_state: self.upstream_state, 104 | cache_state: self.cache_state, 105 | } 106 | } 107 | } 108 | 109 | #[async_trait] 110 | impl RuntimeAdapter for MockAdapter 111 | where 112 | T: Clone + Send + Sync + CacheableResponse + 'static, 113 | { 114 | type UpstreamResult = T; 115 | async fn poll_upstream(&mut self) -> AdapterResult { 116 | match self.clone().upstream_state { 117 | MockUpstreamState::Ok(value) => Ok(value), 118 | MockUpstreamState::Error => Err(CacheError::DeserializeError), 119 | } 120 | } 121 | 122 | async fn poll_cache(&self) -> AdapterResult> { 123 | match self.clone().cache_state { 124 | MockCacheState::Actual(value) => Ok(CacheState::Actual(CachedValue::new( 125 | value, 126 | chrono::Utc::now(), 127 | ))), 128 | MockCacheState::Stale(value) => { 129 | Ok(CacheState::Stale(CachedValue::new(value.0, value.1))) 130 | } 131 | MockCacheState::Miss => Ok(CacheState::Miss), 132 | MockCacheState::Error => Err(CacheError::DeserializeError), 133 | } 134 | } 135 | 136 | async fn update_cache<'a>(&self, _: &'a CachedValue) -> AdapterResult<()> { 137 | Ok(()) 138 | } 139 | 140 | fn eviction_settings(&self) -> EvictionPolicy { 141 | EvictionPolicy::Ttl(TtlSettings { 142 | ttl: 0, 143 | stale_ttl: 0, 144 | }) 145 | } 146 | 147 | fn upstream_name(&self) -> Cow<'static, str> { 148 | "MockAdapter".into() 149 | } 150 | 151 | fn message_name(&self) -> Cow<'static, str> { 152 | "MockMessage".into() 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /hitbox/src/dev/mock_backend.rs: -------------------------------------------------------------------------------- 1 | //! Structures and traits for custom backend development and testing process. 2 | pub use hitbox_backend::{Backend, BackendError, Delete, DeleteStatus, Get, Lock, LockStatus, Set}; 3 | 4 | #[doc(hidden)] 5 | /// Mocked backend implementation module. 6 | pub mod backend { 7 | 8 | /* #[derive(Debug, Clone, PartialEq)] 9 | pub enum MockMessage {jj 10 | Get(Get), 11 | Set(Set), 12 | Delete(Delete), 13 | Lock(Lock), 14 | } 15 | 16 | pub struct MockBackend { 17 | pub messages: Vec, 18 | } 19 | 20 | impl MockBackend { 21 | pub fn new() -> Self { 22 | MockBackend::default() 23 | } 24 | } 25 | 26 | impl Default for MockBackend { 27 | fn default() -> Self { 28 | MockBackend { 29 | messages: Vec::with_capacity(10), 30 | } 31 | } 32 | } 33 | 34 | impl Actor for MockBackend { 35 | type Context = Context; 36 | } 37 | 38 | impl Backend for MockBackend { 39 | type Actor = Self; 40 | type Context = Context; 41 | } 42 | 43 | impl Handler for MockBackend { 44 | type Result = ::Result; 45 | 46 | fn handle(&mut self, msg: Get, _: &mut Self::Context) -> Self::Result { 47 | self.messages.push(MockMessage::Get(msg)); 48 | Ok(None) 49 | } 50 | } 51 | 52 | impl Handler for MockBackend { 53 | type Result = ::Result; 54 | 55 | fn handle(&mut self, msg: Set, _: &mut Self::Context) -> Self::Result { 56 | self.messages.push(MockMessage::Set(msg)); 57 | Ok("".to_owned()) 58 | } 59 | } 60 | 61 | impl Handler for MockBackend { 62 | type Result = ::Result; 63 | 64 | fn handle(&mut self, msg: Lock, _: &mut Self::Context) -> Self::Result { 65 | self.messages.push(MockMessage::Lock(msg)); 66 | Ok(LockStatus::Locked) 67 | } 68 | } 69 | 70 | impl Handler for MockBackend { 71 | type Result = ::Result; 72 | 73 | fn handle(&mut self, msg: Delete, _: &mut Self::Context) -> Self::Result { 74 | self.messages.push(MockMessage::Delete(msg)); 75 | Ok(DeleteStatus::Missing) 76 | } 77 | } 78 | 79 | #[derive(Message)] 80 | #[rtype(result = "GetMessagesResult")] 81 | pub struct GetMessages; 82 | 83 | #[derive(MessageResponse)] 84 | pub struct GetMessagesResult(pub Vec); 85 | 86 | impl Handler for MockBackend { 87 | type Result = GetMessagesResult; 88 | 89 | fn handle(&mut self, _msg: GetMessages, _: &mut Self::Context) -> Self::Result { 90 | GetMessagesResult(self.messages.clone()) 91 | } 92 | } 93 | */ 94 | } 95 | -------------------------------------------------------------------------------- /hitbox/src/dev/mod.rs: -------------------------------------------------------------------------------- 1 | //! Structures and traits for custom backend development and testing process. 2 | mod mock_adapter; 3 | pub mod mock_backend; 4 | 5 | pub use hitbox_backend::{ 6 | Backend, BackendError, CacheBackend, Delete, DeleteStatus, Get, Lock, LockStatus, Set, 7 | }; 8 | pub use mock_adapter::MockAdapter; 9 | -------------------------------------------------------------------------------- /hitbox/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error implementation and transformations. 2 | use thiserror::Error; 3 | 4 | /// Base hitbox error. 5 | #[derive(Error, Debug)] 6 | pub enum CacheError { 7 | /// Error described all problems with cache backend interactions. 8 | #[error(transparent)] 9 | BackendError(#[from] hitbox_backend::BackendError), 10 | /// Wrapper for upstream errors. 11 | #[error("Upstream error: {0}")] 12 | UpstreamError(Box), 13 | /// Wrapper for cache data serialization problems. 14 | #[error("Cached data serialization error")] 15 | SerializeError(#[from] serde_json::Error), 16 | /// Wrapper for cache data deserialization problems. 17 | #[error("Cached data deserialization error")] 18 | DeserializeError, 19 | /// Wrapper error for problems with cache key generation. 20 | #[error("Cache key generation error")] 21 | CacheKeyGenerationError(String), 22 | } 23 | -------------------------------------------------------------------------------- /hitbox/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An a implementation and infrastructure for asynchronous and clear cache integration. 2 | //! 3 | //! # A quick tour of hitbox 4 | //! 5 | //! Our crates consist of next main part: 6 | //! * [Cacheable] trait. 7 | //! * [Backend] trait and its implementation ([RedisBackend]). 8 | //! * [CacheableResponse] trait. 9 | //! * Cache implementation. ([hitbox-actix]) 10 | //! 11 | //! ## Features 12 | //! - [x] Automatic cache key generation. 13 | //! - [x] Framework integrations: 14 | //! - [x] Actix ([hitbox-actix]) 15 | //! - [ ] Actix-Web 16 | //! - [x] Multiple cache backend implementations: 17 | //! - [x] [RedisBackend] 18 | //! - [ ] In-memory backend 19 | //! - [x] Stale cache mechanics. 20 | //! - [ ] Cache locks for [dogpile effect] preventions. 21 | //! - [ ] Distributed cache locks. 22 | //! - [ ] Detailed metrics out of the box. 23 | //! 24 | //! ## Feature flags 25 | //! * derive - Support for [Cacheable] trait derive macros. 26 | //! * metrics - Support for metrics. 27 | //! 28 | //! ## Restrictions 29 | //! Default cache key implementation based on serde_qs crate 30 | //! and have some [restrictions](https://docs.rs/serde_qs/latest/serde_qs/#supported-types). 31 | //! 32 | //! ## Example 33 | //! First of all, you should derive [Cacheable] trait for your struct or enum: 34 | //! 35 | //! ```rust 36 | //! use hitbox::prelude::*; 37 | //! use serde::{Deserialize, Serialize}; 38 | //! 39 | //! #[derive(Cacheable, Serialize)] // With features=["derive"] 40 | //! struct Ping { 41 | //! id: i32, 42 | //! } 43 | //! ``` 44 | //! Or implement that trait manually: 45 | //! 46 | //! ```rust 47 | //! # use hitbox::{Cacheable, CacheError}; 48 | //! # struct Ping { id: i32 } 49 | //! impl Cacheable for Ping { 50 | //! fn cache_key(&self) -> Result { 51 | //! Ok(format!("{}::{}", self.cache_key_prefix(), self.id)) 52 | //! } 53 | //! 54 | //! fn cache_key_prefix(&self) -> String { "Ping".to_owned() } 55 | //! } 56 | //! ``` 57 | //! 58 | //! [Cacheable]: crate::Cacheable 59 | //! [CacheableResponse]: crate::CacheableResponse 60 | //! [Backend]: hitbox_backend::Backend 61 | //! [RedisBackend]: https://docs.rs/hitbox_redis/ 62 | //! [hitbox-actix]: https://docs.rs/hitbox_actix/ 63 | //! [dogpile effect]: https://www.sobstel.org/blog/preventing-dogpile-effect/ 64 | #![warn(missing_docs)] 65 | #![cfg_attr(docsrs, feature(doc_cfg))] 66 | 67 | pub mod cache; 68 | pub mod dev; 69 | pub mod error; 70 | #[cfg(feature = "metrics")] 71 | #[cfg_attr(docsrs, doc(cfg(feature = "metrics")))] 72 | pub mod metrics; 73 | pub mod response; 74 | pub mod runtime; 75 | pub mod settings; 76 | pub mod states; 77 | pub mod transition_groups; 78 | pub mod value; 79 | 80 | pub use cache::Cacheable; 81 | pub use error::CacheError; 82 | pub use value::CacheState; 83 | pub use hitbox_backend::{CachedValue, CacheableResponse, CachePolicy}; 84 | 85 | #[cfg(feature = "derive")] 86 | pub use hitbox_derive::CacheableResponse; 87 | 88 | #[cfg(feature = "derive")] 89 | #[doc(hidden)] 90 | pub use serde_qs as hitbox_serializer; 91 | 92 | /// The `hitbox` prelude. 93 | pub mod prelude { 94 | #[cfg(feature = "derive")] 95 | pub use crate::hitbox_serializer; 96 | pub use crate::{CacheError, Cacheable, CacheableResponse}; 97 | } 98 | -------------------------------------------------------------------------------- /hitbox/src/metrics.rs: -------------------------------------------------------------------------------- 1 | //! Metrics declaration and initialization. 2 | use lazy_static::lazy_static; 3 | 4 | lazy_static! { 5 | /// Track number of cache hit events. 6 | pub static ref CACHE_HIT_COUNTER: &'static str = { 7 | metrics::describe_counter!( 8 | "cache_hit_count", 9 | "Total number of cache hit events by message and actor." 10 | ); 11 | "cache_hit_count" 12 | }; 13 | /// Track number of cache miss events. 14 | pub static ref CACHE_MISS_COUNTER: &'static str = { 15 | metrics::describe_counter!( 16 | "cache_miss_count", 17 | "Total number of cache miss events by message and actor." 18 | ); 19 | "cache_miss_count" 20 | }; 21 | /// Track number of cache stale events. 22 | pub static ref CACHE_STALE_COUNTER: &'static str = { 23 | metrics::describe_counter!( 24 | "cache_stale_count", 25 | "Total number of cache stale events by message and actor." 26 | ); 27 | "cache_stale_count" 28 | }; 29 | /// Metric of upstream message handling timings. 30 | pub static ref CACHE_UPSTREAM_HANDLING_HISTOGRAM: &'static str = { 31 | metrics::describe_histogram!( 32 | "cache_upstream_message_handling_duration_seconds", 33 | metrics::Unit::Seconds, 34 | "Cache upstream actor message handling latencies in seconds." 35 | ); 36 | "cache_upstream_message_handling_duration_seconds" 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /hitbox/src/response.rs: -------------------------------------------------------------------------------- 1 | //! Trait and datatypes that describes which data should be store in cache. 2 | //! 3 | //! For more detailed information and examples please see [CacheableResponse 4 | //! documentation](trait.CacheableResponse.html). 5 | use serde::{de::DeserializeOwned, Serialize}; 6 | 7 | #[cfg(feature = "derive")] 8 | #[cfg_attr(docsrs, doc(cfg(feature = "derive")))] 9 | pub use hitbox_derive::CacheableResponse; 10 | 11 | /// This trait determines which types should be cached or not. 12 | pub enum CachePolicy { 13 | /// This variant should be stored in cache backend 14 | Cacheable(T), 15 | /// This variant shouldn't be stored in the cache backend. 16 | NonCacheable(U), 17 | } 18 | 19 | /// This is one of the basic trait which determines should data store in cache backend or not. 20 | /// 21 | /// For primitive types and for user-defined types (with derive macro) 22 | /// cache_policy returns CachePolicy::Cached variant. 23 | /// 24 | /// For `Result` cache_policy method return `CachePolicy::Cacheable(T)` only for data included into 25 | /// `Ok(T)` variant. 26 | /// 27 | /// `Option` is the same with Result: for `Some(T)` returns `CachedPolicy::Cacheable(T)`. `None` are 28 | /// `NonCacheable` by default. 29 | /// 30 | /// ## User defined types: 31 | /// If you want decribe custom caching rules for your own types (for example Enum) you should 32 | /// implement `CacheableResponse` for that type: 33 | /// 34 | /// ```rust 35 | /// use hitbox::{CacheableResponse, CachePolicy}; 36 | /// 37 | /// enum HttpResponse { 38 | /// Ok(String), 39 | /// Unauthorized(i32), 40 | /// } 41 | /// 42 | /// impl CacheableResponse for HttpResponse { 43 | /// type Cached = String; 44 | /// fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 45 | /// match self { 46 | /// HttpResponse::Ok(body) => CachePolicy::Cacheable(body), 47 | /// _ => CachePolicy::NonCacheable(()), 48 | /// } 49 | /// } 50 | /// fn into_cache_policy(self) -> CachePolicy { 51 | /// match self { 52 | /// HttpResponse::Ok(body) => CachePolicy::Cacheable(body), 53 | /// _ => CachePolicy::NonCacheable(self), 54 | /// } 55 | /// } 56 | /// fn from_cached(cached: Self::Cached) -> Self { 57 | /// HttpResponse::Ok(cached) 58 | /// } 59 | /// } 60 | /// ``` 61 | /// In that case only `HttpResponse::Ok` variant will be saved into the cache backend. 62 | /// And all `String`s from the cache backend will be treated as `HttpReponse::Ok(String)` variant. 63 | pub trait CacheableResponse 64 | where 65 | Self: Sized, 66 | Self::Cached: Serialize, 67 | { 68 | /// Describes what type will be stored into the cache backend. 69 | type Cached; 70 | /// Returns cache policy for current type with borrowed data. 71 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()>; 72 | /// Returns cache policy for current type with owned data. 73 | fn into_cache_policy(self) -> CachePolicy; 74 | /// Describes how previously cached data will be transformed into the original type. 75 | fn from_cached(cached: Self::Cached) -> Self; 76 | } 77 | 78 | // There are several CacheableResponse implementations for the most common types. 79 | 80 | /// Implementation `CacheableResponse` for `Result` type. 81 | /// We store to cache only `Ok` variant. 82 | impl CacheableResponse for Result 83 | where 84 | I: Serialize + DeserializeOwned, 85 | { 86 | type Cached = I; 87 | fn into_cache_policy(self) -> CachePolicy { 88 | match self { 89 | Ok(value) => CachePolicy::Cacheable(value), 90 | Err(_) => CachePolicy::NonCacheable(self), 91 | } 92 | } 93 | fn from_cached(cached: Self::Cached) -> Self { 94 | Ok(cached) 95 | } 96 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 97 | match self { 98 | Ok(value) => CachePolicy::Cacheable(value), 99 | Err(_) => CachePolicy::NonCacheable(()), 100 | } 101 | } 102 | } 103 | 104 | /// Implementation `CacheableResponse` for `Option` type. 105 | /// We store to cache only `Some` variant. 106 | impl CacheableResponse for Option 107 | where 108 | I: Serialize + DeserializeOwned, 109 | { 110 | type Cached = I; 111 | fn into_cache_policy(self) -> CachePolicy { 112 | match self { 113 | Some(value) => CachePolicy::Cacheable(value), 114 | None => CachePolicy::NonCacheable(self), 115 | } 116 | } 117 | fn from_cached(cached: Self::Cached) -> Self { 118 | Some(cached) 119 | } 120 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 121 | match self { 122 | Some(value) => CachePolicy::Cacheable(value), 123 | None => CachePolicy::NonCacheable(()), 124 | } 125 | } 126 | } 127 | 128 | /// Implementation `CacheableResponse` for primitive types. 129 | macro_rules! CACHEABLE_RESPONSE_IMPL { 130 | ($type:ty) => { 131 | impl CacheableResponse for $type { 132 | type Cached = $type; 133 | fn into_cache_policy(self) -> CachePolicy { 134 | CachePolicy::Cacheable(self) 135 | } 136 | fn from_cached(cached: Self::Cached) -> Self { 137 | cached 138 | } 139 | fn cache_policy(&self) -> CachePolicy<&Self::Cached, ()> { 140 | CachePolicy::Cacheable(self) 141 | } 142 | } 143 | }; 144 | } 145 | 146 | CACHEABLE_RESPONSE_IMPL!(()); 147 | CACHEABLE_RESPONSE_IMPL!(u8); 148 | CACHEABLE_RESPONSE_IMPL!(u16); 149 | CACHEABLE_RESPONSE_IMPL!(u32); 150 | CACHEABLE_RESPONSE_IMPL!(u64); 151 | CACHEABLE_RESPONSE_IMPL!(usize); 152 | CACHEABLE_RESPONSE_IMPL!(i8); 153 | CACHEABLE_RESPONSE_IMPL!(i16); 154 | CACHEABLE_RESPONSE_IMPL!(i32); 155 | CACHEABLE_RESPONSE_IMPL!(i64); 156 | CACHEABLE_RESPONSE_IMPL!(isize); 157 | CACHEABLE_RESPONSE_IMPL!(f32); 158 | CACHEABLE_RESPONSE_IMPL!(f64); 159 | CACHEABLE_RESPONSE_IMPL!(String); 160 | CACHEABLE_RESPONSE_IMPL!(&'static str); 161 | CACHEABLE_RESPONSE_IMPL!(bool); 162 | -------------------------------------------------------------------------------- /hitbox/src/runtime/adapter.rs: -------------------------------------------------------------------------------- 1 | use crate::{CacheError, CacheState, CacheableResponse, CachedValue}; 2 | use async_trait::async_trait; 3 | use std::borrow::Cow; 4 | pub use hitbox_backend::{EvictionPolicy, TtlSettings}; 5 | 6 | /// Type alias for backend or upstream operations in runtime adapter. 7 | pub type AdapterResult = Result; 8 | 9 | /// Trait describes interaction with cache states (FSM) and cache backend. 10 | /// 11 | /// Main idea of this trait is a separation of FSM transitions logic from 12 | /// specific backend implementation. 13 | #[async_trait] 14 | pub trait RuntimeAdapter 15 | where 16 | Self::UpstreamResult: CacheableResponse, 17 | { 18 | /// Associated type describes the upstream result. 19 | type UpstreamResult; 20 | 21 | /// Send data to upstream and return [`Self::UpstreamResult`] 22 | async fn poll_upstream(&mut self) -> AdapterResult; 23 | 24 | /// Check cache and return current [state](`crate::CacheState`) of cached data. 25 | async fn poll_cache(&self) -> AdapterResult>; 26 | 27 | /// Write or update [`Self::UpstreamResult`] into cache. 28 | async fn update_cache<'a>( 29 | &self, 30 | cached_value: &'a CachedValue, 31 | ) -> AdapterResult<()>; 32 | 33 | /// Returns eviction settings for current cacheable data. 34 | fn eviction_settings(&self) -> EvictionPolicy; 35 | 36 | /// Return upstream name for metrics 37 | fn upstream_name(&self) -> Cow<'static, str>; 38 | 39 | /// Return name of cacheable message for metrics 40 | fn message_name(&self) -> Cow<'static, str>; 41 | } 42 | -------------------------------------------------------------------------------- /hitbox/src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | //! Cache backend runtime agnostic interaction. 2 | mod adapter; 3 | 4 | pub use adapter::{AdapterResult, EvictionPolicy, RuntimeAdapter, TtlSettings}; 5 | -------------------------------------------------------------------------------- /hitbox/src/settings.rs: -------------------------------------------------------------------------------- 1 | //! Cache settings declaration. 2 | 3 | /// Cache setting status state. 4 | #[derive(Debug, Clone)] 5 | pub enum Status { 6 | /// Setting is enabled. 7 | Enabled, 8 | /// Setting is disabled. 9 | Disabled, 10 | } 11 | 12 | /// Describes all awailable cache settings. 13 | #[derive(Debug, Clone)] 14 | pub struct CacheSettings { 15 | /// Enable or disable cache at all. 16 | pub cache: Status, 17 | /// Enable or disable cache stale mechanics. 18 | pub stale: Status, 19 | /// Enable or disable cache lock mechanics. 20 | pub lock: Status, 21 | } 22 | 23 | #[derive(Debug, Clone, PartialEq)] 24 | pub(crate) enum InitialCacheSettings { 25 | Disabled, 26 | Enabled, 27 | Stale, 28 | Lock, 29 | StaleLock, 30 | } 31 | 32 | impl From for InitialCacheSettings { 33 | fn from(settings: CacheSettings) -> Self { 34 | match settings { 35 | CacheSettings { 36 | cache: Status::Disabled, 37 | .. 38 | } => InitialCacheSettings::Disabled, 39 | CacheSettings { 40 | cache: Status::Enabled, 41 | stale: Status::Disabled, 42 | lock: Status::Disabled, 43 | } => InitialCacheSettings::Enabled, 44 | CacheSettings { 45 | cache: Status::Enabled, 46 | stale: Status::Enabled, 47 | lock: Status::Disabled, 48 | } => InitialCacheSettings::Stale, 49 | CacheSettings { 50 | cache: Status::Enabled, 51 | stale: Status::Disabled, 52 | lock: Status::Enabled, 53 | } => InitialCacheSettings::Lock, 54 | CacheSettings { 55 | cache: Status::Enabled, 56 | stale: Status::Enabled, 57 | lock: Status::Enabled, 58 | } => InitialCacheSettings::StaleLock, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_policy/base.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::CacheableResponse; 4 | use crate::runtime::RuntimeAdapter; 5 | use crate::states::cache_policy::{CachePolicyCacheable, CachePolicyNonCacheable}; 6 | 7 | /// Enum represents cacheable and non cacheable states. 8 | /// For example: we don't cache `Err` option from `Result` 9 | /// and cache `Ok`. 10 | /// Please take a look at [CacheableResponse] 11 | pub enum CachePolicyChecked 12 | where 13 | A: RuntimeAdapter, 14 | T: Debug + CacheableResponse, 15 | { 16 | /// This variant should be stored in cache backend. 17 | Cacheable(CachePolicyCacheable), 18 | /// This variant shouldn't be stored in the cache backend. 19 | NonCacheable(CachePolicyNonCacheable), 20 | } 21 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_policy/cacheable.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use tracing::{instrument, trace, warn}; 4 | 5 | use crate::CacheableResponse; 6 | use crate::runtime::RuntimeAdapter; 7 | use crate::states::cache_updated::CacheUpdated; 8 | use crate::CachedValue; 9 | 10 | /// This state is a cacheable variant from [CachePolicyChecked](enum.CachePolicyChecked.html). 11 | pub struct CachePolicyCacheable 12 | where 13 | A: RuntimeAdapter, 14 | T: CacheableResponse, 15 | { 16 | /// Runtime adapter. 17 | pub adapter: A, 18 | /// Value retrieved from upstream. 19 | pub result: T, 20 | } 21 | 22 | /// Required `Debug` implementation to use `instrument` macro. 23 | impl fmt::Debug for CachePolicyCacheable 24 | where 25 | A: RuntimeAdapter, 26 | T: CacheableResponse, 27 | { 28 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 | f.write_str("CachePolicyCacheable") 30 | } 31 | } 32 | 33 | impl CachePolicyCacheable 34 | where 35 | A: RuntimeAdapter, 36 | T: CacheableResponse, 37 | { 38 | #[instrument] 39 | /// Method stores `result` from `CachePolicyCacheable` into cache. 40 | pub async fn update_cache(self) -> CacheUpdated { 41 | let cached_value = CachedValue::from((self.result, self.adapter.eviction_settings())); 42 | let cache_update_result = self.adapter.update_cache(&cached_value).await; 43 | if let Err(error) = cache_update_result { 44 | warn!("Updating cache error: {}", error.to_string()) 45 | }; 46 | trace!("CachePolicyCacheable"); 47 | CacheUpdated { 48 | adapter: self.adapter, 49 | result: cached_value.into_inner(), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_policy/mod.rs: -------------------------------------------------------------------------------- 1 | mod base; 2 | mod cacheable; 3 | mod non_cacheable; 4 | 5 | pub use base::CachePolicyChecked; 6 | pub use cacheable::CachePolicyCacheable; 7 | pub use non_cacheable::CachePolicyNonCacheable; 8 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_policy/non_cacheable.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use tracing::{instrument, trace}; 4 | 5 | use crate::states::finish::Finish; 6 | 7 | /// This state is a non cacheable variant from [CachePolicyChecked](enum.CachePolicyChecked.html). 8 | pub struct CachePolicyNonCacheable { 9 | /// Value retrieved from upstream. 10 | pub result: T, 11 | } 12 | 13 | /// Required `Debug` implementation to use `instrument` macro. 14 | impl fmt::Debug for CachePolicyNonCacheable { 15 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 16 | f.write_str("CachePolicyNonCacheable") 17 | } 18 | } 19 | 20 | impl CachePolicyNonCacheable { 21 | #[instrument] 22 | /// If the value cannot be cached, we have to return it. 23 | pub fn finish(self) -> Finish { 24 | trace!("Finish"); 25 | Finish { 26 | result: Ok(self.result), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_polled/actual.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Debug; 3 | 4 | use tracing::{instrument, trace}; 5 | 6 | use crate::CacheableResponse; 7 | use crate::runtime::RuntimeAdapter; 8 | use crate::states::finish::Finish; 9 | use crate::CachedValue; 10 | #[cfg(feature = "metrics")] 11 | use crate::metrics::CACHE_HIT_COUNTER; 12 | 13 | /// This state is a variant with actual data from [CachePolled](enum.CachePolled.html). 14 | pub struct CachePolledActual 15 | where 16 | A: RuntimeAdapter, 17 | T: CacheableResponse, 18 | { 19 | /// Runtime adapter. 20 | pub adapter: A, 21 | /// Value retrieved from cache. 22 | pub result: CachedValue, 23 | } 24 | 25 | /// Required `Debug` implementation to use `instrument` macro. 26 | impl fmt::Debug for CachePolledActual 27 | where 28 | A: RuntimeAdapter, 29 | T: CacheableResponse, 30 | { 31 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 32 | f.write_str("CachePolledActual") 33 | } 34 | } 35 | 36 | impl CachePolledActual 37 | where 38 | A: RuntimeAdapter, 39 | T: Debug + CacheableResponse, 40 | { 41 | #[instrument] 42 | /// We have to return actual data. 43 | pub fn finish(self) -> Finish { 44 | trace!("Finish"); 45 | #[cfg(feature = "metrics")] 46 | metrics::increment_counter!( 47 | CACHE_HIT_COUNTER.as_ref(), 48 | "upstream" => self.adapter.upstream_name(), 49 | "message" => self.adapter.message_name(), 50 | ); 51 | Finish { 52 | result: Ok(self.result.into_inner()), 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_polled/base.rs: -------------------------------------------------------------------------------- 1 | use crate::CacheableResponse; 2 | use crate::runtime::RuntimeAdapter; 3 | use crate::states::cache_polled::{ 4 | CacheErrorOccurred, CacheMissed, CachePolledActual, CachePolledStale, 5 | }; 6 | 7 | /// Enum represents all possible cache states. 8 | pub enum CachePolled 9 | where 10 | A: RuntimeAdapter, 11 | T: CacheableResponse, 12 | { 13 | /// Cache found, ttl and stale ttl not expired. 14 | Actual(CachePolledActual), 15 | /// Cache found, stale ttl expired. 16 | Stale(CachePolledStale), 17 | /// Cache not found. 18 | Miss(CacheMissed), 19 | /// Unable to get cache from [hitbox_backend::Backend]. 20 | Error(CacheErrorOccurred), 21 | } 22 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_polled/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use tracing::{instrument, trace, warn}; 4 | 5 | use crate::CacheableResponse; 6 | use crate::runtime::RuntimeAdapter; 7 | use crate::states::upstream_polled::{ 8 | UpstreamPolled, UpstreamPolledError, UpstreamPolledSuccessful, 9 | }; 10 | 11 | /// This state is a variant without data from [CachePolled](enum.CachePolled.html). 12 | pub struct CacheErrorOccurred 13 | where 14 | A: RuntimeAdapter, 15 | { 16 | /// Runtime adapter. 17 | pub adapter: A, 18 | } 19 | 20 | /// Required `Debug` implementation to use `instrument` macro. 21 | impl fmt::Debug for CacheErrorOccurred 22 | where 23 | A: RuntimeAdapter, 24 | { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | f.write_str("CacheErrorOccurred") 27 | } 28 | } 29 | 30 | impl CacheErrorOccurred 31 | where 32 | A: RuntimeAdapter, 33 | { 34 | #[instrument] 35 | /// If we can't retrieve data from cache we have to poll upstream. 36 | pub async fn poll_upstream(mut self) -> UpstreamPolled 37 | where 38 | A: RuntimeAdapter, 39 | T: CacheableResponse, 40 | { 41 | match self.adapter.poll_upstream().await { 42 | Ok(result) => { 43 | trace!("UpstreamPolledSuccessful"); 44 | UpstreamPolled::Successful(UpstreamPolledSuccessful { 45 | adapter: self.adapter, 46 | result, 47 | }) 48 | } 49 | Err(error) => { 50 | trace!("UpstreamPolledError"); 51 | warn!("Upstream error {}", error); 52 | UpstreamPolled::Error(UpstreamPolledError { error }) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_polled/missed.rs: -------------------------------------------------------------------------------- 1 | use tracing::{instrument, trace, warn}; 2 | 3 | use crate::CacheableResponse; 4 | use crate::runtime::RuntimeAdapter; 5 | use crate::states::upstream_polled::{ 6 | UpstreamPolled, UpstreamPolledError, UpstreamPolledSuccessful, 7 | }; 8 | #[cfg(feature = "metrics")] 9 | use crate::metrics::{CACHE_MISS_COUNTER, CACHE_UPSTREAM_HANDLING_HISTOGRAM}; 10 | use std::fmt; 11 | 12 | /// This state means that there is no cached data. 13 | pub struct CacheMissed 14 | where 15 | A: RuntimeAdapter, 16 | { 17 | /// Runtime adapter. 18 | pub adapter: A, 19 | } 20 | 21 | /// Required `Debug` implementation to use `instrument` macro. 22 | impl fmt::Debug for CacheMissed 23 | where 24 | A: RuntimeAdapter, 25 | { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | f.write_str("CacheMissed") 28 | } 29 | } 30 | 31 | impl CacheMissed 32 | where 33 | A: RuntimeAdapter, 34 | { 35 | #[instrument] 36 | /// Poll data from upstream. 37 | pub async fn poll_upstream(mut self) -> UpstreamPolled 38 | where 39 | A: RuntimeAdapter, 40 | T: CacheableResponse, 41 | { 42 | #[cfg(feature = "metrics")] 43 | let timer = std::time::Instant::now(); 44 | let upstream_response = self.adapter.poll_upstream().await; 45 | #[cfg(feature = "metrics")] 46 | metrics::histogram!( 47 | CACHE_UPSTREAM_HANDLING_HISTOGRAM.as_ref(), 48 | timer.elapsed().as_millis() as f64 / 1000.0, 49 | "upstream" => self.adapter.upstream_name(), 50 | "message" => self.adapter.message_name(), 51 | ); 52 | #[cfg(feature = "metrics")] 53 | metrics::increment_counter!( 54 | CACHE_MISS_COUNTER.as_ref(), 55 | "upstream" => self.adapter.upstream_name(), 56 | "message" => self.adapter.message_name(), 57 | ); 58 | match upstream_response { 59 | Ok(result) => { 60 | trace!("UpstreamPolledSuccessful"); 61 | UpstreamPolled::Successful(UpstreamPolledSuccessful { 62 | adapter: self.adapter, 63 | result, 64 | }) 65 | } 66 | Err(error) => { 67 | trace!("UpstreamPolledError"); 68 | warn!("Upstream error {}", error); 69 | UpstreamPolled::Error(UpstreamPolledError { error }) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_polled/mod.rs: -------------------------------------------------------------------------------- 1 | mod actual; 2 | mod base; 3 | mod error; 4 | mod missed; 5 | mod stale; 6 | 7 | pub use actual::CachePolledActual; 8 | pub use base::CachePolled; 9 | pub use error::CacheErrorOccurred; 10 | pub use missed::CacheMissed; 11 | pub use stale::CachePolledStale; 12 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_polled/stale.rs: -------------------------------------------------------------------------------- 1 | use tracing::{instrument, trace, warn}; 2 | 3 | #[cfg(feature = "metrics")] 4 | use crate::metrics::{CACHE_HIT_COUNTER, CACHE_STALE_COUNTER}; 5 | use crate::runtime::RuntimeAdapter; 6 | use crate::states::finish::Finish; 7 | use crate::states::upstream_polled::{ 8 | UpstreamPolledErrorStaleRetrieved, UpstreamPolledStaleRetrieved, UpstreamPolledSuccessful, 9 | }; 10 | use crate::CacheableResponse; 11 | use crate::CachedValue; 12 | use std::fmt; 13 | 14 | /// This state means that the data in the cache is stale. 15 | pub struct CachePolledStale 16 | where 17 | A: RuntimeAdapter, 18 | T: CacheableResponse, 19 | { 20 | /// Runtime adapter. 21 | pub adapter: A, 22 | /// Value retrieved from cache. 23 | pub result: CachedValue, 24 | } 25 | 26 | /// Required `Debug` implementation to use `instrument` macro. 27 | impl fmt::Debug for CachePolledStale 28 | where 29 | A: RuntimeAdapter, 30 | T: CacheableResponse, 31 | { 32 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 33 | f.write_str("CachePolledStale") 34 | } 35 | } 36 | 37 | impl CachePolledStale 38 | where 39 | A: RuntimeAdapter, 40 | T: fmt::Debug + CacheableResponse, 41 | { 42 | #[instrument] 43 | /// Poll data from upstream. 44 | pub async fn poll_upstream(mut self) -> UpstreamPolledStaleRetrieved 45 | where 46 | A: RuntimeAdapter, 47 | { 48 | let upstream_response = self.adapter.poll_upstream().await; 49 | #[cfg(feature = "metrics")] 50 | metrics::increment_counter!( 51 | CACHE_HIT_COUNTER.as_ref(), 52 | "upstream" => self.adapter.upstream_name(), 53 | "message" => self.adapter.message_name(), 54 | ); 55 | #[cfg(feature = "metrics")] 56 | metrics::increment_counter!( 57 | CACHE_STALE_COUNTER.as_ref(), 58 | "upstream" => self.adapter.upstream_name(), 59 | "message" => self.adapter.message_name(), 60 | ); 61 | match upstream_response { 62 | Ok(result) => { 63 | trace!("UpstreamPolledSuccessful"); 64 | UpstreamPolledStaleRetrieved::Successful(UpstreamPolledSuccessful { 65 | adapter: self.adapter, 66 | result, 67 | }) 68 | } 69 | Err(error) => { 70 | trace!("UpstreamPolledErrorStaleRetrieved"); 71 | warn!("Upstream error {}", error); 72 | UpstreamPolledStaleRetrieved::Error(UpstreamPolledErrorStaleRetrieved { 73 | error, 74 | result: self.result.into_inner(), 75 | }) 76 | } 77 | } 78 | } 79 | 80 | #[instrument] 81 | /// Return data with Finish state. 82 | pub fn finish(self) -> Finish { 83 | trace!("Finish"); 84 | Finish { 85 | result: Ok(self.result.into_inner()), 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_updated/base.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Debug; 3 | 4 | use tracing::{instrument, trace}; 5 | 6 | use crate::runtime::RuntimeAdapter; 7 | use crate::states::finish::Finish; 8 | 9 | /// State after transition `update_cache`. 10 | /// 11 | /// The transition to this state doesn't depend on the success of the cache update operation. 12 | pub struct CacheUpdated 13 | where 14 | A: RuntimeAdapter, 15 | { 16 | /// Runtime adapter. 17 | pub adapter: A, 18 | /// Value retrieved from cache or from upstream. 19 | pub result: T, 20 | } 21 | 22 | /// Required `Debug` implementation to use `instrument` macro. 23 | impl fmt::Debug for CacheUpdated 24 | where 25 | A: RuntimeAdapter, 26 | { 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | f.write_str("CacheUpdated") 29 | } 30 | } 31 | 32 | impl CacheUpdated 33 | where 34 | A: RuntimeAdapter, 35 | T: Debug, 36 | { 37 | #[instrument] 38 | /// We have to return actual data. 39 | pub fn finish(self) -> Finish { 40 | trace!("Finish"); 41 | Finish { 42 | result: Ok(self.result), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /hitbox/src/states/cache_updated/mod.rs: -------------------------------------------------------------------------------- 1 | mod base; 2 | pub use base::CacheUpdated; 3 | -------------------------------------------------------------------------------- /hitbox/src/states/finish/base.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use tracing::{instrument, trace}; 4 | 5 | use crate::CacheError; 6 | 7 | /// Finite state. 8 | pub struct Finish { 9 | /// The field represents the return value. 10 | pub result: Result, 11 | } 12 | 13 | /// Required `Debug` implementation to use `instrument` macro. 14 | impl fmt::Debug for Finish { 15 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 16 | f.write_str("Finish") 17 | } 18 | } 19 | 20 | impl Finish { 21 | #[instrument] 22 | /// Return inner value `result`. 23 | pub fn result(self) -> Result { 24 | trace!("Result"); 25 | self.result 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /hitbox/src/states/finish/mod.rs: -------------------------------------------------------------------------------- 1 | mod base; 2 | pub use base::Finish; 3 | -------------------------------------------------------------------------------- /hitbox/src/states/initial/base.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use tracing::{instrument, trace, warn}; 4 | 5 | use crate::CacheableResponse; 6 | use crate::runtime::RuntimeAdapter; 7 | use crate::settings::{CacheSettings, InitialCacheSettings}; 8 | use crate::states::cache_polled::{ 9 | CacheErrorOccurred, CacheMissed, CachePolled, CachePolledActual, CachePolledStale, 10 | }; 11 | use crate::states::upstream_polled::{ 12 | UpstreamPolled, UpstreamPolledError, UpstreamPolledSuccessful, 13 | }; 14 | use crate::transition_groups::{only_cache, stale, upstream}; 15 | use crate::{CacheError, CacheState}; 16 | 17 | /// Initial state. 18 | pub struct Initial 19 | where 20 | A: RuntimeAdapter, 21 | { 22 | /// Base point for deciding what type of transition will be used. 23 | settings: InitialCacheSettings, 24 | /// Runtime adapter. 25 | pub adapter: A, 26 | } 27 | 28 | /// Required `Debug` implementation to use `instrument` macro. 29 | impl fmt::Debug for Initial 30 | where 31 | A: RuntimeAdapter, 32 | { 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | f.write_str("Initial") 35 | } 36 | } 37 | 38 | impl Initial 39 | where 40 | A: RuntimeAdapter, 41 | { 42 | /// Create new Initial state. 43 | pub fn new(settings: CacheSettings, adapter: A) -> Self { 44 | Self { 45 | settings: InitialCacheSettings::from(settings), 46 | adapter, 47 | } 48 | } 49 | 50 | #[instrument] 51 | /// Retrieve value from upstream. 52 | pub async fn poll_upstream(mut self) -> UpstreamPolled 53 | where 54 | A: RuntimeAdapter, 55 | T: CacheableResponse, 56 | { 57 | match self.adapter.poll_upstream().await { 58 | Ok(result) => { 59 | trace!("UpstreamPolledSuccessful"); 60 | UpstreamPolled::Successful(UpstreamPolledSuccessful { 61 | adapter: self.adapter, 62 | result, 63 | }) 64 | } 65 | Err(error) => { 66 | trace!("UpstreamPolledError"); 67 | warn!("Upstream error {}", error); 68 | UpstreamPolled::Error(UpstreamPolledError { error }) 69 | } 70 | } 71 | } 72 | 73 | #[instrument] 74 | /// Retrieve value from cache. 75 | pub async fn poll_cache(self) -> CachePolled 76 | where 77 | A: RuntimeAdapter, 78 | T: CacheableResponse, 79 | { 80 | let cache_result: Result, CacheError> = self.adapter.poll_cache().await; 81 | match cache_result { 82 | Ok(value) => match value { 83 | CacheState::Actual(result) => { 84 | trace!("CachePolledActual"); 85 | CachePolled::Actual(CachePolledActual { 86 | adapter: self.adapter, 87 | result, 88 | }) 89 | } 90 | CacheState::Stale(result) => { 91 | trace!("CachePolledStale"); 92 | CachePolled::Stale(CachePolledStale { 93 | adapter: self.adapter, 94 | result, 95 | }) 96 | } 97 | CacheState::Miss => { 98 | trace!("CacheMissed"); 99 | CachePolled::Miss(CacheMissed { 100 | adapter: self.adapter, 101 | }) 102 | } 103 | }, 104 | Err(error) => { 105 | trace!("CacheErrorOccurred"); 106 | warn!("Cache error {}", error); 107 | CachePolled::Error(CacheErrorOccurred { 108 | adapter: self.adapter, 109 | }) 110 | } 111 | } 112 | } 113 | 114 | /// Run all transitions from Initial state to Result. 115 | pub async fn transitions(self) -> Result 116 | where 117 | A: RuntimeAdapter, 118 | T: CacheableResponse + fmt::Debug, 119 | { 120 | match self.settings { 121 | InitialCacheSettings::Disabled => upstream::transition(self).await.result(), 122 | InitialCacheSettings::Enabled => only_cache::transition(self).await.result(), 123 | InitialCacheSettings::Stale => stale::transition(self).await.result(), 124 | InitialCacheSettings::Lock => unimplemented!(), 125 | InitialCacheSettings::StaleLock => unimplemented!(), 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /hitbox/src/states/initial/mod.rs: -------------------------------------------------------------------------------- 1 | mod base; 2 | pub use base::Initial; 3 | -------------------------------------------------------------------------------- /hitbox/src/states/mod.rs: -------------------------------------------------------------------------------- 1 | //! The set of states of the Hitbox finite state machine. 2 | //! 3 | //! # Motivation 4 | //! Different caching options are suitable for different load profiles. 5 | //! We devised Hitbox as a one-stop solution and therefore provide several different caching policies. 6 | //! They are all described in [CacheSettings]. 7 | //! The behavior of Hitbox depends on which caching policy option is selected, which means that 8 | //! all Hitbox actions depend on the [Initial] state. 9 | //! To implement caching logic that depends on the initial state, we chose a finite state machine. 10 | //! State machine transitions are defined by the [transition_groups] module and correspond 11 | //! to the [CacheSettings] options. 12 | //! 13 | //! States shouldn't depend on specific implementations of the cache backend and upstream, 14 | //! and to interact with them, many states contain an `adapter` field. 15 | //! The adapter provides the necessary operations such as getting the cache by key, 16 | //! saving the cache, etc. 17 | //! 18 | //! # States 19 | //! The states module is a set of states of the Hitbox finite state machine. 20 | //! 21 | //! [transition_groups]: crate::transition_groups 22 | //! [CacheSettings]: crate::settings::CacheSettings 23 | //! [Initial]: crate::states::initial::Initial 24 | 25 | /// Defines whether the result returned from upstream will be cached. 26 | pub mod cache_policy; 27 | /// Defines the state of the data that was retrieved from the cache. 28 | pub mod cache_polled; 29 | /// The state that Hitbox enters after updating the cache. 30 | pub mod cache_updated; 31 | /// Final state of Hitbox. 32 | pub mod finish; 33 | /// Initial state of Hitbox. 34 | pub mod initial; 35 | /// Defines the state of the data that was retrieved from the upstream. 36 | pub mod upstream_polled; 37 | -------------------------------------------------------------------------------- /hitbox/src/states/upstream_polled/base.rs: -------------------------------------------------------------------------------- 1 | use crate::CacheableResponse; 2 | use crate::runtime::RuntimeAdapter; 3 | use crate::states::upstream_polled::{ 4 | UpstreamPolledError, UpstreamPolledErrorStaleRetrieved, UpstreamPolledSuccessful, 5 | }; 6 | 7 | /// Enum represents all possible upstream states. 8 | pub enum UpstreamPolled 9 | where 10 | A: RuntimeAdapter, 11 | T: CacheableResponse, 12 | { 13 | /// Value successful polled. 14 | Successful(UpstreamPolledSuccessful), 15 | /// Error happened. 16 | Error(UpstreamPolledError), 17 | } 18 | 19 | /// Enum represents all possible upstream states when a stale value was retrieved. 20 | pub enum UpstreamPolledStaleRetrieved 21 | where 22 | A: RuntimeAdapter, 23 | T: CacheableResponse, 24 | { 25 | /// Value successful polled. 26 | Successful(UpstreamPolledSuccessful), 27 | /// Error happened. 28 | Error(UpstreamPolledErrorStaleRetrieved), 29 | } 30 | -------------------------------------------------------------------------------- /hitbox/src/states/upstream_polled/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use tracing::{instrument, trace}; 4 | 5 | use crate::states::finish::Finish; 6 | use crate::CacheError; 7 | 8 | /// This state is a variant without data. 9 | pub struct UpstreamPolledError { 10 | /// Returned error. 11 | pub error: CacheError, 12 | } 13 | 14 | /// Required `Debug` implementation to use `instrument` macro. 15 | impl fmt::Debug for UpstreamPolledError { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | f.write_str("UpstreamPolledError") 18 | } 19 | } 20 | 21 | impl UpstreamPolledError { 22 | #[instrument] 23 | /// Upstream returns an error. FSM goes to Finish. 24 | pub fn finish(self) -> Finish { 25 | trace!("Finish"); 26 | Finish { 27 | result: Err(self.error), 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /hitbox/src/states/upstream_polled/error_with_stale.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use tracing::{instrument, trace}; 4 | 5 | use crate::states::finish::Finish; 6 | use crate::CacheError; 7 | 8 | /// Stale value was retrieved and poll upstream returned an error. 9 | pub struct UpstreamPolledErrorStaleRetrieved { 10 | /// Returned error. 11 | pub error: CacheError, 12 | /// Stale value retrieved from cache. 13 | pub result: T, 14 | } 15 | 16 | /// Required `Debug` implementation to use `instrument` macro. 17 | impl fmt::Debug for UpstreamPolledErrorStaleRetrieved { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | f.write_str("UpstreamPolledErrorStaleRetrieved") 20 | } 21 | } 22 | 23 | impl UpstreamPolledErrorStaleRetrieved { 24 | #[instrument] 25 | /// Upstream returns an error. FSM goes to Finish. 26 | pub fn finish(self) -> Finish { 27 | trace!("Finish"); 28 | Finish { 29 | result: Ok(self.result), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /hitbox/src/states/upstream_polled/mod.rs: -------------------------------------------------------------------------------- 1 | mod base; 2 | mod error; 3 | mod error_with_stale; 4 | mod successful; 5 | 6 | pub use base::{UpstreamPolled, UpstreamPolledStaleRetrieved}; 7 | pub use error::UpstreamPolledError; 8 | pub use error_with_stale::UpstreamPolledErrorStaleRetrieved; 9 | pub use successful::UpstreamPolledSuccessful; 10 | -------------------------------------------------------------------------------- /hitbox/src/states/upstream_polled/successful.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Debug; 3 | 4 | use tracing::{instrument, trace, warn}; 5 | 6 | use crate::{CachePolicy, CacheableResponse}; 7 | use crate::runtime::RuntimeAdapter; 8 | use crate::states::cache_policy::{ 9 | CachePolicyCacheable, CachePolicyChecked, CachePolicyNonCacheable, 10 | }; 11 | use crate::states::cache_updated::CacheUpdated; 12 | use crate::states::finish::Finish; 13 | use crate::CachedValue; 14 | 15 | /// Upstream returns value. 16 | pub struct UpstreamPolledSuccessful 17 | where 18 | A: RuntimeAdapter, 19 | T: CacheableResponse, 20 | { 21 | /// Runtime adapter. 22 | pub adapter: A, 23 | /// Value from upstream. 24 | pub result: T, 25 | } 26 | 27 | /// Required `Debug` implementation to use `instrument` macro. 28 | impl fmt::Debug for UpstreamPolledSuccessful 29 | where 30 | A: RuntimeAdapter, 31 | T: CacheableResponse, 32 | { 33 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 34 | f.write_str("UpstreamPolledSuccessful") 35 | } 36 | } 37 | 38 | impl UpstreamPolledSuccessful 39 | where 40 | A: RuntimeAdapter, 41 | T: Debug + CacheableResponse, 42 | { 43 | #[instrument] 44 | /// Return retrieved value. 45 | pub fn finish(self) -> Finish { 46 | trace!("Finish"); 47 | Finish { 48 | result: Ok(self.result), 49 | } 50 | } 51 | 52 | #[instrument] 53 | /// Check if the value can be cached. 54 | pub fn check_cache_policy(self) -> CachePolicyChecked { 55 | match self.result.cache_policy() { 56 | CachePolicy::Cacheable(_) => { 57 | trace!("CachePolicyCacheable"); 58 | CachePolicyChecked::Cacheable(CachePolicyCacheable { 59 | result: self.result, 60 | adapter: self.adapter, 61 | }) 62 | } 63 | CachePolicy::NonCacheable(_) => { 64 | trace!("CachePolicyNonCacheable"); 65 | CachePolicyChecked::NonCacheable(CachePolicyNonCacheable { 66 | result: self.result, 67 | }) 68 | } 69 | } 70 | } 71 | 72 | #[instrument] 73 | /// Store the value in the cache. 74 | pub async fn update_cache(self) -> CacheUpdated { 75 | let cached_value = CachedValue::from((self.result, self.adapter.eviction_settings())); 76 | let cache_update_result = self.adapter.update_cache(&cached_value).await; 77 | if let Err(error) = cache_update_result { 78 | warn!("Updating cache error: {}", error.to_string()) 79 | }; 80 | trace!("CacheUpdated"); 81 | CacheUpdated { 82 | adapter: self.adapter, 83 | result: cached_value.into_inner(), 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /hitbox/src/transition_groups/mod.rs: -------------------------------------------------------------------------------- 1 | //! Module that implements transitions between states of the Hitbox finite state machine. 2 | /// transition [Transition diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/hit-box/hitbox/master/documentation/transitions/only_cache.puml) 3 | pub mod only_cache; 4 | /// transition [Transition diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/hit-box/hitbox/master/documentation/transitions/stale.puml) 5 | pub mod stale; 6 | /// transition [Transition diagram](http://www.plantuml.com/plantuml/proxy?src=https://raw.githubusercontent.com/hit-box/hitbox/master/documentation/transitions/upstream.puml) 7 | pub mod upstream; 8 | -------------------------------------------------------------------------------- /hitbox/src/transition_groups/only_cache.rs: -------------------------------------------------------------------------------- 1 | use crate::CacheableResponse; 2 | use crate::runtime::RuntimeAdapter; 3 | use crate::states::cache_policy::CachePolicyChecked; 4 | use crate::states::cache_polled::CachePolled; 5 | use crate::states::finish::Finish; 6 | use crate::states::initial::Initial; 7 | use crate::states::upstream_polled::UpstreamPolled; 8 | use std::fmt::Debug; 9 | 10 | /// Transition for `InitialCacheSettings::Enabled` option. 11 | pub async fn transition(state: Initial) -> Finish 12 | where 13 | A: RuntimeAdapter, 14 | A: RuntimeAdapter, 15 | T: Debug + CacheableResponse, 16 | { 17 | match state.poll_cache().await { 18 | CachePolled::Actual(state) => state.finish(), 19 | CachePolled::Stale(state) => state.finish(), 20 | CachePolled::Miss(state) => match state.poll_upstream().await { 21 | UpstreamPolled::Successful(state) => match state.check_cache_policy() { 22 | CachePolicyChecked::Cacheable(state) => state.update_cache().await.finish(), 23 | CachePolicyChecked::NonCacheable(state) => state.finish(), 24 | }, 25 | UpstreamPolled::Error(error) => error.finish(), 26 | }, 27 | CachePolled::Error(state) => match state.poll_upstream().await { 28 | UpstreamPolled::Successful(state) => state.update_cache().await.finish(), 29 | UpstreamPolled::Error(error) => error.finish(), 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /hitbox/src/transition_groups/stale.rs: -------------------------------------------------------------------------------- 1 | use crate::CacheableResponse; 2 | use crate::runtime::RuntimeAdapter; 3 | use crate::states::cache_policy::CachePolicyChecked; 4 | use crate::states::cache_polled::CachePolled; 5 | use crate::states::finish::Finish; 6 | use crate::states::initial::Initial; 7 | use crate::states::upstream_polled::{UpstreamPolled, UpstreamPolledStaleRetrieved}; 8 | use std::fmt::Debug; 9 | 10 | /// Transition for `InitialCacheSettings::CacheStale` option. 11 | pub async fn transition(state: Initial) -> Finish 12 | where 13 | A: RuntimeAdapter, 14 | A: RuntimeAdapter, 15 | T: Debug + CacheableResponse, 16 | { 17 | match state.poll_cache().await { 18 | CachePolled::Actual(state) => state.finish(), 19 | CachePolled::Stale(state) => match state.poll_upstream().await { 20 | UpstreamPolledStaleRetrieved::Successful(state) => match state.check_cache_policy() { 21 | CachePolicyChecked::Cacheable(state) => state.update_cache().await.finish(), 22 | CachePolicyChecked::NonCacheable(state) => state.finish(), 23 | }, 24 | UpstreamPolledStaleRetrieved::Error(state) => state.finish(), 25 | }, 26 | CachePolled::Miss(state) => match state.poll_upstream().await { 27 | UpstreamPolled::Successful(state) => match state.check_cache_policy() { 28 | CachePolicyChecked::Cacheable(state) => state.update_cache().await.finish(), 29 | CachePolicyChecked::NonCacheable(state) => state.finish(), 30 | }, 31 | UpstreamPolled::Error(error) => error.finish(), 32 | }, 33 | CachePolled::Error(state) => match state.poll_upstream().await { 34 | UpstreamPolled::Successful(state) => state.update_cache().await.finish(), 35 | UpstreamPolled::Error(error) => error.finish(), 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /hitbox/src/transition_groups/upstream.rs: -------------------------------------------------------------------------------- 1 | use crate::CacheableResponse; 2 | use crate::runtime::RuntimeAdapter; 3 | use crate::states::finish::Finish; 4 | use crate::states::initial::Initial; 5 | use crate::states::upstream_polled::UpstreamPolled; 6 | use std::fmt::Debug; 7 | 8 | /// Transition for `InitialCacheSettings::CacheDisabled` option. 9 | pub async fn transition(state: Initial) -> Finish 10 | where 11 | A: RuntimeAdapter, 12 | A: RuntimeAdapter, 13 | T: Debug + CacheableResponse, 14 | { 15 | match state.poll_upstream().await { 16 | UpstreamPolled::Successful(state) => state.finish(), 17 | UpstreamPolled::Error(error) => error.finish(), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /hitbox/src/value.rs: -------------------------------------------------------------------------------- 1 | //! Cached data representation and wrappers. 2 | use chrono::{DateTime, Utc}; 3 | use serde::Serialize; 4 | 5 | /*/// This struct wraps and represents cached data. 6 | /// 7 | /// The expired field defines the UTC data expiration time. 8 | /// Used for detection of stale data. 9 | #[derive(Deserialize)] 10 | pub struct CachedValue { 11 | data: T, 12 | expired: DateTime, 13 | }*/ 14 | pub use crate::CachedValue; 15 | 16 | #[derive(Serialize)] 17 | struct CachedInnerValue<'a, U> 18 | where 19 | U: Serialize, 20 | { 21 | data: &'a U, 22 | expired: DateTime, 23 | } 24 | 25 | /*impl CachedValue 26 | where 27 | T: CacheableResponse, 28 | ::Cached: Serialize, 29 | { 30 | /// Creates new CachedValue 31 | pub fn new(data: T, expired: DateTime) -> Self { 32 | Self { data, expired } 33 | } 34 | 35 | fn from_inner(cached_data: CachedValue) -> Self 36 | where 37 | T: CacheableResponse, 38 | { 39 | Self { 40 | data: T::from_cached(cached_data.data), 41 | expired: cached_data.expired, 42 | } 43 | } 44 | 45 | /// Serialize CachedValue into bytes. 46 | pub fn serialize(&self) -> Result, CacheError> { 47 | match self.data.cache_policy() { 48 | CachePolicy::Cacheable(cache_value) => serde_json::to_vec(&CachedInnerValue { 49 | data: cache_value, 50 | expired: self.expired, 51 | }) 52 | .map_err(CacheError::from), 53 | CachePolicy::NonCacheable(_) => Err(CacheError::DeserializeError), 54 | } 55 | } 56 | 57 | /// Returns original data from CachedValue 58 | pub fn into_inner(self) -> T { 59 | self.data 60 | } 61 | }*/ 62 | 63 | /// Represents cuurent state of cached data. 64 | pub enum CacheState { 65 | /// Cached data is exists and actual. 66 | Actual(CachedValue), 67 | /// Cached data is exists and stale. 68 | Stale(CachedValue), 69 | /// Cached data does not exists. 70 | Miss, 71 | } 72 | 73 | /*impl CacheState 74 | where 75 | T: CacheableResponse, 76 | U: DeserializeOwned + Serialize, 77 | { 78 | /// Deserialize optional vector of bytes and check the actuality. 79 | pub fn from_bytes(bytes: Option<&Vec>) -> Result { 80 | let cached_data = bytes 81 | .map(|bytes| serde_json::from_slice::>(bytes)) 82 | .transpose()?; 83 | Ok(Self::from(cached_data)) 84 | } 85 | }*/ 86 | 87 | impl From>> for CacheState 88 | { 89 | fn from(cached_value: Option>) -> Self { 90 | match cached_value { 91 | Some(value) => { 92 | if value.expired < Utc::now() { 93 | Self::Stale(value) 94 | } else { 95 | Self::Actual(value) 96 | } 97 | } 98 | None => Self::Miss, 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /hitbox/tests/cache_key.rs: -------------------------------------------------------------------------------- 1 | use hitbox::prelude::*; 2 | use serde::Serialize; 3 | 4 | #[derive(Serialize)] 5 | struct Message { 6 | id: i32, 7 | alias: String, 8 | } 9 | 10 | impl Cacheable for Message { 11 | fn cache_key(&self) -> Result { 12 | Ok("overloaded cache key".to_owned()) 13 | } 14 | 15 | fn cache_key_prefix(&self) -> String { 16 | "Message".to_owned() 17 | } 18 | } 19 | 20 | #[test] 21 | fn test_cache_key() { 22 | let message = Message { 23 | id: 42, 24 | alias: "test".to_owned(), 25 | }; 26 | assert_eq!( 27 | message.cache_key().unwrap().as_str(), 28 | "overloaded cache key" 29 | ); 30 | assert_eq!(message.cache_key_prefix().as_str(), "Message"); 31 | let message = Message { 32 | id: 28, 33 | alias: "cow level".to_owned(), 34 | }; 35 | assert_eq!( 36 | message.cache_key().unwrap().as_str(), 37 | "overloaded cache key" 38 | ); 39 | assert_eq!(message.cache_key_prefix().as_str(), "Message"); 40 | } 41 | -------------------------------------------------------------------------------- /hitbox/tests/cacheable_derive.rs: -------------------------------------------------------------------------------- 1 | use hitbox::prelude::*; 2 | use serde::Serialize; 3 | 4 | #[derive(Cacheable, Serialize)] 5 | struct Message { 6 | id: i32, 7 | alias: String, 8 | } 9 | 10 | #[test] 11 | fn test_all_keys() { 12 | let message = Message { 13 | id: 0, 14 | alias: "alias".to_string(), 15 | }; 16 | assert_eq!( 17 | message.cache_key().unwrap(), 18 | "Message::v0::id=0&alias=alias".to_string() 19 | ); 20 | } 21 | 22 | #[derive(Cacheable, Serialize)] 23 | #[allow(dead_code)] 24 | struct PartialSerializeMessage { 25 | id: i32, 26 | #[serde(skip_serializing)] 27 | alias: String, 28 | } 29 | 30 | #[test] 31 | fn test_partial() { 32 | let message = PartialSerializeMessage { 33 | id: 0, 34 | alias: "alias".to_string(), 35 | }; 36 | assert_eq!( 37 | message.cache_key().unwrap(), 38 | "PartialSerializeMessage::v0::id=0".to_string() 39 | ); 40 | } 41 | 42 | #[derive(Cacheable, Serialize)] 43 | struct VecMessage { 44 | id: Vec, 45 | } 46 | 47 | #[test] 48 | fn test_message_with_vector() { 49 | let message = VecMessage { id: vec![1, 2, 3] }; 50 | assert_eq!( 51 | message.cache_key().unwrap(), 52 | "VecMessage::v0::id[0]=1&id[1]=2&id[2]=3".to_string() 53 | ); 54 | } 55 | 56 | #[derive(Serialize)] 57 | enum MessageType { 58 | External, 59 | } 60 | 61 | #[derive(Cacheable, Serialize)] 62 | struct EnumMessage { 63 | message_type: MessageType, 64 | } 65 | 66 | #[test] 67 | fn test_message_with_enum() { 68 | let message = EnumMessage { 69 | message_type: MessageType::External, 70 | }; 71 | assert_eq!( 72 | message.cache_key().unwrap(), 73 | "EnumMessage::v0::message_type=External".to_string() 74 | ); 75 | } 76 | 77 | #[derive(Serialize)] 78 | enum TupleMessageType { 79 | External(i32), 80 | } 81 | 82 | #[derive(Cacheable, Serialize)] 83 | struct TupleEnumMessage { 84 | message_type: TupleMessageType, 85 | } 86 | 87 | #[test] 88 | fn test_message_with_enum_tuple() { 89 | let message = TupleEnumMessage { 90 | message_type: TupleMessageType::External(1), 91 | }; 92 | assert_eq!( 93 | message.cache_key().unwrap(), 94 | "TupleEnumMessage::v0::message_type[External]=1".to_string() 95 | ); 96 | } 97 | 98 | // Should we support tuple struct? 99 | #[derive(Cacheable, Serialize)] 100 | struct TupleMessage(i32); 101 | 102 | #[test] 103 | fn test_tuple_returns_error() { 104 | let message = TupleMessage(1); 105 | assert!(message.cache_key().is_err()); 106 | } 107 | 108 | #[derive(Cacheable, Serialize)] 109 | #[hitbox(cache_ttl=42, cache_stale_ttl=30, cache_version=1)] 110 | struct MacroHelpersMessage { 111 | message_type: i32, 112 | } 113 | 114 | #[test] 115 | fn test_macro_helpers_work() { 116 | let message = MacroHelpersMessage { message_type: 1 }; 117 | assert_eq!(message.cache_ttl(), 42); 118 | assert_eq!(message.cache_stale_ttl(), 30); 119 | assert_eq!(message.cache_version(), 1); 120 | assert_eq!( 121 | message.cache_key().unwrap(), 122 | "MacroHelpersMessage::v1::message_type=1".to_string() 123 | ); 124 | } 125 | 126 | #[derive(Cacheable, Serialize)] 127 | struct DefaultMessage { 128 | message_type: i32, 129 | } 130 | 131 | #[test] 132 | fn test_default_ttl_stale_ttl_version_work() { 133 | let message = DefaultMessage { message_type: 1 }; 134 | assert_eq!(message.cache_ttl(), 60); 135 | assert_eq!(message.cache_stale_ttl(), 55); 136 | assert_eq!(message.cache_version(), 0); 137 | } 138 | -------------------------------------------------------------------------------- /hitbox/tests/cacheable_response_derive.rs: -------------------------------------------------------------------------------- 1 | use hitbox::{CachePolicy, CacheableResponse}; 2 | use serde::Serialize; 3 | 4 | #[derive(CacheableResponse, Serialize, Clone, Debug, Eq, PartialEq)] 5 | struct Message { 6 | id: i32, 7 | alias: String, 8 | } 9 | 10 | #[test] 11 | fn test_custom_message_into_policy() { 12 | let message = Message { 13 | id: 0, 14 | alias: String::from("alias"), 15 | }; 16 | let policy = message.clone().into_cache_policy(); 17 | match policy { 18 | CachePolicy::Cacheable(value) => assert_eq!(value, message), 19 | CachePolicy::NonCacheable(_) => panic!(), 20 | }; 21 | } 22 | 23 | #[derive(CacheableResponse, Serialize, Clone, Debug, Eq, PartialEq)] 24 | enum EnumMessage { 25 | Variant(i32), 26 | } 27 | 28 | #[test] 29 | fn test_custom_enum_message_into_policy() { 30 | let message = EnumMessage::Variant(1); 31 | let policy = message.clone().into_cache_policy(); 32 | match policy { 33 | CachePolicy::Cacheable(value) => assert_eq!(value, message), 34 | CachePolicy::NonCacheable(_) => panic!(), 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /hitbox/tests/metrics.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(test, feature = "metrics"))] 2 | mod tests { 3 | use hitbox::dev::MockAdapter; 4 | use hitbox::metrics::{CACHE_HIT_COUNTER, CACHE_MISS_COUNTER, CACHE_STALE_COUNTER}; 5 | use hitbox::settings::Status; 6 | use hitbox::states::initial::Initial; 7 | use metrics::{Counter, Gauge, Histogram, Key, KeyName, Label, Recorder, SharedString, Unit}; 8 | use metrics_util::registry::{AtomicStorage, Registry}; 9 | use std::sync::{atomic::Ordering, Arc}; 10 | 11 | static LABELS: [Label; 2] = [ 12 | Label::from_static_parts("upstream", "MockAdapter"), 13 | Label::from_static_parts("message", "MockMessage"), 14 | ]; 15 | 16 | struct MockRecorder { 17 | pub registry: Arc>, 18 | } 19 | 20 | impl Clone for MockRecorder { 21 | fn clone(&self) -> Self { 22 | Self { 23 | registry: self.registry.clone(), 24 | } 25 | } 26 | } 27 | 28 | impl MockRecorder { 29 | pub fn new() -> Self { 30 | Self { 31 | registry: Arc::new(Registry::atomic()), 32 | } 33 | } 34 | } 35 | 36 | impl Recorder for MockRecorder { 37 | fn describe_counter(&self, _: KeyName, _: Option, _: SharedString) {} 38 | 39 | fn describe_gauge(&self, _: KeyName, _: Option, _: SharedString) {} 40 | 41 | fn describe_histogram(&self, _: KeyName, _: Option, _: SharedString) {} 42 | 43 | fn register_counter(&self, key: &Key) -> Counter { 44 | self.registry 45 | .get_or_create_counter(key, |c| c.clone().into()) 46 | } 47 | 48 | fn register_gauge(&self, key: &Key) -> Gauge { 49 | self.registry.get_or_create_gauge(key, |c| c.clone().into()) 50 | } 51 | 52 | fn register_histogram(&self, key: &Key) -> Histogram { 53 | self.registry 54 | .get_or_create_histogram(key, |c| c.clone().into()) 55 | } 56 | } 57 | 58 | #[tokio::test] 59 | async fn test_hit_counter() { 60 | unsafe { metrics::clear_recorder() }; 61 | let recorder = MockRecorder::new(); 62 | let handler = recorder.clone(); 63 | metrics::set_boxed_recorder(Box::new(recorder)).unwrap(); 64 | let settings = hitbox::settings::CacheSettings { 65 | cache: Status::Enabled, 66 | stale: Status::Disabled, 67 | lock: Status::Disabled, 68 | }; 69 | let adapter = MockAdapter::build().with_cache_actual(42).finish(); 70 | let initial_state = Initial::new(settings.clone(), adapter.clone()); 71 | let _ = initial_state.transitions().await.unwrap(); 72 | 73 | let hit_key = Key::from_parts(CACHE_HIT_COUNTER.as_ref(), LABELS.to_vec()); 74 | let counters = handler.registry.get_counter_handles(); 75 | let hit_counter = counters.get(&hit_key); 76 | assert!(hit_counter.is_some()); 77 | assert_eq!(hit_counter.unwrap().load(Ordering::Acquire), 1); 78 | } 79 | 80 | #[tokio::test] 81 | async fn test_miss_counter() { 82 | unsafe { metrics::clear_recorder() }; 83 | let recorder = MockRecorder::new(); 84 | let handler = recorder.clone(); 85 | metrics::set_boxed_recorder(Box::new(recorder)).unwrap(); 86 | let settings = hitbox::settings::CacheSettings { 87 | cache: Status::Enabled, 88 | stale: Status::Disabled, 89 | lock: Status::Disabled, 90 | }; 91 | let adapter = MockAdapter::build() 92 | .with_cache_miss() 93 | .with_upstream_value(42) 94 | .finish(); 95 | let initial_state = Initial::new(settings.clone(), adapter.clone()); 96 | let _ = initial_state.transitions().await.unwrap(); 97 | 98 | let miss_key = Key::from_parts(CACHE_MISS_COUNTER.as_ref(), LABELS.to_vec()); 99 | let counters = handler.registry.get_counter_handles(); 100 | let miss_counter = counters.get(&miss_key); 101 | assert!(miss_counter.is_some()); 102 | assert_eq!(miss_counter.unwrap().load(Ordering::Acquire), 1); 103 | } 104 | 105 | #[tokio::test] 106 | async fn test_stale_counter() { 107 | unsafe { metrics::clear_recorder() }; 108 | let recorder = MockRecorder::new(); 109 | let handler = recorder.clone(); 110 | metrics::set_boxed_recorder(Box::new(recorder)).unwrap(); 111 | let settings = hitbox::settings::CacheSettings { 112 | cache: Status::Enabled, 113 | stale: Status::Enabled, 114 | lock: Status::Disabled, 115 | }; 116 | let adapter = MockAdapter::build() 117 | .with_cache_stale(41, chrono::Utc::now()) 118 | .with_upstream_value(42) 119 | .finish(); 120 | let initial_state = Initial::new(settings.clone(), adapter.clone()); 121 | let _ = initial_state.transitions().await.unwrap(); 122 | 123 | let stale_key = Key::from_parts(CACHE_STALE_COUNTER.as_ref(), LABELS.to_vec()); 124 | let counters = handler.registry.get_counter_handles(); 125 | let stale_counter = counters.get(&stale_key); 126 | assert!(stale_counter.is_some()); 127 | assert_eq!(stale_counter.unwrap().load(Ordering::Acquire), 1); 128 | 129 | let hit_key = Key::from_parts(CACHE_HIT_COUNTER.as_ref(), LABELS.to_vec()); 130 | let counters = handler.registry.get_counter_handles(); 131 | let hit_counter = counters.get(&hit_key); 132 | assert!(hit_counter.is_some()); 133 | assert_eq!(hit_counter.unwrap().load(Ordering::Acquire), 1); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /hitbox/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod states; 2 | mod transitions; 3 | -------------------------------------------------------------------------------- /hitbox/tests/response.rs: -------------------------------------------------------------------------------- 1 | use hitbox::{CachePolicy, CacheableResponse}; 2 | 3 | #[test] 4 | fn test_optinal_cacheable_response() { 5 | let maybe1: Option = Some(12); 6 | let maybe2: Option = None; 7 | 8 | assert!(matches!(maybe1.cache_policy(), CachePolicy::Cacheable(12))); 9 | assert!(matches!(maybe2.cache_policy(), CachePolicy::NonCacheable(()))); 10 | 11 | assert!(matches!(maybe1.into_cache_policy(), CachePolicy::Cacheable(12))); 12 | assert!(matches!(maybe2.into_cache_policy(), CachePolicy::NonCacheable(None))); 13 | 14 | assert!(matches!(Option::from_cached(12), Some(12))); 15 | } 16 | 17 | #[test] 18 | fn test_result_cacheable_response() { 19 | let result1: Result = Ok(12); 20 | let result2: Result = Err("error"); 21 | 22 | assert!(matches!(result1.cache_policy(), CachePolicy::Cacheable(12))); 23 | assert!(matches!(result2.cache_policy(), CachePolicy::NonCacheable(()))); 24 | 25 | assert!(matches!(result1.into_cache_policy(), CachePolicy::Cacheable(12))); 26 | assert!(matches!(result2.into_cache_policy(), CachePolicy::NonCacheable(Err("error")))); 27 | 28 | let result3: Result = Result::from_cached(12); 29 | assert!(matches!(result3, Ok(12))); 30 | } -------------------------------------------------------------------------------- /hitbox/tests/settings.rs: -------------------------------------------------------------------------------- 1 | // use fsm_cache::{CacheSettings, SettingState, Initial, Message, InitialStateSettings}; 2 | // use actix::Message; 3 | // use hitbox::settings::{CacheSettings, SettingState, InitialStateSettings}; 4 | // 5 | // #[derive(Debug, PartialEq)] 6 | // pub struct Ping; 7 | // 8 | // #[derive(Debug, PartialEq)] 9 | // pub struct Pong; 10 | // 11 | // 12 | // #[test] 13 | // fn test_settings() { 14 | // let settings = CacheSettings { 15 | // cache: SettingState::Disabled, 16 | // stale: SettingState::Disabled, 17 | // lock: SettingState::Disabled, 18 | // }; 19 | // let message = Ping; 20 | // let initial = Initial::from((settings, message)); 21 | // assert_eq!(initial.settings, InitialStateSettings::CacheDisabled); 22 | // assert_eq!(initial.message, Ping); 23 | // } 24 | -------------------------------------------------------------------------------- /hitbox/tests/states/cache_policy.rs: -------------------------------------------------------------------------------- 1 | use hitbox::dev::MockAdapter; 2 | use hitbox::states::cache_policy::{CachePolicyCacheable, CachePolicyNonCacheable}; 3 | 4 | #[test] 5 | fn test_cacheable_debug() { 6 | let adapter = MockAdapter::build().with_upstream_value(42).finish(); 7 | let cacheable = CachePolicyCacheable { 8 | adapter, 9 | result: 42, 10 | }; 11 | assert_eq!(format!("{:?}", cacheable), "CachePolicyCacheable"); 12 | } 13 | 14 | #[test] 15 | fn test_non_cacheable_debug() { 16 | let non_cacheable = CachePolicyNonCacheable { result: 42 }; 17 | assert_eq!(format!("{:?}", non_cacheable), "CachePolicyNonCacheable"); 18 | } 19 | 20 | #[test] 21 | fn test_non_cacheable_finish() { 22 | let non_cacheable = CachePolicyNonCacheable { result: 42 }; 23 | assert_eq!(non_cacheable.finish().result.unwrap(), 42); 24 | } 25 | -------------------------------------------------------------------------------- /hitbox/tests/states/cache_polled.rs: -------------------------------------------------------------------------------- 1 | use hitbox::dev::MockAdapter; 2 | use hitbox::states::cache_polled::{CachePolledActual, CachePolledStale}; 3 | use hitbox::CachedValue; 4 | 5 | #[test] 6 | fn test_cache_actual_debug() { 7 | let adapter = MockAdapter::build().with_upstream_value(42).finish(); 8 | let actual = CachePolledActual { 9 | adapter, 10 | result: CachedValue::new(41, chrono::Utc::now()), 11 | }; 12 | assert_eq!(format!("{:?}", actual), "CachePolledActual"); 13 | } 14 | 15 | #[test] 16 | fn test_stale_finish() { 17 | let adapter: MockAdapter = MockAdapter::build().with_upstream_error().finish(); 18 | let actual = CachePolledStale { 19 | adapter, 20 | result: CachedValue::new(42, chrono::Utc::now()), 21 | }; 22 | assert_eq!(actual.finish().result.unwrap(), 42) 23 | } 24 | -------------------------------------------------------------------------------- /hitbox/tests/states/mod.rs: -------------------------------------------------------------------------------- 1 | mod cache_policy; 2 | mod cache_polled; 3 | mod upstream_polled; 4 | -------------------------------------------------------------------------------- /hitbox/tests/states/upstream_polled.rs: -------------------------------------------------------------------------------- 1 | use hitbox::dev::MockAdapter; 2 | use hitbox::states::cache_policy::{CachePolicyChecked, CachePolicyNonCacheable}; 3 | use hitbox::states::upstream_polled::UpstreamPolledSuccessful; 4 | 5 | #[test] 6 | fn test_successful_check_policy_non_cacheable() { 7 | let adapter = MockAdapter::build().with_upstream_value(42).finish(); 8 | let successful = UpstreamPolledSuccessful { 9 | adapter, 10 | result: 42, 11 | }; 12 | let _expected: CachePolicyChecked, i32> = 13 | CachePolicyChecked::NonCacheable(CachePolicyNonCacheable { result: 42 }); 14 | assert!(matches!(successful.check_cache_policy(), _expected)); 15 | } 16 | -------------------------------------------------------------------------------- /hitbox/tests/transitions/cache_disabled.rs: -------------------------------------------------------------------------------- 1 | use hitbox::dev::MockAdapter; 2 | use hitbox::settings::{CacheSettings, Status}; 3 | use hitbox::states::initial::Initial; 4 | use hitbox::transition_groups::upstream; 5 | 6 | #[actix::test] 7 | async fn test_cache_disabled_upstream_polled() { 8 | let settings = CacheSettings { 9 | cache: Status::Disabled, 10 | stale: Status::Disabled, 11 | lock: Status::Disabled, 12 | }; 13 | let adapter = MockAdapter::build().with_upstream_value(42).finish(); 14 | let initial_state = Initial::new(settings, adapter); 15 | let finish = upstream::transition(initial_state).await; 16 | assert_eq!(finish.result().unwrap(), 42); 17 | } 18 | 19 | #[actix::test] 20 | async fn test_cache_disabled_upstream_error() { 21 | let settings = CacheSettings { 22 | cache: Status::Disabled, 23 | stale: Status::Disabled, 24 | lock: Status::Disabled, 25 | }; 26 | let adapter: MockAdapter = MockAdapter::build().with_upstream_error().finish(); 27 | let initial_state = Initial::new(settings, adapter); 28 | let finish = upstream::transition(initial_state).await; 29 | assert!(finish.result().is_err()); 30 | } 31 | -------------------------------------------------------------------------------- /hitbox/tests/transitions/cache_enabled.rs: -------------------------------------------------------------------------------- 1 | use hitbox::dev::MockAdapter; 2 | use hitbox::settings::{CacheSettings, Status}; 3 | use hitbox::states::initial::Initial; 4 | use hitbox::transition_groups::only_cache; 5 | 6 | #[actix::test] 7 | async fn test_cache_enabled_cache_miss() { 8 | let settings = CacheSettings { 9 | cache: Status::Enabled, 10 | stale: Status::Disabled, 11 | lock: Status::Disabled, 12 | }; 13 | let adapter = MockAdapter::build() 14 | .with_upstream_value(42) 15 | .with_cache_miss() 16 | .finish(); 17 | let initial_state = Initial::new(settings, adapter); 18 | let finish = only_cache::transition(initial_state).await; 19 | assert_eq!(finish.result().unwrap(), 42); 20 | } 21 | 22 | #[actix::test] 23 | async fn test_cache_enabled_cache_hit() { 24 | let settings = CacheSettings { 25 | cache: Status::Enabled, 26 | stale: Status::Disabled, 27 | lock: Status::Disabled, 28 | }; 29 | let adapter = MockAdapter::build() 30 | .with_upstream_error() 31 | .with_cache_actual(42) 32 | .finish(); 33 | let initial_state = Initial::new(settings, adapter); 34 | let finish = only_cache::transition(initial_state).await; 35 | assert_eq!(finish.result().unwrap(), 42); 36 | } 37 | 38 | #[actix::test] 39 | async fn test_cache_enabled_cache_miss_upstream_error() { 40 | let settings = CacheSettings { 41 | cache: Status::Enabled, 42 | stale: Status::Disabled, 43 | lock: Status::Disabled, 44 | }; 45 | let adapter: MockAdapter = MockAdapter::build() 46 | .with_upstream_error() 47 | .with_cache_miss() 48 | .finish(); 49 | let initial_state = Initial::new(settings, adapter); 50 | let finish = only_cache::transition(initial_state).await; 51 | assert!(finish.result().is_err()); 52 | } 53 | -------------------------------------------------------------------------------- /hitbox/tests/transitions/mod.rs: -------------------------------------------------------------------------------- 1 | mod cache_disabled; 2 | mod cache_enabled; 3 | mod stale; 4 | -------------------------------------------------------------------------------- /hitbox/tests/transitions/stale.rs: -------------------------------------------------------------------------------- 1 | use hitbox::dev::MockAdapter; 2 | use hitbox::settings::{CacheSettings, Status}; 3 | use hitbox::states::initial::Initial; 4 | use hitbox::transition_groups::stale; 5 | 6 | #[actix::test] 7 | async fn test_cache_stale() { 8 | let settings = CacheSettings { 9 | cache: Status::Enabled, 10 | stale: Status::Enabled, 11 | lock: Status::Disabled, 12 | }; 13 | let adapter = MockAdapter::build() 14 | .with_upstream_value("upstream value") 15 | .with_cache_stale("stale cache", chrono::Utc::now()) 16 | .finish(); 17 | let initial_state = Initial::new(settings, adapter); 18 | let finish = stale::transition(initial_state).await; 19 | assert_eq!(finish.result().unwrap(), "upstream value"); 20 | } 21 | 22 | #[actix::test] 23 | async fn test_upstream_error() { 24 | let settings = CacheSettings { 25 | cache: Status::Enabled, 26 | stale: Status::Enabled, 27 | lock: Status::Disabled, 28 | }; 29 | let adapter = MockAdapter::build() 30 | .with_upstream_error() 31 | .with_cache_stale("stale cache", chrono::Utc::now()) 32 | .finish(); 33 | let initial_state = Initial::new(settings, adapter); 34 | let finish = stale::transition(initial_state).await; 35 | assert_eq!(finish.result().unwrap(), "stale cache"); 36 | } 37 | 38 | #[actix::test] 39 | async fn test_cache_actual() { 40 | let settings = CacheSettings { 41 | cache: Status::Enabled, 42 | stale: Status::Enabled, 43 | lock: Status::Disabled, 44 | }; 45 | let adapter = MockAdapter::build() 46 | .with_upstream_value("upstream value") 47 | .with_cache_actual("actual cache") 48 | .finish(); 49 | let initial_state = Initial::new(settings, adapter); 50 | let finish = stale::transition(initial_state).await; 51 | assert_eq!(finish.result().unwrap(), "actual cache"); 52 | } 53 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | sign-commit = true 2 | sign-tag = true 3 | pre-release-commit-message = "Release {{crate_name}} {{version}} 🎉" 4 | post-release-commit-message = "Start next {{crate_name}} development iteration {{next_version}}" 5 | tag-message = "Release {{crate_name}} {{version}}" 6 | tag-name = "{{prefix}}{{version}}" 7 | no-dev-version = true 8 | 9 | pre-release-replacements = [ 10 | {file="CHANGELOG.md", search="\\[Unreleased\\]", replace="[{{version}}] {{date}}"}, 11 | {file="CHANGELOG.md", search="\\(https://semver.org/spec/v2.0.0.html\\).", replace="(https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]"} 12 | ] 13 | --------------------------------------------------------------------------------