├── .cargo └── config.toml ├── .github ├── dependabot.yml ├── pull_request_template.md ├── semantic.yml └── workflows │ ├── ci.yml │ └── license_check.yml ├── .gitignore ├── .licenserc.yaml ├── .typos.toml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── codecov.yml ├── docker-compose.yaml ├── etc ├── grafana │ ├── dashboards │ │ ├── foyer.json │ │ └── node-exporter-full.json │ ├── grafana.ini │ └── provisioning │ │ ├── dashboards │ │ └── foyer.yml │ │ └── datasources │ │ └── foyer.yml ├── logo │ ├── brand.min.svg │ ├── brand.svg │ ├── ferris.min.svg │ ├── ferris.svg │ ├── logo.min.svg │ ├── logo.svg │ ├── slogan.min.svg │ └── slogan.svg └── prometheus │ └── prometheus.yml ├── examples ├── Cargo.toml ├── equivalent.rs ├── event_listener.rs ├── export_metrics_prometheus_hyper.rs ├── hybrid.rs ├── hybrid_full.rs ├── memory.rs ├── serde.rs └── tail_based_tracing.rs ├── foyer-bench ├── Cargo.toml ├── etc │ └── sample.txt └── src │ ├── analyze.rs │ ├── exporter.rs │ ├── main.rs │ ├── rate.rs │ └── text.rs ├── foyer-cli ├── Cargo.toml └── src │ ├── args │ ├── error.rs │ ├── fio.rs │ └── mod.rs │ └── main.rs ├── foyer-common ├── Cargo.toml ├── benches │ └── bench_serde │ │ ├── bench.rs │ │ └── main.rs └── src │ ├── assert.rs │ ├── asyncify.rs │ ├── bits.rs │ ├── buf.rs │ ├── code.rs │ ├── countdown.rs │ ├── event.rs │ ├── future.rs │ ├── hasher.rs │ ├── lib.rs │ ├── metrics.rs │ ├── properties.rs │ ├── rate.rs │ ├── rated_ticket.rs │ ├── runtime.rs │ ├── tracing.rs │ └── utils │ ├── mod.rs │ ├── option.rs │ ├── range.rs │ └── scope.rs ├── foyer-memory ├── Cargo.toml ├── benches │ ├── bench_dynamic_dispatch.rs │ └── bench_hit_ratio.rs └── src │ ├── cache.rs │ ├── error.rs │ ├── eviction │ ├── fifo.rs │ ├── lfu.rs │ ├── lru.rs │ ├── mod.rs │ ├── s3fifo.rs │ └── test_utils.rs │ ├── indexer │ ├── hash_table.rs │ ├── mod.rs │ └── sentry.rs │ ├── lib.rs │ ├── pipe.rs │ ├── prelude.rs │ ├── raw.rs │ ├── record.rs │ └── test_utils.rs ├── foyer-storage ├── Cargo.toml ├── src │ ├── compress.rs │ ├── device │ │ ├── allocator.rs │ │ ├── direct_file.rs │ │ ├── direct_fs.rs │ │ ├── mod.rs │ │ ├── monitor.rs │ │ └── test_utils.rs │ ├── engine.rs │ ├── error.rs │ ├── io │ │ ├── buffer.rs │ │ ├── mod.rs │ │ └── throttle.rs │ ├── large │ │ ├── buffer.rs │ │ ├── flusher.rs │ │ ├── generic.rs │ │ ├── indexer.rs │ │ ├── mod.rs │ │ ├── reclaimer.rs │ │ ├── recover.rs │ │ ├── scanner.rs │ │ ├── serde.rs │ │ ├── test_utils.rs │ │ └── tombstone.rs │ ├── lib.rs │ ├── picker │ │ ├── mod.rs │ │ └── utils.rs │ ├── prelude.rs │ ├── region.rs │ ├── runtime.rs │ ├── serde.rs │ ├── small │ │ ├── batch.rs │ │ ├── bloom_filter.rs │ │ ├── flusher.rs │ │ ├── generic.rs │ │ ├── mod.rs │ │ ├── serde.rs │ │ ├── set.rs │ │ ├── set_cache.rs │ │ └── set_manager.rs │ ├── statistics.rs │ ├── storage │ │ ├── either.rs │ │ ├── mod.rs │ │ └── noop.rs │ ├── store.rs │ └── test_utils.rs └── tests │ └── storage_fuzzy_test.rs ├── foyer ├── Cargo.toml └── src │ ├── hybrid │ ├── builder.rs │ ├── cache.rs │ ├── mod.rs │ └── writer.rs │ ├── lib.rs │ └── prelude.rs ├── rustfmt.nightly.toml ├── rustfmt.toml └── scripts ├── install-deps.sh ├── minimize-dashboards.sh └── monitor.sh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all())'] 2 | rustflags = ["--cfg", "tokio_unstable"] 3 | -------------------------------------------------------------------------------- /.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://docs.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 | rebase-strategy: "disabled" 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What's changed and what's your intention? 2 | 3 | > Please explain **IN DETAIL** what the changes are in this PR and why they are needed. :D 4 | 5 | ## Checklist 6 | 7 | - [ ] I have written the necessary rustdoc comments 8 | - [ ] I have added the necessary unit tests and integration tests 9 | - [ ] I have passed `make all` (or `make fast` instead if the old tests are not modified) in my local environment. 10 | 11 | ## Related issues or PRs (optional) 12 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # The pull request's title should be fulfilled the following pattern: 2 | # 3 | # [optional scope]: 4 | # 5 | # ... where valid types and scopes can be found below; for example: 6 | # 7 | # build(maven): One level down for native profile 8 | # 9 | # More about configurations on https://github.com/Ezard/semantic-prs#configuration 10 | 11 | enabled: true 12 | 13 | titleOnly: true 14 | 15 | types: 16 | - feat 17 | - fix 18 | - docs 19 | - style 20 | - refactor 21 | - perf 22 | - test 23 | - build 24 | - ci 25 | - chore 26 | - revert 27 | 28 | targetUrl: https://github.com/foyer-rs/foyer/blob/main/.github/semantic.yml 29 | -------------------------------------------------------------------------------- /.github/workflows/license_check.yml: -------------------------------------------------------------------------------- 1 | name: License Checker 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - "forks/*" 8 | - release-*.* 9 | pull_request: 10 | branches: 11 | - main 12 | - "v*.*.*-rc" 13 | - release-*.* 14 | jobs: 15 | license-header-check: 16 | runs-on: ubuntu-latest 17 | name: license-header-check 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | persist-credentials: false 22 | - name: Check License Header 23 | uses: apache/skywalking-eyes/header@v0.6.0 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | 3 | /target 4 | 5 | Cargo.lock 6 | lcov.info 7 | 8 | docker-compose.override.yaml 9 | .tmp 10 | 11 | perf.data* 12 | flamegraph.svg 13 | 14 | trace.txt 15 | jeprof.out.* 16 | *.collapsed -------------------------------------------------------------------------------- /.licenserc.yaml: -------------------------------------------------------------------------------- 1 | header: 2 | license: 3 | spdx-id: Apache-2.0 4 | copyright-owner: foyer Project Authors 5 | 6 | paths: 7 | - "**/*.rs" 8 | 9 | comment: on-failure 10 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | # "ot" short for "OpenTelemetry" 3 | ot = "ot" 4 | 5 | [files] 6 | extend-exclude = ["foyer-workspace-hack/Cargo.toml", "*.svg"] 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "examples", 5 | "foyer", 6 | "foyer-bench", 7 | "foyer-cli", 8 | "foyer-common", 9 | "foyer-memory", 10 | "foyer-storage", 11 | ] 12 | 13 | [workspace.package] 14 | version = "0.17.3" 15 | edition = "2021" 16 | rust-version = "1.81.0" 17 | repository = "https://github.com/foyer-rs/foyer" 18 | homepage = "https://foyer.rs" 19 | keywords = ["cache", "hybrid"] 20 | authors = ["MrCroxx "] 21 | license = "Apache-2.0" 22 | readme = "README.md" 23 | 24 | [workspace.dependencies] 25 | # foyer components 26 | foyer-common = { version = "0.17.3", path = "foyer-common" } 27 | foyer-memory = { version = "0.17.3", path = "foyer-memory" } 28 | foyer-storage = { version = "0.17.3", path = "foyer-storage" } 29 | foyer = { version = "0.17.3", path = "foyer" } 30 | 31 | # dependencies 32 | ahash = "0.8" 33 | anyhow = "1" 34 | bincode = "1" 35 | bytes = "1" 36 | bytesize = { package = "foyer-bytesize", version = "2" } 37 | clap = { version = "4", features = ["derive"] } 38 | equivalent = "1" 39 | fastrace = "0.7" 40 | futures-core = { version = "0.3" } 41 | futures-util = { version = "0.3", default-features = false, features = ["std"] } 42 | hashbrown = "0.15" 43 | itertools = "0.14" 44 | parking_lot = { version = "0.12" } 45 | paste = "1" 46 | pin-project = "1" 47 | rand = { version = "0.9" } 48 | rand_distr = { version = "0.5" } 49 | serde = { version = "1", features = ["derive"] } 50 | test-log = { version = "0.2", default-features = false, features = [ 51 | "trace", 52 | "color", 53 | ] } 54 | thiserror = "2" 55 | tracing = "0.1" 56 | prometheus = "0.14" 57 | mixtrics = "0.1" 58 | criterion = "0.5" 59 | 60 | [workspace.lints.rust] 61 | missing_docs = "warn" 62 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(madsim)'] } 63 | 64 | [workspace.lints.clippy] 65 | allow_attributes = "warn" 66 | 67 | [profile.release] 68 | debug = "full" 69 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | .PHONY: deps check test test-ignored test-all all fast monitor clear madsim example msrv udeps ffmt machete misc 3 | 4 | deps: 5 | ./scripts/install-deps.sh 6 | 7 | misc: 8 | typos 9 | shellcheck ./scripts/* 10 | ./scripts/minimize-dashboards.sh 11 | 12 | check: 13 | cargo sort -w 14 | taplo fmt 15 | cargo fmt --all 16 | cargo clippy --all-targets 17 | 18 | check-all: 19 | cargo sort -w 20 | taplo fmt 21 | cargo fmt --all 22 | cargo clippy --all-targets --features deadlock 23 | cargo clippy --all-targets --features tokio-console -- -A "clippy::large_enum_variant" 24 | cargo clippy --all-targets --features tracing 25 | cargo clippy --all-targets --features serde 26 | cargo clippy --all-targets 27 | 28 | test: 29 | RUST_BACKTRACE=1 cargo nextest run --all --features "strict_assertions" 30 | RUST_BACKTRACE=1 cargo test --doc 31 | RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo +nightly doc --features "nightly" --no-deps 32 | 33 | test-ignored: 34 | RUST_BACKTRACE=1 cargo nextest run --run-ignored ignored-only --no-capture --workspace --features "strict_assertions" 35 | 36 | test-all: test test-ignored 37 | 38 | madsim: 39 | RUSTFLAGS="--cfg madsim --cfg tokio_unstable" cargo clippy --all-targets 40 | RUSTFLAGS="--cfg madsim --cfg tokio_unstable" RUST_BACKTRACE=1 cargo nextest run --all --features "strict_assertions" 41 | 42 | example: 43 | cargo run --example memory 44 | cargo run --example hybrid 45 | cargo run --example hybrid_full 46 | cargo run --example event_listener 47 | cargo run --features "tracing,jaeger" --example tail_based_tracing 48 | cargo run --features "tracing,ot" --example tail_based_tracing 49 | cargo run --example equivalent 50 | cargo run --example export_metrics_prometheus_hyper 51 | cargo run --features serde --example serde 52 | 53 | msrv: 54 | shellcheck ./scripts/* 55 | ./scripts/minimize-dashboards.sh 56 | cargo +1.81.0 sort -w 57 | cargo +1.81.0 fmt --all 58 | cargo +1.81.0 clippy --all-targets --features deadlock 59 | cargo +1.81.0 clippy --all-targets --features tokio-console 60 | cargo +1.81.0 clippy --all-targets 61 | RUST_BACKTRACE=1 cargo +1.81.0 nextest run --all 62 | RUST_BACKTRACE=1 cargo +1.81.0 test --doc 63 | RUST_BACKTRACE=1 cargo +1.81.0 nextest run --run-ignored ignored-only --no-capture --workspace 64 | 65 | udeps: 66 | RUSTFLAGS="--cfg tokio_unstable -Awarnings" cargo +nightly-2024-08-30 udeps --all-targets 67 | 68 | machete: 69 | cargo machete 70 | 71 | monitor: 72 | ./scripts/monitor.sh 73 | 74 | clear: 75 | rm -rf .tmp 76 | 77 | ffmt: 78 | cargo +nightly fmt --all -- --config-path rustfmt.nightly.toml 79 | 80 | all: misc ffmt check-all test-all example machete udeps 81 | 82 | fast: misc ffmt check test example machete -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # codecov config 2 | # Reference: https://docs.codecov.com/docs/codecovyml-reference 3 | # Tips. You may run following command to validate before committing any changes 4 | # curl --data-binary @codecov.yml https://codecov.io/validate 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "70..100" 9 | status: 10 | project: 11 | default: 12 | target: auto 13 | threshold: "1%" 14 | only_pulls: true 15 | patch: 16 | default: 17 | informational: true 18 | only_pulls: true 19 | changes: 20 | default: 21 | informational: true 22 | only_pulls: true 23 | ignore: 24 | - "foyer-cli" -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | node-exporter: 3 | image: prom/node-exporter:latest 4 | container_name: node-exporter 5 | restart: unless-stopped 6 | volumes: 7 | - /proc:/host/proc:ro 8 | - /sys:/host/sys:ro 9 | - /:/rootfs:ro 10 | command: 11 | - '--path.procfs=/host/proc' 12 | - '--path.rootfs=/rootfs' 13 | - '--path.sysfs=/host/sys' 14 | - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' 15 | ports: 16 | - 9100:9100 17 | 18 | prometheus: 19 | image: prom/prometheus:latest 20 | container_name: prometheus 21 | restart: unless-stopped 22 | volumes: 23 | - ./etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml 24 | - ./.tmp/prometheus:/prometheus 25 | command: 26 | - '--config.file=/etc/prometheus/prometheus.yml' 27 | - '--storage.tsdb.path=/prometheus' 28 | - '--web.console.libraries=/etc/prometheus/console_libraries' 29 | - '--web.console.templates=/etc/prometheus/consoles' 30 | - '--web.enable-lifecycle' 31 | ports: 32 | - 9090:9090 33 | extra_hosts: 34 | - "host.docker.internal:host-gateway" 35 | 36 | grafana: 37 | image: grafana/grafana:latest 38 | container_name: grafana 39 | volumes: 40 | - ./etc/grafana/grafana.ini:/etc/grafana/grafana.ini 41 | - ./etc/grafana/provisioning:/etc/grafana/provisioning 42 | - ./etc/grafana/dashboards:/var/lib/grafana/dashboards 43 | ports: 44 | - 3000:3000 45 | environment: 46 | - GF_AUTH_ANONYMOUS_ENABLED=true 47 | - NO_PROXY=prometheus 48 | 49 | jaeger: 50 | image: jaegertracing/all-in-one:latest 51 | container_name: jaeger 52 | command: 53 | - '--collector.otlp.enabled=true' 54 | ports: 55 | # https://www.jaegertracing.io/docs/1.58/getting-started/ 56 | - 6831:6831/udp 57 | - 6832:6832/udp 58 | - 16686:16686 59 | - 4317:4317 60 | -------------------------------------------------------------------------------- /etc/grafana/grafana.ini: -------------------------------------------------------------------------------- 1 | [auth.anonymous] 2 | enabled = true 3 | org_name = Main Org. 4 | org_role = Admin -------------------------------------------------------------------------------- /etc/grafana/provisioning/dashboards/foyer.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: "foyer provider" 5 | disableDeletion: true 6 | allowUiUpdates: false 7 | options: 8 | path: /var/lib/grafana/dashboards/foyer.json 9 | foldersFromFilesStructure: false 10 | - name: "node exporter full provider" 11 | disableDeletion: true 12 | allowUiUpdates: false 13 | options: 14 | path: /var/lib/grafana/dashboards/node-exporter-full.json 15 | foldersFromFilesStructure: false -------------------------------------------------------------------------------- /etc/grafana/provisioning/datasources/foyer.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | deleteDatasources: 3 | - name: foyer 4 | datasources: 5 | - name: foyer 6 | type: prometheus 7 | access: proxy 8 | url: http://prometheus:9090 9 | withCredentials: false 10 | tlsAuth: false 11 | tlsAuthWithCACert: false 12 | version: 1 13 | editable: true 14 | isDefault: true -------------------------------------------------------------------------------- /etc/logo/brand.min.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /etc/logo/brand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /etc/logo/ferris.min.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /etc/logo/logo.min.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /etc/logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /etc/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 1s 3 | evaluation_interval: 1s 4 | 5 | scrape_configs: 6 | - job_name: 'prometheus' 7 | static_configs: 8 | - targets: ['localhost:9090'] 9 | 10 | - job_name: 'node' 11 | static_configs: 12 | - targets: ['node-exporter:9100'] 13 | 14 | - job_name: 'foyer-bench' 15 | static_configs: 16 | - targets: ['host.docker.internal:19970'] 17 | -------------------------------------------------------------------------------- /examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "examples" 3 | description = "examples for foyer - Hybrid cache for Rust" 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | rust-version = { workspace = true } 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = { workspace = true } 10 | authors = { workspace = true } 11 | license = { workspace = true } 12 | readme = { workspace = true } 13 | publish = false 14 | 15 | [dependencies] 16 | anyhow = { workspace = true } 17 | chrono = "0.4" 18 | equivalent = { workspace = true } 19 | fastrace = { workspace = true } 20 | fastrace-jaeger = { version = "0.7", optional = true } 21 | fastrace-opentelemetry = { version = "0.7", optional = true } 22 | foyer = { workspace = true } 23 | http-body-util = "0.1" 24 | hyper = { version = "1", default-features = false, features = [ 25 | "server", 26 | "http1", 27 | ] } 28 | hyper-util = { version = "0.1", default-features = false, features = ["tokio"] } 29 | mixtrics = { workspace = true, features = ["prometheus"] } 30 | opentelemetry = { version = "0.26", optional = true } 31 | opentelemetry-otlp = { version = "0.26", optional = true } 32 | opentelemetry-semantic-conventions = { version = "0.26", optional = true } 33 | opentelemetry_sdk = { version = "0.26", features = [ 34 | "rt-tokio", 35 | "trace", 36 | ], optional = true } 37 | prometheus = { workspace = true } 38 | serde = { workspace = true } 39 | tempfile = "3" 40 | tokio = { version = "1", features = ["rt"] } 41 | tracing = { workspace = true } 42 | 43 | [features] 44 | serde = ["foyer/serde"] 45 | jaeger = ["fastrace-jaeger"] 46 | ot = [ 47 | "fastrace-opentelemetry", 48 | "opentelemetry", 49 | "opentelemetry-otlp", 50 | "opentelemetry-semantic-conventions", 51 | "opentelemetry_sdk", 52 | ] 53 | 54 | [[example]] 55 | name = "memory" 56 | path = "memory.rs" 57 | 58 | [[example]] 59 | name = "hybrid" 60 | path = "hybrid.rs" 61 | 62 | [[example]] 63 | name = "hybrid_full" 64 | path = "hybrid_full.rs" 65 | 66 | [[example]] 67 | name = "event_listener" 68 | path = "event_listener.rs" 69 | 70 | [[example]] 71 | name = "tail_based_tracing" 72 | path = "tail_based_tracing.rs" 73 | 74 | [[example]] 75 | name = "equivalent" 76 | path = "equivalent.rs" 77 | 78 | [[example]] 79 | name = "export_metrics_prometheus_hyper" 80 | path = "export_metrics_prometheus_hyper.rs" 81 | 82 | [[example]] 83 | name = "serde" 84 | path = "serde.rs" 85 | -------------------------------------------------------------------------------- /examples/equivalent.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use equivalent::Equivalent; 16 | use foyer::{Cache, CacheBuilder}; 17 | 18 | #[derive(Hash, PartialEq, Eq)] 19 | pub struct Pair(pub A, pub B); 20 | 21 | // FYI: https://docs.rs/equivalent 22 | impl<'a, A: ?Sized, B: ?Sized, C, D> Equivalent<(C, D)> for Pair<&'a A, &'a B> 23 | where 24 | A: Equivalent, 25 | B: Equivalent, 26 | { 27 | fn equivalent(&self, key: &(C, D)) -> bool { 28 | self.0.equivalent(&key.0) && self.1.equivalent(&key.1) 29 | } 30 | } 31 | 32 | fn main() { 33 | let cache: Cache<(String, String), String> = CacheBuilder::new(16).build(); 34 | 35 | let entry = cache.insert( 36 | ("hello".to_string(), "world".to_string()), 37 | "This is a string tuple pair.".to_string(), 38 | ); 39 | // With `Equivalent`, `Pair(&str, &str)` can be used to compared `(String, String)`! 40 | let e = cache.get(&Pair("hello", "world")).unwrap(); 41 | 42 | assert_eq!(entry.value(), e.value()); 43 | } 44 | -------------------------------------------------------------------------------- /examples/event_listener.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::Arc; 16 | 17 | use foyer::{Cache, CacheBuilder, CacheProperties, Event, EventListener, FifoConfig}; 18 | 19 | struct EchoEventListener; 20 | 21 | impl EventListener for EchoEventListener { 22 | type Key = u64; 23 | type Value = String; 24 | 25 | fn on_leave(&self, _reason: Event, key: &Self::Key, value: &Self::Value) { 26 | println!("Entry [key = {key}] [value = {value}] is released.") 27 | } 28 | } 29 | 30 | /// Output: 31 | /// 32 | /// ```plain 33 | /// Entry [key = 2] [value = First] is released. 34 | /// Entry [key = 1] [value = Second] is released. 35 | /// Entry [key = 3] [value = Third] is released. 36 | /// Entry [key = 3] [value = Forth] is released. 37 | /// ``` 38 | fn main() { 39 | let cache: Cache = CacheBuilder::new(2) 40 | .with_event_listener(Arc::new(EchoEventListener)) 41 | .with_eviction_config(FifoConfig::default()) 42 | .with_shards(1) 43 | .build(); 44 | 45 | cache.insert(1, "Second".to_string()); 46 | cache.insert_with_properties(2, "First".to_string(), CacheProperties::default().with_ephemeral(true)); 47 | cache.insert(3, "Third".to_string()); 48 | cache.insert(3, "Forth".to_string()); 49 | } 50 | -------------------------------------------------------------------------------- /examples/export_metrics_prometheus_hyper.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{future::Future, net::SocketAddr, pin::Pin}; 16 | 17 | use anyhow::Ok; 18 | use foyer::{Cache, CacheBuilder}; 19 | use http_body_util::Full; 20 | use hyper::{ 21 | body::{Bytes, Incoming}, 22 | header::CONTENT_TYPE, 23 | server::conn::http1, 24 | service::Service, 25 | Request, Response, 26 | }; 27 | use hyper_util::rt::TokioIo; 28 | use mixtrics::registry::prometheus::PrometheusMetricsRegistry; 29 | use prometheus::{Encoder, Registry, TextEncoder}; 30 | use tokio::net::TcpListener; 31 | 32 | pub struct PrometheusExporter { 33 | registry: Registry, 34 | addr: SocketAddr, 35 | } 36 | 37 | impl PrometheusExporter { 38 | pub fn new(registry: Registry, addr: SocketAddr) -> Self { 39 | Self { registry, addr } 40 | } 41 | 42 | pub fn run(self) { 43 | tokio::spawn(async move { 44 | let listener = TcpListener::bind(&self.addr).await.unwrap(); 45 | loop { 46 | let (stream, _) = match listener.accept().await { 47 | Result::Ok(res) => res, 48 | Err(e) => { 49 | tracing::error!("[prometheus exporter]: accept connection error: {e}"); 50 | continue; 51 | } 52 | }; 53 | 54 | let io = TokioIo::new(stream); 55 | let handle = Handle { 56 | registry: self.registry.clone(), 57 | }; 58 | 59 | tokio::spawn(async move { 60 | if let Err(e) = http1::Builder::new().serve_connection(io, handle).await { 61 | tracing::error!("[prometheus exporter]: serve request error: {e}"); 62 | } 63 | }); 64 | } 65 | }); 66 | } 67 | } 68 | 69 | struct Handle { 70 | registry: Registry, 71 | } 72 | 73 | impl Service> for Handle { 74 | type Response = Response>; 75 | type Error = anyhow::Error; 76 | type Future = Pin> + Send>>; 77 | 78 | fn call(&self, _: Request) -> Self::Future { 79 | let mfs = self.registry.gather(); 80 | 81 | Box::pin(async move { 82 | let encoder = TextEncoder::new(); 83 | let mut buffer = vec![]; 84 | encoder.encode(&mfs, &mut buffer)?; 85 | 86 | Ok(Response::builder() 87 | .status(200) 88 | .header(CONTENT_TYPE, encoder.format_type()) 89 | .body(Full::new(Bytes::from(buffer))) 90 | .unwrap()) 91 | }) 92 | } 93 | } 94 | 95 | #[tokio::main] 96 | async fn main() { 97 | // Create a new registry or use the global registry of `prometheus` lib. 98 | let registry = Registry::new(); 99 | 100 | // Create a `PrometheusExporter` powered by hyper and run it. 101 | let addr = "127.0.0.1:19970".parse().unwrap(); 102 | PrometheusExporter::new(registry.clone(), addr).run(); 103 | 104 | // Build a cache with `PrometheusMetricsRegistry`. 105 | let _: Cache = CacheBuilder::new(100) 106 | .with_metrics_registry(Box::new(PrometheusMetricsRegistry::new(registry))) 107 | .build(); 108 | 109 | // > curl http://127.0.0.1:7890 110 | // 111 | // # HELP foyer_hybrid_op_duration foyer hybrid cache operation durations 112 | // # TYPE foyer_hybrid_op_duration histogram 113 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="0.005"} 0 114 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="0.01"} 0 115 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="0.025"} 0 116 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="0.05"} 0 117 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="0.1"} 0 118 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="0.25"} 0 119 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="0.5"} 0 120 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="1"} 0 121 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="2.5"} 0 122 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="5"} 0 123 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="10"} 0 124 | // foyer_hybrid_op_duration_bucket{name="foyer",op="fetch",le="+Inf"} 0 125 | // ... ... 126 | } 127 | -------------------------------------------------------------------------------- /examples/hybrid.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use foyer::{DirectFsDeviceOptions, Engine, HybridCache, HybridCacheBuilder}; 16 | 17 | #[tokio::main] 18 | async fn main() -> anyhow::Result<()> { 19 | let dir = tempfile::tempdir()?; 20 | 21 | let hybrid: HybridCache = HybridCacheBuilder::new() 22 | .memory(64 * 1024 * 1024) 23 | .storage(Engine::large()) // use large object disk cache engine with default configuration 24 | .with_device_options(DirectFsDeviceOptions::new(dir.path()).with_capacity(256 * 1024 * 1024)) 25 | .build() 26 | .await?; 27 | 28 | hybrid.insert(42, "The answer to life, the universe, and everything.".to_string()); 29 | assert_eq!( 30 | hybrid.get(&42).await?.unwrap().value(), 31 | "The answer to life, the universe, and everything." 32 | ); 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /examples/hybrid_full.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{hash::BuildHasherDefault, num::NonZeroUsize, sync::Arc}; 16 | 17 | use anyhow::Result; 18 | use chrono::Datelike; 19 | use foyer::{ 20 | AdmitAllPicker, DirectFsDeviceOptions, Engine, FifoPicker, HybridCache, HybridCacheBuilder, HybridCachePolicy, 21 | IopsCounter, LargeEngineOptions, LruConfig, RecoverMode, RejectAllPicker, RuntimeOptions, SmallEngineOptions, 22 | Throttle, TokioRuntimeOptions, TombstoneLogConfigBuilder, 23 | }; 24 | use tempfile::tempdir; 25 | 26 | #[tokio::main] 27 | async fn main() -> Result<()> { 28 | let dir = tempdir()?; 29 | 30 | let hybrid: HybridCache = HybridCacheBuilder::new() 31 | .with_name("my-hybrid-cache") 32 | .with_policy(HybridCachePolicy::WriteOnEviction) 33 | .memory(1024) 34 | .with_shards(4) 35 | .with_eviction_config(LruConfig { 36 | high_priority_pool_ratio: 0.1, 37 | }) 38 | .with_hash_builder(BuildHasherDefault::default()) 39 | .with_weighter(|_key, value: &String| value.len()) 40 | .storage(Engine::Mixed { 41 | ratio: 0.1, 42 | large: LargeEngineOptions::new() 43 | .with_indexer_shards(64) 44 | .with_recover_concurrency(8) 45 | .with_flushers(2) 46 | .with_reclaimers(2) 47 | .with_buffer_pool_size(256 * 1024 * 1024) 48 | .with_clean_region_threshold(4) 49 | .with_eviction_pickers(vec![Box::::default()]) 50 | .with_reinsertion_picker(Arc::::default()) 51 | .with_tombstone_log_config( 52 | TombstoneLogConfigBuilder::new(dir.path().join("tombstone-log-file")) 53 | .with_flush(true) 54 | .build(), 55 | ), 56 | small: SmallEngineOptions::new() 57 | .with_set_size(16 * 1024) 58 | .with_set_cache_capacity(64) 59 | .with_flushers(2), 60 | }) 61 | .with_device_options( 62 | DirectFsDeviceOptions::new(dir.path()) 63 | .with_capacity(64 * 1024 * 1024) 64 | .with_file_size(4 * 1024 * 1024) 65 | .with_throttle( 66 | Throttle::new() 67 | .with_read_iops(4000) 68 | .with_write_iops(2000) 69 | .with_write_throughput(100 * 1024 * 1024) 70 | .with_read_throughput(800 * 1024 * 1024) 71 | .with_iops_counter(IopsCounter::PerIoSize(NonZeroUsize::new(128 * 1024).unwrap())), 72 | ), 73 | ) 74 | .with_flush(true) 75 | .with_recover_mode(RecoverMode::Quiet) 76 | .with_admission_picker(Arc::::default()) 77 | .with_compression(foyer::Compression::Lz4) 78 | .with_runtime_options(RuntimeOptions::Separated { 79 | read_runtime_options: TokioRuntimeOptions { 80 | worker_threads: 4, 81 | max_blocking_threads: 8, 82 | }, 83 | write_runtime_options: TokioRuntimeOptions { 84 | worker_threads: 4, 85 | max_blocking_threads: 8, 86 | }, 87 | }) 88 | .build() 89 | .await?; 90 | 91 | hybrid.insert(42, "The answer to life, the universe, and everything.".to_string()); 92 | assert_eq!( 93 | hybrid.get(&42).await?.unwrap().value(), 94 | "The answer to life, the universe, and everything." 95 | ); 96 | 97 | let e = hybrid 98 | .fetch(20230512, || async { 99 | let value = mock().await?; 100 | Ok(value) 101 | }) 102 | .await?; 103 | assert_eq!(e.key(), &20230512); 104 | assert_eq!(e.value(), "Hello, foyer."); 105 | 106 | hybrid.close().await.unwrap(); 107 | 108 | Ok(()) 109 | } 110 | 111 | async fn mock() -> Result { 112 | let now = chrono::Utc::now(); 113 | if format!("{}{}{}", now.year(), now.month(), now.day()) == "20230512" { 114 | return Err(anyhow::anyhow!("Hi, time traveler!")); 115 | } 116 | Ok("Hello, foyer.".to_string()) 117 | } 118 | -------------------------------------------------------------------------------- /examples/memory.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use foyer::{Cache, CacheBuilder}; 16 | 17 | fn main() { 18 | let cache: Cache = CacheBuilder::new(16).build(); 19 | 20 | let entry = cache.insert("hello".to_string(), "world".to_string()); 21 | let e = cache.get("hello").unwrap(); 22 | 23 | assert_eq!(entry.value(), e.value()); 24 | } 25 | -------------------------------------------------------------------------------- /examples/serde.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt::Debug; 16 | 17 | use foyer::{ 18 | Code, CodeResult, DirectFsDeviceOptions, Engine, HybridCache, HybridCacheBuilder, HybridCachePolicy, StorageValue, 19 | }; 20 | 21 | #[cfg(feature = "serde")] 22 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 23 | struct Foo { 24 | a: u64, 25 | b: String, 26 | } 27 | 28 | #[derive(Debug, Clone, PartialEq, Eq)] 29 | struct Bar { 30 | a: u64, 31 | b: String, 32 | } 33 | 34 | impl Code for Bar { 35 | fn encode(&self, writer: &mut impl std::io::Write) -> CodeResult<()> { 36 | writer.write_all(&self.a.to_le_bytes())?; 37 | writer.write_all(&(self.b.len() as u64).to_le_bytes())?; 38 | writer.write_all(self.b.as_bytes())?; 39 | Ok(()) 40 | } 41 | 42 | fn decode(reader: &mut impl std::io::Read) -> CodeResult 43 | where 44 | Self: Sized, 45 | { 46 | let mut buf = [0u8; 8]; 47 | reader.read_exact(&mut buf)?; 48 | let a = u64::from_le_bytes(buf); 49 | reader.read_exact(&mut buf)?; 50 | let b_len = u64::from_le_bytes(buf) as usize; 51 | let bytes = vec![0u8; b_len]; 52 | let b = String::from_utf8(bytes).map_err(std::io::Error::other)?; 53 | Ok(Self { a, b }) 54 | } 55 | 56 | fn estimated_size(&self) -> usize { 57 | 8 + 8 + self.b.len() 58 | } 59 | } 60 | 61 | async fn case(value: V) -> anyhow::Result<()> { 62 | let dir = tempfile::tempdir()?; 63 | 64 | let hybrid: HybridCache = HybridCacheBuilder::new() 65 | .with_policy(HybridCachePolicy::WriteOnInsertion) 66 | .memory(64 * 1024 * 1024) 67 | .storage(Engine::large()) // use large object disk cache engine only 68 | .with_device_options(DirectFsDeviceOptions::new(dir.path()).with_capacity(256 * 1024 * 1024)) 69 | .build() 70 | .await?; 71 | 72 | hybrid.insert(42, value.clone()); 73 | assert_eq!(hybrid.get(&42).await?.unwrap().value(), &value); 74 | 75 | Ok(()) 76 | } 77 | 78 | #[tokio::main] 79 | async fn main() -> anyhow::Result<()> { 80 | case("The answer to life, the universe, and everything.".to_string()).await?; 81 | 82 | #[cfg(feature = "serde")] 83 | case(Foo { 84 | a: 42, 85 | b: "The answer to life, the universe, and everything.".to_string(), 86 | }) 87 | .await?; 88 | 89 | case(Bar { 90 | a: 42, 91 | b: "The answer to life, the universe, and everything.".to_string(), 92 | }) 93 | .await?; 94 | 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /examples/tail_based_tracing.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::time::Duration; 16 | 17 | use foyer::{DirectFsDeviceOptions, Engine, HybridCache, HybridCacheBuilder, TracingOptions}; 18 | 19 | #[cfg(feature = "jaeger")] 20 | fn init_jaeger_exporter() { 21 | let reporter = fastrace_jaeger::JaegerReporter::new("127.0.0.1:6831".parse().unwrap(), "example").unwrap(); 22 | fastrace::set_reporter( 23 | reporter, 24 | fastrace::collector::Config::default().report_interval(Duration::from_millis(1)), 25 | ); 26 | } 27 | 28 | #[cfg(feature = "ot")] 29 | fn init_opentelemetry_exporter() { 30 | use opentelemetry_otlp::WithExportConfig; 31 | 32 | let exporter = opentelemetry_otlp::new_exporter() 33 | .tonic() 34 | .with_endpoint("http://127.0.0.1:4317") 35 | .with_protocol(opentelemetry_otlp::Protocol::Grpc) 36 | .with_timeout(Duration::from_secs( 37 | opentelemetry_otlp::OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT, 38 | )) 39 | .build_span_exporter() 40 | .unwrap(); 41 | let reporter = fastrace_opentelemetry::OpenTelemetryReporter::new( 42 | exporter, 43 | opentelemetry::trace::SpanKind::Server, 44 | std::borrow::Cow::Owned(opentelemetry_sdk::Resource::new([opentelemetry::KeyValue::new( 45 | opentelemetry_semantic_conventions::resource::SERVICE_NAME, 46 | "example", 47 | )])), 48 | opentelemetry::InstrumentationLibrary::builder("opentelemetry-instrumentation-foyer").build(), 49 | ); 50 | fastrace::set_reporter(reporter, fastrace::collector::Config::default()); 51 | } 52 | 53 | fn init_exporter() { 54 | #[cfg(feature = "jaeger")] 55 | init_jaeger_exporter(); 56 | 57 | #[cfg(feature = "ot")] 58 | init_opentelemetry_exporter(); 59 | 60 | #[cfg(not(any(feature = "jaeger", feature = "ot")))] 61 | panic!("Either jaeger or opentelemetry feature must be enabled!"); 62 | } 63 | 64 | /// NOTE: To run this example, please enable feature "tracing" and either "jaeger" or "ot". 65 | #[tokio::main] 66 | async fn main() -> anyhow::Result<()> { 67 | init_exporter(); 68 | 69 | let dir = tempfile::tempdir()?; 70 | 71 | let hybrid: HybridCache = HybridCacheBuilder::new() 72 | .memory(64 * 1024 * 1024) 73 | .storage(Engine::large()) 74 | .with_device_options(DirectFsDeviceOptions::new(dir.path()).with_capacity(256 * 1024 * 1024)) 75 | .build() 76 | .await?; 77 | 78 | hybrid.enable_tracing(); 79 | hybrid.update_tracing_options(TracingOptions::new().with_record_hybrid_get_threshold(Duration::from_millis(10))); 80 | 81 | hybrid.insert(42, "The answer to life, the universe, and everything.".to_string()); 82 | assert_eq!( 83 | hybrid.get(&42).await?.unwrap().value(), 84 | "The answer to life, the universe, and everything." 85 | ); 86 | 87 | Ok(()) 88 | } 89 | -------------------------------------------------------------------------------- /foyer-bench/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foyer-bench" 3 | description = "bench tool for foyer - Hybrid cache for Rust" 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | rust-version = { workspace = true } 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = { workspace = true } 10 | authors = { workspace = true } 11 | license = { workspace = true } 12 | readme = { workspace = true } 13 | 14 | [dependencies] 15 | anyhow = { workspace = true } 16 | bytesize = { workspace = true } 17 | clap = { workspace = true } 18 | console-subscriber = { version = "0.4", optional = true } 19 | fastrace = { workspace = true, optional = true } 20 | fastrace-jaeger = { version = "0.7", optional = true } 21 | foyer = { workspace = true, features = ["tracing"] } 22 | futures-util = { workspace = true } 23 | hdrhistogram = "7" 24 | http-body-util = "0.1" 25 | humantime = "2" 26 | hyper = { version = "1", default-features = false, features = [ 27 | "server", 28 | "http1", 29 | ] } 30 | hyper-util = { version = "0.1", default-features = false, features = ["tokio"] } 31 | itertools = { workspace = true } 32 | mixtrics = { workspace = true, features = ["prometheus"] } 33 | parking_lot = { workspace = true } 34 | prometheus = { workspace = true } 35 | rand = { workspace = true } 36 | rand_distr = { workspace = true } 37 | tracing = { workspace = true } 38 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 39 | 40 | [target.'cfg(not(target_env = "msvc"))'.dependencies] 41 | tikv-jemallocator = { version = "0.6", optional = true } 42 | 43 | [target.'cfg(madsim)'.dependencies] 44 | tokio = { package = "madsim-tokio", version = "0.2", features = [ 45 | "rt", 46 | "rt-multi-thread", 47 | "sync", 48 | "macros", 49 | "time", 50 | "signal", 51 | "fs", 52 | ] } 53 | 54 | [target.'cfg(not(madsim))'.dependencies] 55 | tokio = { package = "tokio", version = "1", features = [ 56 | "rt", 57 | "rt-multi-thread", 58 | "sync", 59 | "macros", 60 | "time", 61 | "signal", 62 | "fs", 63 | ] } 64 | 65 | [features] 66 | default = ["jemalloc"] 67 | deadlock = ["parking_lot/deadlock_detection", "foyer/deadlock"] 68 | tokio-console = ["dep:console-subscriber"] 69 | strict_assertions = ["foyer/strict_assertions"] 70 | jemalloc = ["dep:tikv-jemallocator"] 71 | jeprof = ["jemalloc", "tikv-jemallocator?/profiling"] 72 | tracing = ["foyer/tracing", "dep:fastrace-jaeger", "dep:fastrace"] 73 | 74 | [lints] 75 | workspace = true 76 | -------------------------------------------------------------------------------- /foyer-bench/src/exporter.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{future::Future, net::SocketAddr, pin::Pin}; 16 | 17 | use anyhow::Ok; 18 | use http_body_util::Full; 19 | use hyper::{ 20 | body::{Bytes, Incoming}, 21 | header::CONTENT_TYPE, 22 | server::conn::http1, 23 | service::Service, 24 | Request, Response, 25 | }; 26 | use hyper_util::rt::TokioIo; 27 | use prometheus::{Encoder, Registry, TextEncoder}; 28 | use tokio::net::TcpListener; 29 | 30 | pub struct PrometheusExporter { 31 | registry: Registry, 32 | addr: SocketAddr, 33 | } 34 | 35 | impl PrometheusExporter { 36 | pub fn new(registry: Registry, addr: SocketAddr) -> Self { 37 | Self { registry, addr } 38 | } 39 | 40 | pub fn run(self) { 41 | tokio::spawn(async move { 42 | let listener = TcpListener::bind(&self.addr).await.unwrap(); 43 | loop { 44 | let (stream, _) = match listener.accept().await { 45 | Result::Ok(res) => res, 46 | Err(e) => { 47 | tracing::error!("[prometheus exporter]: accept connection error: {e}"); 48 | continue; 49 | } 50 | }; 51 | 52 | let io = TokioIo::new(stream); 53 | let handle = Handle { 54 | registry: self.registry.clone(), 55 | }; 56 | 57 | tokio::spawn(async move { 58 | if let Err(e) = http1::Builder::new().serve_connection(io, handle).await { 59 | tracing::error!("[prometheus exporter]: serve request error: {e}"); 60 | } 61 | }); 62 | } 63 | }); 64 | } 65 | } 66 | 67 | struct Handle { 68 | registry: Registry, 69 | } 70 | 71 | impl Service> for Handle { 72 | type Response = Response>; 73 | type Error = anyhow::Error; 74 | type Future = Pin> + Send>>; 75 | 76 | fn call(&self, _: Request) -> Self::Future { 77 | let mfs = self.registry.gather(); 78 | 79 | Box::pin(async move { 80 | let encoder = TextEncoder::new(); 81 | let mut buffer = vec![]; 82 | encoder.encode(&mfs, &mut buffer)?; 83 | 84 | Ok(Response::builder() 85 | .status(200) 86 | .header(CONTENT_TYPE, encoder.format_type()) 87 | .body(Full::new(Bytes::from(buffer))) 88 | .unwrap()) 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /foyer-bench/src/rate.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright 2023 RisingWave Labs 16 | // 17 | // Licensed under the Apache License, Version 2.0 (the "License"); 18 | // you may not use this file except in compliance with the License. 19 | // You may obtain a copy of the License at 20 | // 21 | // http://www.apache.org/licenses/LICENSE-2.0 22 | // 23 | // Unless required by applicable law or agreed to in writing, software 24 | // distributed under the License is distributed on an "AS IS" BASIS, 25 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | // See the License for the specific language governing permissions and 27 | // limitations under the License. 28 | 29 | use std::time::{Duration, Instant}; 30 | 31 | pub struct RateLimiter { 32 | capacity: f64, 33 | quota: f64, 34 | 35 | last: Instant, 36 | } 37 | 38 | impl RateLimiter { 39 | pub fn new(capacity: f64) -> Self { 40 | Self { 41 | capacity, 42 | quota: 0.0, 43 | last: Instant::now(), 44 | } 45 | } 46 | 47 | pub fn consume(&mut self, weight: f64) -> Option { 48 | let now = Instant::now(); 49 | let refill = now.duration_since(self.last).as_secs_f64() * self.capacity; 50 | self.last = now; 51 | self.quota = f64::min(self.quota + refill, self.capacity); 52 | self.quota -= weight; 53 | if self.quota >= 0.0 { 54 | return None; 55 | } 56 | let wait = Duration::from_secs_f64((-self.quota) / self.capacity); 57 | Some(wait) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /foyer-bench/src/text.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const TEXT: &[u8] = include_bytes!("../etc/sample.txt"); 16 | 17 | pub fn text(offset: usize, len: usize) -> Vec { 18 | let mut res = Vec::with_capacity(len); 19 | let mut cursor = offset % TEXT.len(); 20 | let mut remain = len; 21 | while remain > 0 { 22 | let bytes = std::cmp::min(remain, TEXT.len() - cursor); 23 | res.extend(&TEXT[cursor..cursor + bytes]); 24 | cursor = (cursor + bytes) % TEXT.len(); 25 | remain -= bytes; 26 | } 27 | res 28 | } 29 | -------------------------------------------------------------------------------- /foyer-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foyer-cli" 3 | description = "cli tool for foyer - Hybrid cache for Rust" 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | rust-version = { workspace = true } 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = { workspace = true } 10 | authors = { workspace = true } 11 | license = { workspace = true } 12 | readme = { workspace = true } 13 | publish = false 14 | 15 | [dependencies] 16 | anyhow = { workspace = true } 17 | bytesize = { workspace = true } 18 | clap = { workspace = true } 19 | thiserror = { workspace = true } 20 | 21 | [dev-dependencies] 22 | 23 | [lints] 24 | workspace = true 25 | -------------------------------------------------------------------------------- /foyer-cli/src/args/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt::Debug; 16 | 17 | /// Disk cache error type. 18 | #[derive(thiserror::Error, Debug)] 19 | pub enum Error { 20 | /// I/O error. 21 | #[error("io error: {0}")] 22 | Io(#[from] std::io::Error), 23 | /// `fio` not available. 24 | #[error("fio not available")] 25 | FioNotAvailable, 26 | /// Other error. 27 | #[error(transparent)] 28 | Other(#[from] anyhow::Error), 29 | } 30 | 31 | /// Disk cache result type. 32 | pub type Result = core::result::Result; 33 | -------------------------------------------------------------------------------- /foyer-cli/src/args/fio.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{collections::HashSet, process::Command}; 16 | 17 | use anyhow::anyhow; 18 | 19 | use crate::args::error::{Error, Result}; 20 | 21 | type IoEngine = String; 22 | 23 | #[derive(Debug)] 24 | pub struct Fio { 25 | io_engines: HashSet, 26 | } 27 | 28 | impl Fio { 29 | pub fn init() -> Result { 30 | if !Self::available() { 31 | return Err(Error::FioNotAvailable); 32 | } 33 | 34 | let io_engines = Self::list_io_engines()?; 35 | 36 | Ok(Self { io_engines }) 37 | } 38 | 39 | fn available() -> bool { 40 | let output = match Command::new("fio").arg("--version").output() { 41 | Ok(output) => output, 42 | Err(_) => return false, 43 | }; 44 | output.status.success() 45 | } 46 | 47 | pub fn io_engines(&self) -> &HashSet { 48 | &self.io_engines 49 | } 50 | 51 | fn list_io_engines() -> Result> { 52 | let output = Command::new("fio").arg("--enghelp").output()?; 53 | if !output.status.success() { 54 | return Err(anyhow!("fail to get available io engines with fio").into()); 55 | } 56 | 57 | let io_engines = String::from_utf8_lossy(&output.stdout) 58 | .split('\n') 59 | .skip(1) 60 | .map(|s| s.trim()) 61 | .filter(|s| !s.is_empty()) 62 | .map(String::from) 63 | .collect(); 64 | 65 | Ok(io_engines) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /foyer-cli/src/args/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | mod error; 16 | mod fio; 17 | 18 | use bytesize::ByteSize; 19 | use clap::{ArgGroup, Args}; 20 | use fio::Fio; 21 | 22 | #[derive(Debug, Args)] 23 | #[command(group = ArgGroup::new("exclusive").required(true).args(&["file", "dir"]))] 24 | pub struct ArgsArgs { 25 | /// File for disk cache data. Use `DirectFile` as device. 26 | /// 27 | /// Either `file` or `dir` must be set. 28 | #[arg(short, long)] 29 | file: Option, 30 | 31 | /// Directory for disk cache data. Use `DirectFs` as device. 32 | /// 33 | /// Either `file` or `dir` must be set. 34 | #[arg(short, long)] 35 | dir: Option, 36 | 37 | /// Size of the disk cache occupies. 38 | #[arg(short, long)] 39 | size: Option, 40 | } 41 | 42 | pub fn run(args: ArgsArgs) { 43 | println!("{args:#?}"); 44 | 45 | let fio = Fio::init().unwrap(); 46 | 47 | println!("{:#?}", fio.io_engines()); 48 | } 49 | -------------------------------------------------------------------------------- /foyer-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! `foyer-cli` provides debug tools for foyer, 16 | 17 | mod args; 18 | 19 | use args::ArgsArgs; 20 | use clap::{Parser, Subcommand}; 21 | 22 | #[derive(Debug, Parser)] 23 | #[command(author, version, about)] 24 | struct Cli { 25 | #[command(subcommand)] 26 | command: Command, 27 | } 28 | 29 | #[derive(Debug, Subcommand)] 30 | enum Command { 31 | /// Automatic arguments detector. 32 | Args(ArgsArgs), 33 | } 34 | 35 | fn main() { 36 | let cli = Cli::parse(); 37 | 38 | match cli.command { 39 | Command::Args(args) => args::run(args), 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /foyer-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foyer-common" 3 | description = "common components for foyer - Hybrid cache for Rust" 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | rust-version = { workspace = true } 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = { workspace = true } 10 | authors = { workspace = true } 11 | license = { workspace = true } 12 | readme = { workspace = true } 13 | 14 | [dependencies] 15 | ahash = { workspace = true } 16 | bincode = { workspace = true, optional = true } 17 | bytes = { workspace = true } 18 | cfg-if = "1" 19 | fastrace = { workspace = true, optional = true } 20 | itertools = { workspace = true } 21 | mixtrics = { workspace = true } 22 | parking_lot = { workspace = true } 23 | pin-project = "1" 24 | serde = { workspace = true, optional = true } 25 | thiserror = { workspace = true } 26 | 27 | [dev-dependencies] 28 | bytes = { version = "1", features = ["serde"] } 29 | criterion = { workspace = true } 30 | futures-util = { workspace = true } 31 | mixtrics = { workspace = true, features = ["test-utils"] } 32 | paste = { workspace = true } 33 | rand = { workspace = true } 34 | serde = { workspace = true } 35 | serde_bytes = "0.11" 36 | 37 | [target.'cfg(madsim)'.dependencies] 38 | tokio = { package = "madsim-tokio", version = "0.2", features = [ 39 | "rt", 40 | "rt-multi-thread", 41 | "sync", 42 | "macros", 43 | "time", 44 | "signal", 45 | "fs", 46 | ] } 47 | 48 | [target.'cfg(not(madsim))'.dependencies] 49 | tokio = { package = "tokio", version = "1", features = [ 50 | "rt", 51 | "rt-multi-thread", 52 | "sync", 53 | "macros", 54 | "time", 55 | "signal", 56 | "fs", 57 | ] } 58 | 59 | [features] 60 | serde = ["dep:serde", "dep:bincode"] 61 | strict_assertions = [] 62 | tracing = ["fastrace/enable"] 63 | 64 | [lints] 65 | workspace = true 66 | 67 | [[bench]] 68 | name = "bench_serde" 69 | harness = false 70 | 71 | [package.metadata.cargo-udeps.ignore] 72 | # Only used in benches and with `serde` feature enabled. 73 | development = ["criterion", "serde_bytes"] 74 | -------------------------------------------------------------------------------- /foyer-common/benches/bench_serde/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![expect(missing_docs)] 16 | 17 | #[cfg(feature = "serde")] 18 | mod bench; 19 | 20 | #[cfg(feature = "serde")] 21 | criterion::criterion_group!(benches, bench::bench_encode, bench::bench_decode); 22 | #[cfg(feature = "serde")] 23 | criterion::criterion_main!(benches); 24 | 25 | #[cfg(not(feature = "serde"))] 26 | fn main() { 27 | println!("Please enable the `serde` feature to run the benchmarks."); 28 | } 29 | -------------------------------------------------------------------------------- /foyer-common/src/assert.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Use `debug_assert!` by default. Use `assert!` when feature "strict_assertions" is enabled. 16 | #[macro_export] 17 | macro_rules! strict_assert { 18 | ($($arg:tt)*) => { 19 | #[cfg(feature = "strict_assertions")] 20 | assert!($($arg)*); 21 | #[cfg(not(feature = "strict_assertions"))] 22 | debug_assert!($($arg)*); 23 | } 24 | } 25 | 26 | /// Use `debug_assert_eq!` by default. Use `assert_eq!` when feature "strict_assertions" is enabled. 27 | #[macro_export] 28 | macro_rules! strict_assert_eq { 29 | ($($arg:tt)*) => { 30 | #[cfg(feature = "strict_assertions")] 31 | assert_eq!($($arg)*); 32 | #[cfg(not(feature = "strict_assertions"))] 33 | debug_assert_eq!($($arg)*); 34 | } 35 | } 36 | 37 | /// Use `debug_assert_ne!` by default. Use `assert_ne!` when feature "strict_assertions" is enabled. 38 | #[macro_export] 39 | macro_rules! strict_assert_ne { 40 | ($($arg:tt)*) => { 41 | #[cfg(feature = "strict_assertions")] 42 | assert_ne!($($arg)*); 43 | #[cfg(not(feature = "strict_assertions"))] 44 | debug_assert_ne!($($arg)*); 45 | } 46 | } 47 | 48 | /// Extend functions for [`Option`]. 49 | pub trait OptionExt: Sized { 50 | /// Use `unwrap_unchecked` by default. Use `unwrap` when feature "strict_assertions" is enabled. 51 | /// 52 | /// # Safety 53 | /// 54 | /// See [`Option::unwrap_unchecked`]. 55 | unsafe fn strict_unwrap_unchecked(self) -> T; 56 | } 57 | 58 | impl OptionExt for Option { 59 | unsafe fn strict_unwrap_unchecked(self) -> T { 60 | #[cfg(feature = "strict_assertions")] 61 | { 62 | self.unwrap() 63 | } 64 | #[cfg(not(feature = "strict_assertions"))] 65 | { 66 | unsafe { self.unwrap_unchecked() } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /foyer-common/src/asyncify.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::runtime::SingletonHandle; 16 | 17 | /// Convert the block call to async call with given runtime handle. 18 | #[cfg(not(madsim))] 19 | pub async fn asyncify_with_runtime(runtime: &SingletonHandle, f: F) -> T 20 | where 21 | F: FnOnce() -> T + Send + 'static, 22 | T: Send + 'static, 23 | { 24 | runtime.spawn_blocking(f).await.unwrap() 25 | } 26 | 27 | #[cfg(madsim)] 28 | /// Convert the block call to async call with given runtime. 29 | /// 30 | /// madsim compatible mode. 31 | pub async fn asyncify_with_runtime(_: &SingletonHandle, f: F) -> T 32 | where 33 | F: FnOnce() -> T + Send + 'static, 34 | T: Send + 'static, 35 | { 36 | f() 37 | } 38 | -------------------------------------------------------------------------------- /foyer-common/src/bits.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Copyright 2023 RisingWave Labs 16 | // 17 | // Licensed under the Apache License, Version 2.0 (the "License"); 18 | // you may not use this file except in compliance with the License. 19 | // You may obtain a copy of the License at 20 | // 21 | // http://www.apache.org/licenses/LICENSE-2.0 22 | // 23 | // Unless required by applicable law or agreed to in writing, software 24 | // distributed under the License is distributed on an "AS IS" BASIS, 25 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | // See the License for the specific language governing permissions and 27 | // limitations under the License. 28 | 29 | use std::{ 30 | fmt::{Debug, Display}, 31 | ops::{Add, BitAnd, Not, Sub}, 32 | }; 33 | 34 | /// An unsigned trait that used by the utils. 35 | pub trait Unsigned: 36 | Add 37 | + Sub 38 | + BitAnd 39 | + Not 40 | + Sized 41 | + From 42 | + Eq 43 | + Debug 44 | + Display 45 | + Clone 46 | + Copy 47 | { 48 | } 49 | 50 | impl< 51 | U: Add 52 | + Sub 53 | + BitAnd 54 | + Not 55 | + Sized 56 | + From 57 | + Eq 58 | + Debug 59 | + Display 60 | + Clone 61 | + Copy, 62 | > Unsigned for U 63 | { 64 | } 65 | 66 | /// Check if the given value is a power of 2. 67 | #[inline(always)] 68 | pub fn is_pow2(v: U) -> bool { 69 | v & (v - U::from(1)) == U::from(0) 70 | } 71 | 72 | /// Assert that the given value is a power of 2. 73 | #[inline(always)] 74 | pub fn assert_pow2(v: U) { 75 | assert_eq!(v & (v - U::from(1)), U::from(0), "v: {}", v); 76 | } 77 | 78 | /// Debug assert that the given value is a power of 2. 79 | #[inline(always)] 80 | pub fn debug_assert_pow2(v: U) { 81 | debug_assert_eq!(v & (v - U::from(1)), U::from(0), "v: {}", v); 82 | } 83 | 84 | /// Check if the given value is aligned with the given align. 85 | /// 86 | /// Note: The given align must be a power of 2. 87 | #[inline(always)] 88 | pub fn is_aligned(align: U, v: U) -> bool { 89 | debug_assert_pow2(align); 90 | v & (align - U::from(1)) == U::from(0) 91 | } 92 | 93 | /// Assert that the given value is aligned with the given align. 94 | /// 95 | /// Note: The given align must be a power of 2. 96 | #[inline(always)] 97 | pub fn assert_aligned(align: U, v: U) { 98 | debug_assert_pow2(align); 99 | assert!(is_aligned(align, v), "align: {}, v: {}", align, v); 100 | } 101 | 102 | /// Debug assert that the given value is aligned with the given align. 103 | /// 104 | /// Note: The given align must be a power of 2. 105 | #[inline(always)] 106 | pub fn debug_assert_aligned(align: U, v: U) { 107 | debug_assert_pow2(align); 108 | debug_assert!(is_aligned(align, v), "align: {}, v: {}", align, v); 109 | } 110 | 111 | /// Align up the given value with the given align. 112 | /// 113 | /// Note: The given align must be a power of 2. 114 | #[inline(always)] 115 | pub fn align_up(align: U, v: U) -> U { 116 | debug_assert_pow2(align); 117 | (v + align - U::from(1)) & !(align - U::from(1)) 118 | } 119 | 120 | /// Align down the given value with the given align. 121 | /// 122 | /// Note: The given align must be a power of 2. 123 | #[inline(always)] 124 | pub fn align_down(align: U, v: U) -> U { 125 | debug_assert_pow2(align); 126 | v & !(align - U::from(1)) 127 | } 128 | -------------------------------------------------------------------------------- /foyer-common/src/buf.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use bytes::{Buf, BufMut}; 16 | 17 | /// Extend [`Buf`] with `get_isize()` and `get_usize()`. 18 | pub trait BufExt: Buf { 19 | // TODO(MrCroxx): Use `cfg_match` after stable. 20 | // cfg_match! { 21 | // cfg(target_pointer_width = "16") => { 22 | // fn get_usize(&mut self) -> usize { 23 | // self.get_u16() as usize 24 | // } 25 | 26 | // fn get_isize(&mut self) -> isize { 27 | // self.get_i16() as isize 28 | // } 29 | // } 30 | // cfg(target_pointer_width = "32") => { 31 | // fn get_usize(&mut self) -> usize { 32 | // self.get_u32() as usize 33 | // } 34 | 35 | // fn get_isize(&mut self) -> isize { 36 | // self.get_i32() as isize 37 | // } 38 | // } 39 | // cfg(target_pointer_width = "64") => { 40 | // fn get_usize(&mut self) -> usize { 41 | // self.get_u64() as usize 42 | // } 43 | 44 | // fn get_isize(&mut self) -> isize { 45 | // self.get_i64() as isize 46 | // } 47 | // } 48 | // } 49 | cfg_if::cfg_if! { 50 | if #[cfg(target_pointer_width = "16")] { 51 | /// Gets an usize from self in big-endian byte order and advance the current position. 52 | fn get_usize(&mut self) -> usize { 53 | self.get_u16() as usize 54 | } 55 | /// Gets an isize from self in big-endian byte order and advance the current position. 56 | fn get_isize(&mut self) -> isize { 57 | self.get_i16() as isize 58 | } 59 | } 60 | else if #[cfg(target_pointer_width = "32")] { 61 | /// Gets an usize from self in big-endian byte order and advance the current position. 62 | fn get_usize(&mut self) -> usize { 63 | self.get_u32() as usize 64 | } 65 | /// Gets an isize from self in big-endian byte order and advance the current position. 66 | fn get_isize(&mut self) -> isize { 67 | self.get_i32() as isize 68 | } 69 | } 70 | else if #[cfg(target_pointer_width = "64")] { 71 | /// Gets an usize from self in big-endian byte order and advance the current position. 72 | fn get_usize(&mut self) -> usize { 73 | self.get_u64() as usize 74 | } 75 | /// Gets an isize from self in big-endian byte order and advance the current position. 76 | fn get_isize(&mut self) -> isize { 77 | self.get_i64() as isize 78 | } 79 | } 80 | } 81 | } 82 | 83 | impl BufExt for T {} 84 | 85 | /// Extend [`BufMut`] with `put_isize()` and `put_usize()`. 86 | pub trait BufMutExt: BufMut { 87 | // TODO(MrCroxx): Use `cfg_match` after stable. 88 | // cfg_match! { 89 | // cfg(target_pointer_width = "16") => { 90 | // fn put_usize(&mut self, v: usize) { 91 | // self.put_u16(v as u16); 92 | // } 93 | 94 | // fn put_isize(&mut self, v: isize) { 95 | // self.put_i16(v as i16); 96 | // } 97 | // } 98 | // cfg(target_pointer_width = "32") => { 99 | // fn put_usize(&mut self, v: usize) { 100 | // self.put_u32(v as u32); 101 | // } 102 | 103 | // fn put_isize(&mut self, v: isize) { 104 | // self.put_i32(v as i32); 105 | // } 106 | // } 107 | // cfg(target_pointer_width = "64") => { 108 | // fn put_usize(&mut self, v: usize) { 109 | // self.put_u64(v as u64); 110 | // } 111 | 112 | // fn put_isize(&mut self, v: isize) { 113 | // self.put_i64(v as i64); 114 | // } 115 | // } 116 | // } 117 | cfg_if::cfg_if! { 118 | if #[cfg(target_pointer_width = "16")] { 119 | /// Writes an usize to self in the big-endian byte order and advance the current position. 120 | fn put_usize(&mut self, v: usize) { 121 | self.put_u16(v as u16); 122 | } 123 | /// Writes an usize to self in the big-endian byte order and advance the current position. 124 | fn put_isize(&mut self, v: isize) { 125 | self.put_i16(v as i16); 126 | } 127 | } 128 | else if #[cfg(target_pointer_width = "32")] { 129 | /// Writes an usize to self in the big-endian byte order and advance the current position. 130 | fn put_usize(&mut self, v: usize) { 131 | self.put_u32(v as u32); 132 | } 133 | /// Writes an usize to self in the big-endian byte order and advance the current position. 134 | fn put_isize(&mut self, v: isize) { 135 | self.put_i32(v as i32); 136 | } 137 | } 138 | else if #[cfg(target_pointer_width = "64")] { 139 | /// Writes an usize to self in the big-endian byte order and advance the current position. 140 | fn put_usize(&mut self, v: usize) { 141 | self.put_u64(v as u64); 142 | } 143 | /// Writes an usize to self in the big-endian byte order and advance the current position. 144 | fn put_isize(&mut self, v: isize) { 145 | self.put_i64(v as i64); 146 | } 147 | } 148 | } 149 | } 150 | 151 | impl BufMutExt for T {} 152 | -------------------------------------------------------------------------------- /foyer-common/src/countdown.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::atomic::{AtomicBool, AtomicIsize, Ordering}; 16 | 17 | /// A concurrent count down util. 18 | #[derive(Debug)] 19 | pub struct Countdown { 20 | finish: AtomicBool, 21 | counter: AtomicIsize, 22 | } 23 | 24 | impl Countdown { 25 | /// Countdown `counter` times. 26 | /// 27 | /// # Safety 28 | /// 29 | /// Panics if `counter` exceeds [`isize::MAX`]. 30 | pub fn new(counter: usize) -> Self { 31 | Self { 32 | finish: AtomicBool::new(false), 33 | counter: AtomicIsize::new(isize::try_from(counter).expect("`counter` must NOT exceed `isize::MAX`.")), 34 | } 35 | } 36 | 37 | /// Returns `false` for the first `counter` times, then always returns `true`. 38 | pub fn countdown(&self) -> bool { 39 | if self.finish.load(Ordering::Relaxed) { 40 | return true; 41 | } 42 | self.counter.fetch_sub(1, Ordering::Relaxed) <= 0 43 | } 44 | 45 | /// Reset [`Countdown`] with `counter`. 46 | pub fn reset(&self, counter: usize) { 47 | self.finish.store(false, Ordering::Relaxed); 48 | self.counter.store( 49 | isize::try_from(counter).expect("`counter` must NOT exceed `isize::MAX`."), 50 | Ordering::Relaxed, 51 | ); 52 | } 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use std::time::Duration; 58 | 59 | use futures_util::future::join_all; 60 | 61 | use super::*; 62 | 63 | async fn case(counter: usize, concurrency: usize) { 64 | let cd = Countdown::new(counter); 65 | let res = join_all((0..concurrency).map(|_| async { 66 | tokio::time::sleep(Duration::from_millis(10)).await; 67 | cd.countdown() 68 | })) 69 | .await; 70 | assert_eq!(counter, res.into_iter().filter(|b| !b).count()); 71 | } 72 | 73 | #[tokio::test] 74 | async fn test_countdown() { 75 | for counter in [1, 4, 8, 16] { 76 | for concurrency in [16, 32, 64, 128] { 77 | case(counter, concurrency).await; 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /foyer-common/src/event.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::code::{Key, Value}; 16 | 17 | /// Event identifier. 18 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 19 | pub enum Event { 20 | /// Cache eviction on insertion. 21 | Evict, 22 | /// Cache replacement on insertion. 23 | Replace, 24 | /// Cache remove. 25 | Remove, 26 | /// Cache clear. 27 | Clear, 28 | } 29 | 30 | /// Trait for the customized event listener. 31 | pub trait EventListener: Send + Sync + 'static { 32 | /// Associated key type. 33 | type Key; 34 | /// Associated value type. 35 | type Value; 36 | 37 | /// Called when a cache entry leaves the in-memory cache with the reason. 38 | #[expect(unused_variables)] 39 | fn on_leave(&self, reason: Event, key: &Self::Key, value: &Self::Value) 40 | where 41 | Self::Key: Key, 42 | Self::Value: Value, 43 | { 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /foyer-common/src/future.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | future::Future, 17 | marker::PhantomData, 18 | ops::Deref, 19 | pin::Pin, 20 | task::{ready, Context, Poll}, 21 | }; 22 | 23 | use pin_project::pin_project; 24 | 25 | /// Result that the inner future of a [`DiversionFuture`] should return. 26 | /// 27 | /// - The `target` will be further returned by [`DiversionFuture`]. 28 | /// - The `store` will be stored in the [`DiversionFuture`]. 29 | pub struct Diversion { 30 | /// The `target` will be further returned by [`DiversionFuture`]. 31 | pub target: T, 32 | /// The `store` will be stored in the [`DiversionFuture`]. 33 | pub store: Option, 34 | } 35 | 36 | impl From for Diversion { 37 | fn from(value: T) -> Self { 38 | Self { 39 | target: value, 40 | store: None, 41 | } 42 | } 43 | } 44 | 45 | /// [`DiversionFuture`] is a future wrapper that partially store and partially return the future result. 46 | #[must_use] 47 | #[pin_project] 48 | pub struct DiversionFuture { 49 | #[pin] 50 | inner: FU, 51 | store: Option, 52 | _marker: PhantomData, 53 | } 54 | 55 | impl DiversionFuture { 56 | /// Create a new [`DiversionFuture`] wrapper. 57 | pub fn new(future: FU) -> Self { 58 | Self { 59 | inner: future, 60 | store: None, 61 | _marker: PhantomData, 62 | } 63 | } 64 | 65 | /// Get the stored state. 66 | pub fn store(&self) -> &Option { 67 | &self.store 68 | } 69 | } 70 | 71 | impl Deref for DiversionFuture { 72 | type Target = FU; 73 | 74 | fn deref(&self) -> &Self::Target { 75 | &self.inner 76 | } 77 | } 78 | 79 | impl Future for DiversionFuture 80 | where 81 | FU: Future, 82 | I: Into>, 83 | { 84 | type Output = T; 85 | 86 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 87 | let this = self.project(); 88 | let Diversion { target, store } = ready!(this.inner.poll(cx)).into(); 89 | *this.store = store; 90 | Poll::Ready(target) 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use std::{future::poll_fn, pin::pin}; 97 | 98 | use super::*; 99 | 100 | #[tokio::test] 101 | async fn test_diversion_future() { 102 | let mut f = pin!(DiversionFuture::new(async move { 103 | Diversion { 104 | target: "The answer to life, the universe, and everything.".to_string(), 105 | store: Some(42), 106 | } 107 | },)); 108 | 109 | let question: String = poll_fn(|cx| f.as_mut().poll(cx)).await; 110 | let answer = f.store().unwrap(); 111 | 112 | assert_eq!( 113 | (question.as_str(), answer), 114 | ("The answer to life, the universe, and everything.", 42) 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /foyer-common/src/hasher.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::hash::{BuildHasher, Hasher}; 16 | 17 | pub use ahash::RandomState as AhashRandomState; 18 | 19 | /// A hasher return u64 mod result. 20 | #[derive(Debug, Default)] 21 | pub struct ModRandomState { 22 | state: u64, 23 | } 24 | 25 | impl Hasher for ModRandomState { 26 | fn finish(&self) -> u64 { 27 | self.state 28 | } 29 | 30 | fn write(&mut self, bytes: &[u8]) { 31 | for byte in bytes { 32 | self.state = (self.state << 8) + *byte as u64; 33 | } 34 | } 35 | 36 | fn write_u8(&mut self, i: u8) { 37 | self.write(&[i]) 38 | } 39 | 40 | fn write_u16(&mut self, i: u16) { 41 | self.write(&i.to_be_bytes()) 42 | } 43 | 44 | fn write_u32(&mut self, i: u32) { 45 | self.write(&i.to_be_bytes()) 46 | } 47 | 48 | fn write_u64(&mut self, i: u64) { 49 | self.write(&i.to_be_bytes()) 50 | } 51 | 52 | fn write_u128(&mut self, i: u128) { 53 | self.write(&i.to_be_bytes()) 54 | } 55 | 56 | fn write_usize(&mut self, i: usize) { 57 | self.write(&i.to_be_bytes()) 58 | } 59 | 60 | fn write_i8(&mut self, i: i8) { 61 | self.write_u8(i as u8) 62 | } 63 | 64 | fn write_i16(&mut self, i: i16) { 65 | self.write_u16(i as u16) 66 | } 67 | 68 | fn write_i32(&mut self, i: i32) { 69 | self.write_u32(i as u32) 70 | } 71 | 72 | fn write_i64(&mut self, i: i64) { 73 | self.write_u64(i as u64) 74 | } 75 | 76 | fn write_i128(&mut self, i: i128) { 77 | self.write_u128(i as u128) 78 | } 79 | 80 | fn write_isize(&mut self, i: isize) { 81 | self.write_usize(i as usize) 82 | } 83 | } 84 | 85 | impl BuildHasher for ModRandomState { 86 | type Hasher = Self; 87 | 88 | fn build_hasher(&self) -> Self::Hasher { 89 | Self::default() 90 | } 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use super::*; 96 | 97 | #[test] 98 | fn test_mod_hasher() { 99 | for i in 0..255u8 { 100 | assert_eq!(i, ModRandomState::default().hash_one(i) as u8,) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /foyer-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Shared components and utils for foyer. 16 | 17 | /// Allow to enable debug assertions in release profile with feature "strict_assertion". 18 | pub mod assert; 19 | /// The util that convert the blocking call to async call. 20 | pub mod asyncify; 21 | /// The bitwise utils. 22 | pub mod bits; 23 | /// The [`bytes::Buf`] and [`bytes::BufMut`] extensions. 24 | pub mod buf; 25 | /// The trait for the key and value encoding and decoding. 26 | pub mod code; 27 | /// A concurrent count down util. 28 | pub mod countdown; 29 | /// Components for monitoring internal events. 30 | pub mod event; 31 | /// Future extensions. 32 | pub mod future; 33 | /// Provisioned hashers. 34 | pub mod hasher; 35 | /// The shared metrics for foyer. 36 | pub mod metrics; 37 | /// Entry-level properties. 38 | pub mod properties; 39 | /// A rate limiter that returns the wait duration for limitation. 40 | pub mod rate; 41 | /// A ticket-based rate limiter. 42 | pub mod rated_ticket; 43 | /// A runtime that automatically shutdown itself on drop. 44 | pub mod runtime; 45 | /// Tracing related components. 46 | #[cfg(feature = "tracing")] 47 | pub mod tracing; 48 | /// Useful helpers. 49 | pub mod utils; 50 | -------------------------------------------------------------------------------- /foyer-common/src/properties.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt::Debug; 16 | 17 | /// Hint for the cache eviction algorithm to decide the priority of the specific entry if needed. 18 | /// 19 | /// The meaning of the hint differs in each cache eviction algorithm, and some of them can be ignore by specific 20 | /// algorithm. 21 | /// 22 | /// If the given cache hint does not suitable for the cache eviction algorithm that is active, the algorithm may modify 23 | /// it to a proper one. 24 | /// 25 | /// For more details, please refer to the document of each enum options. 26 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 27 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 28 | pub enum Hint { 29 | /// The default hint shared by all cache eviction algorithms. 30 | Normal, 31 | /// Suggest the priority of the entry is low. 32 | /// 33 | /// Used by LRU. 34 | Low, 35 | } 36 | 37 | impl Default for Hint { 38 | fn default() -> Self { 39 | Self::Normal 40 | } 41 | } 42 | 43 | // TODO(MrCroxx): Is it necessary to make popluated entry still follow the cache location advice? 44 | /// Advice cache location for the cache entry. 45 | /// 46 | /// Useful when using hybrid cache. 47 | /// 48 | /// NOTE: `CacheLocation` only affects the first time the entry is handle. 49 | /// After it is populated, the entry may not follow the given advice. 50 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 51 | pub enum Location { 52 | /// The default location. 53 | /// 54 | /// Prefer to store the entry in the in-memory cache with in-memory cache. 55 | /// And prefer to store the entry in the hybrid cache with hybrid cache. 56 | Default, 57 | /// Prefer to store the entry in the in-memory cache. 58 | InMem, 59 | /// Prefer to store the entry on the disk cache. 60 | OnDisk, 61 | } 62 | 63 | impl Default for Location { 64 | fn default() -> Self { 65 | Self::Default 66 | } 67 | } 68 | 69 | /// Entry age in the disk cache. Used by hybrid cache. 70 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 71 | pub enum Age { 72 | /// THe entry is still young and will be reserved in the disk cache for a while. 73 | Young, 74 | /// The entry is old any will be eviction from the disk cache soon. 75 | Old, 76 | } 77 | 78 | /// Source context for populated entry. 79 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 80 | pub struct Populated { 81 | /// The age of the entry. 82 | pub age: Age, 83 | } 84 | 85 | /// Entry source used by hybrid cache. 86 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 87 | pub enum Source { 88 | /// Comes from outer system of foyer. 89 | Outer, 90 | /// Populated from the disk cache. 91 | Populated(Populated), 92 | } 93 | 94 | impl Default for Source { 95 | fn default() -> Self { 96 | Self::Outer 97 | } 98 | } 99 | 100 | /// Entry level properties trait. 101 | /// 102 | /// The in-memory only cache and the hybrid cache may have different properties implementations to minimize the overhead 103 | /// of necessary properties in different scenarios. 104 | pub trait Properties: Send + Sync + 'static + Clone + Default + Debug { 105 | /// Set entry ephemeral. 106 | fn with_ephemeral(self, ephemeral: bool) -> Self; 107 | 108 | /// Entry ephemeral. 109 | fn ephemeral(&self) -> Option; 110 | 111 | /// Set entry hint. 112 | fn with_hint(self, hint: Hint) -> Self; 113 | 114 | /// Entry hint. 115 | fn hint(&self) -> Option; 116 | 117 | /// Set entry location. 118 | fn with_location(self, location: Location) -> Self; 119 | 120 | /// Entry location. 121 | fn location(&self) -> Option; 122 | 123 | /// Set entry source. 124 | fn with_source(self, source: Source) -> Self; 125 | 126 | /// Entry source. 127 | fn source(&self) -> Option; 128 | } 129 | -------------------------------------------------------------------------------- /foyer-common/src/rate.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::time::{Duration, Instant}; 16 | 17 | use parking_lot::Mutex; 18 | 19 | /// A rate limiter that returns the wait duration for limitation. 20 | #[derive(Debug)] 21 | pub struct RateLimiter { 22 | inner: Mutex, 23 | rate: f64, 24 | } 25 | 26 | #[derive(Debug)] 27 | struct Inner { 28 | quota: f64, 29 | 30 | last: Instant, 31 | } 32 | 33 | impl RateLimiter { 34 | /// Create a rate limiter that returns the wait duration for limitation. 35 | pub fn new(rate: f64) -> Self { 36 | let inner = Inner { 37 | quota: 0.0, 38 | last: Instant::now(), 39 | }; 40 | Self { 41 | rate, 42 | inner: Mutex::new(inner), 43 | } 44 | } 45 | 46 | /// Consume some quota from the rate limiter. 47 | /// 48 | /// If there is not enough quota left, return a duration for the caller to wait. 49 | pub fn consume(&self, weight: f64) -> Option { 50 | let mut inner = self.inner.lock(); 51 | let now = Instant::now(); 52 | let refill = now.duration_since(inner.last).as_secs_f64() * self.rate; 53 | inner.last = now; 54 | inner.quota = f64::min(inner.quota + refill, self.rate); 55 | inner.quota -= weight; 56 | if inner.quota >= 0.0 { 57 | return None; 58 | } 59 | let wait = Duration::from_secs_f64((-inner.quota) / self.rate); 60 | Some(wait) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use std::sync::{ 67 | atomic::{AtomicUsize, Ordering}, 68 | Arc, 69 | }; 70 | 71 | use rand::{rng, Rng}; 72 | 73 | use super::*; 74 | 75 | const ERATIO: f64 = 0.05; 76 | const THREADS: usize = 8; 77 | const RATE: usize = 1000; 78 | const DURATION: Duration = Duration::from_secs(10); 79 | 80 | #[ignore] 81 | #[test] 82 | fn test_rate_limiter() { 83 | let v = Arc::new(AtomicUsize::new(0)); 84 | let limiter = Arc::new(RateLimiter::new(RATE as f64)); 85 | let task = |rate: usize, v: Arc, limiter: Arc| { 86 | let start = Instant::now(); 87 | loop { 88 | if start.elapsed() >= DURATION { 89 | break; 90 | } 91 | if let Some(dur) = limiter.consume(rate as f64) { 92 | std::thread::sleep(dur); 93 | } 94 | v.fetch_add(rate, Ordering::Relaxed); 95 | } 96 | }; 97 | let mut handles = vec![]; 98 | let mut rng = rng(); 99 | for _ in 0..THREADS { 100 | let rate = rng.random_range(10..20); 101 | let handle = std::thread::spawn({ 102 | let v = v.clone(); 103 | let limiter = limiter.clone(); 104 | move || task(rate, v, limiter) 105 | }); 106 | handles.push(handle); 107 | } 108 | 109 | for handle in handles { 110 | handle.join().unwrap(); 111 | } 112 | 113 | let error = (v.load(Ordering::Relaxed) as isize - RATE as isize * DURATION.as_secs() as isize).unsigned_abs(); 114 | let eratio = error as f64 / (RATE as f64 * DURATION.as_secs_f64()); 115 | assert!(eratio < ERATIO, "eratio: {}, target: {}", eratio, ERATIO); 116 | println!("eratio {eratio} < ERATIO {ERATIO}"); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /foyer-common/src/rated_ticket.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::time::Instant; 16 | 17 | use parking_lot::Mutex; 18 | 19 | /// A ticket-based rate limiter. 20 | #[derive(Debug)] 21 | pub struct RatedTicket { 22 | inner: Mutex, 23 | rate: f64, 24 | } 25 | 26 | #[derive(Debug)] 27 | struct Inner { 28 | quota: f64, 29 | 30 | last: Instant, 31 | } 32 | 33 | impl RatedTicket { 34 | /// Create a ticket-based rate limiter. 35 | pub fn new(rate: f64) -> Self { 36 | let inner = Inner { 37 | quota: 0.0, 38 | last: Instant::now(), 39 | }; 40 | Self { 41 | rate, 42 | inner: Mutex::new(inner), 43 | } 44 | } 45 | 46 | /// Check if there is still some quota left. 47 | pub fn probe(&self) -> bool { 48 | let mut inner = self.inner.lock(); 49 | 50 | let now = Instant::now(); 51 | let refill = now.duration_since(inner.last).as_secs_f64() * self.rate; 52 | inner.last = now; 53 | inner.quota = f64::min(inner.quota + refill, self.rate); 54 | 55 | inner.quota > 0.0 56 | } 57 | 58 | /// Reduce some quota manually. 59 | pub fn reduce(&self, weight: f64) { 60 | self.inner.lock().quota -= weight; 61 | } 62 | 63 | /// Consume some quota from the rate limiter. 64 | /// 65 | /// If there enough quota left, returns `true`; otherwise, returns `false`. 66 | pub fn consume(&self, weight: f64) -> bool { 67 | let mut inner = self.inner.lock(); 68 | 69 | let now = Instant::now(); 70 | let refill = now.duration_since(inner.last).as_secs_f64() * self.rate; 71 | inner.last = now; 72 | inner.quota = f64::min(inner.quota + refill, self.rate); 73 | 74 | if inner.quota <= 0.0 { 75 | return false; 76 | } 77 | 78 | inner.quota -= weight; 79 | 80 | true 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod tests { 86 | use std::{ 87 | sync::{ 88 | atomic::{AtomicUsize, Ordering}, 89 | Arc, 90 | }, 91 | time::Duration, 92 | }; 93 | 94 | use itertools::Itertools; 95 | use rand::{rng, Rng}; 96 | 97 | use super::*; 98 | 99 | #[ignore] 100 | #[test] 101 | fn test_rated_ticket_consume() { 102 | test(consume) 103 | } 104 | 105 | #[ignore] 106 | #[test] 107 | fn test_rated_ticket_probe_reduce() { 108 | test(probe_reduce) 109 | } 110 | 111 | fn test(f: F) 112 | where 113 | F: Fn(usize, &Arc, &Arc) + Send + Sync + Copy + 'static, 114 | { 115 | const CASES: usize = 10; 116 | const ERATIO: f64 = 0.05; 117 | 118 | let handles = (0..CASES).map(|_| std::thread::spawn(move || case(f))).collect_vec(); 119 | let mut eratios = vec![]; 120 | for handle in handles { 121 | let eratio = handle.join().unwrap(); 122 | assert!(eratio < ERATIO, "eratio: {} < ERATIO: {}", eratio, ERATIO); 123 | eratios.push(eratio); 124 | } 125 | println!("========== RatedTicket error ratio begin =========="); 126 | for eratio in eratios { 127 | println!("eratio: {eratio}"); 128 | } 129 | println!("=========== RatedTicket error ratio end ==========="); 130 | } 131 | 132 | fn consume(weight: usize, v: &Arc, limiter: &Arc) { 133 | if limiter.consume(weight as f64) { 134 | v.fetch_add(weight, Ordering::Relaxed); 135 | } 136 | } 137 | 138 | fn probe_reduce(weight: usize, v: &Arc, limiter: &Arc) { 139 | if limiter.probe() { 140 | limiter.reduce(weight as f64); 141 | v.fetch_add(weight, Ordering::Relaxed); 142 | } 143 | } 144 | 145 | fn case(f: F) -> f64 146 | where 147 | F: Fn(usize, &Arc, &Arc) + Send + Sync + Copy + 'static, 148 | { 149 | const THREADS: usize = 8; 150 | const RATE: usize = 1000; 151 | const DURATION: Duration = Duration::from_secs(10); 152 | 153 | let v = Arc::new(AtomicUsize::new(0)); 154 | let limiter = Arc::new(RatedTicket::new(RATE as f64)); 155 | let task = |rate: usize, v: Arc, limiter: Arc, f: F| { 156 | let start = Instant::now(); 157 | let mut rng = rng(); 158 | loop { 159 | if start.elapsed() >= DURATION { 160 | break; 161 | } 162 | std::thread::sleep(Duration::from_millis(rng.random_range(1..10))); 163 | f(rate, &v, &limiter) 164 | } 165 | }; 166 | let mut handles = vec![]; 167 | let mut rng = rng(); 168 | for _ in 0..THREADS { 169 | let rate = rng.random_range(10..20); 170 | let handle = std::thread::spawn({ 171 | let v = v.clone(); 172 | let limiter = limiter.clone(); 173 | move || task(rate, v, limiter, f) 174 | }); 175 | handles.push(handle); 176 | } 177 | 178 | for handle in handles { 179 | handle.join().unwrap(); 180 | } 181 | 182 | let error = (v.load(Ordering::Relaxed) as isize - RATE as isize * DURATION.as_secs() as isize).unsigned_abs(); 183 | error as f64 / (RATE as f64 * DURATION.as_secs_f64()) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /foyer-common/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Extensions for [`std::option::Option`]. 16 | pub mod option; 17 | /// The range extensions. 18 | pub mod range; 19 | /// A kotlin like functional programming helper. 20 | pub mod scope; 21 | -------------------------------------------------------------------------------- /foyer-common/src/utils/option.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Extension for [`std::option::Option`]. 16 | pub trait OptionExt { 17 | /// Wrapped type by [`Option`]. 18 | type Val; 19 | 20 | /// Consume the wrapped value with the given function if there is. 21 | fn then(self, f: F) 22 | where 23 | F: FnOnce(Self::Val); 24 | } 25 | 26 | impl OptionExt for Option { 27 | type Val = T; 28 | 29 | fn then(self, f: F) 30 | where 31 | F: FnOnce(Self::Val), 32 | { 33 | if let Some(val) = self { 34 | f(val) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /foyer-common/src/utils/range.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::ops::{Add, Bound, Range, RangeBounds, Sub}; 16 | 17 | mod private { 18 | 19 | pub trait ZeroOne { 20 | fn zero() -> Self; 21 | fn one() -> Self; 22 | } 23 | 24 | macro_rules! impl_one { 25 | ($($t:ty),*) => { 26 | $( 27 | impl ZeroOne for $t { 28 | fn zero() -> Self { 29 | 0 as $t 30 | } 31 | 32 | fn one() -> Self { 33 | 1 as $t 34 | } 35 | } 36 | )* 37 | }; 38 | } 39 | 40 | macro_rules! for_all_num_type { 41 | ($macro:ident) => { 42 | $macro! { u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64 } 43 | }; 44 | } 45 | 46 | for_all_num_type! { impl_one } 47 | } 48 | 49 | use private::ZeroOne; 50 | 51 | /// The range extensions. 52 | pub trait RangeBoundsExt< 53 | T: PartialOrd + Add + Sub + Clone + Copy + Send + Sync + 'static + ZeroOne, 54 | >: RangeBounds 55 | { 56 | /// Get the start bound of the range. 57 | fn start(&self) -> Option { 58 | match self.start_bound() { 59 | Bound::Included(v) => Some(*v), 60 | Bound::Excluded(v) => Some(*v + ZeroOne::one()), 61 | Bound::Unbounded => None, 62 | } 63 | } 64 | 65 | /// Get the end bound of the range. 66 | fn end(&self) -> Option { 67 | match self.end_bound() { 68 | Bound::Included(v) => Some(*v + ZeroOne::one()), 69 | Bound::Excluded(v) => Some(*v), 70 | Bound::Unbounded => None, 71 | } 72 | } 73 | 74 | /// Get the start bound with a default value of the range. 75 | fn start_with_bound(&self, bound: T) -> T { 76 | self.start().unwrap_or(bound) 77 | } 78 | 79 | /// Get the end bound with a default value of the range. 80 | fn end_with_bound(&self, bound: T) -> T { 81 | self.end().unwrap_or(bound) 82 | } 83 | 84 | /// Get the new range with the given range bounds. 85 | fn bounds(&self, range: Range) -> Range { 86 | let start = self.start_with_bound(range.start); 87 | let end = self.end_with_bound(range.end); 88 | start..end 89 | } 90 | 91 | /// Get the range size. 92 | fn size(&self) -> Option { 93 | let start = self.start()?; 94 | let end = self.end()?; 95 | Some(end - start) 96 | } 97 | 98 | /// Check if the range is empty. 99 | fn is_empty(&self) -> bool { 100 | match self.size() { 101 | Some(len) => len == ZeroOne::zero(), 102 | None => false, 103 | } 104 | } 105 | 106 | /// Check is the range is a full range. 107 | fn is_full(&self) -> bool { 108 | self.start_bound() == Bound::Unbounded && self.end_bound() == Bound::Unbounded 109 | } 110 | 111 | /// Map the range with the given method. 112 | fn map(&self, f: F) -> (Bound, Bound) 113 | where 114 | F: Fn(&T) -> R, 115 | { 116 | (self.start_bound().map(&f), self.end_bound().map(&f)) 117 | } 118 | } 119 | 120 | impl< 121 | T: PartialOrd + Add + Sub + Clone + Copy + Send + Sync + 'static + ZeroOne, 122 | RB: RangeBounds, 123 | > RangeBoundsExt for RB 124 | { 125 | } 126 | -------------------------------------------------------------------------------- /foyer-common/src/utils/scope.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// Scoped functional programming extensions. 16 | pub trait Scope { 17 | /// Scoped with ownership. 18 | fn with(self, f: F) -> R 19 | where 20 | Self: Sized, 21 | F: FnOnce(Self) -> R, 22 | { 23 | f(self) 24 | } 25 | 26 | /// Scoped with reference. 27 | fn with_ref(&self, f: F) -> R 28 | where 29 | F: FnOnce(&Self) -> R, 30 | { 31 | f(self) 32 | } 33 | 34 | /// Scoped with mutable reference. 35 | fn with_mut(&mut self, f: F) -> R 36 | where 37 | F: FnOnce(&mut Self) -> R, 38 | { 39 | f(self) 40 | } 41 | } 42 | impl Scope for T {} 43 | -------------------------------------------------------------------------------- /foyer-memory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foyer-memory" 3 | description = "memory cache for foyer - Hybrid cache for Rust" 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | rust-version = { workspace = true } 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = { workspace = true } 10 | authors = { workspace = true } 11 | license = { workspace = true } 12 | readme = { workspace = true } 13 | 14 | [dependencies] 15 | ahash = { workspace = true } 16 | arc-swap = "1" 17 | bitflags = "2" 18 | cmsketch = "0.2.1" 19 | equivalent = { workspace = true } 20 | fastrace = { workspace = true, optional = true } 21 | foyer-common = { workspace = true } 22 | hashbrown = { workspace = true } 23 | intrusive-collections = { package = "foyer-intrusive-collections", version = "0.10.0-dev" } 24 | itertools = { workspace = true } 25 | mixtrics = { workspace = true } 26 | parking_lot = { workspace = true } 27 | pin-project = "1" 28 | serde = { workspace = true } 29 | thiserror = { workspace = true } 30 | tracing = { workspace = true } 31 | 32 | [dev-dependencies] 33 | csv = "1.3.0" 34 | futures-util = { workspace = true } 35 | moka = { version = "0.12", features = ["sync"] } 36 | rand = { workspace = true, features = ["small_rng"] } 37 | rand_distr = { workspace = true } 38 | test-log = { workspace = true } 39 | 40 | [target.'cfg(madsim)'.dependencies] 41 | tokio = { package = "madsim-tokio", version = "0.2", features = [ 42 | "rt", 43 | "rt-multi-thread", 44 | "sync", 45 | "macros", 46 | "time", 47 | "signal", 48 | "fs", 49 | ] } 50 | 51 | [target.'cfg(not(madsim))'.dependencies] 52 | tokio = { package = "tokio", version = "1", features = [ 53 | "rt", 54 | "rt-multi-thread", 55 | "sync", 56 | "macros", 57 | "time", 58 | "signal", 59 | "fs", 60 | ] } 61 | 62 | [features] 63 | nightly = ["hashbrown/nightly"] 64 | test_utils = [] 65 | deadlock = ["parking_lot/deadlock_detection"] 66 | strict_assertions = ["foyer-common/strict_assertions"] 67 | tracing = ["fastrace/enable", "foyer-common/tracing"] 68 | 69 | [[bench]] 70 | name = "bench_hit_ratio" 71 | harness = false 72 | 73 | [[bench]] 74 | name = "bench_dynamic_dispatch" 75 | harness = false 76 | 77 | [lints] 78 | workspace = true 79 | -------------------------------------------------------------------------------- /foyer-memory/benches/bench_dynamic_dispatch.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! micro benchmark for dynamic dispatch 16 | 17 | use std::{ 18 | sync::Arc, 19 | time::{Duration, Instant}, 20 | }; 21 | 22 | use rand::{distr::Alphanumeric, rng, Rng}; 23 | 24 | struct T 25 | where 26 | F: Fn(&str) -> usize, 27 | { 28 | f1: F, 29 | f2: Box usize>, 30 | f3: Arc usize>, 31 | } 32 | 33 | fn rand_string(len: usize) -> String { 34 | rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() 35 | } 36 | 37 | fn bench_static_dispatch(t: &T, loops: usize) -> Duration 38 | where 39 | F: Fn(&str) -> usize, 40 | { 41 | let mut dur = Duration::default(); 42 | for _ in 0..loops { 43 | let s = rand_string(rng().random_range(0..100)); 44 | let now = Instant::now(); 45 | let _ = (t.f1)(&s); 46 | dur += now.elapsed(); 47 | } 48 | Duration::from_nanos((dur.as_nanos() as usize / loops) as _) 49 | } 50 | 51 | fn bench_box_dynamic_dispatch(t: &T, loops: usize) -> Duration 52 | where 53 | F: Fn(&str) -> usize, 54 | { 55 | let mut dur = Duration::default(); 56 | for _ in 0..loops { 57 | let s = rand_string(rng().random_range(0..100)); 58 | let now = Instant::now(); 59 | let _ = (t.f3)(&s); 60 | dur += now.elapsed(); 61 | } 62 | Duration::from_nanos((dur.as_nanos() as usize / loops) as _) 63 | } 64 | 65 | fn bench_arc_dynamic_dispatch(t: &T, loops: usize) -> Duration 66 | where 67 | F: Fn(&str) -> usize, 68 | { 69 | let mut dur = Duration::default(); 70 | for _ in 0..loops { 71 | let s = rand_string(rng().random_range(0..100)); 72 | let now = Instant::now(); 73 | let _ = (t.f2)(&s); 74 | dur += now.elapsed(); 75 | } 76 | Duration::from_nanos((dur.as_nanos() as usize / loops) as _) 77 | } 78 | 79 | fn main() { 80 | let t = T { 81 | f1: |s: &str| s.len(), 82 | f2: Box::new(|s: &str| s.len()), 83 | f3: Arc::new(|s: &str| s.len()), 84 | }; 85 | 86 | let _ = T { 87 | f1: |s: &str| s.len(), 88 | f2: Box::new(|s: &str| s.len() + 1), 89 | f3: Arc::new(|s: &str| s.len() + 1), 90 | }; 91 | 92 | let _ = T { 93 | f1: |s: &str| s.len(), 94 | f2: Box::new(|s: &str| s.len() + 2), 95 | f3: Arc::new(|s: &str| s.len() + 2), 96 | }; 97 | 98 | let _ = T { 99 | f1: |s: &str| s.len(), 100 | f2: Box::new(|s: &str| s.len() + 3), 101 | f3: Arc::new(|s: &str| s.len() + 3), 102 | }; 103 | 104 | for loops in [100_000, 1_000_000, 10_000_000] { 105 | println!(); 106 | 107 | println!(" static - {} loops : {:?}", loops, bench_static_dispatch(&t, loops)); 108 | println!( 109 | "box dynamic - {} loops : {:?}", 110 | loops, 111 | bench_box_dynamic_dispatch(&t, loops) 112 | ); 113 | println!( 114 | "arc dynamic - {} loops : {:?}", 115 | loops, 116 | bench_arc_dynamic_dispatch(&t, loops) 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /foyer-memory/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::fmt::Display; 16 | 17 | /// In-memory cache error. 18 | #[derive(thiserror::Error, Debug)] 19 | pub enum Error { 20 | /// Multiple error list. 21 | #[error(transparent)] 22 | Multiple(MultipleError), 23 | /// Config error. 24 | #[error("config error: {0}")] 25 | ConfigError(String), 26 | } 27 | 28 | impl Error { 29 | /// Combine multiple errors into one error. 30 | pub fn multiple(errs: Vec) -> Self { 31 | Self::Multiple(MultipleError(errs)) 32 | } 33 | } 34 | 35 | #[derive(thiserror::Error, Debug)] 36 | pub struct MultipleError(Vec); 37 | 38 | impl Display for MultipleError { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | write!(f, "multiple errors: [")?; 41 | if let Some((last, errs)) = self.0.as_slice().split_last() { 42 | for err in errs { 43 | write!(f, "{}, ", err)?; 44 | } 45 | write!(f, "{}", last)?; 46 | } 47 | write!(f, "]")?; 48 | Ok(()) 49 | } 50 | } 51 | 52 | /// In-memory cache result. 53 | pub type Result = std::result::Result; 54 | -------------------------------------------------------------------------------- /foyer-memory/src/eviction/fifo.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{mem::offset_of, sync::Arc}; 16 | 17 | use foyer_common::{ 18 | code::{Key, Value}, 19 | properties::Properties, 20 | }; 21 | use intrusive_collections::{intrusive_adapter, LinkedList, LinkedListAtomicLink}; 22 | use serde::{Deserialize, Serialize}; 23 | 24 | use super::{Eviction, Op}; 25 | use crate::{error::Result, record::Record}; 26 | 27 | /// Fifo eviction algorithm config. 28 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 29 | pub struct FifoConfig {} 30 | 31 | /// Fifo eviction algorithm state. 32 | #[derive(Debug, Default)] 33 | pub struct FifoState { 34 | link: LinkedListAtomicLink, 35 | } 36 | 37 | intrusive_adapter! { Adapter = Arc>>: Record> { ?offset = Record::>::STATE_OFFSET + offset_of!(FifoState, link) => LinkedListAtomicLink } where K: Key, V: Value, P: Properties } 38 | 39 | pub struct Fifo 40 | where 41 | K: Key, 42 | V: Value, 43 | P: Properties, 44 | { 45 | queue: LinkedList>, 46 | } 47 | 48 | impl Eviction for Fifo 49 | where 50 | K: Key, 51 | V: Value, 52 | P: Properties, 53 | { 54 | type Config = FifoConfig; 55 | type Key = K; 56 | type Value = V; 57 | type Properties = P; 58 | type State = FifoState; 59 | 60 | fn new(_capacity: usize, _config: &Self::Config) -> Self 61 | where 62 | Self: Sized, 63 | { 64 | Self { 65 | queue: LinkedList::new(Adapter::new()), 66 | } 67 | } 68 | 69 | fn update(&mut self, _: usize, _: Option<&Self::Config>) -> Result<()> { 70 | Ok(()) 71 | } 72 | 73 | fn push(&mut self, record: Arc>) { 74 | record.set_in_eviction(true); 75 | self.queue.push_back(record); 76 | } 77 | 78 | fn pop(&mut self) -> Option>> { 79 | self.queue.pop_front().inspect(|record| record.set_in_eviction(false)) 80 | } 81 | 82 | fn remove(&mut self, record: &Arc>) { 83 | unsafe { self.queue.remove_from_ptr(Arc::as_ptr(record)) }; 84 | record.set_in_eviction(false); 85 | } 86 | 87 | fn acquire() -> Op { 88 | Op::noop() 89 | } 90 | 91 | fn release() -> Op { 92 | Op::noop() 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | pub mod tests { 98 | 99 | use itertools::Itertools; 100 | 101 | use super::*; 102 | use crate::{ 103 | eviction::test_utils::{assert_ptr_eq, assert_ptr_vec_eq, Dump, TestProperties}, 104 | record::Data, 105 | }; 106 | 107 | impl Dump for Fifo 108 | where 109 | K: Key + Clone, 110 | V: Value + Clone, 111 | { 112 | type Output = Vec>>; 113 | fn dump(&self) -> Self::Output { 114 | let mut res = vec![]; 115 | let mut cursor = self.queue.cursor(); 116 | loop { 117 | cursor.move_next(); 118 | match cursor.clone_pointer() { 119 | Some(record) => res.push(record), 120 | None => break, 121 | } 122 | } 123 | res 124 | } 125 | } 126 | 127 | type TestFifo = Fifo; 128 | 129 | #[test] 130 | fn test_fifo() { 131 | let rs = (0..8) 132 | .map(|i| { 133 | Arc::new(Record::new(Data { 134 | key: i, 135 | value: i, 136 | properties: TestProperties::default(), 137 | hash: i, 138 | weight: 1, 139 | })) 140 | }) 141 | .collect_vec(); 142 | let r = |i: usize| rs[i].clone(); 143 | 144 | let mut fifo = TestFifo::new(100, &FifoConfig {}); 145 | 146 | // 0, 1, 2, 3 147 | fifo.push(r(0)); 148 | fifo.push(r(1)); 149 | fifo.push(r(2)); 150 | fifo.push(r(3)); 151 | 152 | // 2, 3 153 | let r0 = fifo.pop().unwrap(); 154 | let r1 = fifo.pop().unwrap(); 155 | assert_ptr_eq(&rs[0], &r0); 156 | assert_ptr_eq(&rs[1], &r1); 157 | 158 | // 2, 3, 4, 5, 6 159 | fifo.push(r(4)); 160 | fifo.push(r(5)); 161 | fifo.push(r(6)); 162 | 163 | // 2, 6 164 | fifo.remove(&rs[3]); 165 | fifo.remove(&rs[4]); 166 | fifo.remove(&rs[5]); 167 | 168 | assert_ptr_vec_eq(fifo.dump(), vec![r(2), r(6)]); 169 | 170 | fifo.clear(); 171 | 172 | assert_ptr_vec_eq(fifo.dump(), vec![]); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /foyer-memory/src/eviction/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::Arc; 16 | 17 | use foyer_common::{ 18 | code::{Key, Value}, 19 | properties::Properties, 20 | }; 21 | use serde::{de::DeserializeOwned, Serialize}; 22 | 23 | use crate::{error::Result, record::Record}; 24 | 25 | pub trait State: Send + Sync + 'static + Default {} 26 | impl State for T where T: Send + Sync + 'static + Default {} 27 | 28 | pub trait Config: Send + Sync + 'static + Clone + Serialize + DeserializeOwned + Default {} 29 | impl Config for T where T: Send + Sync + 'static + Clone + Serialize + DeserializeOwned + Default {} 30 | 31 | /// Wrapper for one of the three kind of operations for the eviction container: 32 | /// 33 | /// 1. no operation 34 | /// 2. immutable operation 35 | /// 3. mutable operation 36 | #[expect(clippy::type_complexity)] 37 | pub enum Op 38 | where 39 | E: Eviction, 40 | { 41 | /// no operation 42 | Noop, 43 | /// immutable operation 44 | Immutable(Box>) + Send + Sync + 'static>), 45 | /// mutable operation 46 | Mutable(Box>) + Send + Sync + 'static>), 47 | } 48 | 49 | impl Op 50 | where 51 | E: Eviction, 52 | { 53 | /// no operation 54 | pub fn noop() -> Self { 55 | Self::Noop 56 | } 57 | 58 | /// immutable operation 59 | pub fn immutable(f: F) -> Self 60 | where 61 | F: Fn(&E, &Arc>) + Send + Sync + 'static, 62 | { 63 | Self::Immutable(Box::new(f)) 64 | } 65 | 66 | /// mutable operation 67 | pub fn mutable(f: F) -> Self 68 | where 69 | F: FnMut(&mut E, &Arc>) + Send + Sync + 'static, 70 | { 71 | Self::Mutable(Box::new(f)) 72 | } 73 | } 74 | 75 | /// Cache eviction algorithm abstraction. 76 | /// 77 | /// [`Eviction`] provides essential APIs for the plug-and-play algorithm abstraction. 78 | /// 79 | /// [`Eviction`] is needs to be implemented to support a new cache eviction algorithm. 80 | /// 81 | /// # Safety 82 | /// 83 | /// The pointer can be dereferenced as a mutable reference ***iff*** the `self` reference is also mutable. 84 | /// Dereferencing a pointer as a mutable reference when `self` is immutable will cause UB. 85 | pub trait Eviction: Send + Sync + 'static + Sized { 86 | /// Cache eviction algorithm configurations. 87 | type Config: Config; 88 | /// Cache key. Generally, it is supposed to be a generic type of the implementation. 89 | type Key: Key; 90 | /// Cache value. Generally, it is supposed to be a generic type of the implementation. 91 | type Value: Value; 92 | /// Properties for a cache entry, it is supposed to be a generic type of the implementation. 93 | /// 94 | /// Can be used to support priority at the entry granularity. 95 | type Properties: Properties; 96 | /// State for a cache entry. Mutable state for maintaining the cache eviction algorithm implementation. 97 | type State: State; 98 | 99 | /// Create a new cache eviction algorithm instance with the given arguments. 100 | fn new(capacity: usize, config: &Self::Config) -> Self; 101 | 102 | /// Update the arguments of the ache eviction algorithm instance. 103 | fn update(&mut self, capacity: usize, config: Option<&Self::Config>) -> Result<()>; 104 | 105 | /// Push a record into the cache eviction algorithm instance. 106 | /// 107 | /// The caller guarantees that the record is NOT in the cache eviction algorithm instance. 108 | /// 109 | /// The cache eviction algorithm instance MUST hold the record and set its `IN_EVICTION` flag to true. 110 | fn push(&mut self, record: Arc>); 111 | 112 | /// Push a record from the cache eviction algorithm instance. 113 | /// 114 | /// The cache eviction algorithm instance MUST remove the record and set its `IN_EVICTION` flag to false. 115 | fn pop(&mut self) -> Option>>; 116 | 117 | /// Remove a record from the cache eviction algorithm instance. 118 | /// 119 | /// The caller guarantees that the record is in the cache eviction algorithm instance. 120 | /// 121 | /// The cache eviction algorithm instance MUST remove the record and set its `IN_EVICTION` flag to false. 122 | fn remove(&mut self, record: &Arc>); 123 | 124 | /// Remove all records from the cache eviction algorithm instance. 125 | /// 126 | /// The cache eviction algorithm instance MUST remove the records and set its `IN_EVICTION` flag to false. 127 | fn clear(&mut self) { 128 | while self.pop().is_some() {} 129 | } 130 | 131 | /// `acquire` is called when an external caller acquire a cache entry from the cache. 132 | /// 133 | /// The entry can be EITHER in the cache eviction algorithm instance or not. 134 | fn acquire() -> Op; 135 | 136 | /// `release` is called when the last external caller drops the cache entry. 137 | /// 138 | /// The entry can be EITHER in the cache eviction algorithm instance or not. 139 | fn release() -> Op; 140 | } 141 | 142 | pub mod fifo; 143 | pub mod lfu; 144 | pub mod lru; 145 | pub mod s3fifo; 146 | 147 | #[cfg(any(test, feature = "test_utils"))] 148 | pub mod test_utils; 149 | -------------------------------------------------------------------------------- /foyer-memory/src/eviction/test_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::Arc; 16 | 17 | use foyer_common::properties::{Hint, Location, Properties, Source}; 18 | use itertools::Itertools; 19 | 20 | use crate::{ 21 | eviction::{Eviction, Op}, 22 | record::Record, 23 | }; 24 | #[expect(dead_code)] 25 | pub trait OpExt: Eviction { 26 | fn acquire_immutable(&self, record: &Arc>) { 27 | match Self::acquire() { 28 | Op::Immutable(f) => f(self, record), 29 | _ => unreachable!(), 30 | } 31 | } 32 | 33 | fn acquire_mutable(&mut self, record: &Arc>) { 34 | match Self::acquire() { 35 | Op::Mutable(mut f) => f(self, record), 36 | _ => unreachable!(), 37 | } 38 | } 39 | 40 | fn release_immutable(&self, record: &Arc>) { 41 | match Self::release() { 42 | Op::Immutable(f) => f(self, record), 43 | _ => unreachable!(), 44 | } 45 | } 46 | 47 | fn release_mutable(&mut self, record: &Arc>) { 48 | match Self::release() { 49 | Op::Mutable(mut f) => f(self, record), 50 | _ => unreachable!(), 51 | } 52 | } 53 | } 54 | 55 | impl OpExt for E where E: Eviction {} 56 | 57 | #[cfg_attr(not(test), expect(dead_code))] 58 | pub trait Dump: Eviction { 59 | type Output; 60 | fn dump(&self) -> Self::Output; 61 | } 62 | 63 | #[cfg_attr(not(test), expect(dead_code))] 64 | pub fn assert_ptr_eq(a: &Arc, b: &Arc) { 65 | assert_eq!(Arc::as_ptr(a), Arc::as_ptr(b)); 66 | } 67 | 68 | #[cfg_attr(not(test), expect(dead_code))] 69 | pub fn assert_ptr_vec_eq(va: Vec>, vb: Vec>) { 70 | let trans = |v: Vec>| v.iter().map(Arc::as_ptr).collect_vec(); 71 | assert_eq!(trans(va), trans(vb)); 72 | } 73 | 74 | #[cfg_attr(not(test), expect(dead_code))] 75 | pub fn assert_ptr_vec_vec_eq(vva: Vec>>, vvb: Vec>>) { 76 | let trans = |vv: Vec>>| vv.iter().map(|v| v.iter().map(Arc::as_ptr).collect_vec()).collect_vec(); 77 | 78 | assert_eq!(trans(vva), trans(vvb)); 79 | } 80 | 81 | /// Properties for test, support all properties. 82 | #[derive(Debug, Clone, Default)] 83 | pub struct TestProperties { 84 | ephemeral: bool, 85 | hint: Hint, 86 | location: Location, 87 | source: Source, 88 | } 89 | 90 | impl Properties for TestProperties { 91 | fn with_ephemeral(mut self, ephemeral: bool) -> Self { 92 | self.ephemeral = ephemeral; 93 | self 94 | } 95 | 96 | fn ephemeral(&self) -> Option { 97 | Some(self.ephemeral) 98 | } 99 | 100 | fn with_hint(mut self, hint: Hint) -> Self { 101 | self.hint = hint; 102 | self 103 | } 104 | 105 | fn hint(&self) -> Option { 106 | Some(self.hint) 107 | } 108 | 109 | fn with_location(mut self, location: Location) -> Self { 110 | self.location = location; 111 | self 112 | } 113 | 114 | fn location(&self) -> Option { 115 | None 116 | } 117 | 118 | fn with_source(mut self, source: Source) -> Self { 119 | self.source = source; 120 | self 121 | } 122 | 123 | fn source(&self) -> Option { 124 | None 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /foyer-memory/src/indexer/hash_table.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::Arc; 16 | 17 | use hashbrown::hash_table::{Entry as HashTableEntry, HashTable}; 18 | 19 | use super::Indexer; 20 | use crate::{eviction::Eviction, record::Record}; 21 | 22 | pub struct HashTableIndexer 23 | where 24 | E: Eviction, 25 | { 26 | table: HashTable>>, 27 | } 28 | 29 | unsafe impl Send for HashTableIndexer where E: Eviction {} 30 | unsafe impl Sync for HashTableIndexer where E: Eviction {} 31 | 32 | impl Default for HashTableIndexer 33 | where 34 | E: Eviction, 35 | { 36 | fn default() -> Self { 37 | Self { 38 | table: Default::default(), 39 | } 40 | } 41 | } 42 | 43 | impl Indexer for HashTableIndexer 44 | where 45 | E: Eviction, 46 | { 47 | type Eviction = E; 48 | 49 | fn insert(&mut self, mut record: Arc>) -> Option>> { 50 | match self 51 | .table 52 | .entry(record.hash(), |r| r.key() == record.key(), |r| r.hash()) 53 | { 54 | HashTableEntry::Occupied(mut o) => { 55 | std::mem::swap(o.get_mut(), &mut record); 56 | Some(record) 57 | } 58 | HashTableEntry::Vacant(v) => { 59 | v.insert(record); 60 | None 61 | } 62 | } 63 | } 64 | 65 | fn get(&self, hash: u64, key: &Q) -> Option<&Arc>> 66 | where 67 | Q: std::hash::Hash + equivalent::Equivalent<::Key> + ?Sized, 68 | { 69 | self.table.find(hash, |r| key.equivalent(r.key())) 70 | } 71 | 72 | fn remove(&mut self, hash: u64, key: &Q) -> Option>> 73 | where 74 | Q: std::hash::Hash + equivalent::Equivalent<::Key> + ?Sized, 75 | { 76 | match self.table.entry(hash, |r| key.equivalent(r.key()), |r| r.hash()) { 77 | HashTableEntry::Occupied(o) => { 78 | let (r, _) = o.remove(); 79 | Some(r) 80 | } 81 | HashTableEntry::Vacant(_) => None, 82 | } 83 | } 84 | 85 | fn drain(&mut self) -> impl Iterator>> { 86 | self.table.drain() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /foyer-memory/src/indexer/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{hash::Hash, sync::Arc}; 16 | 17 | use equivalent::Equivalent; 18 | 19 | use crate::{eviction::Eviction, record::Record}; 20 | 21 | pub trait Indexer: Send + Sync + 'static + Default { 22 | type Eviction: Eviction; 23 | 24 | fn insert(&mut self, record: Arc>) -> Option>>; 25 | fn get(&self, hash: u64, key: &Q) -> Option<&Arc>> 26 | where 27 | Q: Hash + Equivalent<::Key> + ?Sized; 28 | fn remove(&mut self, hash: u64, key: &Q) -> Option>> 29 | where 30 | Q: Hash + Equivalent<::Key> + ?Sized; 31 | fn drain(&mut self) -> impl Iterator>>; 32 | } 33 | 34 | pub mod hash_table; 35 | pub mod sentry; 36 | -------------------------------------------------------------------------------- /foyer-memory/src/indexer/sentry.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{hash::Hash, sync::Arc}; 16 | 17 | use equivalent::Equivalent; 18 | use foyer_common::strict_assert; 19 | 20 | use super::Indexer; 21 | use crate::{eviction::Eviction, record::Record}; 22 | 23 | /// [`Sentry`] is a guard for all [`Indexer`] implementations to set `IN_INDEXER` flag properly. 24 | pub struct Sentry 25 | where 26 | I: Indexer, 27 | { 28 | indexer: I, 29 | } 30 | 31 | impl Default for Sentry 32 | where 33 | I: Indexer, 34 | { 35 | fn default() -> Self { 36 | Self { indexer: I::default() } 37 | } 38 | } 39 | 40 | impl Indexer for Sentry 41 | where 42 | I: Indexer, 43 | { 44 | type Eviction = I::Eviction; 45 | 46 | fn insert(&mut self, record: Arc>) -> Option>> { 47 | strict_assert!(!record.is_in_indexer()); 48 | record.set_in_indexer(true); 49 | self.indexer.insert(record).inspect(|old| { 50 | strict_assert!(old.is_in_indexer()); 51 | old.set_in_indexer(false); 52 | }) 53 | } 54 | 55 | fn get(&self, hash: u64, key: &Q) -> Option<&Arc>> 56 | where 57 | Q: Hash + Equivalent<::Key> + ?Sized, 58 | { 59 | self.indexer.get(hash, key).inspect(|r| { 60 | strict_assert!(r.is_in_indexer()); 61 | }) 62 | } 63 | 64 | fn remove(&mut self, hash: u64, key: &Q) -> Option>> 65 | where 66 | Q: Hash + Equivalent<::Key> + ?Sized, 67 | { 68 | self.indexer.remove(hash, key).inspect(|r| { 69 | strict_assert!(r.is_in_indexer()); 70 | r.set_in_indexer(false) 71 | }) 72 | } 73 | 74 | fn drain(&mut self) -> impl Iterator>> { 75 | self.indexer.drain().inspect(|r| { 76 | strict_assert!(r.is_in_indexer()); 77 | r.set_in_indexer(false) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /foyer-memory/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! This crate provides a concurrent in-memory cache component that supports replaceable eviction algorithm. 16 | //! 17 | //! # Motivation 18 | //! 19 | //! There are a few goals to achieve with the crate: 20 | //! 21 | //! 1. Plug-and-Play eviction algorithm with the same abstraction. 22 | //! 2. Tracking the real memory usage by the cache. Including both holding by the cache and by the external users. 23 | //! 3. Reduce the concurrent read-through requests into one. 24 | //! 25 | //! To achieve them, the crate needs to combine the advantages of the implementations of RocksDB and CacheLib. 26 | //! 27 | //! # Components 28 | //! 29 | //! The cache is mainly composed of the following components: 30 | //! 1. record : Carries the cached entry, reference count, pointer links in the eviction container, etc. 31 | //! 2. indexer : Indexes cached keys to the records. 32 | //! 3. eviction container : Defines the order of eviction. Usually implemented with intrusive data structures. 33 | //! 34 | //! Because a record needs to be referenced and mutated by both the indexer and the eviction container in the same 35 | //! thread, it is hard to implement in 100% safe Rust without overhead. So, accessing the algorithm managed per-entry 36 | //! state requires operation on the `UnsafeCell`. 37 | 38 | mod cache; 39 | mod error; 40 | mod eviction; 41 | mod indexer; 42 | mod pipe; 43 | mod raw; 44 | mod record; 45 | 46 | mod prelude; 47 | pub use prelude::*; 48 | 49 | #[cfg(any(test, feature = "test_utils"))] 50 | pub mod test_utils; 51 | -------------------------------------------------------------------------------- /foyer-memory/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub use ahash::RandomState; 16 | 17 | #[cfg(any(test, feature = "test_utils"))] 18 | pub use crate::eviction::test_utils::TestProperties; 19 | pub use crate::{ 20 | cache::{Cache, CacheBuilder, CacheEntry, CacheProperties, EvictionConfig, Fetch}, 21 | error::{Error, Result}, 22 | eviction::{fifo::FifoConfig, lfu::LfuConfig, lru::LruConfig, s3fifo::S3FifoConfig, Eviction, Op}, 23 | pipe::{Piece, Pipe}, 24 | raw::{FetchContext, FetchState, Weighter}, 25 | }; 26 | -------------------------------------------------------------------------------- /foyer-memory/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Utilities for testing. 16 | 17 | use std::{fmt::Debug, future::Future, pin::Pin, sync::Arc}; 18 | 19 | use foyer_common::{ 20 | code::{Key, Value}, 21 | properties::Properties, 22 | }; 23 | use parking_lot::{Mutex, MutexGuard}; 24 | 25 | use crate::{Piece, Pipe}; 26 | 27 | /// A pipe that records all sent pieces. 28 | pub struct PiecePipe { 29 | pieces: Arc>>>, 30 | } 31 | 32 | impl Debug for PiecePipe { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | f.debug_struct("PiecePipe").field("pieces", &self.pieces).finish() 35 | } 36 | } 37 | 38 | impl Clone for PiecePipe { 39 | fn clone(&self) -> Self { 40 | Self { 41 | pieces: self.pieces.clone(), 42 | } 43 | } 44 | } 45 | 46 | impl Default for PiecePipe { 47 | fn default() -> Self { 48 | Self { 49 | pieces: Default::default(), 50 | } 51 | } 52 | } 53 | 54 | impl Pipe for PiecePipe 55 | where 56 | K: Key, 57 | V: Value, 58 | P: Properties, 59 | { 60 | type Key = K; 61 | type Value = V; 62 | type Properties = P; 63 | 64 | fn is_enabled(&self) -> bool { 65 | true 66 | } 67 | 68 | fn send(&self, piece: Piece) { 69 | self.pieces.lock().push(piece); 70 | } 71 | 72 | fn flush( 73 | &self, 74 | mut pieces: Vec>, 75 | ) -> Pin + Send>> { 76 | self.pieces.lock().append(&mut pieces); 77 | Box::pin(async {}) 78 | } 79 | } 80 | 81 | impl PiecePipe { 82 | /// Get all sent pieces. 83 | pub fn pieces(&self) -> MutexGuard<'_, Vec>> { 84 | self.pieces.lock() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /foyer-storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foyer-storage" 3 | description = "storage engine for foyer - Hybrid cache for Rust" 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | rust-version = { workspace = true } 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = { workspace = true } 10 | authors = { workspace = true } 11 | license = { workspace = true } 12 | readme = { workspace = true } 13 | 14 | [dependencies] 15 | # TODO(MrCroxx): Remove this after `allocator_api` is stable. 16 | allocator-api2 = "0.2" 17 | anyhow = "1.0" 18 | # TODO(MrCroxx): use `array_chunks` after `#![feature(array_chunks)]` is stable. 19 | array-util = "1" 20 | auto_enums = { version = "0.8", features = ["futures03"] } 21 | bytes = { workspace = true } 22 | clap = { workspace = true } 23 | equivalent = { workspace = true } 24 | fastrace = { workspace = true, optional = true } 25 | flume = "0.11" 26 | foyer-common = { workspace = true } 27 | foyer-memory = { workspace = true } 28 | fs4 = { version = "0.13", default-features = false } 29 | futures-core = { workspace = true } 30 | futures-util = { workspace = true } 31 | itertools = { workspace = true } 32 | libc = "0.2" 33 | lz4 = "1.24" 34 | ordered_hash_map = "0.4" 35 | parking_lot = { workspace = true } 36 | paste = { workspace = true } 37 | pin-project = "1" 38 | rand = { workspace = true } 39 | serde = { workspace = true, optional = true } 40 | thiserror = { workspace = true } 41 | tracing = { workspace = true } 42 | twox-hash = "2" 43 | zstd = "0.13" 44 | 45 | [dev-dependencies] 46 | bytesize = { workspace = true } 47 | foyer-memory = { workspace = true, features = ["test_utils"] } 48 | tempfile = "3" 49 | test-log = { workspace = true } 50 | 51 | [target.'cfg(madsim)'.dependencies] 52 | tokio = { package = "madsim-tokio", version = "0.2", features = [ 53 | "rt", 54 | "rt-multi-thread", 55 | "sync", 56 | "macros", 57 | "time", 58 | "signal", 59 | "fs", 60 | ] } 61 | 62 | [target.'cfg(not(madsim))'.dependencies] 63 | tokio = { package = "tokio", version = "1", features = [ 64 | "rt", 65 | "rt-multi-thread", 66 | "sync", 67 | "macros", 68 | "time", 69 | "signal", 70 | "fs", 71 | ] } 72 | 73 | [features] 74 | default = [] 75 | serde = ["dep:serde"] 76 | tracing = ["fastrace/enable", "foyer-common/tracing", "foyer-memory/tracing"] 77 | nightly = ["allocator-api2/nightly"] 78 | test_utils = [] 79 | deadlock = ["parking_lot/deadlock_detection"] 80 | strict_assertions = [ 81 | "foyer-common/strict_assertions", 82 | "foyer-memory/strict_assertions", 83 | ] 84 | 85 | [lints] 86 | workspace = true 87 | 88 | [[test]] 89 | name = "storage_fuzzy_test" 90 | required-features = ["test_utils"] 91 | -------------------------------------------------------------------------------- /foyer-storage/src/compress.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // TODO(MrCroxx): unify compress interface? 16 | 17 | use clap::ValueEnum; 18 | 19 | use crate::error::Error; 20 | 21 | /// The compression algorithm of the disk cache. 22 | #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] 23 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 24 | pub enum Compression { 25 | /// No compression enabled. 26 | None, 27 | /// Use zstd compression. 28 | Zstd, 29 | /// Use lz4 compression. 30 | Lz4, 31 | } 32 | 33 | impl Compression { 34 | /// Get the u8 that represent the compression algorithm. 35 | pub fn to_u8(&self) -> u8 { 36 | match self { 37 | Self::None => 0, 38 | Self::Zstd => 1, 39 | Self::Lz4 => 2, 40 | } 41 | } 42 | } 43 | 44 | impl From for u8 { 45 | fn from(value: Compression) -> Self { 46 | match value { 47 | Compression::None => 0, 48 | Compression::Zstd => 1, 49 | Compression::Lz4 => 2, 50 | } 51 | } 52 | } 53 | 54 | impl TryFrom for Compression { 55 | type Error = Error; 56 | 57 | fn try_from(value: u8) -> Result { 58 | match value { 59 | 0 => Ok(Self::None), 60 | 1 => Ok(Self::Zstd), 61 | 2 => Ok(Self::Lz4), 62 | _ => Err(Error::CompressionAlgorithmNotSupported(value)), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /foyer-storage/src/device/allocator.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use allocator_api2::alloc::{AllocError, Allocator, Global}; 16 | 17 | #[derive(Debug, Clone, Copy)] 18 | pub struct AlignedAllocator; 19 | 20 | impl Default for AlignedAllocator { 21 | fn default() -> Self { 22 | Self::new() 23 | } 24 | } 25 | 26 | impl AlignedAllocator { 27 | pub const fn new() -> Self { 28 | assert!(N.is_power_of_two()); 29 | Self 30 | } 31 | } 32 | 33 | unsafe impl Allocator for AlignedAllocator { 34 | fn allocate(&self, layout: std::alloc::Layout) -> Result, AllocError> { 35 | Global.allocate(layout.align_to(N).unwrap()) 36 | } 37 | 38 | unsafe fn deallocate(&self, ptr: std::ptr::NonNull, layout: std::alloc::Layout) { 39 | Global.deallocate(ptr, layout.align_to(N).unwrap()) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use allocator_api2::vec::Vec as VecA; 46 | use foyer_common::bits; 47 | 48 | use super::*; 49 | 50 | #[test] 51 | fn test_aligned_buffer() { 52 | const ALIGN: usize = 512; 53 | let allocator = AlignedAllocator::::new(); 54 | 55 | let mut buf: VecA = VecA::with_capacity_in(ALIGN * 8, &allocator); 56 | bits::assert_aligned(ALIGN, buf.as_ptr() as _); 57 | 58 | buf.extend_from_slice(&[b'x'; ALIGN * 8]); 59 | bits::assert_aligned(ALIGN, buf.as_ptr() as _); 60 | assert_eq!(buf, [b'x'; ALIGN * 8]); 61 | 62 | buf.extend_from_slice(&[b'x'; ALIGN * 8]); 63 | bits::assert_aligned(ALIGN, buf.as_ptr() as _); 64 | assert_eq!(buf, [b'x'; ALIGN * 16]) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /foyer-storage/src/device/test_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use super::{monitor::Monitored, Dev, Device, MonitoredDevice, RegionId, Throttle}; 16 | use crate::{error::Result, runtime::Runtime, IoBuf, IoBufMut}; 17 | 18 | #[derive(Debug, Clone, Default)] 19 | pub struct NoopDevice(Throttle); 20 | 21 | impl NoopDevice { 22 | pub fn monitored() -> MonitoredDevice { 23 | Monitored::new_for_test(Device::Noop(Self::default())) 24 | } 25 | } 26 | 27 | impl Dev for NoopDevice { 28 | type Config = (); 29 | 30 | fn capacity(&self) -> usize { 31 | 0 32 | } 33 | 34 | fn region_size(&self) -> usize { 35 | 0 36 | } 37 | 38 | fn throttle(&self) -> &Throttle { 39 | &self.0 40 | } 41 | 42 | async fn open(_: Self::Config, _: Runtime) -> Result { 43 | Ok(Self(Throttle::default())) 44 | } 45 | 46 | async fn write(&self, buf: B, _: RegionId, _: u64) -> (B, Result<()>) 47 | where 48 | B: IoBuf, 49 | { 50 | (buf, Ok(())) 51 | } 52 | 53 | async fn read(&self, buf: B, _: RegionId, _: u64) -> (B, Result<()>) 54 | where 55 | B: IoBufMut, 56 | { 57 | (buf, Ok(())) 58 | } 59 | 60 | async fn flush(&self, _: Option) -> Result<()> { 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /foyer-storage/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | fmt::{Debug, Display}, 17 | ops::Range, 18 | }; 19 | 20 | use foyer_common::code::CodeError; 21 | 22 | /// Disk cache error type. 23 | #[derive(thiserror::Error, Debug)] 24 | pub enum Error { 25 | /// Code error. 26 | #[error("code error: {0}")] 27 | Code(#[from] CodeError), 28 | /// I/O error. 29 | #[error("io error: {0}")] 30 | Io(#[from] std::io::Error), 31 | #[error(transparent)] 32 | /// Multiple error list. 33 | Multiple(MultipleError), 34 | /// Entry magic mismatch. 35 | #[error("magic mismatch, expected: {expected}, get: {get}")] 36 | MagicMismatch { 37 | /// Expected magic. 38 | expected: u32, 39 | /// Gotten magic. 40 | get: u32, 41 | }, 42 | /// Entry checksum mismatch. 43 | #[error("checksum mismatch, expected: {expected}, get: {get}")] 44 | ChecksumMismatch { 45 | /// Expected checksum. 46 | expected: u64, 47 | /// Gotten checksum. 48 | get: u64, 49 | }, 50 | /// Out of range. 51 | #[error("out of range, valid: {valid:?}, get: {get:?}")] 52 | OutOfRange { 53 | /// Valid range. 54 | valid: Range, 55 | /// Gotten range. 56 | get: Range, 57 | }, 58 | /// Invalid I/O range. 59 | #[error("invalid io range: {range:?}, region size: {region_size}, capacity: {capacity}")] 60 | InvalidIoRange { 61 | /// I/O range 62 | range: Range, 63 | /// Region size 64 | region_size: usize, 65 | /// Capacity 66 | capacity: usize, 67 | }, 68 | /// Compression algorithm not supported. 69 | #[error("compression algorithm not supported: {0}")] 70 | CompressionAlgorithmNotSupported(u8), 71 | /// Other error. 72 | #[error(transparent)] 73 | Other(#[from] anyhow::Error), 74 | } 75 | 76 | impl Error { 77 | /// Combine multiple errors into one error. 78 | pub fn multiple(errs: Vec) -> Self { 79 | Self::Multiple(MultipleError(errs)) 80 | } 81 | } 82 | 83 | #[derive(thiserror::Error, Debug)] 84 | pub struct MultipleError(Vec); 85 | 86 | impl Display for MultipleError { 87 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 88 | write!(f, "multiple errors: [")?; 89 | if let Some((last, errs)) = self.0.as_slice().split_last() { 90 | for err in errs { 91 | write!(f, "{}, ", err)?; 92 | } 93 | write!(f, "{}", last)?; 94 | } 95 | write!(f, "]")?; 96 | Ok(()) 97 | } 98 | } 99 | 100 | /// Disk cache result type. 101 | pub type Result = core::result::Result; 102 | -------------------------------------------------------------------------------- /foyer-storage/src/io/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub mod buffer; 16 | pub mod throttle; 17 | 18 | pub const PAGE: usize = 4096; 19 | -------------------------------------------------------------------------------- /foyer-storage/src/large/indexer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | collections::{hash_map::Entry, HashMap}, 17 | sync::Arc, 18 | }; 19 | 20 | use itertools::Itertools; 21 | use parking_lot::RwLock; 22 | 23 | use crate::{device::RegionId, large::serde::Sequence}; 24 | 25 | #[derive(Debug)] 26 | pub struct HashedEntryAddress { 27 | pub hash: u64, 28 | pub address: EntryAddress, 29 | } 30 | 31 | #[derive(Debug, Clone, PartialEq, Eq)] 32 | pub struct EntryAddress { 33 | pub region: RegionId, 34 | pub offset: u32, 35 | pub len: u32, 36 | 37 | pub sequence: Sequence, 38 | } 39 | 40 | /// [`Indexer`] records key hash to entry address on fs. 41 | #[derive(Debug, Clone)] 42 | pub struct Indexer { 43 | shards: Arc>>>, 44 | } 45 | 46 | impl Indexer { 47 | pub fn new(shards: usize) -> Self { 48 | let shards = (0..shards).map(|_| RwLock::new(HashMap::new())).collect_vec(); 49 | Self { 50 | shards: Arc::new(shards), 51 | } 52 | } 53 | 54 | #[cfg_attr( 55 | feature = "tracing", 56 | fastrace::trace(name = "foyer::storage::large::indexer::insert_batch") 57 | )] 58 | pub fn insert_batch(&self, batch: Vec) -> Vec { 59 | let shards: HashMap> = 60 | batch.into_iter().into_group_map_by(|haddr| self.shard(haddr.hash)); 61 | 62 | let mut olds = vec![]; 63 | for (s, batch) in shards { 64 | let mut shard = self.shards[s].write(); 65 | for haddr in batch { 66 | if let Some(old) = self.insert_inner(&mut shard, haddr.hash, haddr.address) { 67 | olds.push(HashedEntryAddress { 68 | hash: haddr.hash, 69 | address: old, 70 | }); 71 | } 72 | } 73 | } 74 | olds 75 | } 76 | 77 | #[cfg_attr(feature = "tracing", fastrace::trace(name = "foyer::storage::large::indexer::get"))] 78 | pub fn get(&self, hash: u64) -> Option { 79 | let shard = self.shard(hash); 80 | self.shards[shard].read().get(&hash).cloned() 81 | } 82 | 83 | #[cfg_attr( 84 | feature = "tracing", 85 | fastrace::trace(name = "foyer::storage::large::indexer::remove") 86 | )] 87 | pub fn remove(&self, hash: u64) -> Option { 88 | let shard = self.shard(hash); 89 | self.shards[shard].write().remove(&hash) 90 | } 91 | 92 | #[cfg_attr( 93 | feature = "tracing", 94 | fastrace::trace(name = "foyer::storage::large::indexer::remove_batch") 95 | )] 96 | pub fn remove_batch(&self, hashes: &[u64]) -> Vec { 97 | let shards = hashes.iter().into_group_map_by(|&hash| self.shard(*hash)); 98 | 99 | let mut olds = vec![]; 100 | for (s, hashes) in shards { 101 | let mut shard = self.shards[s].write(); 102 | for hash in hashes { 103 | if let Some(old) = shard.remove(hash) { 104 | olds.push(old); 105 | } 106 | } 107 | } 108 | olds 109 | } 110 | 111 | #[cfg_attr(feature = "tracing", fastrace::trace(name = "foyer::storage::large::indexer::clear"))] 112 | pub fn clear(&self) { 113 | self.shards.iter().for_each(|shard| shard.write().clear()); 114 | } 115 | 116 | #[inline(always)] 117 | fn shard(&self, hash: u64) -> usize { 118 | hash as usize % self.shards.len() 119 | } 120 | 121 | fn insert_inner( 122 | &self, 123 | shard: &mut HashMap, 124 | hash: u64, 125 | addr: EntryAddress, 126 | ) -> Option { 127 | match shard.entry(hash) { 128 | Entry::Occupied(mut o) => { 129 | // `>` for updates. 130 | // '=' for reinsertions. 131 | if addr.sequence >= o.get().sequence { 132 | Some(o.insert(addr)) 133 | } else { 134 | Some(addr) 135 | } 136 | } 137 | Entry::Vacant(v) => { 138 | v.insert(addr); 139 | None 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /foyer-storage/src/large/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub mod buffer; 16 | pub mod flusher; 17 | pub mod generic; 18 | pub mod indexer; 19 | pub mod reclaimer; 20 | pub mod recover; 21 | pub mod scanner; 22 | pub mod serde; 23 | pub mod tombstone; 24 | 25 | #[cfg(test)] 26 | pub mod test_utils; 27 | -------------------------------------------------------------------------------- /foyer-storage/src/large/serde.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::atomic::AtomicU64; 16 | 17 | use bytes::{Buf, BufMut}; 18 | 19 | use crate::{ 20 | compress::Compression, 21 | error::{Error, Result}, 22 | }; 23 | 24 | const ENTRY_MAGIC: u32 = 0x97_03_27_00; 25 | const ENTRY_MAGIC_MASK: u32 = 0xFF_FF_FF_00; 26 | 27 | pub type Sequence = u64; 28 | pub type AtomicSequence = AtomicU64; 29 | 30 | #[derive(Debug, PartialEq, Eq)] 31 | pub struct EntryHeader { 32 | pub key_len: u32, 33 | pub value_len: u32, 34 | pub hash: u64, 35 | pub sequence: Sequence, 36 | pub checksum: u64, 37 | pub compression: Compression, 38 | } 39 | 40 | impl EntryHeader { 41 | pub const fn serialized_len() -> usize { 42 | 4 + 4 + 8 + 8 + 8 + 4 /* magic & compression */ 43 | } 44 | 45 | pub fn write(&self, mut buf: impl BufMut) { 46 | buf.put_u32(self.key_len); 47 | buf.put_u32(self.value_len); 48 | buf.put_u64(self.hash); 49 | buf.put_u64(self.sequence); 50 | buf.put_u64(self.checksum); 51 | 52 | let v = ENTRY_MAGIC | self.compression.to_u8() as u32; 53 | buf.put_u32(v); 54 | } 55 | 56 | pub fn read(mut buf: impl Buf) -> Result { 57 | let key_len = buf.get_u32(); 58 | let value_len = buf.get_u32(); 59 | let hash = buf.get_u64(); 60 | let sequence = buf.get_u64(); 61 | let checksum = buf.get_u64(); 62 | 63 | let v = buf.get_u32(); 64 | 65 | tracing::trace!("read entry header, key len: {key_len}, value_len: {value_len}, hash: {hash}, sequence: {sequence}, checksum: {checksum}, extra: {v}"); 66 | 67 | let magic = v & ENTRY_MAGIC_MASK; 68 | if magic != ENTRY_MAGIC { 69 | return Err(Error::MagicMismatch { 70 | expected: ENTRY_MAGIC, 71 | get: magic, 72 | }); 73 | } 74 | let compression = Compression::try_from(v as u8)?; 75 | 76 | Ok(Self { 77 | key_len, 78 | value_len, 79 | hash, 80 | sequence, 81 | checksum, 82 | compression, 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /foyer-storage/src/large/test_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::{ 16 | atomic::{AtomicBool, Ordering}, 17 | Arc, 18 | }; 19 | 20 | #[derive(Debug, Clone, Default)] 21 | pub struct FlushHolder { 22 | hold: Arc, 23 | } 24 | 25 | impl FlushHolder { 26 | pub fn is_held(&self) -> bool { 27 | self.hold.load(Ordering::Relaxed) 28 | } 29 | 30 | pub fn hold(&self) { 31 | self.hold.store(true, Ordering::Relaxed); 32 | } 33 | 34 | pub fn unhold(&self) { 35 | self.hold.store(false, Ordering::Relaxed); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /foyer-storage/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! A disk cache engine that serves as the disk cache backend of `foyer`. 16 | 17 | #![cfg_attr(feature = "nightly", feature(allocator_api))] 18 | #![cfg_attr(feature = "nightly", feature(write_all_vectored))] 19 | 20 | mod compress; 21 | mod device; 22 | mod engine; 23 | mod error; 24 | mod io; 25 | mod large; 26 | mod picker; 27 | mod region; 28 | mod runtime; 29 | mod serde; 30 | mod small; 31 | mod statistics; 32 | mod storage; 33 | mod store; 34 | 35 | mod prelude; 36 | pub use prelude::*; 37 | 38 | #[cfg(any(test, feature = "test_utils"))] 39 | pub mod test_utils; 40 | -------------------------------------------------------------------------------- /foyer-storage/src/picker/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{collections::HashSet, fmt::Debug, ops::Range, sync::Arc, time::Duration}; 16 | 17 | use crate::{device::RegionId, region::Region, statistics::Statistics}; 18 | 19 | /// Pick result for admission pickers and reinsertion pickers. 20 | #[derive(Debug, Clone, Copy)] 21 | pub enum Pick { 22 | /// Admittion. 23 | Admit, 24 | /// Rejection. 25 | Reject, 26 | /// This result indicates that the disk cache is throttled caused by the current io throttle. 27 | /// The minimal duration to retry this submission is returned for the caller to decide whether to retry it later. 28 | Throttled(Duration), 29 | } 30 | 31 | impl Pick { 32 | /// Return `true` if the pick result is `Admit`. 33 | pub fn admitted(&self) -> bool { 34 | matches! {self, Self::Admit} 35 | } 36 | 37 | /// Return `true` if the pick result is `Reject`. 38 | pub fn rejected(&self) -> bool { 39 | matches! {self, Self::Reject} 40 | } 41 | } 42 | 43 | impl From for Pick { 44 | fn from(value: bool) -> Self { 45 | match value { 46 | true => Self::Admit, 47 | false => Self::Reject, 48 | } 49 | } 50 | } 51 | 52 | /// The admission picker for the disk cache. 53 | pub trait AdmissionPicker: Send + Sync + 'static + Debug { 54 | /// Decide whether to pick an entry by hash. 55 | fn pick(&self, stats: &Arc, hash: u64) -> Pick; 56 | } 57 | 58 | /// The reinsertion picker for the disk cache. 59 | pub trait ReinsertionPicker: Send + Sync + 'static + Debug { 60 | /// Decide whether to pick an entry by hash. 61 | fn pick(&self, stats: &Arc, hash: u64) -> Pick; 62 | } 63 | 64 | /// Eviction related information for eviction picker to make decisions. 65 | #[derive(Debug)] 66 | pub struct EvictionInfo<'a> { 67 | /// All regions in the disk cache. 68 | pub regions: &'a [Region], 69 | /// Evictable regions. 70 | pub evictable: &'a HashSet, 71 | /// Clean regions counts. 72 | pub clean: usize, 73 | } 74 | 75 | /// The eviction picker for the disk cache. 76 | pub trait EvictionPicker: Send + Sync + 'static + Debug { 77 | /// Init the eviction picker with information. 78 | #[expect(unused_variables)] 79 | fn init(&mut self, regions: Range, region_size: usize) {} 80 | 81 | /// Pick a region to evict. 82 | /// 83 | /// `pick` can return `None` if no region can be picked based on its rules, and the next picker will be used. 84 | /// 85 | /// If no picker picks a region, the disk cache will pick randomly pick one. 86 | fn pick(&mut self, info: EvictionInfo<'_>) -> Option; 87 | 88 | /// Notify the picker that a region is ready to pick. 89 | fn on_region_evictable(&mut self, info: EvictionInfo<'_>, region: RegionId); 90 | 91 | /// Notify the picker that a region is evicted. 92 | fn on_region_evict(&mut self, info: EvictionInfo<'_>, region: RegionId); 93 | } 94 | 95 | pub mod utils; 96 | -------------------------------------------------------------------------------- /foyer-storage/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub use crate::{ 16 | compress::Compression, 17 | device::{ 18 | direct_file::{DirectFileDevice, DirectFileDeviceOptions}, 19 | direct_fs::{DirectFsDevice, DirectFsDeviceOptions}, 20 | Dev, DevConfig, DevExt, IopsCounter, Throttle, 21 | }, 22 | error::{Error, Result}, 23 | io::{ 24 | buffer::{IoBuf, IoBufMut, IoBuffer, OwnedIoSlice, OwnedSlice, SharedIoSlice}, 25 | throttle::IoThrottler, 26 | }, 27 | large::{ 28 | recover::RecoverMode, 29 | tombstone::{TombstoneLogConfig, TombstoneLogConfigBuilder}, 30 | }, 31 | picker::{ 32 | utils::{ 33 | AdmitAllPicker, ChainedAdmissionPicker, ChainedAdmissionPickerBuilder, FifoPicker, InvalidRatioPicker, 34 | IoThrottlerPicker, IoThrottlerTarget, RejectAllPicker, 35 | }, 36 | AdmissionPicker, EvictionInfo, EvictionPicker, Pick, ReinsertionPicker, 37 | }, 38 | region::{Region, RegionStatistics}, 39 | runtime::Runtime, 40 | statistics::Statistics, 41 | storage::{either::Order, Storage}, 42 | store::{ 43 | DeviceOptions, Engine, LargeEngineOptions, Load, RuntimeOptions, SmallEngineOptions, Store, StoreBuilder, 44 | TokioRuntimeOptions, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /foyer-storage/src/runtime.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::Arc; 16 | 17 | use foyer_common::runtime::{BackgroundShutdownRuntime, SingletonHandle}; 18 | use tokio::runtime::Handle; 19 | 20 | #[derive(Debug)] 21 | struct RuntimeInner { 22 | _read_runtime: Option>, 23 | _write_runtime: Option>, 24 | 25 | read_runtime_handle: SingletonHandle, 26 | write_runtime_handle: SingletonHandle, 27 | user_runtime_handle: SingletonHandle, 28 | } 29 | 30 | /// [`Runtime`] holds the runtime reference and non-cloneable handles to prevent handle usage after runtime shutdown. 31 | #[derive(Debug, Clone)] 32 | pub struct Runtime { 33 | inner: Arc, 34 | } 35 | 36 | impl Runtime { 37 | /// Create a new runtime with runtimes if given. 38 | pub fn new( 39 | read_runtime: Option>, 40 | write_runtime: Option>, 41 | user_runtime_handle: Handle, 42 | ) -> Self { 43 | let read_runtime_handle = read_runtime 44 | .as_ref() 45 | .map(|rt| rt.handle().clone()) 46 | .unwrap_or(user_runtime_handle.clone()); 47 | let write_runtime_handle = write_runtime 48 | .as_ref() 49 | .map(|rt| rt.handle().clone()) 50 | .unwrap_or(user_runtime_handle.clone()); 51 | Self { 52 | inner: Arc::new(RuntimeInner { 53 | _read_runtime: read_runtime, 54 | _write_runtime: write_runtime, 55 | read_runtime_handle: read_runtime_handle.into(), 56 | write_runtime_handle: write_runtime_handle.into(), 57 | user_runtime_handle: user_runtime_handle.into(), 58 | }), 59 | } 60 | } 61 | 62 | /// Create a new runtime with current runtime env only. 63 | pub fn current() -> Self { 64 | Self { 65 | inner: Arc::new(RuntimeInner { 66 | _read_runtime: None, 67 | _write_runtime: None, 68 | read_runtime_handle: Handle::current().into(), 69 | write_runtime_handle: Handle::current().into(), 70 | user_runtime_handle: Handle::current().into(), 71 | }), 72 | } 73 | } 74 | 75 | /// Get the non-cloneable read runtime handle. 76 | pub fn read(&self) -> &SingletonHandle { 77 | &self.inner.read_runtime_handle 78 | } 79 | 80 | /// Get the non-cloneable write runtime handle. 81 | pub fn write(&self) -> &SingletonHandle { 82 | &self.inner.write_runtime_handle 83 | } 84 | 85 | /// Get the non-cloneable user runtime handle. 86 | pub fn user(&self) -> &SingletonHandle { 87 | &self.inner.user_runtime_handle 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /foyer-storage/src/small/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub mod batch; 16 | pub mod bloom_filter; 17 | pub mod flusher; 18 | pub mod generic; 19 | pub mod serde; 20 | pub mod set; 21 | pub mod set_cache; 22 | pub mod set_manager; 23 | -------------------------------------------------------------------------------- /foyer-storage/src/small/serde.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use bytes::{Buf, BufMut}; 16 | 17 | /// max key/value len: `64 KiB - 1` 18 | /// 19 | /// # Format 20 | /// 21 | /// ```plain 22 | /// | hash 64b | key len 16b | value len 16b | 23 | /// ``` 24 | #[derive(Debug, PartialEq, Eq)] 25 | pub struct EntryHeader { 26 | hash: u64, 27 | key_len: u16, 28 | value_len: u16, 29 | } 30 | 31 | impl EntryHeader { 32 | pub const ENTRY_HEADER_SIZE: usize = (16 + 16 + 64) / 8; 33 | 34 | pub fn new(hash: u64, key_len: usize, value_len: usize) -> Self { 35 | Self { 36 | hash, 37 | key_len: key_len as _, 38 | value_len: value_len as _, 39 | } 40 | } 41 | 42 | #[inline] 43 | pub fn hash(&self) -> u64 { 44 | self.hash 45 | } 46 | 47 | #[inline] 48 | pub fn key_len(&self) -> usize { 49 | self.key_len as _ 50 | } 51 | 52 | #[inline] 53 | pub fn value_len(&self) -> usize { 54 | self.value_len as _ 55 | } 56 | 57 | #[inline] 58 | pub fn entry_len(&self) -> usize { 59 | Self::ENTRY_HEADER_SIZE + self.key_len() + self.value_len() 60 | } 61 | 62 | pub fn write(&self, mut buf: impl BufMut) { 63 | buf.put_u64(self.hash); 64 | buf.put_u16(self.key_len); 65 | buf.put_u16(self.value_len); 66 | } 67 | 68 | pub fn read(mut buf: impl Buf) -> Self { 69 | let hash = buf.get_u64(); 70 | let key_len = buf.get_u16(); 71 | let value_len = buf.get_u16(); 72 | Self { 73 | hash, 74 | key_len, 75 | value_len, 76 | } 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | 84 | #[test] 85 | fn test_entry_header_serde() { 86 | let header = EntryHeader { 87 | hash: 114514, 88 | key_len: 114, 89 | value_len: 514, 90 | }; 91 | let mut buf = vec![]; 92 | header.write(&mut buf); 93 | let h = EntryHeader::read(&buf[..]); 94 | assert_eq!(header, h); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /foyer-storage/src/small/set_cache.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use itertools::Itertools; 16 | use ordered_hash_map::OrderedHashMap; 17 | use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; 18 | 19 | use super::set::{SetId, SetStorage}; 20 | 21 | /// In-memory set cache to reduce disk io. 22 | /// 23 | /// Simple FIFO cache. 24 | #[derive(Debug)] 25 | pub struct SetCache { 26 | shards: Vec>>, 27 | shard_capacity: usize, 28 | } 29 | 30 | impl SetCache { 31 | pub fn new(capacity: usize, shards: usize) -> Self { 32 | let shard_capacity = capacity / shards; 33 | let shards = (0..shards) 34 | .map(|_| RwLock::new(OrderedHashMap::with_capacity(shard_capacity))) 35 | .collect_vec(); 36 | Self { shards, shard_capacity } 37 | } 38 | 39 | pub fn insert(&self, id: SetId, storage: SetStorage) { 40 | let mut shard = self.shards[self.shard(&id)].write(); 41 | if shard.len() == self.shard_capacity { 42 | shard.pop_front(); 43 | } 44 | 45 | assert!(shard.len() < self.shard_capacity); 46 | 47 | shard.insert(id, storage); 48 | } 49 | 50 | pub fn invalid(&self, id: &SetId) { 51 | let mut shard = self.shards[self.shard(id)].write(); 52 | shard.remove(id); 53 | } 54 | 55 | pub fn lookup(&self, id: &SetId) -> Option> { 56 | RwLockReadGuard::try_map(self.shards[self.shard(id)].read(), |shard| shard.get(id)).ok() 57 | } 58 | 59 | pub fn clear(&self) { 60 | self.shards.iter().for_each(|shard| shard.write().clear()); 61 | } 62 | 63 | fn shard(&self, id: &SetId) -> usize { 64 | *id as usize % self.shards.len() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /foyer-storage/src/statistics.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::sync::atomic::{AtomicUsize, Ordering}; 16 | 17 | use crate::IopsCounter; 18 | 19 | /// The statistics of the disk cache, which is used by the pickers. 20 | #[derive(Debug)] 21 | pub struct Statistics { 22 | iops_counter: IopsCounter, 23 | 24 | disk_write_bytes: AtomicUsize, 25 | disk_read_bytes: AtomicUsize, 26 | disk_write_ios: AtomicUsize, 27 | disk_read_ios: AtomicUsize, 28 | 29 | disk_flush_ios: AtomicUsize, 30 | } 31 | 32 | impl Statistics { 33 | /// Create a new statistics. 34 | pub fn new(iops_counter: IopsCounter) -> Self { 35 | Self { 36 | iops_counter, 37 | disk_write_bytes: AtomicUsize::new(0), 38 | disk_read_bytes: AtomicUsize::new(0), 39 | disk_write_ios: AtomicUsize::new(0), 40 | disk_read_ios: AtomicUsize::new(0), 41 | disk_flush_ios: AtomicUsize::new(0), 42 | } 43 | } 44 | 45 | /// Get the disk cache written bytes. 46 | pub fn disk_write_bytes(&self) -> usize { 47 | self.disk_write_bytes.load(Ordering::Relaxed) 48 | } 49 | 50 | /// Get the disk cache read bytes. 51 | pub fn disk_read_bytes(&self) -> usize { 52 | self.disk_read_bytes.load(Ordering::Relaxed) 53 | } 54 | 55 | /// Get the disk cache written ios. 56 | pub fn disk_write_ios(&self) -> usize { 57 | self.disk_write_ios.load(Ordering::Relaxed) 58 | } 59 | 60 | /// Get the disk cache read bytes. 61 | pub fn disk_read_ios(&self) -> usize { 62 | self.disk_read_ios.load(Ordering::Relaxed) 63 | } 64 | 65 | /// Record the write IO and update the statistics. 66 | pub fn record_disk_write(&self, bytes: usize) { 67 | self.disk_write_bytes.fetch_add(bytes, Ordering::Relaxed); 68 | self.disk_write_ios 69 | .fetch_add(self.iops_counter.count(bytes), Ordering::Relaxed); 70 | } 71 | 72 | /// Record the read IO and update the statistics. 73 | pub fn record_disk_read(&self, bytes: usize) { 74 | self.disk_read_bytes.fetch_add(bytes, Ordering::Relaxed); 75 | self.disk_read_ios 76 | .fetch_add(self.iops_counter.count(bytes), Ordering::Relaxed); 77 | } 78 | 79 | /// Record the read IO and update the statistics. 80 | pub fn record_disk_flush(&self) { 81 | self.disk_flush_ios.fetch_add(1, Ordering::Relaxed); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /foyer-storage/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub mod either; 16 | pub mod noop; 17 | 18 | use std::{fmt::Debug, future::Future, sync::Arc}; 19 | 20 | use foyer_common::{ 21 | code::{StorageKey, StorageValue}, 22 | properties::Properties, 23 | }; 24 | use foyer_memory::Piece; 25 | 26 | use crate::{error::Result, Load, Statistics, Throttle}; 27 | 28 | /// The storage trait for the disk cache storage engine. 29 | pub trait Storage: Send + Sync + 'static + Clone + Debug { 30 | /// Disk cache key type. 31 | type Key: StorageKey; 32 | /// Disk cache value type. 33 | type Value: StorageValue; 34 | /// Disk cache properties type. 35 | type Properties: Properties; 36 | /// Disk cache config type. 37 | type Config: Send + Debug + 'static; 38 | 39 | /// Open the disk cache with the given configurations. 40 | #[must_use] 41 | fn open(config: Self::Config) -> impl Future> + Send + 'static; 42 | 43 | /// Close the disk cache gracefully. 44 | /// 45 | /// `close` will wait for all ongoing flush and reclaim tasks to finish. 46 | #[must_use] 47 | fn close(&self) -> impl Future> + Send; 48 | 49 | /// Push a in-memory cache piece to the disk cache write queue. 50 | fn enqueue(&self, piece: Piece, estimated_size: usize); 51 | 52 | /// Load a cache entry from the disk cache. 53 | /// 54 | /// `load` may return a false-positive result on entry key hash collision. It's the caller's responsibility to 55 | /// check if the returned key matches the given key. 56 | #[must_use] 57 | fn load(&self, hash: u64) -> impl Future>> + Send + 'static; 58 | 59 | /// Delete the cache entry with the given key from the disk cache. 60 | fn delete(&self, hash: u64); 61 | 62 | /// Check if the disk cache contains a cached entry with the given key. 63 | /// 64 | /// `contains` may return a false-positive result if there is a hash collision with the given key. 65 | fn may_contains(&self, hash: u64) -> bool; 66 | 67 | /// Delete all cached entries of the disk cache. 68 | #[must_use] 69 | fn destroy(&self) -> impl Future> + Send; 70 | 71 | /// Get the statistics information of the disk cache. 72 | fn statistics(&self) -> &Arc; 73 | 74 | /// Get the throttle of the disk cache. 75 | fn throttle(&self) -> &Throttle; 76 | 77 | /// Wait for the ongoing flush and reclaim tasks to finish. 78 | #[must_use] 79 | fn wait(&self) -> impl Future + Send + 'static; 80 | } 81 | -------------------------------------------------------------------------------- /foyer-storage/src/storage/noop.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | fmt::Debug, 17 | future::{ready, Future}, 18 | marker::PhantomData, 19 | sync::Arc, 20 | }; 21 | 22 | use foyer_common::{ 23 | code::{StorageKey, StorageValue}, 24 | properties::Properties, 25 | }; 26 | use foyer_memory::Piece; 27 | 28 | use crate::{error::Result, storage::Storage, Load, Statistics, Throttle}; 29 | 30 | pub struct Noop 31 | where 32 | K: StorageKey, 33 | V: StorageValue, 34 | P: Properties, 35 | { 36 | throttle: Arc, 37 | statistics: Arc, 38 | _marker: PhantomData<(K, V, P)>, 39 | } 40 | 41 | impl Debug for Noop 42 | where 43 | K: StorageKey, 44 | V: StorageValue, 45 | P: Properties, 46 | { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | f.debug_tuple("NoneStore").finish() 49 | } 50 | } 51 | 52 | impl Clone for Noop 53 | where 54 | K: StorageKey, 55 | V: StorageValue, 56 | P: Properties, 57 | { 58 | fn clone(&self) -> Self { 59 | Self { 60 | throttle: self.throttle.clone(), 61 | statistics: self.statistics.clone(), 62 | _marker: PhantomData, 63 | } 64 | } 65 | } 66 | 67 | impl Storage for Noop 68 | where 69 | K: StorageKey, 70 | V: StorageValue, 71 | P: Properties, 72 | { 73 | type Key = K; 74 | type Value = V; 75 | type Properties = P; 76 | type Config = (); 77 | 78 | async fn open(_: Self::Config) -> Result { 79 | let throttle = Arc::::default(); 80 | let statistics = Arc::new(Statistics::new(throttle.iops_counter.clone())); 81 | Ok(Self { 82 | throttle, 83 | statistics, 84 | _marker: PhantomData, 85 | }) 86 | } 87 | 88 | async fn close(&self) -> Result<()> { 89 | Ok(()) 90 | } 91 | 92 | fn enqueue(&self, _piece: Piece, _estimated_size: usize) {} 93 | 94 | fn load(&self, _: u64) -> impl Future>> + Send + 'static { 95 | ready(Ok(Load::Miss)) 96 | } 97 | 98 | fn delete(&self, _: u64) {} 99 | 100 | fn may_contains(&self, _: u64) -> bool { 101 | false 102 | } 103 | 104 | async fn destroy(&self) -> Result<()> { 105 | Ok(()) 106 | } 107 | 108 | fn throttle(&self) -> &Throttle { 109 | &self.throttle 110 | } 111 | 112 | fn statistics(&self) -> &Arc { 113 | &self.statistics 114 | } 115 | 116 | fn wait(&self) -> impl Future + Send + 'static { 117 | ready(()) 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use foyer_common::hasher::ModRandomState; 124 | use foyer_memory::{Cache, CacheBuilder, FifoConfig, TestProperties}; 125 | 126 | use super::*; 127 | 128 | fn cache_for_test() -> Cache, ModRandomState, TestProperties> { 129 | CacheBuilder::new(10) 130 | .with_hash_builder(ModRandomState::default()) 131 | .with_eviction_config(FifoConfig::default()) 132 | .build() 133 | } 134 | 135 | #[tokio::test] 136 | async fn test_none_store() { 137 | let memory = cache_for_test(); 138 | let store: Noop, TestProperties> = Noop::open(()).await.unwrap(); 139 | 140 | store.enqueue(memory.insert(0, vec![b'x'; 16384]).piece(), 16384); 141 | store.wait().await; 142 | assert!(store.load(memory.hash(&0)).await.unwrap().is_miss()); 143 | store.delete(memory.hash(&0)); 144 | store.wait().await; 145 | store.destroy().await.unwrap(); 146 | store.close().await.unwrap(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /foyer-storage/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Test utils for the `foyer-storage` crate. 16 | 17 | use std::{ 18 | collections::HashSet, 19 | fmt::Debug, 20 | sync::{ 21 | atomic::{AtomicBool, Ordering}, 22 | Arc, 23 | }, 24 | }; 25 | 26 | use parking_lot::Mutex; 27 | 28 | use crate::{ 29 | picker::{AdmissionPicker, ReinsertionPicker}, 30 | statistics::Statistics, 31 | Pick, 32 | }; 33 | 34 | /// A picker that only admits hash from the given list. 35 | #[derive(Debug)] 36 | pub struct BiasedPicker { 37 | admits: HashSet, 38 | } 39 | 40 | impl BiasedPicker { 41 | /// Create a biased picker with the given admit list. 42 | pub fn new(admits: impl IntoIterator) -> Self { 43 | Self { 44 | admits: admits.into_iter().collect(), 45 | } 46 | } 47 | } 48 | 49 | impl AdmissionPicker for BiasedPicker { 50 | fn pick(&self, _: &Arc, hash: u64) -> Pick { 51 | self.admits.contains(&hash).into() 52 | } 53 | } 54 | 55 | impl ReinsertionPicker for BiasedPicker { 56 | fn pick(&self, _: &Arc, hash: u64) -> Pick { 57 | self.admits.contains(&hash).into() 58 | } 59 | } 60 | 61 | /// The record entry for admission and eviction. 62 | #[derive(Debug, Clone, PartialEq, Eq)] 63 | pub enum Record { 64 | /// Admission record entry hash. 65 | Admit(u64), 66 | /// Eviction record entry hash. 67 | Evict(u64), 68 | } 69 | 70 | /// A recorder that records the cache entry admission and eviction of a disk cache. 71 | /// 72 | /// [`Recorder`] should be used as both the admission picker and the reinsertion picker to record. 73 | #[derive(Debug, Default)] 74 | pub struct Recorder { 75 | records: Mutex>, 76 | } 77 | 78 | impl Recorder { 79 | /// Dump the record entries of the recorder. 80 | pub fn dump(&self) -> Vec { 81 | self.records.lock().clone() 82 | } 83 | 84 | /// Get the hash set of the remaining hash at the moment. 85 | pub fn remains(&self) -> HashSet { 86 | let records = self.dump(); 87 | let mut res = HashSet::default(); 88 | for record in records { 89 | match record { 90 | Record::Admit(key) => { 91 | res.insert(key); 92 | } 93 | Record::Evict(key) => { 94 | res.remove(&key); 95 | } 96 | } 97 | } 98 | res 99 | } 100 | } 101 | 102 | impl AdmissionPicker for Recorder { 103 | fn pick(&self, _: &Arc, hash: u64) -> Pick { 104 | self.records.lock().push(Record::Admit(hash)); 105 | Pick::Admit 106 | } 107 | } 108 | 109 | impl ReinsertionPicker for Recorder { 110 | fn pick(&self, _: &Arc, hash: u64) -> Pick { 111 | self.records.lock().push(Record::Evict(hash)); 112 | Pick::Reject 113 | } 114 | } 115 | 116 | /// A switch to throttle/unthrottle all loads. 117 | #[derive(Debug, Clone, Default)] 118 | pub struct LoadThrottleSwitch { 119 | throttled: Arc, 120 | } 121 | 122 | impl LoadThrottleSwitch { 123 | /// If all loads are throttled. 124 | pub fn is_throttled(&self) -> bool { 125 | self.throttled.load(Ordering::Relaxed) 126 | } 127 | 128 | /// Throttle all loads. 129 | pub fn throttle(&self) { 130 | self.throttled.store(true, Ordering::Relaxed); 131 | } 132 | 133 | /// Unthrottle all loads. 134 | pub fn unthrottle(&self) { 135 | self.throttled.store(false, Ordering::Relaxed); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /foyer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "foyer" 3 | description = "foyer - Hybrid cache for Rust" 4 | version = { workspace = true } 5 | edition = { workspace = true } 6 | rust-version = { workspace = true } 7 | repository = { workspace = true } 8 | homepage = { workspace = true } 9 | keywords = { workspace = true } 10 | authors = { workspace = true } 11 | license = { workspace = true } 12 | readme = { workspace = true } 13 | 14 | [package.metadata.docs.rs] 15 | features = ["serde", "tracing", "nightly", "deadlock", "strict_assertions"] 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [dependencies] 19 | anyhow = { workspace = true } 20 | equivalent = { workspace = true } 21 | fastrace = { workspace = true, optional = true } 22 | foyer-common = { workspace = true } 23 | foyer-memory = { workspace = true } 24 | foyer-storage = { workspace = true } 25 | mixtrics = { workspace = true } 26 | pin-project = { workspace = true } 27 | serde = { workspace = true } 28 | tracing = { workspace = true } 29 | 30 | [dev-dependencies] 31 | foyer-storage = { workspace = true, features = ["test_utils"] } 32 | tempfile = "3" 33 | test-log = { workspace = true } 34 | 35 | [target.'cfg(madsim)'.dependencies] 36 | tokio = { package = "madsim-tokio", version = "0.2", features = [ 37 | "rt", 38 | "rt-multi-thread", 39 | "sync", 40 | "macros", 41 | "time", 42 | "signal", 43 | "fs", 44 | ] } 45 | 46 | [target.'cfg(not(madsim))'.dependencies] 47 | tokio = { package = "tokio", version = "1", features = [ 48 | "rt", 49 | "rt-multi-thread", 50 | "sync", 51 | "macros", 52 | "time", 53 | "signal", 54 | "fs", 55 | ] } 56 | 57 | [features] 58 | default = [] 59 | serde = ["foyer-common/serde", "foyer-storage/serde"] 60 | tracing = [ 61 | "fastrace/enable", 62 | "foyer-common/tracing", 63 | "foyer-memory/tracing", 64 | "foyer-storage/tracing", 65 | ] 66 | nightly = ["foyer-storage/nightly", "foyer-memory/nightly"] 67 | deadlock = ["foyer-storage/deadlock"] 68 | strict_assertions = [ 69 | "foyer-common/strict_assertions", 70 | "foyer-memory/strict_assertions", 71 | "foyer-storage/strict_assertions", 72 | ] 73 | 74 | [lints] 75 | workspace = true 76 | -------------------------------------------------------------------------------- /foyer/src/hybrid/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub mod builder; 16 | pub mod cache; 17 | pub mod writer; 18 | -------------------------------------------------------------------------------- /foyer/src/hybrid/writer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | fmt::Debug, 17 | time::{Duration, Instant}, 18 | }; 19 | 20 | use foyer_common::code::{DefaultHasher, HashBuilder, StorageKey, StorageValue}; 21 | use foyer_storage::Pick; 22 | 23 | use crate::{HybridCache, HybridCacheEntry, HybridCachePolicy, HybridCacheProperties}; 24 | 25 | /// Writer for hybrid cache to support more flexible write APIs. 26 | pub struct HybridCacheWriter 27 | where 28 | K: StorageKey, 29 | V: StorageValue, 30 | S: HashBuilder + Debug, 31 | { 32 | hybrid: HybridCache, 33 | key: K, 34 | } 35 | 36 | impl HybridCacheWriter 37 | where 38 | K: StorageKey, 39 | V: StorageValue, 40 | S: HashBuilder + Debug, 41 | { 42 | pub(crate) fn new(hybrid: HybridCache, key: K) -> Self { 43 | Self { hybrid, key } 44 | } 45 | 46 | /// Insert the entry to the hybrid cache. 47 | pub fn insert(self, value: V) -> HybridCacheEntry { 48 | self.hybrid.insert(self.key, value) 49 | } 50 | 51 | /// Insert the entry with properties to the hybrid cache. 52 | pub fn insert_with_properties(self, value: V, properties: HybridCacheProperties) -> HybridCacheEntry { 53 | self.hybrid.insert_with_properties(self.key, value, properties) 54 | } 55 | 56 | /// Convert [`HybridCacheWriter`] to [`HybridCacheStorageWriter`]. 57 | pub fn storage(self) -> HybridCacheStorageWriter { 58 | HybridCacheStorageWriter::new(self.hybrid, self.key) 59 | } 60 | } 61 | 62 | /// Writer for disk cache of a hybrid cache to support more flexible write APIs. 63 | pub struct HybridCacheStorageWriter 64 | where 65 | K: StorageKey, 66 | V: StorageValue, 67 | S: HashBuilder + Debug, 68 | { 69 | hybrid: HybridCache, 70 | key: K, 71 | hash: u64, 72 | 73 | force: bool, 74 | picked: Option, 75 | pick_duration: Duration, 76 | } 77 | 78 | impl HybridCacheStorageWriter 79 | where 80 | K: StorageKey, 81 | V: StorageValue, 82 | S: HashBuilder + Debug, 83 | { 84 | pub(crate) fn new(hybrid: HybridCache, key: K) -> Self { 85 | let hash = hybrid.memory().hash(&key); 86 | Self { 87 | hybrid, 88 | key, 89 | hash, 90 | force: false, 91 | picked: None, 92 | pick_duration: Duration::default(), 93 | } 94 | } 95 | 96 | /// Check if the entry can be admitted by the admission picker of the disk cache. 97 | /// 98 | /// After calling `pick`, the writer will not be checked by the admission picker again. 99 | pub fn pick(&mut self) -> Pick { 100 | let now = Instant::now(); 101 | 102 | let picked = self.hybrid.storage().pick(self.hash); 103 | self.picked = Some(picked); 104 | 105 | self.pick_duration = now.elapsed(); 106 | 107 | picked 108 | } 109 | 110 | fn may_pick(&mut self) -> Pick { 111 | if let Some(picked) = self.picked { 112 | picked 113 | } else { 114 | self.pick() 115 | } 116 | } 117 | 118 | /// Force the disk cache to admit the writer. 119 | /// 120 | /// Note: There is still chance that the entry is ignored because of the storage engine buffer full. 121 | pub fn force(mut self) -> Self { 122 | self.force = true; 123 | self 124 | } 125 | 126 | fn insert_inner(mut self, value: V, properties: HybridCacheProperties) -> Option> { 127 | let now = Instant::now(); 128 | 129 | if !self.force && !self.may_pick().admitted() { 130 | return None; 131 | } 132 | 133 | let entry = self 134 | .hybrid 135 | .memory() 136 | .insert_with_properties(self.key, value, properties.with_ephemeral(true)); 137 | if self.hybrid.policy() == HybridCachePolicy::WriteOnInsertion { 138 | self.hybrid.storage().enqueue(entry.piece(), true); 139 | } 140 | self.hybrid.metrics().hybrid_insert.increase(1); 141 | self.hybrid 142 | .metrics() 143 | .hybrid_insert_duration 144 | .record((now.elapsed() + self.pick_duration).as_secs_f64()); 145 | 146 | Some(entry) 147 | } 148 | 149 | /// Insert the entry to the disk cache only. 150 | pub fn insert(self, value: V) -> Option> { 151 | self.insert_inner(value, HybridCacheProperties::default()) 152 | } 153 | 154 | /// Insert the entry with properties to the disk cache only. 155 | pub fn insert_with_properties( 156 | self, 157 | value: V, 158 | properties: HybridCacheProperties, 159 | ) -> Option> { 160 | self.insert_inner(value, properties) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /foyer/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![cfg_attr(feature = "nightly", feature(allocator_api))] 16 | #![cfg_attr(docsrs, feature(doc_cfg))] 17 | 18 | //! A hybrid cache library that supports plug-and-play cache algorithms, in-memory cache and disk cache. 19 | //! 20 | //! ![Website](https://img.shields.io/website?url=https%3A%2F%2Ffoyer.rs&up_message=foyer.rs&down_message=website&style=for-the-badge&logo=htmx&link=https%3A%2F%2Ffoyer.rs) 21 | //! ![Crates.io Version](https://img.shields.io/crates/v/foyer?style=for-the-badge&logo=crates.io&labelColor=555555&link=https%3A%2F%2Fcrates.io%2Fcrates%2Ffoyer) 22 | //! ![docs.rs](https://img.shields.io/docsrs/foyer?style=for-the-badge&logo=rust&label=docs.rs&labelColor=555555&link=https%3A%2F%2Fdocs.rs%2Ffoyer) 23 | //! 24 | //! [Website](https://foyer.rs) | 25 | //! [Tutorial](https://foyer.rs/docs/overview) | 26 | //! [API Docs](https://docs.rs/foyer) | 27 | //! [Crate](https://crates.io/crates/foyer) 28 | 29 | use foyer_common as common; 30 | use foyer_memory as memory; 31 | use foyer_storage as storage; 32 | 33 | mod hybrid; 34 | 35 | mod prelude; 36 | pub use prelude::*; 37 | -------------------------------------------------------------------------------- /foyer/src/prelude.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2025 foyer Project Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #[cfg(feature = "tracing")] 16 | pub use crate::common::tracing::TracingOptions; 17 | pub use crate::{ 18 | common::{ 19 | buf::{BufExt, BufMutExt}, 20 | code::{Code, CodeError, CodeResult, DefaultHasher, Key, StorageKey, StorageValue, Value}, 21 | event::{Event, EventListener}, 22 | properties::{Age, Hint, Location, Source}, 23 | utils::{option::OptionExt, range::RangeBoundsExt, scope::Scope}, 24 | }, 25 | hybrid::{ 26 | builder::{HybridCacheBuilder, HybridCacheBuilderPhaseMemory, HybridCacheBuilderPhaseStorage}, 27 | cache::{HybridCache, HybridCacheEntry, HybridCachePolicy, HybridCacheProperties, HybridFetch}, 28 | writer::{HybridCacheStorageWriter, HybridCacheWriter}, 29 | }, 30 | memory::{ 31 | Cache, CacheBuilder, CacheEntry, CacheProperties, EvictionConfig, FetchState, FifoConfig, LfuConfig, LruConfig, 32 | S3FifoConfig, Weighter, 33 | }, 34 | storage::{ 35 | AdmissionPicker, AdmitAllPicker, ChainedAdmissionPicker, ChainedAdmissionPickerBuilder, Compression, Dev, 36 | DevConfig, DevExt, DirectFileDevice, DirectFileDeviceOptions, DirectFsDevice, DirectFsDeviceOptions, Engine, 37 | EvictionInfo, EvictionPicker, FifoPicker, InvalidRatioPicker, IopsCounter, LargeEngineOptions, Load, Pick, 38 | RecoverMode, Region, RegionStatistics, ReinsertionPicker, RejectAllPicker, Runtime, RuntimeOptions, 39 | SmallEngineOptions, Statistics, Storage, Store, StoreBuilder, Throttle, TokioRuntimeOptions, 40 | TombstoneLogConfigBuilder, 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /rustfmt.nightly.toml: -------------------------------------------------------------------------------- 1 | # TODO(MrCroxx): Move features into `rustfmt.toml` after stable. 2 | 3 | imports_granularity = "Crate" # unsatble 4 | group_imports = "StdExternalCrate" # unstable 5 | tab_spaces = 4 6 | wrap_comments = true # unstable 7 | max_width = 120 8 | comment_width = 120 # unstable 9 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 4 2 | max_width = 120 3 | -------------------------------------------------------------------------------- /scripts/install-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cargo install cargo-sort typos-cli 3 | cargo install cargo-nextest cargo-udeps taplo-cli --locked -------------------------------------------------------------------------------- /scripts/minimize-dashboards.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # You will need to install jq to use this tool. 4 | # brew install jq 5 | 6 | set -e 7 | 8 | DIR="etc/grafana/dashboards" 9 | 10 | for dashboard in "${DIR}"/*.json; do 11 | if [[ "$dashboard" == *.min.json ]]; then 12 | continue 13 | fi 14 | name=$(basename "$dashboard" .json) 15 | jq -c < "$dashboard" > "${DIR}/${name}.min.json" 16 | done 17 | 18 | for dashboard in "${DIR}"/*.min.json; do 19 | name=$(basename "$dashboard" .min.json) 20 | mv "$dashboard" "${DIR}/${name}.json" 21 | done 22 | 23 | if [ "$1" == "--check" ] ; then 24 | if ! git diff --exit-code; then 25 | echo "Please run minimize-dashboards.sh and commit after editing the grafana dashboards." 26 | exit 1 27 | fi 28 | fi -------------------------------------------------------------------------------- /scripts/monitor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | compose=$(docker compose ps -q | wc -l) 4 | 5 | cat < docker-compose.override.yaml 6 | services: 7 | prometheus: 8 | user: "${UID}" 9 | EOF 10 | 11 | if [ "$compose" = 0 ] ; then 12 | mkdir -p .tmp/prometheus 13 | docker compose up -d 14 | else 15 | docker compose down 16 | fi --------------------------------------------------------------------------------