diff --git a/.gitignore b/.gitignore index 0b7eb790..13a441f2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ crates/jacquard-lexicon/target /binaries/releases/ rustdoc-host.nix **/**.car +result diff --git a/Cargo.lock b/Cargo.lock index e2fee3a3..8a64ef79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2453,7 +2453,7 @@ dependencies = [ "tempfile", "thiserror 2.0.17", "tokio", - "toml", + "toml 0.8.23", "walkdir", ] @@ -2624,9 +2624,9 @@ checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -2915,17 +2915,31 @@ dependencies = [ [[package]] name = "mini-moka" version = "0.11.0" -source = "git+https://github.com/moka-rs/mini-moka?rev=da864e849f5d034f32e02197fee9bb5d5af36d3d#da864e849f5d034f32e02197fee9bb5d5af36d3d" dependencies = [ + "anyhow", "crossbeam-channel", "crossbeam-utils", "dashmap", + "getrandom 0.2.16", + "once_cell", "smallvec", "tagptr", "triomphe", + "trybuild", + "wasm-bindgen-test", "web-time", ] +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4389,6 +4403,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4741,7 +4764,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml", + "toml 0.8.23", "version-compare", ] @@ -4757,6 +4780,12 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "target-triple" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" + [[package]] name = "tempfile" version = "3.23.0" @@ -5057,11 +5086,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.12.0", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -5071,6 +5115,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -5079,18 +5132,33 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.12.0", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow 0.7.13", ] +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", +] + [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "tower" version = "0.5.2" @@ -5224,6 +5292,21 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "trybuild" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17e807bff86d2a06b52bca4276746584a78375055b6e45843925ce2802b335" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "target-triple", + "termcolor", + "toml 0.9.8", +] + [[package]] name = "tungstenite" version = "0.24.0" @@ -5476,9 +5559,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -5487,25 +5570,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.108", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -5516,9 +5585,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5526,26 +5595,50 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.108", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-bindgen-test" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc379bfb624eb59050b509c13e77b4eb53150c350db69628141abce842f2373" +dependencies = [ + "js-sys", + "minicov", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085b2df989e1e6f9620c1311df6c996e83fe16f57792b272ce1e024ac16a90f1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -5561,9 +5654,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/crates/jacquard-identity/Cargo.toml b/crates/jacquard-identity/Cargo.toml index a169ab86..a26ac96d 100644 --- a/crates/jacquard-identity/Cargo.toml +++ b/crates/jacquard-identity/Cargo.toml @@ -37,7 +37,7 @@ serde_html_form.workspace = true urlencoding.workspace = true tracing = { workspace = true, optional = true } n0-future = { workspace = true, optional = true } -mini-moka = { version = "0.11.0", git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d", optional = true } +mini-moka = { version = "0.11.0", path = "../mini-moka-vendored", optional = true } # mini-moka = { version = "0.10", optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] @@ -46,7 +46,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -mini-moka = { version = "0.11.0", git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d", features = ["js"], optional = true } +mini-moka = { version = "0.11.0",path = "../mini-moka-vendored", features = ["js"], optional = true } [[example]] diff --git a/crates/jacquard-lexicon/src/codegen.rs b/crates/jacquard-lexicon/src/codegen.rs index a435137c..c17a82a6 100644 --- a/crates/jacquard-lexicon/src/codegen.rs +++ b/crates/jacquard-lexicon/src/codegen.rs @@ -513,28 +513,28 @@ mod tests { assert!(formatted.contains("Account")); } - #[test] - fn test_generate_token_type() { - let corpus = - LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); - let codegen = CodeGenerator::new(&corpus, "jacquard_api"); - - let doc = corpus.get("app.bsky.embed.images").expect("get images"); - let def = doc.defs.get("viewImage").expect("get viewImage def"); - - let tokens = codegen - .generate_def("app.bsky.embed.images", "viewImage", def) - .expect("generate"); - - let file: syn::File = syn::parse2(tokens).expect("parse tokens"); - let formatted = prettyplease::unparse(&file); - println!("\n{}\n", formatted); - - // Token types are unit structs - assert!(formatted.contains("struct ViewImage")); - // Should have Display implementation - assert!(formatted.contains("impl std::fmt::Display")); - } + // #[test] + // fn test_generate_token_type() { + // let corpus = + // LexiconCorpus::load_from_dir("tests/fixtures/test_lexicons").expect("load corpus"); + // let codegen = CodeGenerator::new(&corpus, "jacquard_api"); + + // let doc = corpus.get("app.bsky.embed.images").expect("get images"); + // let def = doc.defs.get("viewImage").expect("get viewImage def"); + + // let tokens = codegen + // .generate_def("app.bsky.embed.images", "viewImage", def) + // .expect("generate"); + + // let file: syn::File = syn::parse2(tokens).expect("parse tokens"); + // let formatted = prettyplease::unparse(&file); + // println!("\n{}\n", formatted); + + // // Token types are unit structs + // assert!(formatted.contains("struct ViewImage")); + // // Should have Display implementation + // assert!(formatted.contains("impl std::fmt::Display")); + // } #[test] fn test_generate_array_types() { diff --git a/crates/mini-moka-vendored/.github/workflows/Audit.yml b/crates/mini-moka-vendored/.github/workflows/Audit.yml new file mode 100644 index 00000000..95810e3a --- /dev/null +++ b/crates/mini-moka-vendored/.github/workflows/Audit.yml @@ -0,0 +1,37 @@ +name: Cargo Audit + +on: + push: + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + schedule: + # https://crontab.guru/ + - cron: '5 20 * * 5' + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - name: Checkout Mini Moka + uses: actions/checkout@v4 + + - name: Install Rust toolchain (Nightly) + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly + + - run: cargo clean + + - name: Check for known security vulnerabilities (Latest versions) + uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Downgrade dependencies to minimal versions + run: cargo update -Z minimal-versions + + - name: Check for known security vulnerabilities (Minimal versions) + uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/crates/mini-moka-vendored/.github/workflows/CI.yml b/crates/mini-moka-vendored/.github/workflows/CI.yml new file mode 100644 index 00000000..68569d81 --- /dev/null +++ b/crates/mini-moka-vendored/.github/workflows/CI.yml @@ -0,0 +1,52 @@ +name: CI + +on: + push: + paths-ignore: + - '.devcontainer/**' + - '.gitpod.yml' + - '.vscode/**' + pull_request: + paths-ignore: + - '.devcontainer/**' + - '.gitpod.yml' + - '.vscode/**' + schedule: + # Run against the last commit on the default branch on Friday at 8pm (UTC?) + - cron: '0 20 * * 5' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + rust: + - stable + - beta + - 1.76.0 # MSRV + - nightly # For checking minimum version dependencies. + + steps: + - name: Checkout Mini Moka + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - run: cargo clean + + - name: Downgrade dependencies to minimal versions (Nightly only) + if: ${{ matrix.rust == 'nightly' }} + run: cargo update -Z minimal-versions + + - name: Show cargo tree + run: cargo tree + + - name: Run tests (debug, sync feature) + run: cargo test --features sync + + - name: Run tests (release, sync feature) + run: cargo test --release --features sync diff --git a/crates/mini-moka-vendored/.github/workflows/Kani.yml b/crates/mini-moka-vendored/.github/workflows/Kani.yml new file mode 100644 index 00000000..d3e3bec3 --- /dev/null +++ b/crates/mini-moka-vendored/.github/workflows/Kani.yml @@ -0,0 +1,31 @@ +name: Kani CI + +on: + pull_request: + paths-ignore: + - '.vscode/**' + - CHANGELOG.md + - README.md + push: + paths-ignore: + - '.vscode/**' + - CHANGELOG.md + - README.md + +jobs: + run-kani: + runs-on: ubuntu-latest + steps: + - name: Checkout Moka + uses: actions/checkout@v4 + + - name: Show CPU into + run: | + nproc + lscpu + free -m + - name: Run Kani + uses: model-checking/kani-github-action@v1.0 + # Workaround for https://github.com/moka-rs/mini-moka/issues/36 + with: + kani-version: '0.54.0' diff --git a/crates/mini-moka-vendored/.github/workflows/Lints.yml b/crates/mini-moka-vendored/.github/workflows/Lints.yml new file mode 100644 index 00000000..b674f6a1 --- /dev/null +++ b/crates/mini-moka-vendored/.github/workflows/Lints.yml @@ -0,0 +1,42 @@ +name: Clippy lints and Rustfmt + +on: + push: + paths-ignore: + - '.devcontainer/**' + - '.gitpod.yml' + - '.vscode/**' + schedule: + # Run against the last commit on the default branch on Friday at 7pm (UTC?) + - cron: '0 19 * * 5' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - toolchain: stable + - toolchain: beta + rustflags: '--cfg beta_clippy' + + steps: + - name: Checkout Mini Moka + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust.toolchain }} + components: rustfmt, clippy + + - run: cargo clean + + - name: Run Clippy + run: cargo clippy --lib --tests --all-features --all-targets -- -D warnings + env: + RUSTFLAGS: ${{ matrix.rust.rustflags }} + + - name: Run Rustfmt + if: ${{ matrix.rust.toolchain == 'stable' }} + run: cargo fmt --all -- --check diff --git a/crates/mini-moka-vendored/.github/workflows/LinuxCrossCompileTest.yml b/crates/mini-moka-vendored/.github/workflows/LinuxCrossCompileTest.yml new file mode 100644 index 00000000..00af150b --- /dev/null +++ b/crates/mini-moka-vendored/.github/workflows/LinuxCrossCompileTest.yml @@ -0,0 +1,61 @@ +name: Linux cross compile tests + +on: + push: + paths-ignore: + - ".devcontainer/**" + - ".gitpod.yml" + - ".vscode/**" + - "tests/**" + pull_request: + paths-ignore: + - ".devcontainer/**" + - ".gitpod.yml" + - ".vscode/**" + - "tests/**" + schedule: + # Run against the last commit on the default branch on Friday at 9pm (UTC?) + - cron: "0 21 * * 5" + +jobs: + linux-cross: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + platform: + - target: aarch64-unknown-linux-musl + rust-version: stable + - target: i686-unknown-linux-musl + rust-version: stable + - target: armv7-unknown-linux-musleabihf + rust-version: stable + - target: armv5te-unknown-linux-musleabi + rust-version: stable + + steps: + - name: Checkout Mini Moka + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.platform.rust-version }} + targets: ${{ matrix.platform.target }} + + - name: Install cross + uses: taiki-e/install-action@v2 + with: + tool: cross + + - name: Remove integration tests and force enable rustc_version crate + run: | + rm -rf tests + sed -i '/actix-rt\|async-std\|reqwest/d' Cargo.toml + + - run: cargo clean + + - name: Run tests (sync feature) + run: | + cross ${{ matrix.platform.carge-version }} test --release -F sync \ + --target ${{ matrix.platform.target }} diff --git a/crates/mini-moka-vendored/.github/workflows/Miri.yml b/crates/mini-moka-vendored/.github/workflows/Miri.yml new file mode 100644 index 00000000..bd8768d2 --- /dev/null +++ b/crates/mini-moka-vendored/.github/workflows/Miri.yml @@ -0,0 +1,37 @@ +name: Miri tests + +on: + push: + paths-ignore: + - '.devcontainer/**' + - '.gitpod.yml' + - '.vscode/**' + - 'tests/**' + pull_request: + paths-ignore: + - '.devcontainer/**' + - '.gitpod.yml' + - '.vscode/**' + - 'tests/**' + schedule: + # Run against the last commit on the default branch on Friday at 9pm (UTC?) + - cron: '0 21 * * 5' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Mini Moka + uses: actions/checkout@v4 + + - name: Install Rust nightly toolchain with Miri + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly + components: miri + + - run: cargo clean + + - name: Run Miri test (deque) + run: cargo miri test deque diff --git a/crates/mini-moka-vendored/.github/workflows/Trybuild.yml b/crates/mini-moka-vendored/.github/workflows/Trybuild.yml new file mode 100644 index 00000000..a8a5924a --- /dev/null +++ b/crates/mini-moka-vendored/.github/workflows/Trybuild.yml @@ -0,0 +1,35 @@ +name: Trybuild + +on: + push: + paths-ignore: + - '.devcontainer/**' + - '.gitpod.yml' + - '.vscode/**' + schedule: + # Run against the last commit on the default branch on Friday at 9pm (UTC?) + - cron: '0 21 * * 5' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - beta + + steps: + - name: Checkout Mini Moka + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: Run compile error tests (sync feature, trybuild) + if: ${{ matrix.rust == 'stable' }} + run: cargo test ui_trybuild --release --features sync + env: + RUSTFLAGS: '--cfg trybuild' diff --git a/crates/mini-moka-vendored/.gitignore b/crates/mini-moka-vendored/.gitignore new file mode 100644 index 00000000..22fc05af --- /dev/null +++ b/crates/mini-moka-vendored/.gitignore @@ -0,0 +1,8 @@ +**/*.rs.bk +**/*~ +.DS_Store +/target/ +Cargo.lock + +# intellij cache +.idea diff --git a/crates/mini-moka-vendored/.vscode/settings.json b/crates/mini-moka-vendored/.vscode/settings.json new file mode 100644 index 00000000..cbdb4d33 --- /dev/null +++ b/crates/mini-moka-vendored/.vscode/settings.json @@ -0,0 +1,71 @@ +{ + "rust-analyzer.cargo.features": [], + "rust-analyzer.server.extraEnv": { + "CARGO_TARGET_DIR": "target/ra" + }, + "editor.rulers": [85], + "cSpell.words": [ + "aarch", + "actix", + "ahash", + "armv", + "benmanes", + "circleci", + "CLFU", + "clippy", + "compat", + "cpus", + "dashmap", + "deqs", + "Deque", + "Deques", + "devcontainer", + "docsrs", + "doctest", + "doctests", + "Einziger", + "else's", + "Eytan", + "getrandom", + "hashbrown", + "Hasher", + "Kawano", + "mapref", + "Moka", + "mpsc", + "MSRV", + "nanos", + "nocapture", + "Ohad", + "peekable", + "preds", + "repr", + "reqwest", + "runtimes", + "rustc", + "rustdoc", + "RUSTFLAGS", + "rustfmt", + "semver", + "SIGABRT", + "SIGILL", + "smallvec", + "structs", + "tagptr", + "Tatsuya", + "thiserror", + "toolchain", + "triomphe", + "trybuild", + "Uninit", + "unsync", + "Upsert", + "usize" + ], + "files.watcherExclude": { + "**/target": true + }, + "cSpell.enableFiletypes": [ + "toml" + ] +} diff --git a/crates/mini-moka-vendored/CHANGELOG.md b/crates/mini-moka-vendored/CHANGELOG.md new file mode 100644 index 00000000..62b69436 --- /dev/null +++ b/crates/mini-moka-vendored/CHANGELOG.md @@ -0,0 +1,70 @@ +# Mini Moka Cache — Change Log + +## Version 0.10.3 + +### Fixed + +- Fixed occasional panic in internal `FrequencySketch` in debug build. + ([#21][gh-issue-0021]) + + +## Version 0.10.2 + +### Fixed + +- Fixed a memory corruption bug caused by the timing of concurrent `insert`, + `get` and removal of the same cached entry. ([#15][gh-pull-0015]). + + +## Version 0.10.1 + +Bumped the minimum supported Rust version (MSRV) to 1.61 (May 19, 2022). +([#5][gh-pull-0005]) + +### Fixed + +- Fixed the caches mutating a deque node through a `NonNull` pointer derived from a + shared reference. ([#6][gh-pull-0006]). + + +## Version 0.10.0 + +In this version, we removed some dependencies from Mini Moka to make it more +lightweight. + +### Removed + +- Remove the background threads from the `sync::Cache` ([#1][gh-pull-0001]): + - Also remove the following dependencies: + - `scheduled-thread-pool` + - `num_cpus` + - `once_cell` (Moved to the dev-dependencies) +- Remove the following dependencies and crate features ([#2][gh-pull-0002]): + - Removed dependencies: + - `quanta` + - `parking_lot` + - `rustc_version` (from the build-dependencies) + - Removed crate features: + - `quanta` (was enabled by default) + - `atomic64` (was enabled by default) + +## Version 0.9.6 + +### Added + +- Move the relevant source code from the GitHub moka-rs/moka repository (at + [v0.9.6][moka-v0.9.6] tag) to this moka-rs/mini-moka repository. + - Rename `moka::dash` module to `mini_moka::sync`. + - Rename `moka::unsync` module to `mini_moka::unsync`. + - Rename a crate feature `dash` to `sync` and make it a default. + + +[moka-v0.9.6]: https://github.com/moka-rs/moka/tree/v0.9.6 + +[gh-issue-0021]: https://github.com/moka-rs/mini-moka/issues/21/ + +[gh-pull-0015]: https://github.com/moka-rs/mini-moka/pull/15/ +[gh-pull-0006]: https://github.com/moka-rs/mini-moka/pull/6/ +[gh-pull-0005]: https://github.com/moka-rs/mini-moka/pull/5/ +[gh-pull-0002]: https://github.com/moka-rs/mini-moka/pull/2/ +[gh-pull-0001]: https://github.com/moka-rs/mini-moka/pull/1/ diff --git a/crates/mini-moka-vendored/Cargo.toml b/crates/mini-moka-vendored/Cargo.toml new file mode 100644 index 00000000..5b4d4b54 --- /dev/null +++ b/crates/mini-moka-vendored/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "mini-moka" +version = "0.11.0" +edition = "2018" +rust-version = "1.76" + +description = "A lighter edition of Moka, a fast and concurrent cache library" +license = "MIT OR Apache-2.0" +# homepage = "https://" +documentation = "https://docs.rs/mini-moka/" +repository = "https://github.com/moka-rs/mini-moka" +keywords = ["cache", "concurrent"] +categories = ["caching", "concurrency"] +readme = "README.md" +exclude = [".circleci", ".devcontainer", ".github", ".gitpod.yml", ".vscode"] + +[features] +default = ["sync"] +js = ["dep:web-time"] + +sync = ["dashmap"] + +[dependencies] +crossbeam-channel = "0.5.5" +crossbeam-utils = "0.8" +smallvec = "1.8" +tagptr = "0.2" +web-time = { version = "1.1.0", optional = true } + +# Opt-out serde and stable_deref_trait features +# https://github.com/Manishearth/triomphe/pull/5 +triomphe = { version = "0.1.13", default-features = false } + +# Optional dependencies (enabled by default) +dashmap = { version = "6.1", optional = true } + +[dev-dependencies] +anyhow = "1.0.19" +getrandom = "0.2" +once_cell = "1.7" +wasm-bindgen-test = "0.3.50" + +[target.wasm32-unknown-unknown.dev-dependencies] +getrandom = { version="0.2", features = ["js"] } + +[target.'cfg(trybuild)'.dev-dependencies] +trybuild = "1.0" + +# https://docs.rs/about/metadata +[package.metadata.docs.rs] +# Build the doc with some features enabled. +features = [] +rustdoc-args = ["--cfg", "docsrs"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ + "cfg(kani)", + "cfg(skeptic)", + "cfg(circleci)", + "cfg(trybuild)", + "cfg(beta_clippy)", +] } diff --git a/crates/mini-moka-vendored/LICENSE-APACHE b/crates/mini-moka-vendored/LICENSE-APACHE new file mode 100644 index 00000000..cf25572b --- /dev/null +++ b/crates/mini-moka-vendored/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 - 2024 Tatsuya Kawano + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/mini-moka-vendored/LICENSE-MIT b/crates/mini-moka-vendored/LICENSE-MIT new file mode 100644 index 00000000..8271bdb9 --- /dev/null +++ b/crates/mini-moka-vendored/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 - 2024 Tatsuya Kawano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/mini-moka-vendored/README.md b/crates/mini-moka-vendored/README.md new file mode 100644 index 00000000..f1be2c4d --- /dev/null +++ b/crates/mini-moka-vendored/README.md @@ -0,0 +1,321 @@ +# Vendored in until upstream PR for wasm compat is merged or I reimplement. + +# Mini Moka + +[![GitHub Actions][gh-actions-badge]][gh-actions] +[![crates.io release][release-badge]][crate] +[![docs][docs-badge]][docs] +[![dependency status][deps-rs-badge]][deps-rs] + +[![license][license-badge]](#license) + + +Mini Moka is a fast, concurrent cache library for Rust. Mini Moka is a light edition +of [Moka][moka-git]. + +Mini Moka provides cache implementations on top of hash maps. They support full +concurrency of retrievals and a high expected concurrency for updates. Mini Moka also +provides a non-thread-safe cache implementation for single thread applications. + +All caches perform a best-effort bounding of a hash map using an entry replacement +algorithm to determine which entries to evict when the capacity is exceeded. + +[gh-actions-badge]: https://github.com/moka-rs/mini-moka/workflows/CI/badge.svg +[release-badge]: https://img.shields.io/crates/v/mini-moka.svg +[docs-badge]: https://docs.rs/mini-moka/badge.svg +[deps-rs-badge]: https://deps.rs/repo/github/moka-rs/mini-moka/status.svg + +[license-badge]: https://img.shields.io/crates/l/mini-moka.svg + + +[gh-actions]: https://github.com/moka-rs/mini-moka/actions?query=workflow%3ACI +[crate]: https://crates.io/crates/mini-moka +[docs]: https://docs.rs/mini-moka +[deps-rs]: https://deps.rs/repo/github/moka-rs/mini-moka + + + +[moka-git]: https://github.com/moka-rs/moka +[caffeine-git]: https://github.com/ben-manes/caffeine + + +## Features + +- Thread-safe, highly concurrent in-memory cache implementation. +- A cache can be bounded by one of the followings: + - The maximum number of entries. + - The total weighted size of entries. (Size aware eviction) +- Maintains near optimal hit ratio by using an entry replacement algorithms inspired + by Caffeine: + - Admission to a cache is controlled by the Least Frequently Used (LFU) policy. + - Eviction from a cache is controlled by the Least Recently Used (LRU) policy. + - [More details and some benchmark results are available here][tiny-lfu]. +- Supports expiration policies: + - Time to live + - Time to idle + + + +[tiny-lfu]: https://github.com/moka-rs/moka/wiki#admission-and-eviction-policies + + + +## Change Log + +- [CHANGELOG.md](https://github.com/moka-rs/mini-moka/blob/main/CHANGELOG.md) + + +## Table of Contents + +- [Features](#features) +- [Change Log](#change-log) +- [Usage](#usage) +- [Example: Synchronous Cache](#example-synchronous-cache) +- [Avoiding to clone the value at `get`](#avoiding-to-clone-the-value-at-get) +- Examples (Part 2) + - [Size Aware Eviction](#example-size-aware-eviction) + - [Expiration Policies](#example-expiration-policies) +- [Minimum Supported Rust Versions](#minimum-supported-rust-versions) +- [Developing Mini Moka](#developing-mini-moka) +- [Credits](#credits) +- [License](#license) + + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +mini_moka = "0.10" +``` + + +## Example: Synchronous Cache + +The thread-safe, synchronous caches are defined in the `sync` module. + +Cache entries are manually added using `insert` method, and are stored in the cache +until either evicted or manually invalidated. + +Here's an example of reading and updating a cache by using multiple threads: + +```rust +// Use the synchronous cache. +use mini_moka::sync::Cache; + +use std::thread; + +fn value(n: usize) -> String { + format!("value {}", n) +} + +fn main() { + const NUM_THREADS: usize = 16; + const NUM_KEYS_PER_THREAD: usize = 64; + + // Create a cache that can store up to 10,000 entries. + let cache = Cache::new(10_000); + + // Spawn threads and read and update the cache simultaneously. + let threads: Vec<_> = (0..NUM_THREADS) + .map(|i| { + // To share the same cache across the threads, clone it. + // This is a cheap operation. + let my_cache = cache.clone(); + let start = i * NUM_KEYS_PER_THREAD; + let end = (i + 1) * NUM_KEYS_PER_THREAD; + + thread::spawn(move || { + // Insert 64 entries. (NUM_KEYS_PER_THREAD = 64) + for key in start..end { + my_cache.insert(key, value(key)); + // get() returns Option, a clone of the stored value. + assert_eq!(my_cache.get(&key), Some(value(key))); + } + + // Invalidate every 4 element of the inserted entries. + for key in (start..end).step_by(4) { + my_cache.invalidate(&key); + } + }) + }) + .collect(); + + // Wait for all threads to complete. + threads.into_iter().for_each(|t| t.join().expect("Failed")); + + // Verify the result. + for key in 0..(NUM_THREADS * NUM_KEYS_PER_THREAD) { + if key % 4 == 0 { + assert_eq!(cache.get(&key), None); + } else { + assert_eq!(cache.get(&key), Some(value(key))); + } + } +} +``` + + +## Avoiding to clone the value at `get` + +For the concurrent cache (`sync` cache), the return type of `get` method is +`Option` instead of `Option<&V>`, where `V` is the value type. Every time `get` is +called for an existing key, it creates a clone of the stored value `V` and returns +it. This is because the `Cache` allows concurrent updates from threads so a value +stored in the cache can be dropped or replaced at any time by any other thread. `get` +cannot return a reference `&V` as it is impossible to guarantee the value outlives +the reference. + +If you want to store values that will be expensive to clone, wrap them by +`std::sync::Arc` before storing in a cache. [`Arc`][rustdoc-std-arc] is a thread-safe +reference-counted pointer and its `clone()` method is cheap. + +[rustdoc-std-arc]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html + +```rust,ignore +use std::sync::Arc; + +let key = ... +let large_value = vec![0u8; 2 * 1024 * 1024]; // 2 MiB + +// When insert, wrap the large_value by Arc. +cache.insert(key.clone(), Arc::new(large_value)); + +// get() will call Arc::clone() on the stored value, which is cheap. +cache.get(&key); +``` + + +## Example: Size Aware Eviction + +If different cache entries have different "weights" — e.g. each entry has +different memory footprints — you can specify a `weigher` closure at the cache +creation time. The closure should return a weighted size (relative size) of an entry +in `u32`, and the cache will evict entries when the total weighted size exceeds its +`max_capacity`. + +```rust +use std::convert::TryInto; +use mini_moka::sync::Cache; + +fn main() { + let cache = Cache::builder() + // A weigher closure takes &K and &V and returns a u32 representing the + // relative size of the entry. Here, we use the byte length of the value + // String as the size. + .weigher(|_key, value: &String| -> u32 { + value.len().try_into().unwrap_or(u32::MAX) + }) + // This cache will hold up to 32MiB of values. + .max_capacity(32 * 1024 * 1024) + .build(); + cache.insert(0, "zero".to_string()); +} +``` + +Note that weighted sizes are not used when making eviction selections. + + +## Example: Expiration Policies + +Mini Moka supports the following expiration policies: + +- **Time to live**: A cached entry will be expired after the specified duration past + from `insert`. +- **Time to idle**: A cached entry will be expired after the specified duration past + from `get` or `insert`. + +To set them, use the `CacheBuilder`. + +```rust +use mini_moka::sync::Cache; +use std::time::Duration; + +fn main() { + let cache = Cache::builder() + // Time to live (TTL): 30 minutes + .time_to_live(Duration::from_secs(30 * 60)) + // Time to idle (TTI): 5 minutes + .time_to_idle(Duration::from_secs( 5 * 60)) + // Create the cache. + .build(); + + // This entry will expire after 5 minutes (TTI) if there is no get(). + cache.insert(0, "zero"); + + // This get() will extend the entry life for another 5 minutes. + cache.get(&0); + + // Even though we keep calling get(), the entry will expire + // after 30 minutes (TTL) from the insert(). +} +``` + +### A note on expiration policies + +The cache builders will panic if configured with either `time_to_live` or `time to +idle` longer than 1000 years. This is done to protect against overflow when computing +key expiration. + + +## Minimum Supported Rust Versions + +Mini Moka's minimum supported Rust versions (MSRV) are the followings: + +| Feature | MSRV | +|:-----------------|:--------------------------:| +| default features | Rust 1.76.0 (Feb 8, 2024) | + +It will keep a rolling MSRV policy of at least 6 months. If only the default features +are enabled, MSRV will be updated conservatively. When using other features, MSRV +might be updated more frequently, up to the latest stable. In both cases, increasing +MSRV is _not_ considered a semver-breaking change. + + +## Developing Mini Moka + +**Running All Tests** + +To run all tests including doc tests on the README, use the following command: + +```console +$ RUSTFLAGS='--cfg trybuild' cargo test --all-features +``` + + +**Generating the Doc** + +```console +$ cargo +nightly -Z unstable-options --config 'build.rustdocflags="--cfg docsrs"' \ + doc --no-deps +``` + + +## Credits + +### Caffeine + +Mini Moka's architecture is heavily inspired by the [Caffeine][caffeine-git] library +for Java. Thanks go to Ben Manes and all contributors of Caffeine. + + +## License + +Mini Moka is distributed under either of + +- The MIT license +- The Apache License (Version 2.0) + +at your option. + +See [LICENSE-MIT](LICENSE-MIT) and [LICENSE-APACHE](LICENSE-APACHE) for details. + + diff --git a/crates/mini-moka-vendored/src/common.rs b/crates/mini-moka-vendored/src/common.rs new file mode 100644 index 00000000..81355179 --- /dev/null +++ b/crates/mini-moka-vendored/src/common.rs @@ -0,0 +1,49 @@ +use std::convert::TryInto; + +#[cfg(feature = "sync")] +pub(crate) mod concurrent; + +pub(crate) mod builder_utils; +pub(crate) mod deque; +pub(crate) mod frequency_sketch; +pub(crate) mod time; + +// Note: `CacheRegion` cannot have more than four enum variants. This is because +// `crate::{sync,unsync}::DeqNodes` uses a `tagptr::TagNonNull, 2>` +// pointer, where the 2-bit tag is `CacheRegion`. +#[derive(Clone, Copy, Debug, Eq)] +pub(crate) enum CacheRegion { + Window = 0, + MainProbation = 1, + MainProtected = 2, + Other = 3, +} + +impl From for CacheRegion { + fn from(n: usize) -> Self { + match n { + 0 => Self::Window, + 1 => Self::MainProbation, + 2 => Self::MainProtected, + 3 => Self::Other, + _ => panic!("No such CacheRegion variant for {}", n), + } + } +} + +impl PartialEq for CacheRegion { + fn eq(&self, other: &Self) -> bool { + core::mem::discriminant(self) == core::mem::discriminant(other) + } +} + +impl PartialEq for CacheRegion { + fn eq(&self, other: &usize) -> bool { + *self as usize == *other + } +} + +// Ensures the value fits in a range of `128u32..=u32::MAX`. +pub(crate) fn sketch_capacity(max_capacity: u64) -> u32 { + max_capacity.try_into().unwrap_or(u32::MAX).max(128) +} diff --git a/crates/mini-moka-vendored/src/common/builder_utils.rs b/crates/mini-moka-vendored/src/common/builder_utils.rs new file mode 100644 index 00000000..4db23a61 --- /dev/null +++ b/crates/mini-moka-vendored/src/common/builder_utils.rs @@ -0,0 +1,16 @@ +use std::time::Duration; + +const YEAR_SECONDS: u64 = 365 * 24 * 3600; + +pub(crate) fn ensure_expirations_or_panic( + time_to_live: Option, + time_to_idle: Option, +) { + let max_duration = Duration::from_secs(1_000 * YEAR_SECONDS); + if let Some(d) = time_to_live { + assert!(d <= max_duration, "time_to_live is longer than 1000 years"); + } + if let Some(d) = time_to_idle { + assert!(d <= max_duration, "time_to_idle is longer than 1000 years"); + } +} diff --git a/crates/mini-moka-vendored/src/common/concurrent.rs b/crates/mini-moka-vendored/src/common/concurrent.rs new file mode 100644 index 00000000..239e0b63 --- /dev/null +++ b/crates/mini-moka-vendored/src/common/concurrent.rs @@ -0,0 +1,255 @@ +use crate::common::{deque::DeqNode, time::Instant}; + +use std::{ptr::NonNull, sync::Arc}; +use tagptr::TagNonNull; +use triomphe::Arc as TrioArc; + +pub(crate) mod constants; +pub(crate) mod deques; +pub(crate) mod entry_info; +pub(crate) mod housekeeper; + +pub(crate) mod atomic_time; + +use self::entry_info::EntryInfo; + +pub(crate) type Weigher = Arc u32 + Send + Sync + 'static>; + +pub(crate) trait AccessTime { + fn last_accessed(&self) -> Option; + fn set_last_accessed(&self, timestamp: Instant); + fn last_modified(&self) -> Option; + fn set_last_modified(&self, timestamp: Instant); +} + +pub(crate) struct KeyHash { + pub(crate) key: Arc, + pub(crate) hash: u64, +} + +impl KeyHash { + pub(crate) fn new(key: Arc, hash: u64) -> Self { + Self { key, hash } + } +} + +impl Clone for KeyHash { + fn clone(&self) -> Self { + Self { + key: Arc::clone(&self.key), + hash: self.hash, + } + } +} + +pub(crate) struct KeyDate { + key: Arc, + entry_info: TrioArc>, +} + +impl KeyDate { + pub(crate) fn new(key: Arc, entry_info: &TrioArc>) -> Self { + Self { + key, + entry_info: TrioArc::clone(entry_info), + } + } + + pub(crate) fn key(&self) -> &Arc { + &self.key + } +} + +pub(crate) struct KeyHashDate { + key: Arc, + hash: u64, + entry_info: TrioArc>, +} + +impl KeyHashDate { + pub(crate) fn new(kh: KeyHash, entry_info: &TrioArc>) -> Self { + Self { + key: kh.key, + hash: kh.hash, + entry_info: TrioArc::clone(entry_info), + } + } + + pub(crate) fn key(&self) -> &Arc { + &self.key + } + + pub(crate) fn hash(&self) -> u64 { + self.hash + } + + pub(crate) fn entry_info(&self) -> &EntryInfo { + &self.entry_info + } +} + +pub(crate) struct KvEntry { + pub(crate) key: Arc, + pub(crate) entry: TrioArc>, +} + +impl KvEntry { + pub(crate) fn new(key: Arc, entry: TrioArc>) -> Self { + Self { key, entry } + } +} + +impl AccessTime for DeqNode> { + #[inline] + fn last_accessed(&self) -> Option { + None + } + + #[inline] + fn set_last_accessed(&self, _timestamp: Instant) { + unreachable!(); + } + + #[inline] + fn last_modified(&self) -> Option { + self.element.entry_info.last_modified() + } + + #[inline] + fn set_last_modified(&self, timestamp: Instant) { + self.element.entry_info.set_last_modified(timestamp); + } +} + +impl AccessTime for DeqNode> { + #[inline] + fn last_accessed(&self) -> Option { + self.element.entry_info.last_accessed() + } + + #[inline] + fn set_last_accessed(&self, timestamp: Instant) { + self.element.entry_info.set_last_accessed(timestamp); + } + + #[inline] + fn last_modified(&self) -> Option { + None + } + + #[inline] + fn set_last_modified(&self, _timestamp: Instant) { + unreachable!(); + } +} + +// DeqNode for an access order queue. +pub(crate) type KeyDeqNodeAo = TagNonNull>, 2>; + +// DeqNode for the write order queue. +pub(crate) type KeyDeqNodeWo = NonNull>>; + +pub(crate) struct ValueEntry { + pub(crate) value: V, + info: TrioArc>, +} + +impl ValueEntry { + pub(crate) fn new(value: V, entry_info: TrioArc>) -> Self { + Self { + value, + info: entry_info, + } + } + + pub(crate) fn entry_info(&self) -> &TrioArc> { + &self.info + } + + pub(crate) fn is_admitted(&self) -> bool { + self.info.is_admitted() + } + + pub(crate) fn set_admitted(&self, value: bool) { + self.info.set_admitted(value); + } + + pub(crate) fn is_dirty(&self) -> bool { + self.info.is_dirty() + } + + pub(crate) fn set_dirty(&self, value: bool) { + self.info.set_dirty(value); + } + + #[inline] + pub(crate) fn policy_weight(&self) -> u32 { + self.info.policy_weight() + } + + pub(crate) fn access_order_q_node(&self) -> Option> { + self.info.access_order_q_node() + } + + pub(crate) fn set_access_order_q_node(&self, node: Option>) { + self.info.set_access_order_q_node(node); + } + + pub(crate) fn take_access_order_q_node(&self) -> Option> { + self.info.take_access_order_q_node() + } + + pub(crate) fn write_order_q_node(&self) -> Option> { + self.info.write_order_q_node() + } + + pub(crate) fn set_write_order_q_node(&self, node: Option>) { + self.info.set_write_order_q_node(node) + } + + pub(crate) fn take_write_order_q_node(&self) -> Option> { + self.info.take_write_order_q_node() + } + + pub(crate) fn unset_q_nodes(&self) { + self.info.unset_q_nodes(); + } +} + +impl AccessTime for TrioArc> { + #[inline] + fn last_accessed(&self) -> Option { + self.info.last_accessed() + } + + #[inline] + fn set_last_accessed(&self, timestamp: Instant) { + self.info.set_last_accessed(timestamp); + } + + #[inline] + fn last_modified(&self) -> Option { + self.info.last_modified() + } + + #[inline] + fn set_last_modified(&self, timestamp: Instant) { + self.info.set_last_modified(timestamp); + } +} + +pub(crate) enum ReadOp { + // u64 is the hash of the key. + Hit(u64, TrioArc>, Instant), + Miss(u64), +} + +pub(crate) enum WriteOp { + Upsert { + key_hash: KeyHash, + value_entry: TrioArc>, + old_weight: u32, + new_weight: u32, + }, + Remove(KvEntry), +} diff --git a/crates/mini-moka-vendored/src/common/concurrent/atomic_time.rs b/crates/mini-moka-vendored/src/common/concurrent/atomic_time.rs new file mode 100644 index 00000000..89d9553e --- /dev/null +++ b/crates/mini-moka-vendored/src/common/concurrent/atomic_time.rs @@ -0,0 +1,35 @@ +use super::Instant; + +use std::sync::RwLock; + +pub(crate) struct AtomicInstant { + instant: RwLock>, +} + +impl Default for AtomicInstant { + fn default() -> Self { + Self { + instant: RwLock::new(None), + } + } +} + +impl AtomicInstant { + pub(crate) fn new(timestamp: Instant) -> Self { + let ai = Self::default(); + ai.set_instant(timestamp); + ai + } + + pub(crate) fn is_set(&self) -> bool { + self.instant.read().expect("lock poisoned").is_some() + } + + pub(crate) fn instant(&self) -> Option { + *self.instant.read().expect("lock poisoned") + } + + pub(crate) fn set_instant(&self, instant: Instant) { + *self.instant.write().expect("lock poisoned") = Some(instant); + } +} diff --git a/crates/mini-moka-vendored/src/common/concurrent/constants.rs b/crates/mini-moka-vendored/src/common/concurrent/constants.rs new file mode 100644 index 00000000..2c2be546 --- /dev/null +++ b/crates/mini-moka-vendored/src/common/concurrent/constants.rs @@ -0,0 +1,10 @@ +pub(crate) const MAX_SYNC_REPEATS: usize = 4; +pub(crate) const PERIODICAL_SYNC_INTERVAL_MILLIS: u64 = 500; + +pub(crate) const READ_LOG_FLUSH_POINT: usize = 64; +pub(crate) const READ_LOG_SIZE: usize = READ_LOG_FLUSH_POINT * (MAX_SYNC_REPEATS + 2); + +pub(crate) const WRITE_LOG_FLUSH_POINT: usize = 64; +pub(crate) const WRITE_LOG_SIZE: usize = WRITE_LOG_FLUSH_POINT * (MAX_SYNC_REPEATS + 2); + +pub(crate) const WRITE_RETRY_INTERVAL_MICROS: u64 = 50; diff --git a/crates/mini-moka-vendored/src/common/concurrent/deques.rs b/crates/mini-moka-vendored/src/common/concurrent/deques.rs new file mode 100644 index 00000000..c7eed6c1 --- /dev/null +++ b/crates/mini-moka-vendored/src/common/concurrent/deques.rs @@ -0,0 +1,181 @@ +use super::{KeyDate, KeyHashDate, ValueEntry}; +use crate::common::{ + deque::{DeqNode, Deque}, + CacheRegion, +}; + +use std::ptr::NonNull; +use tagptr::TagNonNull; +use triomphe::Arc as TrioArc; +pub(crate) struct Deques { + pub(crate) window: Deque>, // Not used yet. + pub(crate) probation: Deque>, + pub(crate) protected: Deque>, // Not used yet. + pub(crate) write_order: Deque>, +} + +impl Default for Deques { + fn default() -> Self { + Self { + window: Deque::new(CacheRegion::Window), + probation: Deque::new(CacheRegion::MainProbation), + protected: Deque::new(CacheRegion::MainProtected), + write_order: Deque::new(CacheRegion::Other), + } + } +} + +impl Deques { + pub(crate) fn push_back_ao( + &mut self, + region: CacheRegion, + khd: KeyHashDate, + entry: &TrioArc>, + ) { + let node = Box::new(DeqNode::new(khd)); + let node = match region { + CacheRegion::Window => self.window.push_back(node), + CacheRegion::MainProbation => self.probation.push_back(node), + CacheRegion::MainProtected => self.protected.push_back(node), + _ => unreachable!(), + }; + let tagged_node = TagNonNull::compose(node, region as usize); + entry.set_access_order_q_node(Some(tagged_node)); + } + + pub(crate) fn push_back_wo(&mut self, kd: KeyDate, entry: &TrioArc>) { + let node = Box::new(DeqNode::new(kd)); + let node = self.write_order.push_back(node); + entry.set_write_order_q_node(Some(node)); + } + + pub(crate) fn move_to_back_ao(&mut self, entry: &TrioArc>) { + if let Some(tagged_node) = entry.access_order_q_node() { + let (node, tag) = tagged_node.decompose(); + let p = unsafe { node.as_ref() }; + match tag.into() { + CacheRegion::Window if self.window.contains(p) => { + unsafe { self.window.move_to_back(node) }; + } + CacheRegion::MainProbation if self.probation.contains(p) => { + unsafe { self.probation.move_to_back(node) }; + } + CacheRegion::MainProtected if self.protected.contains(p) => { + unsafe { self.protected.move_to_back(node) }; + } + _ => unreachable!(), + } + } + } + + pub(crate) fn move_to_back_ao_in_deque( + deq_name: &str, + deq: &mut Deque>, + entry: &TrioArc>, + ) { + if let Some(tagged_node) = entry.access_order_q_node() { + let (node, tag) = tagged_node.decompose(); + let p = unsafe { node.as_ref() }; + if deq.region() == tag { + if deq.contains(p) { + unsafe { deq.move_to_back(node) }; + } + } else { + panic!( + "move_to_back_ao_in_deque - node is not a member of {} deque. {:?}", + deq_name, p, + ) + } + } + } + + pub(crate) fn move_to_back_wo(&mut self, entry: &TrioArc>) { + if let Some(node) = entry.write_order_q_node() { + let p = unsafe { node.as_ref() }; + if self.write_order.contains(p) { + unsafe { self.write_order.move_to_back(node) }; + } + } + } + + pub(crate) fn move_to_back_wo_in_deque( + deq: &mut Deque>, + entry: &TrioArc>, + ) { + if let Some(node) = entry.write_order_q_node() { + let p = unsafe { node.as_ref() }; + if deq.contains(p) { + unsafe { deq.move_to_back(node) }; + } + } + } + + pub(crate) fn unlink_ao(&mut self, entry: &TrioArc>) { + if let Some(node) = entry.take_access_order_q_node() { + self.unlink_node_ao(node); + } + } + + pub(crate) fn unlink_ao_from_deque( + deq_name: &str, + deq: &mut Deque>, + entry: &TrioArc>, + ) { + if let Some(node) = entry.take_access_order_q_node() { + unsafe { Self::unlink_node_ao_from_deque(deq_name, deq, node) }; + } + } + + pub(crate) fn unlink_wo(deq: &mut Deque>, entry: &TrioArc>) { + if let Some(node) = entry.take_write_order_q_node() { + Self::unlink_node_wo(deq, node); + } + } + + pub(crate) fn unlink_node_ao(&mut self, tagged_node: TagNonNull>, 2>) { + unsafe { + match tagged_node.decompose_tag().into() { + CacheRegion::Window => { + Self::unlink_node_ao_from_deque("window", &mut self.window, tagged_node) + } + CacheRegion::MainProbation => { + Self::unlink_node_ao_from_deque("probation", &mut self.probation, tagged_node) + } + CacheRegion::MainProtected => { + Self::unlink_node_ao_from_deque("protected", &mut self.protected, tagged_node) + } + _ => unreachable!(), + } + } + } + + unsafe fn unlink_node_ao_from_deque( + deq_name: &str, + deq: &mut Deque>, + tagged_node: TagNonNull>, 2>, + ) { + let (node, tag) = tagged_node.decompose(); + let p = node.as_ref(); + if deq.region() == tag { + if deq.contains(p) { + // https://github.com/moka-rs/moka/issues/64 + deq.unlink_and_drop(node); + } + } else { + panic!( + "unlink_node - node is not a member of {} deque. {:?}", + deq_name, p + ) + } + } + + pub(crate) fn unlink_node_wo(deq: &mut Deque>, node: NonNull>>) { + unsafe { + let p = node.as_ref(); + if deq.contains(p) { + // https://github.com/moka-rs/moka/issues/64 + deq.unlink_and_drop(node); + } + } + } +} diff --git a/crates/mini-moka-vendored/src/common/concurrent/entry_info.rs b/crates/mini-moka-vendored/src/common/concurrent/entry_info.rs new file mode 100644 index 00000000..8a10c957 --- /dev/null +++ b/crates/mini-moka-vendored/src/common/concurrent/entry_info.rs @@ -0,0 +1,150 @@ +use std::sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + Mutex, +}; + +use super::{AccessTime, KeyDeqNodeAo, KeyDeqNodeWo}; +use crate::common::{concurrent::atomic_time::AtomicInstant, time::Instant}; + +pub(crate) struct DeqNodes { + access_order_q_node: Option>, + write_order_q_node: Option>, +} + +// We need this `unsafe impl` as DeqNodes have NonNull pointers. +unsafe impl Send for DeqNodes {} + +pub(crate) struct EntryInfo { + /// `is_admitted` indicates that the entry has been admitted to the + /// cache. When `false`, it means the entry is _temporary_ admitted to + /// the cache or evicted from the cache (so it should not have LRU nodes). + is_admitted: AtomicBool, + /// `is_dirty` indicates that the entry has been inserted (or updated) + /// in the hash table, but the history of the insertion has not yet + /// been applied to the LRU deques and LFU estimator. + is_dirty: AtomicBool, + last_accessed: AtomicInstant, + last_modified: AtomicInstant, + policy_weight: AtomicU32, + nodes: Mutex>, +} + +impl EntryInfo { + #[inline] + pub(crate) fn new(timestamp: Instant, policy_weight: u32) -> Self { + Self { + is_admitted: Default::default(), + is_dirty: AtomicBool::new(true), + last_accessed: AtomicInstant::new(timestamp), + last_modified: AtomicInstant::new(timestamp), + policy_weight: AtomicU32::new(policy_weight), + nodes: Mutex::new(DeqNodes { + access_order_q_node: None, + write_order_q_node: None, + }), + } + } + + #[inline] + pub(crate) fn is_admitted(&self) -> bool { + self.is_admitted.load(Ordering::Acquire) + } + + #[inline] + pub(crate) fn set_admitted(&self, value: bool) { + self.is_admitted.store(value, Ordering::Release); + } + + #[inline] + pub(crate) fn is_dirty(&self) -> bool { + self.is_dirty.load(Ordering::Acquire) + } + + #[inline] + pub(crate) fn set_dirty(&self, value: bool) { + self.is_dirty.store(value, Ordering::Release); + } + + #[inline] + pub(crate) fn policy_weight(&self) -> u32 { + self.policy_weight.load(Ordering::Acquire) + } + + #[inline] + pub(crate) fn set_policy_weight(&self, size: u32) { + self.policy_weight.store(size, Ordering::Release); + } + + #[inline] + pub(crate) fn access_order_q_node(&self) -> Option> { + self.nodes + .lock() + .expect("lock poisoned") + .access_order_q_node + } + + #[inline] + pub(crate) fn set_access_order_q_node(&self, node: Option>) { + self.nodes + .lock() + .expect("lock poisoned") + .access_order_q_node = node; + } + + #[inline] + pub(crate) fn take_access_order_q_node(&self) -> Option> { + self.nodes + .lock() + .expect("lock poisoned") + .access_order_q_node + .take() + } + + #[inline] + pub(crate) fn write_order_q_node(&self) -> Option> { + self.nodes.lock().expect("lock poisoned").write_order_q_node + } + + #[inline] + pub(crate) fn set_write_order_q_node(&self, node: Option>) { + self.nodes.lock().expect("lock poisoned").write_order_q_node = node; + } + + #[inline] + pub(crate) fn take_write_order_q_node(&self) -> Option> { + self.nodes + .lock() + .expect("lock poisoned") + .write_order_q_node + .take() + } + + #[inline] + pub(crate) fn unset_q_nodes(&self) { + let mut nodes = self.nodes.lock().expect("lock poisoned"); + nodes.access_order_q_node = None; + nodes.write_order_q_node = None; + } +} + +impl AccessTime for EntryInfo { + #[inline] + fn last_accessed(&self) -> Option { + self.last_accessed.instant() + } + + #[inline] + fn set_last_accessed(&self, timestamp: Instant) { + self.last_accessed.set_instant(timestamp); + } + + #[inline] + fn last_modified(&self) -> Option { + self.last_modified.instant() + } + + #[inline] + fn set_last_modified(&self, timestamp: Instant) { + self.last_modified.set_instant(timestamp); + } +} diff --git a/crates/mini-moka-vendored/src/common/concurrent/housekeeper.rs b/crates/mini-moka-vendored/src/common/concurrent/housekeeper.rs new file mode 100644 index 00000000..8fed25d3 --- /dev/null +++ b/crates/mini-moka-vendored/src/common/concurrent/housekeeper.rs @@ -0,0 +1,77 @@ +use super::{ + atomic_time::AtomicInstant, + constants::{ + MAX_SYNC_REPEATS, PERIODICAL_SYNC_INTERVAL_MILLIS, READ_LOG_FLUSH_POINT, + WRITE_LOG_FLUSH_POINT, + }, +}; + +use crate::common::time::{CheckedTimeOps, Instant}; + +use std::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, +}; + +pub(crate) trait InnerSync { + fn sync(&self, max_sync_repeats: usize); + fn now(&self) -> Instant; +} + +pub(crate) struct Housekeeper { + is_sync_running: AtomicBool, + sync_after: AtomicInstant, +} + +impl Default for Housekeeper { + fn default() -> Self { + Self { + is_sync_running: Default::default(), + sync_after: AtomicInstant::new(Self::sync_after(Instant::now())), + } + } +} + +impl Housekeeper { + pub(crate) fn should_apply_reads(&self, ch_len: usize, now: Instant) -> bool { + self.should_apply(ch_len, READ_LOG_FLUSH_POINT, now) + } + + pub(crate) fn should_apply_writes(&self, ch_len: usize, now: Instant) -> bool { + self.should_apply(ch_len, WRITE_LOG_FLUSH_POINT, now) + } + + #[inline] + pub(crate) fn should_apply(&self, ch_len: usize, ch_flush_point: usize, now: Instant) -> bool { + ch_len >= ch_flush_point || self.sync_after.instant().unwrap() >= now + } + + pub(crate) fn try_sync(&self, cache: &T) -> bool { + // Try to flip the value of sync_scheduled from false to true. + match self.is_sync_running.compare_exchange( + false, + true, + Ordering::Acquire, + Ordering::Relaxed, + ) { + Ok(_) => { + let now = cache.now(); + self.sync_after.set_instant(Self::sync_after(now)); + + cache.sync(MAX_SYNC_REPEATS); + + self.is_sync_running.store(false, Ordering::Release); + true + } + Err(_) => false, + } + } + + fn sync_after(now: Instant) -> Instant { + let dur = Duration::from_millis(PERIODICAL_SYNC_INTERVAL_MILLIS); + let ts = now.checked_add(dur); + // Assuming that `now` is current wall clock time, this should never fail at + // least next millions of years. + ts.expect("Timestamp overflow") + } +} diff --git a/crates/mini-moka-vendored/src/common/deque.rs b/crates/mini-moka-vendored/src/common/deque.rs new file mode 100644 index 00000000..30d8b0ff --- /dev/null +++ b/crates/mini-moka-vendored/src/common/deque.rs @@ -0,0 +1,773 @@ +// License and Copyright Notice: +// +// Some of the code and doc comments in this module were copied from +// `std::collections::LinkedList` in the Rust standard library. +// https://github.com/rust-lang/rust/blob/master/src/liballoc/collections/linked_list.rs +// +// The original code/comments from LinkedList are dual-licensed under +// the Apache License, Version 2.0 +// or the MIT license +// +// Copyrights of the original code/comments are retained by their contributors. +// For full authorship information, see the version control history of +// https://github.com/rust-lang/rust/ or https://thanks.rust-lang.org + +use std::{marker::PhantomData, ptr::NonNull}; + +use super::CacheRegion; + +// `crate::{sync,unsync}::DeqNodes` uses a `tagptr::TagNonNull, 2>` +// pointer. To reserve the space for the 2-bit tag, use 4 bytes as the *minimum* +// alignment. +// https://doc.rust-lang.org/reference/type-layout.html#the-alignment-modifiers +#[repr(align(4))] +#[derive(PartialEq, Eq)] +pub(crate) struct DeqNode { + next: Option>>, + prev: Option>>, + pub(crate) element: T, +} + +impl std::fmt::Debug for DeqNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DeqNode") + .field("next", &self.next) + .field("prev", &self.prev) + .finish() + } +} + +impl DeqNode { + pub(crate) fn new(element: T) -> Self { + Self { + next: None, + prev: None, + element, + } + } + + pub(crate) fn next_node_ptr(this: NonNull) -> Option>> { + unsafe { this.as_ref() }.next + } +} + +/// Cursor is used to remember the current iterating position. +enum DeqCursor { + Node(NonNull>), + Done, +} + +pub(crate) struct Deque { + region: CacheRegion, + len: usize, + head: Option>>, + tail: Option>>, + cursor: Option>, + marker: PhantomData>>, +} + +impl Drop for Deque { + fn drop(&mut self) { + struct DropGuard<'a, T>(&'a mut Deque); + + impl Drop for DropGuard<'_, T> { + fn drop(&mut self) { + // Continue the same loop we do below. This only runs when a destructor has + // panicked. If another one panics this will abort. + while self.0.pop_front().is_some() {} + } + } + + while let Some(node) = self.pop_front() { + let guard = DropGuard(self); + drop(node); + std::mem::forget(guard); + } + } +} + +// Inner crate public function/methods +impl Deque { + pub(crate) fn new(region: CacheRegion) -> Self { + Self { + region, + len: 0, + head: None, + tail: None, + cursor: None, + marker: PhantomData, + } + } + + pub(crate) fn region(&self) -> CacheRegion { + self.region + } + + #[cfg(test)] + pub(crate) fn len(&self) -> usize { + self.len + } + + pub(crate) fn contains(&self, node: &DeqNode) -> bool { + node.prev.is_some() || self.is_head(node) + } + + pub(crate) fn peek_front(&self) -> Option<&DeqNode> { + self.head.as_ref().map(|node| unsafe { node.as_ref() }) + } + + pub(crate) fn peek_front_ptr(&self) -> Option>> { + self.head.as_ref().cloned() + } + + /// Removes and returns the node at the front of the list. + pub(crate) fn pop_front(&mut self) -> Option>> { + // This method takes care not to create mutable references to whole nodes, + // to maintain validity of aliasing pointers into `element`. + self.head.map(|node| unsafe { + if self.is_at_cursor(node.as_ref()) { + self.advance_cursor(); + } + + let mut node = Box::from_raw(node.as_ptr()); + self.head = node.next; + + match self.head { + None => self.tail = None, + // Not creating new mutable (unique!) references overlapping `element`. + Some(head) => (*head.as_ptr()).prev = None, + } + + self.len -= 1; + + node.prev = None; + node.next = None; + node + }) + } + + #[cfg(test)] + pub(crate) fn peek_back(&self) -> Option<&DeqNode> { + self.tail.as_ref().map(|node| unsafe { node.as_ref() }) + } + + /// Adds the given node to the back of the list. + pub(crate) fn push_back(&mut self, mut node: Box>) -> NonNull> { + // This method takes care not to create mutable references to whole nodes, + // to maintain validity of aliasing pointers into `element`. + unsafe { + node.next = None; + node.prev = self.tail; + let node = NonNull::new(Box::into_raw(node)).expect("Got a null ptr"); + + match self.tail { + None => self.head = Some(node), + // Not creating new mutable (unique!) references overlapping `element`. + Some(tail) => (*tail.as_ptr()).next = Some(node), + } + + self.tail = Some(node); + self.len += 1; + node + } + } + + pub(crate) unsafe fn move_to_back(&mut self, mut node: NonNull>) { + if self.is_tail(node.as_ref()) { + // Already at the tail. Nothing to do. + return; + } + + if self.is_at_cursor(node.as_ref()) { + self.advance_cursor(); + } + + let node = node.as_mut(); // this one is ours now, we can create an &mut. + + // Not creating new mutable (unique!) references overlapping `element`. + match node.prev { + Some(prev) if node.next.is_some() => (*prev.as_ptr()).next = node.next, + Some(..) => (), + // This node is the head node. + None => self.head = node.next, + }; + + // This node is not the tail node. + if let Some(next) = node.next.take() { + (*next.as_ptr()).prev = node.prev; + + let mut node = NonNull::from(node); + match self.tail { + // Not creating new mutable (unique!) references overlapping `element`. + Some(tail) => { + node.as_mut().prev = Some(tail); + (*tail.as_ptr()).next = Some(node) + } + None => unreachable!(), + } + self.tail = Some(node); + } + } + + pub(crate) fn move_front_to_back(&mut self) { + if let Some(node) = self.head { + unsafe { self.move_to_back(node) }; + } + } + + /// Unlinks the specified node from the current list. + /// + /// This method takes care not to create mutable references to `element`, to + /// maintain validity of aliasing pointers. + /// + /// IMPORTANT: This method does not drop the node. If the node is no longer + /// needed, use `unlink_and_drop` instead, or drop it at the caller side. + /// Otherwise, the node will leak. + pub(crate) unsafe fn unlink(&mut self, mut node: NonNull>) { + if self.is_at_cursor(node.as_ref()) { + self.advance_cursor(); + } + + let node = node.as_mut(); // this one is ours now, we can create an &mut. + + // Not creating new mutable (unique!) references overlapping `element`. + match node.prev { + Some(prev) => (*prev.as_ptr()).next = node.next, + // this node is the head node + None => self.head = node.next, + }; + + match node.next { + Some(next) => (*next.as_ptr()).prev = node.prev, + // this node is the tail node + None => self.tail = node.prev, + }; + + node.prev = None; + node.next = None; + + self.len -= 1; + } + + /// Unlinks the specified node from the current list, and then drop the node. + /// + /// This method takes care not to create mutable references to `element`, to + /// maintain validity of aliasing pointers. + /// + /// Panics: + pub(crate) unsafe fn unlink_and_drop(&mut self, node: NonNull>) { + self.unlink(node); + std::mem::drop(Box::from_raw(node.as_ptr())); + } + + #[cfg(test)] + pub(crate) fn reset_cursor(&mut self) { + self.cursor = None; + } +} + +impl<'a, T> Iterator for &'a mut Deque { + type Item = &'a T; + + fn next(&mut self) -> Option { + if self.cursor.is_none() { + if let Some(head) = self.head { + self.cursor = Some(DeqCursor::Node(head)); + } + } + let elem = if let Some(DeqCursor::Node(node)) = self.cursor { + unsafe { Some(&(*node.as_ptr()).element) } + } else { + None + }; + self.advance_cursor(); + elem + } +} + +// Private function/methods +impl Deque { + fn is_head(&self, node: &DeqNode) -> bool { + if let Some(head) = self.head { + std::ptr::eq(unsafe { head.as_ref() }, node) + } else { + false + } + } + + fn is_tail(&self, node: &DeqNode) -> bool { + if let Some(tail) = self.tail { + std::ptr::eq(unsafe { tail.as_ref() }, node) + } else { + false + } + } + + fn is_at_cursor(&self, node: &DeqNode) -> bool { + if let Some(DeqCursor::Node(cur_node)) = self.cursor { + std::ptr::eq(unsafe { cur_node.as_ref() }, node) + } else { + false + } + } + + fn advance_cursor(&mut self) { + match self.cursor.take() { + None => (), + Some(DeqCursor::Node(node)) => unsafe { + if let Some(next) = (*node.as_ptr()).next { + self.cursor = Some(DeqCursor::Node(next)); + } else { + self.cursor = Some(DeqCursor::Done); + } + }, + Some(DeqCursor::Done) => { + self.cursor = None; + } + } + } +} + +#[cfg(test)] +mod tests { + use super::{CacheRegion::MainProbation, DeqNode, Deque}; + + #[test] + #[allow(clippy::cognitive_complexity)] + fn basics() { + let mut deque: Deque = Deque::new(MainProbation); + assert_eq!(deque.len(), 0); + assert!(deque.peek_front().is_none()); + assert!(deque.peek_back().is_none()); + + // push_back(node1) + let node1 = DeqNode::new("a".to_string()); + assert!(!deque.contains(&node1)); + let node1 = Box::new(node1); + let node1_ptr = deque.push_back(node1); + assert_eq!(deque.len(), 1); + + // peek_front() -> node1 + let head_a = deque.peek_front().unwrap(); + assert!(deque.contains(head_a)); + assert!(deque.is_head(head_a)); + assert!(deque.is_tail(head_a)); + assert_eq!(head_a.element, "a".to_string()); + + // move_to_back(node1) + unsafe { deque.move_to_back(node1_ptr) }; + assert_eq!(deque.len(), 1); + + // peek_front() -> node1 + let head_b = deque.peek_front().unwrap(); + assert!(deque.contains(head_b)); + assert!(deque.is_head(head_b)); + assert!(deque.is_tail(head_b)); + assert!(std::ptr::eq(head_b, node1_ptr.as_ptr())); + assert!(head_b.prev.is_none()); + assert!(head_b.next.is_none()); + + // peek_back() -> node1 + let tail_a = deque.peek_back().unwrap(); + assert!(deque.contains(tail_a)); + assert!(deque.is_head(tail_a)); + assert!(deque.is_tail(tail_a)); + assert!(std::ptr::eq(tail_a, node1_ptr.as_ptr())); + assert!(tail_a.prev.is_none()); + assert!(tail_a.next.is_none()); + + // push_back(node2) + let node2 = DeqNode::new("b".to_string()); + assert!(!deque.contains(&node2)); + let node2_ptr = deque.push_back(Box::new(node2)); + assert_eq!(deque.len(), 2); + + // peek_front() -> node1 + let head_c = deque.peek_front().unwrap(); + assert!(deque.contains(head_c)); + assert!(deque.is_head(head_c)); + assert!(!deque.is_tail(head_c)); + assert!(std::ptr::eq(head_c, node1_ptr.as_ptr())); + assert!(head_c.prev.is_none()); + assert!(std::ptr::eq( + head_c.next.unwrap().as_ptr(), + node2_ptr.as_ptr() + )); + + // move_to_back(node2) + unsafe { deque.move_to_back(node2_ptr) }; + assert_eq!(deque.len(), 2); + + // peek_front() -> node1 + let head_d = deque.peek_front().unwrap(); + assert!(deque.contains(head_d)); + assert!(deque.is_head(head_d)); + assert!(!deque.is_tail(head_d)); + assert!(std::ptr::eq(head_d, node1_ptr.as_ptr())); + assert!(head_d.prev.is_none()); + assert!(std::ptr::eq( + head_d.next.unwrap().as_ptr(), + node2_ptr.as_ptr() + )); + + // peek_back() -> node2 + let tail_b = deque.peek_back().unwrap(); + assert!(deque.contains(tail_b)); + assert!(!deque.is_head(tail_b)); + assert!(deque.is_tail(tail_b)); + assert!(std::ptr::eq(tail_b, node2_ptr.as_ptr())); + assert!(std::ptr::eq( + tail_b.prev.unwrap().as_ptr(), + node1_ptr.as_ptr() + )); + assert_eq!(tail_b.element, "b".to_string()); + assert!(tail_b.next.is_none()); + + // move_to_back(node1) + unsafe { deque.move_to_back(node1_ptr) }; + assert_eq!(deque.len(), 2); + + // peek_front() -> node2 + let head_e = deque.peek_front().unwrap(); + assert!(deque.contains(head_e)); + assert!(deque.is_head(head_e)); + assert!(!deque.is_tail(head_e)); + assert!(std::ptr::eq(head_e, node2_ptr.as_ptr())); + assert!(head_e.prev.is_none()); + assert!(std::ptr::eq( + head_e.next.unwrap().as_ptr(), + node1_ptr.as_ptr() + )); + + // peek_back() -> node1 + let tail_c = deque.peek_back().unwrap(); + assert!(deque.contains(tail_c)); + assert!(!deque.is_head(tail_c)); + assert!(deque.is_tail(tail_c)); + assert!(std::ptr::eq(tail_c, node1_ptr.as_ptr())); + assert!(std::ptr::eq( + tail_c.prev.unwrap().as_ptr(), + node2_ptr.as_ptr() + )); + assert!(tail_c.next.is_none()); + + // push_back(node3) + let node3 = DeqNode::new("c".to_string()); + assert!(!deque.contains(&node3)); + let node3_ptr = deque.push_back(Box::new(node3)); + assert_eq!(deque.len(), 3); + + // peek_front() -> node2 + let head_f = deque.peek_front().unwrap(); + assert!(deque.contains(head_f)); + assert!(deque.is_head(head_f)); + assert!(!deque.is_tail(head_f)); + assert!(std::ptr::eq(head_f, node2_ptr.as_ptr())); + assert!(head_f.prev.is_none()); + assert!(std::ptr::eq( + head_f.next.unwrap().as_ptr(), + node1_ptr.as_ptr() + )); + + // peek_back() -> node3 + let tail_d = deque.peek_back().unwrap(); + assert!(std::ptr::eq(tail_d, node3_ptr.as_ptr())); + assert_eq!(tail_d.element, "c".to_string()); + assert!(deque.contains(tail_d)); + assert!(!deque.is_head(tail_d)); + assert!(deque.is_tail(tail_d)); + assert!(std::ptr::eq(tail_d, node3_ptr.as_ptr())); + assert!(std::ptr::eq( + tail_d.prev.unwrap().as_ptr(), + node1_ptr.as_ptr() + )); + assert!(tail_d.next.is_none()); + + // move_to_back(node1) + unsafe { deque.move_to_back(node1_ptr) }; + assert_eq!(deque.len(), 3); + + // peek_front() -> node2 + let head_g = deque.peek_front().unwrap(); + assert!(deque.contains(head_g)); + assert!(deque.is_head(head_g)); + assert!(!deque.is_tail(head_g)); + assert!(std::ptr::eq(head_g, node2_ptr.as_ptr())); + assert!(head_g.prev.is_none()); + assert!(std::ptr::eq( + head_g.next.unwrap().as_ptr(), + node3_ptr.as_ptr() + )); + + // peek_back() -> node1 + let tail_e = deque.peek_back().unwrap(); + assert!(deque.contains(tail_e)); + assert!(!deque.is_head(tail_e)); + assert!(deque.is_tail(tail_e)); + assert!(std::ptr::eq(tail_e, node1_ptr.as_ptr())); + assert!(std::ptr::eq( + tail_e.prev.unwrap().as_ptr(), + node3_ptr.as_ptr() + )); + assert!(tail_e.next.is_none()); + + // unlink(node3) + unsafe { deque.unlink(node3_ptr) }; + assert_eq!(deque.len(), 2); + let node3_ref = unsafe { node3_ptr.as_ref() }; + assert!(!deque.contains(node3_ref)); + assert!(node3_ref.next.is_none()); + assert!(node3_ref.next.is_none()); + std::mem::drop(unsafe { Box::from_raw(node3_ptr.as_ptr()) }); + + // peek_front() -> node2 + let head_h = deque.peek_front().unwrap(); + assert!(deque.contains(head_h)); + assert!(deque.is_head(head_h)); + assert!(!deque.is_tail(head_h)); + assert!(std::ptr::eq(head_h, node2_ptr.as_ptr())); + assert!(head_h.prev.is_none()); + assert!(std::ptr::eq( + head_h.next.unwrap().as_ptr(), + node1_ptr.as_ptr() + )); + + // peek_back() -> node1 + let tail_f = deque.peek_back().unwrap(); + assert!(deque.contains(tail_f)); + assert!(!deque.is_head(tail_f)); + assert!(deque.is_tail(tail_f)); + assert!(std::ptr::eq(tail_f, node1_ptr.as_ptr())); + assert!(std::ptr::eq( + tail_f.prev.unwrap().as_ptr(), + node2_ptr.as_ptr() + )); + assert!(tail_f.next.is_none()); + + // unlink(node2) + unsafe { deque.unlink(node2_ptr) }; + assert_eq!(deque.len(), 1); + let node2_ref = unsafe { node2_ptr.as_ref() }; + assert!(!deque.contains(node2_ref)); + assert!(node2_ref.next.is_none()); + assert!(node2_ref.next.is_none()); + std::mem::drop(unsafe { Box::from_raw(node2_ptr.as_ptr()) }); + + // peek_front() -> node1 + let head_g = deque.peek_front().unwrap(); + assert!(deque.contains(head_g)); + assert!(deque.is_head(head_g)); + assert!(deque.is_tail(head_g)); + assert!(std::ptr::eq(head_g, node1_ptr.as_ptr())); + assert!(head_g.prev.is_none()); + assert!(head_g.next.is_none()); + + // peek_back() -> node1 + let tail_g = deque.peek_back().unwrap(); + assert!(deque.contains(tail_g)); + assert!(deque.is_head(tail_g)); + assert!(deque.is_tail(tail_g)); + assert!(std::ptr::eq(tail_g, node1_ptr.as_ptr())); + assert!(tail_g.next.is_none()); + assert!(tail_g.next.is_none()); + + // unlink(node1) + unsafe { deque.unlink(node1_ptr) }; + assert_eq!(deque.len(), 0); + let node1_ref = unsafe { node1_ptr.as_ref() }; + assert!(!deque.contains(node1_ref)); + assert!(node1_ref.next.is_none()); + assert!(node1_ref.next.is_none()); + std::mem::drop(unsafe { Box::from_raw(node1_ptr.as_ptr()) }); + + // peek_front() -> node1 + let head_h = deque.peek_front(); + assert!(head_h.is_none()); + + // peek_back() -> node1 + let tail_e = deque.peek_back(); + assert!(tail_e.is_none()); + } + + #[test] + fn iter() { + let mut deque: Deque = Deque::new(MainProbation); + assert!((&mut deque).next().is_none()); + + let node1 = DeqNode::new("a".into()); + deque.push_back(Box::new(node1)); + let node2 = DeqNode::new("b".into()); + let node2_ptr = deque.push_back(Box::new(node2)); + let node3 = DeqNode::new("c".into()); + let node3_ptr = deque.push_back(Box::new(node3)); + + // ------------------------------------------------------- + // First iteration. + assert_eq!((&mut deque).next(), Some(&"a".into())); + assert_eq!((&mut deque).next(), Some(&"b".into())); + assert_eq!((&mut deque).next(), Some(&"c".into())); + assert!((&mut deque).next().is_none()); + + // ------------------------------------------------------- + // Ensure the iterator restarts. + assert_eq!((&mut deque).next(), Some(&"a".into())); + assert_eq!((&mut deque).next(), Some(&"b".into())); + assert_eq!((&mut deque).next(), Some(&"c".into())); + assert!((&mut deque).next().is_none()); + + // ------------------------------------------------------- + // Ensure reset_cursor works. + assert_eq!((&mut deque).next(), Some(&"a".into())); + assert_eq!((&mut deque).next(), Some(&"b".into())); + deque.reset_cursor(); + assert_eq!((&mut deque).next(), Some(&"a".into())); + assert_eq!((&mut deque).next(), Some(&"b".into())); + assert_eq!((&mut deque).next(), Some(&"c".into())); + assert!((&mut deque).next().is_none()); + + // ------------------------------------------------------- + // Try to move_to_back during iteration. + assert_eq!((&mut deque).next(), Some(&"a".into())); + // Next will be "b", but we move it to the back. + unsafe { deque.move_to_back(node2_ptr) }; + // Now, next should be "c", and then "b". + assert_eq!((&mut deque).next(), Some(&"c".into())); + assert_eq!((&mut deque).next(), Some(&"b".into())); + assert!((&mut deque).next().is_none()); + + // ------------------------------------------------------- + // Try to unlink during iteration. + assert_eq!((&mut deque).next(), Some(&"a".into())); + // Next will be "c", but we unlink it. + unsafe { deque.unlink_and_drop(node3_ptr) }; + // Now, next should be "b". + assert_eq!((&mut deque).next(), Some(&"b".into())); + assert!((&mut deque).next().is_none()); + + // ------------------------------------------------------- + // Try pop_front during iteration. + let node3 = DeqNode::new("c".into()); + deque.push_back(Box::new(node3)); + + assert_eq!((&mut deque).next(), Some(&"a".into())); + // Next will be "b", but we call pop_front twice to remove "a" and "b". + deque.pop_front(); // "a" + deque.pop_front(); // "b" + // Now, next should be "c". + assert_eq!((&mut deque).next(), Some(&"c".into())); + assert!((&mut deque).next().is_none()); + + // ------------------------------------------------------- + // Check iterating on an empty deque. + deque.pop_front(); // "c" + assert!((&mut deque).next().is_none()); + assert!((&mut deque).next().is_none()); + } + + #[test] + fn next_node() { + let mut deque: Deque = Deque::new(MainProbation); + + let node1 = DeqNode::new("a".into()); + deque.push_back(Box::new(node1)); + let node2 = DeqNode::new("b".into()); + let node2_ptr = deque.push_back(Box::new(node2)); + let node3 = DeqNode::new("c".into()); + let node3_ptr = deque.push_back(Box::new(node3)); + + // ------------------------------------------------------- + // First iteration. + // peek_front() -> node1 + let node1a = deque.peek_front_ptr().unwrap(); + assert_eq!(unsafe { node1a.as_ref() }.element, "a".to_string()); + let node2a = DeqNode::next_node_ptr(node1a).unwrap(); + assert_eq!(unsafe { node2a.as_ref() }.element, "b".to_string()); + let node3a = DeqNode::next_node_ptr(node2a).unwrap(); + assert_eq!(unsafe { node3a.as_ref() }.element, "c".to_string()); + assert!(DeqNode::next_node_ptr(node3a).is_none()); + + // ------------------------------------------------------- + // Iterate after a move_to_back. + // Move "b" to the back. So now "a" -> "c" -> "b". + unsafe { deque.move_to_back(node2_ptr) }; + let node1a = deque.peek_front_ptr().unwrap(); + assert_eq!(unsafe { node1a.as_ref() }.element, "a".to_string()); + let node3a = DeqNode::next_node_ptr(node1a).unwrap(); + assert_eq!(unsafe { node3a.as_ref() }.element, "c".to_string()); + let node2a = DeqNode::next_node_ptr(node3a).unwrap(); + assert_eq!(unsafe { node2a.as_ref() }.element, "b".to_string()); + assert!(DeqNode::next_node_ptr(node2a).is_none()); + + // ------------------------------------------------------- + // Iterate after an unlink. + // Unlink the second node "c". Now "a" -> "c". + unsafe { deque.unlink_and_drop(node3_ptr) }; + let node1a = deque.peek_front_ptr().unwrap(); + assert_eq!(unsafe { node1a.as_ref() }.element, "a".to_string()); + let node2a = DeqNode::next_node_ptr(node1a).unwrap(); + assert_eq!(unsafe { node2a.as_ref() }.element, "b".to_string()); + assert!(DeqNode::next_node_ptr(node2a).is_none()); + } + + #[test] + fn peek_and_move_to_back() { + let mut deque: Deque = Deque::new(MainProbation); + + let node1 = DeqNode::new("a".into()); + deque.push_back(Box::new(node1)); + let node2 = DeqNode::new("b".into()); + let _ = deque.push_back(Box::new(node2)); + let node3 = DeqNode::new("c".into()); + let _ = deque.push_back(Box::new(node3)); + // "a" -> "b" -> "c" + + let node1a = deque.peek_front_ptr().unwrap(); + assert_eq!(unsafe { node1a.as_ref() }.element, "a".to_string()); + unsafe { deque.move_to_back(node1a) }; + // "b" -> "c" -> "a" + + let node2a = deque.peek_front_ptr().unwrap(); + assert_eq!(unsafe { node2a.as_ref() }.element, "b".to_string()); + + let node3a = DeqNode::next_node_ptr(node2a).unwrap(); + assert_eq!(unsafe { node3a.as_ref() }.element, "c".to_string()); + unsafe { deque.move_to_back(node3a) }; + // "b" -> "a" -> "c" + + deque.move_front_to_back(); + // "a" -> "c" -> "b" + + let node1b = deque.peek_front().unwrap(); + assert_eq!(node1b.element, "a".to_string()); + } + + #[test] + fn drop() { + use std::{cell::RefCell, rc::Rc}; + + struct X(u32, Rc>>); + + impl Drop for X { + fn drop(&mut self) { + self.1.borrow_mut().push(self.0) + } + } + + let mut deque: Deque = Deque::new(MainProbation); + let dropped = Rc::new(RefCell::new(Vec::default())); + + let node1 = DeqNode::new(X(1, Rc::clone(&dropped))); + let node2 = DeqNode::new(X(2, Rc::clone(&dropped))); + let node3 = DeqNode::new(X(3, Rc::clone(&dropped))); + let node4 = DeqNode::new(X(4, Rc::clone(&dropped))); + deque.push_back(Box::new(node1)); + deque.push_back(Box::new(node2)); + deque.push_back(Box::new(node3)); + deque.push_back(Box::new(node4)); + assert_eq!(deque.len(), 4); + + std::mem::drop(deque); + + assert_eq!(*dropped.borrow(), &[1, 2, 3, 4]); + } +} diff --git a/crates/mini-moka-vendored/src/common/frequency_sketch.rs b/crates/mini-moka-vendored/src/common/frequency_sketch.rs new file mode 100644 index 00000000..b9b0b703 --- /dev/null +++ b/crates/mini-moka-vendored/src/common/frequency_sketch.rs @@ -0,0 +1,392 @@ +// License and Copyright Notice: +// +// Some of the code and doc comments in this module were ported or copied from +// a Java class `com.github.benmanes.caffeine.cache.FrequencySketch` of Caffeine. +// https://github.com/ben-manes/caffeine/blob/master/caffeine/src/main/java/com/github/benmanes/caffeine/cache/FrequencySketch.java +// +// The original code/comments from Caffeine are licensed under the Apache License, +// Version 2.0 +// +// Copyrights of the original code/comments are retained by their contributors. +// For full authorship information, see the version control history of +// https://github.com/ben-manes/caffeine/ + +/// A probabilistic multi-set for estimating the popularity of an element within +/// a time window. The maximum frequency of an element is limited to 15 (4-bits) +/// and an aging process periodically halves the popularity of all elements. +#[derive(Default)] +pub(crate) struct FrequencySketch { + sample_size: u32, + table_mask: u32, + table: Box<[u64]>, + size: u32, +} + +// A mixture of seeds from FNV-1a, CityHash, and Murmur3. (Taken from Caffeine) +static SEED: [u64; 4] = [ + 0xc3a5_c85c_97cb_3127, + 0xb492_b66f_be98_f273, + 0x9ae1_6a3b_2f90_404f, + 0xcbf2_9ce4_8422_2325, +]; + +static RESET_MASK: u64 = 0x7777_7777_7777_7777; + +static ONE_MASK: u64 = 0x1111_1111_1111_1111; + +// ------------------------------------------------------------------------------- +// Some of the code and doc comments in this module were ported or copied from +// a Java class `com.github.benmanes.caffeine.cache.FrequencySketch` of Caffeine. +// https://github.com/ben-manes/caffeine/blob/master/caffeine/src/main/java/com/github/benmanes/caffeine/cache/FrequencySketch.java +// ------------------------------------------------------------------------------- +// +// FrequencySketch maintains a 4-bit CountMinSketch [1] with periodic aging to +// provide the popularity history for the TinyLfu admission policy [2]. +// The time and space efficiency of the sketch allows it to cheaply estimate the +// frequency of an entry in a stream of cache access events. +// +// The counter matrix is represented as a single dimensional array holding 16 +// counters per slot. A fixed depth of four balances the accuracy and cost, +// resulting in a width of four times the length of the array. To retain an +// accurate estimation the array's length equals the maximum number of entries +// in the cache, increased to the closest power-of-two to exploit more efficient +// bit masking. This configuration results in a confidence of 93.75% and error +// bound of e / width. +// +// The frequency of all entries is aged periodically using a sampling window +// based on the maximum number of entries in the cache. This is referred to as +// the reset operation by TinyLfu and keeps the sketch fresh by dividing all +// counters by two and subtracting based on the number of odd counters +// found. The O(n) cost of aging is amortized, ideal for hardware pre-fetching, +// and uses inexpensive bit manipulations per array location. +// +// [1] An Improved Data Stream Summary: The Count-Min Sketch and its Applications +// http://dimacs.rutgers.edu/~graham/pubs/papers/cm-full.pdf +// [2] TinyLFU: A Highly Efficient Cache Admission Policy +// https://dl.acm.org/citation.cfm?id=3149371 +// +// ------------------------------------------------------------------------------- + +impl FrequencySketch { + /// Initializes and increases the capacity of this `FrequencySketch` instance, + /// if necessary, to ensure that it can accurately estimate the popularity of + /// elements given the maximum size of the cache. This operation forgets all + /// previous counts when resizing. + pub(crate) fn ensure_capacity(&mut self, cap: u32) { + // The max byte size of the table, Box<[u64; table_size]> + // + // | Pointer width | Max size | + // |:-----------------|---------:| + // | 16 bit | 8 KiB | + // | 32 bit | 128 MiB | + // | 64 bit or bigger | 8 GiB | + + let maximum = if cfg!(target_pointer_width = "16") { + cap.min(1024) + } else if cfg!(target_pointer_width = "32") { + cap.min(2u32.pow(24)) // about 16 millions + } else { + // Same to Caffeine's limit: + // `Integer.MAX_VALUE >>> 1` with `ceilingPowerOfTwo()` applied. + cap.min(2u32.pow(30)) // about 1 billion + }; + let table_size = if maximum == 0 { + 1 + } else { + maximum.next_power_of_two() + }; + + if self.table.len() as u32 >= table_size { + return; + } + + self.table = vec![0; table_size as usize].into_boxed_slice(); + self.table_mask = table_size - 1; + self.sample_size = if cap == 0 { + 10 + } else { + maximum.saturating_mul(10).min(i32::MAX as u32) + }; + } + + /// Takes the hash value of an element, and returns the estimated number of + /// occurrences of the element, up to the maximum (15). + pub(crate) fn frequency(&self, hash: u64) -> u8 { + if self.table.is_empty() { + return 0; + } + + let start = ((hash & 3) << 2) as u8; + let mut frequency = u8::MAX; + for i in 0..4 { + let index = self.index_of(hash, i); + let shift = (start + i) << 2; + let count = ((self.table[index] >> shift) & 0xF) as u8; + frequency = frequency.min(count); + } + frequency + } + + /// Take a hash value of an element and increments the popularity of the + /// element if it does not exceed the maximum (15). The popularity of all + /// elements will be periodically down sampled when the observed events + /// exceeds a threshold. This process provides a frequency aging to allow + /// expired long term entries to fade away. + pub(crate) fn increment(&mut self, hash: u64) { + if self.table.is_empty() { + return; + } + + let start = ((hash & 3) << 2) as u8; + let mut added = false; + for i in 0..4 { + let index = self.index_of(hash, i); + added |= self.increment_at(index, start + i); + } + + if added { + self.size += 1; + if self.size >= self.sample_size { + self.reset(); + } + } + } + + /// Takes a table index (each entry has 16 counters) and counter index, and + /// increments the counter by 1 if it is not already at the maximum value + /// (15). Returns `true` if incremented. + fn increment_at(&mut self, table_index: usize, counter_index: u8) -> bool { + let offset = (counter_index as usize) << 2; + let mask = 0xF_u64 << offset; + if self.table[table_index] & mask != mask { + self.table[table_index] += 1u64 << offset; + true + } else { + false + } + } + + /// Reduces every counter by half of its original value. + fn reset(&mut self) { + let mut count = 0u32; + for entry in self.table.iter_mut() { + // Count number of odd numbers. + count += (*entry & ONE_MASK).count_ones(); + *entry = (*entry >> 1) & RESET_MASK; + } + self.size = (self.size >> 1) - (count >> 2); + } + + /// Returns the table index for the counter at the specified depth. + fn index_of(&self, hash: u64, depth: u8) -> usize { + let i = depth as usize; + let mut hash = hash.wrapping_add(SEED[i]).wrapping_mul(SEED[i]); + hash = hash.wrapping_add(hash >> 32); + (hash & (self.table_mask as u64)) as usize + } +} + +// Methods only available for testing. +#[cfg(test)] +impl FrequencySketch { + pub(crate) fn table_len(&self) -> usize { + self.table.len() + } +} + +// Some test cases were ported from Caffeine at: +// https://github.com/ben-manes/caffeine/blob/master/caffeine/src/test/java/com/github/benmanes/caffeine/cache/FrequencySketchTest.java +// +// To see the debug prints, run test as `cargo test -- --nocapture` +#[cfg(test)] +mod tests { + use super::FrequencySketch; + use once_cell::sync::Lazy; + use std::hash::{BuildHasher, Hash}; + + static ITEM: Lazy = Lazy::new(|| { + let mut buf = [0; 4]; + getrandom::getrandom(&mut buf).unwrap(); + unsafe { std::mem::transmute::<[u8; 4], u32>(buf) } + }); + + // This test was ported from Caffeine. + #[test] + fn increment_once() { + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(512); + let hasher = hasher(); + let item_hash = hasher(*ITEM); + sketch.increment(item_hash); + assert_eq!(sketch.frequency(item_hash), 1); + } + + // This test was ported from Caffeine. + #[test] + fn increment_max() { + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(512); + let hasher = hasher(); + let item_hash = hasher(*ITEM); + for _ in 0..20 { + sketch.increment(item_hash); + } + assert_eq!(sketch.frequency(item_hash), 15); + } + + // This test was ported from Caffeine. + #[test] + fn increment_distinct() { + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(512); + let hasher = hasher(); + sketch.increment(hasher(*ITEM)); + sketch.increment(hasher(ITEM.wrapping_add(1))); + assert_eq!(sketch.frequency(hasher(*ITEM)), 1); + assert_eq!(sketch.frequency(hasher(ITEM.wrapping_add(1))), 1); + assert_eq!(sketch.frequency(hasher(ITEM.wrapping_add(2))), 0); + } + + // This test was ported from Caffeine. + #[test] + fn index_of_around_zero() { + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(512); + let mut indexes = std::collections::HashSet::new(); + let hashes = [u64::MAX, 0, 1]; + for hash in hashes.iter() { + for depth in 0..4 { + indexes.insert(sketch.index_of(*hash, depth)); + } + } + assert_eq!(indexes.len(), 4 * hashes.len()) + } + + // This test was ported from Caffeine. + #[test] + fn reset() { + let mut reset = false; + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(64); + let hasher = hasher(); + + for i in 1..(20 * sketch.table.len() as u32) { + sketch.increment(hasher(i)); + if sketch.size != i { + reset = true; + break; + } + } + + assert!(reset); + assert!(sketch.size <= sketch.sample_size / 2); + } + + // This test was ported from Caffeine. + #[test] + fn heavy_hitters() { + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(65_536); + let hasher = hasher(); + + for i in 100..100_000 { + sketch.increment(hasher(i)); + } + + for i in (0..10).step_by(2) { + for _ in 0..i { + sketch.increment(hasher(i)); + } + } + + // A perfect popularity count yields an array [0, 0, 2, 0, 4, 0, 6, 0, 8, 0] + let popularity = (0..10) + .map(|i| sketch.frequency(hasher(i))) + .collect::>(); + + for (i, freq) in popularity.iter().enumerate() { + match i { + 2 => assert!(freq <= &popularity[4]), + 4 => assert!(freq <= &popularity[6]), + 6 => assert!(freq <= &popularity[8]), + 8 => (), + _ => assert!(freq <= &popularity[2]), + } + } + } + + fn hasher() -> impl Fn(K) -> u64 { + let build_hasher = std::collections::hash_map::RandomState::default(); + move |key| build_hasher.hash_one(&key) + } +} + +// Verify that some properties hold such as no panic occurs on any possible inputs. +#[cfg(kani)] +mod kani { + use super::FrequencySketch; + + const CAPACITIES: &[u32] = &[ + 0, + 1, + 1024, + 1025, + 2u32.pow(24), + 2u32.pow(24) + 1, + 2u32.pow(30), + 2u32.pow(30) + 1, + u32::MAX, + ]; + + #[kani::proof] + fn verify_ensure_capacity() { + // Check for arbitrary capacities. + let capacity = kani::any(); + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(capacity); + } + + #[kani::proof] + fn verify_frequency() { + // Check for some selected capacities. + for capacity in CAPACITIES { + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(*capacity); + + // Check for arbitrary hashes. + let hash = kani::any(); + let frequency = sketch.frequency(hash); + assert!(frequency <= 15); + } + } + + #[kani::proof] + fn verify_increment() { + // Only check for small capacities. Because Kani Rust Verifier is a model + // checking tool, it will take much longer time (exponential) to check larger + // capacities here. + for capacity in &[0, 1, 128] { + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(*capacity); + + // Check for arbitrary hashes. + let hash = kani::any(); + sketch.increment(hash); + } + } + + #[kani::proof] + fn verify_index_of() { + // Check for arbitrary capacities. + let capacity = kani::any(); + let mut sketch = FrequencySketch::default(); + sketch.ensure_capacity(capacity); + + // Check for arbitrary hashes. + let hash = kani::any(); + for i in 0..4 { + let index = sketch.index_of(hash, i); + assert!(index < sketch.table.len()); + } + } +} diff --git a/crates/mini-moka-vendored/src/common/time.rs b/crates/mini-moka-vendored/src/common/time.rs new file mode 100644 index 00000000..7a2fac64 --- /dev/null +++ b/crates/mini-moka-vendored/src/common/time.rs @@ -0,0 +1,32 @@ +use std::time::Duration; + +pub(crate) mod clock; + +pub(crate) use clock::Clock; + +/// a wrapper type over Instant to force checked additions and prevent +/// unintentional overflow. The type preserve the Copy semantics for the wrapped +#[derive(PartialEq, PartialOrd, Clone, Copy)] +pub(crate) struct Instant(clock::Instant); + +pub(crate) trait CheckedTimeOps { + fn checked_add(&self, duration: Duration) -> Option + where + Self: Sized; +} + +impl Instant { + pub(crate) fn new(instant: clock::Instant) -> Instant { + Instant(instant) + } + + pub(crate) fn now() -> Instant { + Instant(clock::Instant::now()) + } +} + +impl CheckedTimeOps for Instant { + fn checked_add(&self, duration: Duration) -> Option { + self.0.checked_add(duration).map(Instant) + } +} diff --git a/crates/mini-moka-vendored/src/common/time/clock.rs b/crates/mini-moka-vendored/src/common/time/clock.rs new file mode 100644 index 00000000..06ae1907 --- /dev/null +++ b/crates/mini-moka-vendored/src/common/time/clock.rs @@ -0,0 +1,52 @@ +use std::sync::{Arc, RwLock}; + +#[cfg(test)] +use std::time::Duration; + +#[cfg(not(feature = "js"))] +pub(crate) type Instant = std::time::Instant; + +#[cfg(feature = "js")] +pub(crate) type Instant = web_time::Instant; + +pub(crate) struct Clock { + mock: Option>, +} + +impl Clock { + #[cfg(test)] + pub(crate) fn mock() -> (Clock, Arc) { + let mock = Arc::new(Mock::default()); + let clock = Clock { + mock: Some(Arc::clone(&mock)), + }; + (clock, mock) + } + + pub(crate) fn now(&self) -> Instant { + if let Some(mock) = &self.mock { + *mock.now.read().expect("lock poisoned") + } else { + Instant::now() + } + } +} + +pub(crate) struct Mock { + now: RwLock, +} + +impl Default for Mock { + fn default() -> Self { + Self { + now: RwLock::new(Instant::now()), + } + } +} + +#[cfg(test)] +impl Mock { + pub(crate) fn increment(&self, amount: Duration) { + *self.now.write().expect("lock poisoned") += amount; + } +} diff --git a/crates/mini-moka-vendored/src/lib.rs b/crates/mini-moka-vendored/src/lib.rs new file mode 100644 index 00000000..e7143b27 --- /dev/null +++ b/crates/mini-moka-vendored/src/lib.rs @@ -0,0 +1,86 @@ +#![warn(clippy::all)] +#![warn(rust_2018_idioms)] +#![deny(rustdoc::broken_intra_doc_links)] +#![cfg_attr(docsrs, feature(doc_cfg))] + +//! Mini Moka is a fast, concurrent cache library for Rust. Mini Moka is a light +//! edition of [Moka][moka-git]. +//! +//! Mini Moka provides an in-memory concurrent cache implementation on top of hash +//! map. It supports high expected concurrency of retrievals and updates. +//! +//! Mini Moka also provides an in-memory, non-thread-safe cache implementation for +//! single thread applications. +//! +//! All cache implementations perform a best-effort bounding of the map using an +//! entry replacement algorithm to determine which entries to evict when the capacity +//! is exceeded. +//! +//! [moka-git]: https://github.com/moka-rs/moka +//! [caffeine-git]: https://github.com/ben-manes/caffeine +//! +//! # Features +//! +//! - A thread-safe, highly concurrent in-memory cache implementation. +//! - A cache can be bounded by one of the followings: +//! - The maximum number of entries. +//! - The total weighted size of entries. (Size aware eviction) +//! - Maintains good hit rate by using entry replacement algorithms inspired by +//! [Caffeine][caffeine-git]: +//! - Admission to a cache is controlled by the Least Frequently Used (LFU) policy. +//! - Eviction from a cache is controlled by the Least Recently Used (LRU) policy. +//! - Supports expiration policies: +//! - Time to live +//! - Time to idle +//! +//! # Examples +//! +//! See the following document: +//! +//! - A thread-safe, synchronous cache: +//! - [`sync::Cache`][sync-cache-struct] +//! - A not thread-safe, blocking cache for single threaded applications: +//! - [`unsync::Cache`][unsync-cache-struct] +//! +//! [sync-cache-struct]: ./sync/struct.Cache.html +//! [unsync-cache-struct]: ./unsync/struct.Cache.html +//! +//! # Minimum Supported Rust Versions +//! +//! This crate's minimum supported Rust versions (MSRV) are the followings: +//! +//! | Feature | MSRV | +//! |:-----------------|:--------------------------:| +//! | default features | Rust 1.76.0 (Feb 8, 2024) | +//! +//! If only the default features are enabled, MSRV will be updated conservatively. +//! When using other features, MSRV might be updated more frequently, up to the +//! latest stable. In both cases, increasing MSRV is _not_ considered a +//! semver-breaking change. + +pub(crate) mod common; +pub(crate) mod policy; +pub mod unsync; + +#[cfg(feature = "sync")] +#[cfg_attr(docsrs, doc(cfg(feature = "sync")))] +pub mod sync; + +pub use policy::Policy; + +#[cfg(test)] +mod tests { + #[cfg(all(trybuild, feature = "sync"))] + #[test] + fn trybuild_sync() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compile_tests/sync/clone/*.rs"); + } +} + +#[cfg(all(doctest, feature = "sync"))] +mod doctests { + // https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#include-items-only-when-collecting-doctests + #[doc = include_str!("../README.md")] + struct ReadMeDoctests; +} diff --git a/crates/mini-moka-vendored/src/policy.rs b/crates/mini-moka-vendored/src/policy.rs new file mode 100644 index 00000000..0cbc6ed3 --- /dev/null +++ b/crates/mini-moka-vendored/src/policy.rs @@ -0,0 +1,38 @@ +use std::time::Duration; + +#[derive(Clone, Debug)] +/// The policy of a cache. +pub struct Policy { + max_capacity: Option, + time_to_live: Option, + time_to_idle: Option, +} + +impl Policy { + pub(crate) fn new( + max_capacity: Option, + time_to_live: Option, + time_to_idle: Option, + ) -> Self { + Self { + max_capacity, + time_to_live, + time_to_idle, + } + } + + /// Returns the `max_capacity` of the cache. + pub fn max_capacity(&self) -> Option { + self.max_capacity + } + + /// Returns the `time_to_live` of the cache. + pub fn time_to_live(&self) -> Option { + self.time_to_live + } + + /// Returns the `time_to_idle` of the cache. + pub fn time_to_idle(&self) -> Option { + self.time_to_idle + } +} diff --git a/crates/mini-moka-vendored/src/sync.rs b/crates/mini-moka-vendored/src/sync.rs new file mode 100644 index 00000000..225e9e3c --- /dev/null +++ b/crates/mini-moka-vendored/src/sync.rs @@ -0,0 +1,21 @@ +//! Provides a thread-safe, concurrent cache implementation built upon +//! [`dashmap::DashMap`][dashmap]. +//! +//! [dashmap]: https://docs.rs/dashmap/*/dashmap/struct.DashMap.html + +mod base_cache; +mod builder; +mod cache; +mod iter; +mod mapref; + +pub use builder::CacheBuilder; +pub use cache::Cache; +pub use iter::Iter; +pub use mapref::EntryRef; + +/// Provides extra methods that will be useful for testing. +pub trait ConcurrentCacheExt { + /// Performs any pending maintenance operations needed by the cache. + fn sync(&self); +} diff --git a/crates/mini-moka-vendored/src/sync/base_cache.rs b/crates/mini-moka-vendored/src/sync/base_cache.rs new file mode 100644 index 00000000..84e8bf49 --- /dev/null +++ b/crates/mini-moka-vendored/src/sync/base_cache.rs @@ -0,0 +1,1380 @@ +use super::{iter::DashMapIter, Iter}; +use crate::{ + common::{ + self, + concurrent::{ + atomic_time::AtomicInstant, + constants::{ + READ_LOG_FLUSH_POINT, READ_LOG_SIZE, WRITE_LOG_FLUSH_POINT, WRITE_LOG_SIZE, + }, + deques::Deques, + entry_info::EntryInfo, + housekeeper::{Housekeeper, InnerSync}, + AccessTime, KeyDate, KeyHash, KeyHashDate, KvEntry, ReadOp, ValueEntry, Weigher, + WriteOp, + }, + deque::{DeqNode, Deque}, + frequency_sketch::FrequencySketch, + time::{CheckedTimeOps, Clock, Instant}, + CacheRegion, + }, + Policy, +}; + +use crossbeam_channel::{Receiver, Sender, TrySendError}; +use crossbeam_utils::atomic::AtomicCell; +use dashmap::mapref::one::Ref as DashMapRef; +use smallvec::SmallVec; +use std::{ + borrow::Borrow, + collections::hash_map::RandomState, + hash::{BuildHasher, Hash}, + ptr::NonNull, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, RwLock, + }, + time::Duration, +}; +use triomphe::Arc as TrioArc; + +pub(crate) struct BaseCache { + pub(crate) inner: Arc>, + read_op_ch: Sender>, + pub(crate) write_op_ch: Sender>, + pub(crate) housekeeper: Option>, +} + +impl Clone for BaseCache { + /// Makes a clone of this shared cache. + /// + /// This operation is cheap as it only creates thread-safe reference counted + /// pointers to the shared internal data structures. + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + read_op_ch: self.read_op_ch.clone(), + write_op_ch: self.write_op_ch.clone(), + housekeeper: self.housekeeper.clone(), + } + } +} + +impl Drop for BaseCache { + fn drop(&mut self) { + // The housekeeper needs to be dropped before the inner is dropped. + std::mem::drop(self.housekeeper.take()); + } +} + +impl BaseCache { + pub(crate) fn policy(&self) -> Policy { + self.inner.policy() + } + + pub(crate) fn entry_count(&self) -> u64 { + self.inner.entry_count() + } + + pub(crate) fn weighted_size(&self) -> u64 { + self.inner.weighted_size() + } +} + +impl BaseCache +where + K: Hash + Eq + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, + S: BuildHasher + Clone + Send + Sync + 'static, +{ + pub(crate) fn new( + max_capacity: Option, + initial_capacity: Option, + build_hasher: S, + weigher: Option>, + time_to_live: Option, + time_to_idle: Option, + ) -> Self { + let (r_snd, r_rcv) = crossbeam_channel::bounded(READ_LOG_SIZE); + let (w_snd, w_rcv) = crossbeam_channel::bounded(WRITE_LOG_SIZE); + + let inner = Inner::new( + max_capacity, + initial_capacity, + build_hasher, + weigher, + r_rcv, + w_rcv, + time_to_live, + time_to_idle, + ); + Self { + #[cfg_attr(beta_clippy, allow(clippy::arc_with_non_send_sync))] + inner: Arc::new(inner), + read_op_ch: r_snd, + write_op_ch: w_snd, + housekeeper: Some(Arc::new(Housekeeper::default())), + } + } + + #[inline] + pub(crate) fn hash(&self, key: &Q) -> u64 + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.inner.hash(key) + } + + pub(crate) fn contains_key(&self, key: &Q) -> bool + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + match self.inner.get(key) { + None => false, + Some(entry) => { + let i = &self.inner; + let (ttl, tti, va) = (&i.time_to_live(), &i.time_to_idle(), &i.valid_after()); + let now = i.current_time_from_expiration_clock(); + let entry = &*entry; + + !is_expired_entry_wo(ttl, va, entry, now) + && !is_expired_entry_ao(tti, va, entry, now) + } + } + } + + pub(crate) fn get_with_hash(&self, key: &Q, hash: u64) -> Option + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + let record = |op, now| { + self.record_read_op(op, now) + .expect("Failed to record a get op"); + }; + let now = self.inner.current_time_from_expiration_clock(); + + match self.inner.get(key) { + None => { + record(ReadOp::Miss(hash), now); + None + } + Some(entry) => { + let i = &self.inner; + let (ttl, tti, va) = (&i.time_to_live(), &i.time_to_idle(), &i.valid_after()); + let arc_entry = &*entry; + + if is_expired_entry_wo(ttl, va, arc_entry, now) + || is_expired_entry_ao(tti, va, arc_entry, now) + { + // Drop the entry to avoid to deadlock with record_read_op. + std::mem::drop(entry); + // Expired or invalidated entry. Record this access as a cache miss + // rather than a hit. + record(ReadOp::Miss(hash), now); + None + } else { + // Valid entry. + let v = arc_entry.value.clone(); + let e = TrioArc::clone(arc_entry); + // Drop the entry to avoid to deadlock with record_read_op. + std::mem::drop(entry); + record(ReadOp::Hit(hash, e, now), now); + Some(v) + } + } + } + } + + #[inline] + pub(crate) fn remove_entry(&self, key: &Q) -> Option> + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.inner.remove_entry(key) + } + + #[inline] + pub(crate) fn apply_reads_writes_if_needed( + inner: &impl InnerSync, + ch: &Sender>, + now: Instant, + housekeeper: Option<&Arc>, + ) { + let w_len = ch.len(); + + if let Some(hk) = housekeeper { + if hk.should_apply_writes(w_len, now) { + hk.try_sync(inner); + } + } + } + + pub(crate) fn invalidate_all(&self) { + let now = self.inner.current_time_from_expiration_clock(); + self.inner.set_valid_after(now); + } +} + +// Clippy beta 0.1.83 (f41c7ed9889 2024-10-31) warns about unused lifetimes on 'a. +// This seems a false positive. The lifetimes are used in the trait bounds. +// https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes +#[allow(clippy::extra_unused_lifetimes)] +impl<'a, K, V, S> BaseCache +where + K: 'a + Eq + Hash, + V: 'a, + S: BuildHasher + Clone, +{ + pub(crate) fn iter(&self) -> Iter<'_, K, V, S> { + Iter::new(self, self.inner.iter()) + } +} + +impl BaseCache { + pub(crate) fn is_expired_entry(&self, entry: &TrioArc>) -> bool { + let i = &self.inner; + let (ttl, tti, va) = (&i.time_to_live(), &i.time_to_idle(), &i.valid_after()); + let now = i.current_time_from_expiration_clock(); + + is_expired_entry_wo(ttl, va, entry, now) || is_expired_entry_ao(tti, va, entry, now) + } +} + +// +// private methods +// +impl BaseCache +where + K: Hash + Eq + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, + S: BuildHasher + Clone + Send + Sync + 'static, +{ + #[inline] + fn record_read_op( + &self, + op: ReadOp, + now: Instant, + ) -> Result<(), TrySendError>> { + self.apply_reads_if_needed(self.inner.as_ref(), now); + let ch = &self.read_op_ch; + match ch.try_send(op) { + // Discard the ReadOp when the channel is full. + Ok(()) | Err(TrySendError::Full(_)) => Ok(()), + Err(e @ TrySendError::Disconnected(_)) => Err(e), + } + } + + #[inline] + pub(crate) fn do_insert_with_hash( + &self, + key: Arc, + hash: u64, + value: V, + ) -> (WriteOp, Instant) { + let ts = self.inner.current_time_from_expiration_clock(); + let weight = self.inner.weigh(&key, &value); + let mut insert_op = None; + let mut update_op = None; + + self.inner + .cache + .entry(Arc::clone(&key)) + // Update + .and_modify(|entry| { + // NOTES on `new_value_entry_from` method: + // 1. The internal EntryInfo will be shared between the old and new + // ValueEntries. + // 2. This method will set the dirty flag to prevent this new + // ValueEntry from being evicted by an expiration policy. + // 3. This method will update the policy_weight with the new weight. + let old_weight = entry.policy_weight(); + *entry = self.new_value_entry_from(value.clone(), ts, weight, entry); + update_op = Some(WriteOp::Upsert { + key_hash: KeyHash::new(Arc::clone(&key), hash), + value_entry: TrioArc::clone(entry), + old_weight, + new_weight: weight, + }); + }) + // Insert + .or_insert_with(|| { + let entry = self.new_value_entry(value.clone(), ts, weight); + insert_op = Some(WriteOp::Upsert { + key_hash: KeyHash::new(Arc::clone(&key), hash), + value_entry: TrioArc::clone(&entry), + old_weight: 0, + new_weight: weight, + }); + entry + }); + + match (insert_op, update_op) { + (Some(ins_op), None) => (ins_op, ts), + (None, Some(upd_op)) => (upd_op, ts), + _ => unreachable!(), + } + } + + #[inline] + fn new_value_entry( + &self, + value: V, + timestamp: Instant, + policy_weight: u32, + ) -> TrioArc> { + let info = TrioArc::new(EntryInfo::new(timestamp, policy_weight)); + TrioArc::new(ValueEntry::new(value, info)) + } + + #[inline] + fn new_value_entry_from( + &self, + value: V, + timestamp: Instant, + policy_weight: u32, + other: &ValueEntry, + ) -> TrioArc> { + let info = TrioArc::clone(other.entry_info()); + // To prevent this updated ValueEntry from being evicted by an expiration policy, + // set the dirty flag to true. It will be reset to false when the write is applied. + info.set_dirty(true); + info.set_last_accessed(timestamp); + info.set_last_modified(timestamp); + info.set_policy_weight(policy_weight); + TrioArc::new(ValueEntry::new(value, info)) + } + + #[inline] + fn apply_reads_if_needed(&self, inner: &impl InnerSync, now: Instant) { + let len = self.read_op_ch.len(); + + if let Some(hk) = &self.housekeeper { + if hk.should_apply_reads(len, now) { + if let Some(h) = &self.housekeeper { + h.try_sync(inner); + } + } + } + } + + #[inline] + pub(crate) fn current_time_from_expiration_clock(&self) -> Instant { + self.inner.current_time_from_expiration_clock() + } +} + +// +// for testing +// +#[cfg(test)] +impl BaseCache +where + K: Hash + Eq + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, + S: BuildHasher + Clone + Send + Sync + 'static, +{ + pub(crate) fn reconfigure_for_testing(&mut self) { + // Enable the frequency sketch. + self.inner.enable_frequency_sketch_for_testing(); + } + + pub(crate) fn set_expiration_clock(&self, clock: Option) { + self.inner.set_expiration_clock(clock); + } +} + +struct EvictionCounters { + entry_count: u64, + weighted_size: u64, +} + +impl EvictionCounters { + #[inline] + fn new(entry_count: u64, weighted_size: u64) -> Self { + Self { + entry_count, + weighted_size, + } + } + + #[inline] + fn saturating_add(&mut self, entry_count: u64, weight: u32) { + self.entry_count += entry_count; + let total = &mut self.weighted_size; + *total = total.saturating_add(weight as u64); + } + + #[inline] + fn saturating_sub(&mut self, entry_count: u64, weight: u32) { + self.entry_count -= entry_count; + let total = &mut self.weighted_size; + *total = total.saturating_sub(weight as u64); + } +} + +#[derive(Default)] +struct EntrySizeAndFrequency { + policy_weight: u64, + freq: u32, +} + +impl EntrySizeAndFrequency { + fn new(policy_weight: u32) -> Self { + Self { + policy_weight: policy_weight as u64, + ..Default::default() + } + } + + fn add_policy_weight(&mut self, weight: u32) { + self.policy_weight += weight as u64; + } + + fn add_frequency(&mut self, freq: &FrequencySketch, hash: u64) { + self.freq += freq.frequency(hash) as u32; + } +} + +// Access-Order Queue Node +type AoqNode = NonNull>>; + +enum AdmissionResult { + Admitted { + victim_nodes: SmallVec<[AoqNode; 8]>, + skipped_nodes: SmallVec<[AoqNode; 4]>, + }, + Rejected { + skipped_nodes: SmallVec<[AoqNode; 4]>, + }, +} + +type CacheStore = dashmap::DashMap, TrioArc>, S>; + +type CacheEntryRef<'a, K, V> = DashMapRef<'a, Arc, TrioArc>>; + +pub(crate) struct Inner { + max_capacity: Option, + entry_count: AtomicCell, + weighted_size: AtomicCell, + cache: CacheStore, + build_hasher: S, + deques: Mutex>, + frequency_sketch: RwLock, + frequency_sketch_enabled: AtomicBool, + read_op_ch: Receiver>, + write_op_ch: Receiver>, + time_to_live: Option, + time_to_idle: Option, + valid_after: AtomicInstant, + weigher: Option>, + has_expiration_clock: AtomicBool, + expiration_clock: RwLock>, +} + +// functions/methods used by BaseCache +impl Inner +where + K: Hash + Eq + Send + Sync + 'static, + V: Send + Sync + 'static, + S: BuildHasher + Clone, +{ + // Disable a Clippy warning for having more than seven arguments. + // https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments + #[allow(clippy::too_many_arguments)] + fn new( + max_capacity: Option, + initial_capacity: Option, + build_hasher: S, + weigher: Option>, + read_op_ch: Receiver>, + write_op_ch: Receiver>, + time_to_live: Option, + time_to_idle: Option, + ) -> Self { + let initial_capacity = initial_capacity + .map(|cap| cap + WRITE_LOG_SIZE) + .unwrap_or_default(); + let cache = + dashmap::DashMap::with_capacity_and_hasher(initial_capacity, build_hasher.clone()); + + Self { + max_capacity, + entry_count: Default::default(), + weighted_size: Default::default(), + cache, + build_hasher, + deques: Mutex::new(Default::default()), + frequency_sketch: RwLock::new(Default::default()), + frequency_sketch_enabled: Default::default(), + read_op_ch, + write_op_ch, + time_to_live, + time_to_idle, + valid_after: Default::default(), + weigher, + has_expiration_clock: AtomicBool::new(false), + expiration_clock: RwLock::new(None), + } + } + + #[inline] + fn hash(&self, key: &Q) -> u64 + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.build_hasher.hash_one(key) + } + + #[inline] + fn get(&self, key: &Q) -> Option> + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.cache.get(key) + } + + #[inline] + fn remove_entry(&self, key: &Q) -> Option> + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.cache + .remove(key) + .map(|(key, entry)| KvEntry::new(key, entry)) + } +} + +// functions/methods used by BaseCache +impl Inner { + fn policy(&self) -> Policy { + Policy::new(self.max_capacity, self.time_to_live, self.time_to_idle) + } + + #[inline] + fn time_to_live(&self) -> Option { + self.time_to_live + } + + #[inline] + fn time_to_idle(&self) -> Option { + self.time_to_idle + } + + #[inline] + fn entry_count(&self) -> u64 { + self.entry_count.load() + } + + #[inline] + pub(crate) fn weighted_size(&self) -> u64 { + self.weighted_size.load() + } + + #[inline] + fn has_expiry(&self) -> bool { + self.time_to_live.is_some() || self.time_to_idle.is_some() + } + + #[inline] + fn is_write_order_queue_enabled(&self) -> bool { + self.time_to_live.is_some() + } + + #[inline] + fn valid_after(&self) -> Option { + self.valid_after.instant() + } + + #[inline] + fn set_valid_after(&self, timestamp: Instant) { + self.valid_after.set_instant(timestamp); + } + + #[inline] + fn has_valid_after(&self) -> bool { + self.valid_after.is_set() + } + + #[inline] + fn weigh(&self, key: &K, value: &V) -> u32 { + self.weigher.as_ref().map(|w| w(key, value)).unwrap_or(1) + } + + #[inline] + fn current_time_from_expiration_clock(&self) -> Instant { + if self.has_expiration_clock.load(Ordering::Relaxed) { + Instant::new( + self.expiration_clock + .read() + .expect("lock poisoned") + .as_ref() + .expect("Cannot get the expiration clock") + .now(), + ) + } else { + Instant::now() + } + } +} + +// Clippy beta 0.1.83 (f41c7ed9889 2024-10-31) warns about unused lifetimes on 'a. +// This seems a false positive. The lifetimes are used in the trait bounds. +// https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes +#[allow(clippy::extra_unused_lifetimes)] +impl<'a, K, V, S> Inner +where + K: 'a + Eq + Hash, + V: 'a, + S: BuildHasher + Clone, +{ + fn iter(&self) -> DashMapIter<'_, K, V, S> { + self.cache.iter() + } +} + +mod batch_size { + pub(crate) const EVICTION_BATCH_SIZE: usize = 500; +} + +// TODO: Divide this method into smaller methods so that unit tests can do more +// precise testing. +// - sync_reads +// - sync_writes +// - evict +// - invalidate_entries +impl InnerSync for Inner +where + K: Hash + Eq + Send + Sync + 'static, + V: Send + Sync + 'static, + S: BuildHasher + Clone + Send + Sync + 'static, +{ + fn sync(&self, max_repeats: usize) { + let mut deqs = self.deques.lock().expect("lock poisoned"); + let mut calls = 0; + let mut should_sync = true; + + let current_ec = self.entry_count.load(); + let current_ws = self.weighted_size.load(); + let mut counters = EvictionCounters::new(current_ec, current_ws); + + while should_sync && calls <= max_repeats { + let r_len = self.read_op_ch.len(); + if r_len > 0 { + self.apply_reads(&mut deqs, r_len); + } + + let w_len = self.write_op_ch.len(); + if w_len > 0 { + self.apply_writes(&mut deqs, w_len, &mut counters); + } + + if self.should_enable_frequency_sketch(&counters) { + self.enable_frequency_sketch(&counters); + } + + calls += 1; + should_sync = self.read_op_ch.len() >= READ_LOG_FLUSH_POINT + || self.write_op_ch.len() >= WRITE_LOG_FLUSH_POINT; + } + + if self.has_expiry() || self.has_valid_after() { + self.evict_expired(&mut deqs, batch_size::EVICTION_BATCH_SIZE, &mut counters); + } + + // Evict if this cache has more entries than its capacity. + let weights_to_evict = self.weights_to_evict(&counters); + if weights_to_evict > 0 { + self.evict_lru_entries( + &mut deqs, + batch_size::EVICTION_BATCH_SIZE, + weights_to_evict, + &mut counters, + ); + } + + debug_assert_eq!(self.entry_count.load(), current_ec); + debug_assert_eq!(self.weighted_size.load(), current_ws); + self.entry_count.store(counters.entry_count); + self.weighted_size.store(counters.weighted_size); + } + + fn now(&self) -> Instant { + self.current_time_from_expiration_clock() + } +} + +// +// private methods +// +impl Inner +where + K: Hash + Eq + Send + Sync + 'static, + V: Send + Sync + 'static, + S: BuildHasher + Clone + Send + Sync + 'static, +{ + fn has_enough_capacity(&self, candidate_weight: u32, counters: &EvictionCounters) -> bool { + self.max_capacity + .map(|limit| counters.weighted_size + candidate_weight as u64 <= limit) + .unwrap_or(true) + } + + fn weights_to_evict(&self, counters: &EvictionCounters) -> u64 { + self.max_capacity + .map(|limit| counters.weighted_size.saturating_sub(limit)) + .unwrap_or_default() + } + + #[inline] + fn should_enable_frequency_sketch(&self, counters: &EvictionCounters) -> bool { + if self.frequency_sketch_enabled.load(Ordering::Acquire) { + false + } else if let Some(max_cap) = self.max_capacity { + counters.weighted_size >= max_cap / 2 + } else { + false + } + } + + #[inline] + fn enable_frequency_sketch(&self, counters: &EvictionCounters) { + if let Some(max_cap) = self.max_capacity { + let c = counters; + let cap = if self.weigher.is_none() { + max_cap + } else { + (c.entry_count as f64 * (c.weighted_size as f64 / max_cap as f64)) as u64 + }; + self.do_enable_frequency_sketch(cap); + } + } + + #[cfg(test)] + fn enable_frequency_sketch_for_testing(&self) { + if let Some(max_cap) = self.max_capacity { + self.do_enable_frequency_sketch(max_cap); + } + } + + #[inline] + fn do_enable_frequency_sketch(&self, cache_capacity: u64) { + let skt_capacity = common::sketch_capacity(cache_capacity); + self.frequency_sketch + .write() + .expect("lock poisoned") + .ensure_capacity(skt_capacity); + self.frequency_sketch_enabled.store(true, Ordering::Release); + } + + fn apply_reads(&self, deqs: &mut Deques, count: usize) { + use ReadOp::*; + let mut freq = self.frequency_sketch.write().expect("lock poisoned"); + let ch = &self.read_op_ch; + for _ in 0..count { + match ch.try_recv() { + Ok(Hit(hash, entry, timestamp)) => { + freq.increment(hash); + entry.set_last_accessed(timestamp); + if entry.is_admitted() { + deqs.move_to_back_ao(&entry); + } + } + Ok(Miss(hash)) => freq.increment(hash), + Err(_) => break, + } + } + } + + fn apply_writes(&self, deqs: &mut Deques, count: usize, counters: &mut EvictionCounters) { + use WriteOp::*; + let freq = self.frequency_sketch.read().expect("lock poisoned"); + let ch = &self.write_op_ch; + + for _ in 0..count { + match ch.try_recv() { + Ok(Upsert { + key_hash: kh, + value_entry: entry, + old_weight, + new_weight, + }) => self.handle_upsert(kh, entry, old_weight, new_weight, deqs, &freq, counters), + Ok(Remove(KvEntry { key: _key, entry })) => { + Self::handle_remove(deqs, entry, counters) + } + Err(_) => break, + }; + } + } + + #[allow(clippy::too_many_arguments)] + fn handle_upsert( + &self, + kh: KeyHash, + entry: TrioArc>, + old_weight: u32, + new_weight: u32, + deqs: &mut Deques, + freq: &FrequencySketch, + counters: &mut EvictionCounters, + ) { + entry.set_dirty(false); + + if entry.is_admitted() { + // The entry has been already admitted, so treat this as an update. + counters.saturating_sub(0, old_weight); + counters.saturating_add(0, new_weight); + deqs.move_to_back_ao(&entry); + deqs.move_to_back_wo(&entry); + return; + } + + if self.has_enough_capacity(new_weight, counters) { + // There are enough room in the cache (or the cache is unbounded). + // Add the candidate to the deques. + self.handle_admit(kh, &entry, new_weight, deqs, counters); + return; + } + + if let Some(max) = self.max_capacity { + if new_weight as u64 > max { + // The candidate is too big to fit in the cache. Reject it. + self.cache.remove(&Arc::clone(&kh.key)); + return; + } + } + + let skipped_nodes; + let mut candidate = EntrySizeAndFrequency::new(new_weight); + candidate.add_frequency(freq, kh.hash); + + // Try to admit the candidate. + match Self::admit(&candidate, &self.cache, deqs, freq) { + AdmissionResult::Admitted { + victim_nodes, + skipped_nodes: mut skipped, + } => { + // Try to remove the victims from the cache (hash map). + for victim in victim_nodes { + if let Some((_vic_key, vic_entry)) = + self.cache.remove(unsafe { victim.as_ref().element.key() }) + { + // And then remove the victim from the deques. + Self::handle_remove(deqs, vic_entry, counters); + } else { + // Could not remove the victim from the cache. Skip this + // victim node as its ValueEntry might have been + // invalidated. Add it to the skipped nodes. + skipped.push(victim); + } + } + skipped_nodes = skipped; + + // Add the candidate to the deques. + self.handle_admit(kh, &entry, new_weight, deqs, counters); + } + AdmissionResult::Rejected { skipped_nodes: s } => { + skipped_nodes = s; + // Remove the candidate from the cache (hash map). + self.cache.remove(&Arc::clone(&kh.key)); + } + }; + + // Move the skipped nodes to the back of the deque. We do not unlink (drop) + // them because ValueEntries in the write op queue should be pointing them. + for node in skipped_nodes { + unsafe { deqs.probation.move_to_back(node) }; + } + } + + /// Performs size-aware admission explained in the paper: + /// [Lightweight Robust Size Aware Cache Management][size-aware-cache-paper] + /// by Gil Einziger, Ohad Eytan, Roy Friedman, Ben Manes. + /// + /// [size-aware-cache-paper]: https://arxiv.org/abs/2105.08770 + /// + /// There are some modifications in this implementation: + /// - To admit to the main space, candidate's frequency must be higher than + /// the aggregated frequencies of the potential victims. (In the paper, + /// `>=` operator is used rather than `>`) The `>` operator will do a better + /// job to prevent the main space from polluting. + /// - When a candidate is rejected, the potential victims will stay at the LRU + /// position of the probation access-order queue. (In the paper, they will be + /// promoted (to the MRU position?) to force the eviction policy to select a + /// different set of victims for the next candidate). We may implement the + /// paper's behavior later? + /// + #[inline] + fn admit( + candidate: &EntrySizeAndFrequency, + cache: &CacheStore, + deqs: &Deques, + freq: &FrequencySketch, + ) -> AdmissionResult { + const MAX_CONSECUTIVE_RETRIES: usize = 5; + let mut retries = 0; + + let mut victims = EntrySizeAndFrequency::default(); + let mut victim_nodes = SmallVec::default(); + let mut skipped_nodes = SmallVec::default(); + + // Get first potential victim at the LRU position. + let mut next_victim = deqs.probation.peek_front_ptr(); + + // Aggregate potential victims. + while victims.policy_weight < candidate.policy_weight { + if candidate.freq < victims.freq { + break; + } + if let Some(victim) = next_victim.take() { + next_victim = DeqNode::next_node_ptr(victim); + let vic_elem = &unsafe { victim.as_ref() }.element; + + if let Some(vic_entry) = cache.get(vic_elem.key()) { + victims.add_policy_weight(vic_entry.policy_weight()); + victims.add_frequency(freq, vic_elem.hash()); + victim_nodes.push(victim); + retries = 0; + } else { + // Could not get the victim from the cache (hash map). Skip this node + // as its ValueEntry might have been invalidated. + skipped_nodes.push(victim); + + retries += 1; + if retries > MAX_CONSECUTIVE_RETRIES { + break; + } + } + } else { + // No more potential victims. + break; + } + } + + // Admit or reject the candidate. + + // TODO: Implement some randomness to mitigate hash DoS attack. + // See Caffeine's implementation. + + if victims.policy_weight >= candidate.policy_weight && candidate.freq > victims.freq { + AdmissionResult::Admitted { + victim_nodes, + skipped_nodes, + } + } else { + AdmissionResult::Rejected { skipped_nodes } + } + } + + fn handle_admit( + &self, + kh: KeyHash, + entry: &TrioArc>, + policy_weight: u32, + deqs: &mut Deques, + counters: &mut EvictionCounters, + ) { + let key = Arc::clone(&kh.key); + counters.saturating_add(1, policy_weight); + deqs.push_back_ao( + CacheRegion::MainProbation, + KeyHashDate::new(kh, entry.entry_info()), + entry, + ); + if self.is_write_order_queue_enabled() { + deqs.push_back_wo(KeyDate::new(key, entry.entry_info()), entry); + } + entry.set_admitted(true); + } + + fn handle_remove( + deqs: &mut Deques, + entry: TrioArc>, + counters: &mut EvictionCounters, + ) { + if entry.is_admitted() { + entry.set_admitted(false); + counters.saturating_sub(1, entry.policy_weight()); + // The following two unlink_* functions will unset the deq nodes. + deqs.unlink_ao(&entry); + Deques::unlink_wo(&mut deqs.write_order, &entry); + } else { + entry.unset_q_nodes(); + } + } + + fn handle_remove_with_deques( + ao_deq_name: &str, + ao_deq: &mut Deque>, + wo_deq: &mut Deque>, + entry: TrioArc>, + counters: &mut EvictionCounters, + ) { + if entry.is_admitted() { + entry.set_admitted(false); + counters.saturating_sub(1, entry.policy_weight()); + // The following two unlink_* functions will unset the deq nodes. + Deques::unlink_ao_from_deque(ao_deq_name, ao_deq, &entry); + Deques::unlink_wo(wo_deq, &entry); + } else { + entry.unset_q_nodes(); + } + } + + fn evict_expired( + &self, + deqs: &mut Deques, + batch_size: usize, + counters: &mut EvictionCounters, + ) { + let now = self.current_time_from_expiration_clock(); + + if self.is_write_order_queue_enabled() { + self.remove_expired_wo(deqs, batch_size, now, counters); + } + + if self.time_to_idle.is_some() || self.has_valid_after() { + let (window, probation, protected, wo) = ( + &mut deqs.window, + &mut deqs.probation, + &mut deqs.protected, + &mut deqs.write_order, + ); + + let mut rm_expired_ao = + |name, deq| self.remove_expired_ao(name, deq, wo, batch_size, now, counters); + + rm_expired_ao("window", window); + rm_expired_ao("probation", probation); + rm_expired_ao("protected", protected); + } + } + + #[inline] + fn remove_expired_ao( + &self, + deq_name: &str, + deq: &mut Deque>, + write_order_deq: &mut Deque>, + batch_size: usize, + now: Instant, + counters: &mut EvictionCounters, + ) { + let tti = &self.time_to_idle; + let va = &self.valid_after(); + for _ in 0..batch_size { + // Peek the front node of the deque and check if it is expired. + let key = deq.peek_front().and_then(|node| { + // TODO: Skip the entry if it is dirty. See `evict_lru_entries` method as an example. + if is_expired_entry_ao(tti, va, node, now) { + Some(Arc::clone(node.element.key())) + } else { + None + } + }); + + if key.is_none() { + break; + } + + let key = key.as_ref().unwrap(); + + // Remove the key from the map only when the entry is really + // expired. This check is needed because it is possible that the entry in + // the map has been updated or deleted but its deque node we checked + // above have not been updated yet. + let maybe_entry = self + .cache + .remove_if(key, |_, v| is_expired_entry_ao(tti, va, v, now)); + + if let Some((_k, entry)) = maybe_entry { + Self::handle_remove_with_deques(deq_name, deq, write_order_deq, entry, counters); + } else if !self.try_skip_updated_entry(key, deq_name, deq, write_order_deq) { + break; + } + } + } + + #[inline] + fn try_skip_updated_entry( + &self, + key: &K, + deq_name: &str, + deq: &mut Deque>, + write_order_deq: &mut Deque>, + ) -> bool { + if let Some(entry) = self.cache.get(key) { + if entry.is_dirty() { + // The key exists and the entry has been updated. + Deques::move_to_back_ao_in_deque(deq_name, deq, &entry); + Deques::move_to_back_wo_in_deque(write_order_deq, &entry); + true + } else { + // The key exists but something unexpected. + false + } + } else { + // Skip this entry as the key might have been invalidated. Since the + // invalidated ValueEntry (which should be still in the write op + // queue) has a pointer to this node, move the node to the back of + // the deque instead of popping (dropping) it. + deq.move_front_to_back(); + true + } + } + + #[inline] + fn remove_expired_wo( + &self, + deqs: &mut Deques, + batch_size: usize, + now: Instant, + counters: &mut EvictionCounters, + ) { + let ttl = &self.time_to_live; + let va = &self.valid_after(); + for _ in 0..batch_size { + let key = deqs.write_order.peek_front().and_then(|node| { + // TODO: Skip the entry if it is dirty. See `evict_lru_entries` method as an example. + if is_expired_entry_wo(ttl, va, node, now) { + Some(Arc::clone(node.element.key())) + } else { + None + } + }); + + if key.is_none() { + break; + } + + let key = key.as_ref().unwrap(); + + let maybe_entry = self + .cache + .remove_if(key, |_, v| is_expired_entry_wo(ttl, va, v, now)); + + if let Some((_k, entry)) = maybe_entry { + Self::handle_remove(deqs, entry, counters); + } else if let Some(entry) = self.cache.get(key) { + if entry.is_dirty() { + deqs.move_to_back_ao(&entry); + deqs.move_to_back_wo(&entry); + } else { + // The key exists but something unexpected. Break. + break; + } + } else { + // Skip this entry as the key might have been invalidated. Since the + // invalidated ValueEntry (which should be still in the write op + // queue) has a pointer to this node, move the node to the back of + // the deque instead of popping (dropping) it. + deqs.write_order.move_front_to_back(); + } + } + } + + fn evict_lru_entries( + &self, + deqs: &mut Deques, + batch_size: usize, + weights_to_evict: u64, + counters: &mut EvictionCounters, + ) { + const DEQ_NAME: &str = "probation"; + let mut evicted = 0u64; + let (deq, write_order_deq) = (&mut deqs.probation, &mut deqs.write_order); + + for _ in 0..batch_size { + if evicted >= weights_to_evict { + break; + } + + let maybe_key_and_ts = deq.peek_front().map(|node| { + let entry_info = node.element.entry_info(); + ( + Arc::clone(node.element.key()), + entry_info.is_dirty(), + entry_info.last_modified(), + ) + }); + + let (key, ts) = match maybe_key_and_ts { + Some((key, false, Some(ts))) => (key, ts), + // TODO: Remove the second pattern `Some((_key, false, None))` once we change + // `last_modified` and `last_accessed` in `EntryInfo` from `Option` to + // `Instant`. + Some((key, true, _)) | Some((key, false, None)) => { + if self.try_skip_updated_entry(&key, DEQ_NAME, deq, write_order_deq) { + continue; + } else { + break; + } + } + None => break, + }; + + let maybe_entry = self.cache.remove_if(&key, |_, v| { + if let Some(lm) = v.last_modified() { + lm == ts + } else { + false + } + }); + + if let Some((_k, entry)) = maybe_entry { + let weight = entry.policy_weight(); + Self::handle_remove_with_deques(DEQ_NAME, deq, write_order_deq, entry, counters); + evicted = evicted.saturating_add(weight as u64); + } else if !self.try_skip_updated_entry(&key, DEQ_NAME, deq, write_order_deq) { + break; + } + } + } +} + +// +// for testing +// +#[cfg(test)] +impl Inner +where + K: Hash + Eq, + S: BuildHasher + Clone, +{ + fn set_expiration_clock(&self, clock: Option) { + let mut exp_clock = self.expiration_clock.write().expect("lock poisoned"); + if let Some(clock) = clock { + *exp_clock = Some(clock); + self.has_expiration_clock.store(true, Ordering::SeqCst); + } else { + self.has_expiration_clock.store(false, Ordering::SeqCst); + *exp_clock = None; + } + } +} + +// +// private free-standing functions +// +#[inline] +fn is_expired_entry_ao( + time_to_idle: &Option, + valid_after: &Option, + entry: &impl AccessTime, + now: Instant, +) -> bool { + if let Some(ts) = entry.last_accessed() { + if let Some(va) = valid_after { + if ts < *va { + return true; + } + } + if let Some(tti) = time_to_idle { + let checked_add = ts.checked_add(*tti); + if checked_add.is_none() { + panic!("ttl overflow") + } + return checked_add.unwrap() <= now; + } + } + false +} + +#[inline] +fn is_expired_entry_wo( + time_to_live: &Option, + valid_after: &Option, + entry: &impl AccessTime, + now: Instant, +) -> bool { + if let Some(ts) = entry.last_modified() { + if let Some(va) = valid_after { + if ts < *va { + return true; + } + } + if let Some(ttl) = time_to_live { + let checked_add = ts.checked_add(*ttl); + if checked_add.is_none() { + panic!("ttl overflow"); + } + return checked_add.unwrap() <= now; + } + } + false +} + +#[cfg(test)] +mod tests { + use super::BaseCache; + + #[cfg_attr(target_pointer_width = "16", ignore)] + #[test] + fn test_skt_capacity_will_not_overflow() { + use std::collections::hash_map::RandomState; + + // power of two + let pot = |exp| 2u64.pow(exp); + + let ensure_sketch_len = |max_capacity, len, name| { + let cache = BaseCache::::new( + Some(max_capacity), + None, + RandomState::default(), + None, + None, + None, + ); + cache.inner.enable_frequency_sketch_for_testing(); + assert_eq!( + cache + .inner + .frequency_sketch + .read() + .expect("lock poisoned") + .table_len(), + len as usize, + "{}", + name + ); + }; + + if cfg!(target_pointer_width = "32") { + let pot24 = pot(24); + let pot16 = pot(16); + ensure_sketch_len(0, 128, "0"); + ensure_sketch_len(128, 128, "128"); + ensure_sketch_len(pot16, pot16, "pot16"); + // due to ceiling to next_power_of_two + ensure_sketch_len(pot16 + 1, pot(17), "pot16 + 1"); + // due to ceiling to next_power_of_two + ensure_sketch_len(pot24 - 1, pot24, "pot24 - 1"); + ensure_sketch_len(pot24, pot24, "pot24"); + ensure_sketch_len(pot(27), pot24, "pot(27)"); + ensure_sketch_len(u32::MAX as u64, pot24, "u32::MAX"); + } else { + // target_pointer_width: 64 or larger. + let pot30 = pot(30); + let pot16 = pot(16); + ensure_sketch_len(0, 128, "0"); + ensure_sketch_len(128, 128, "128"); + ensure_sketch_len(pot16, pot16, "pot16"); + // due to ceiling to next_power_of_two + ensure_sketch_len(pot16 + 1, pot(17), "pot16 + 1"); + + // The following tests will allocate large memory (~8GiB). + // Skip when running on Circle CI. + if !cfg!(circleci) { + // due to ceiling to next_power_of_two + ensure_sketch_len(pot30 - 1, pot30, "pot30- 1"); + ensure_sketch_len(pot30, pot30, "pot30"); + ensure_sketch_len(u64::MAX, pot30, "u64::MAX"); + } + }; + } +} diff --git a/crates/mini-moka-vendored/src/sync/builder.rs b/crates/mini-moka-vendored/src/sync/builder.rs new file mode 100644 index 00000000..77b34b4c --- /dev/null +++ b/crates/mini-moka-vendored/src/sync/builder.rs @@ -0,0 +1,249 @@ +use super::Cache; +use crate::{common::builder_utils, common::concurrent::Weigher}; + +use std::{ + collections::hash_map::RandomState, + hash::{BuildHasher, Hash}, + marker::PhantomData, + sync::Arc, + time::Duration, +}; + +/// Builds a [`Cache`][cache-struct] or with various configuration knobs. +/// +/// [cache-struct]: ./struct.Cache.html +/// +/// # Examples +/// +/// ```rust +/// use mini_moka::sync::Cache; +/// use std::time::Duration; +/// +/// let cache = Cache::builder() +/// // Max 10,000 entries +/// .max_capacity(10_000) +/// // Time to live (TTL): 30 minutes +/// .time_to_live(Duration::from_secs(30 * 60)) +/// // Time to idle (TTI): 5 minutes +/// .time_to_idle(Duration::from_secs( 5 * 60)) +/// // Create the cache. +/// .build(); +/// +/// // This entry will expire after 5 minutes (TTI) if there is no get(). +/// cache.insert(0, "zero"); +/// +/// // This get() will extend the entry life for another 5 minutes. +/// cache.get(&0); +/// +/// // Even though we keep calling get(), the entry will expire +/// // after 30 minutes (TTL) from the insert(). +/// ``` +/// +#[must_use] +pub struct CacheBuilder { + max_capacity: Option, + initial_capacity: Option, + weigher: Option>, + time_to_live: Option, + time_to_idle: Option, + cache_type: PhantomData, +} + +impl Default for CacheBuilder> +where + K: Eq + Hash + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, +{ + fn default() -> Self { + Self { + max_capacity: None, + initial_capacity: None, + weigher: None, + time_to_live: None, + time_to_idle: None, + cache_type: Default::default(), + } + } +} + +impl CacheBuilder> +where + K: Eq + Hash + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, +{ + /// Construct a new `CacheBuilder` that will be used to build a `Cache` or + /// `SegmentedCache` holding up to `max_capacity` entries. + pub fn new(max_capacity: u64) -> Self { + Self { + max_capacity: Some(max_capacity), + ..Default::default() + } + } + + /// Builds a `Cache`. + /// + /// If you want to build a `SegmentedCache`, call `segments` method before + /// calling this method. + /// + /// # Panics + /// + /// Panics if configured with either `time_to_live` or `time_to_idle` higher than + /// 1000 years. This is done to protect against overflow when computing key + /// expiration. + pub fn build(self) -> Cache { + let build_hasher = RandomState::default(); + builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); + Cache::with_everything( + self.max_capacity, + self.initial_capacity, + build_hasher, + self.weigher, + self.time_to_live, + self.time_to_idle, + ) + } + + /// Builds a `Cache`, with the given `hasher`. + /// + /// If you want to build a `SegmentedCache`, call `segments` method before + /// calling this method. + /// + /// # Panics + /// + /// Panics if configured with either `time_to_live` or `time_to_idle` higher than + /// 1000 years. This is done to protect against overflow when computing key + /// expiration. + pub fn build_with_hasher(self, hasher: S) -> Cache + where + S: BuildHasher + Clone + Send + Sync + 'static, + { + builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); + Cache::with_everything( + self.max_capacity, + self.initial_capacity, + hasher, + self.weigher, + self.time_to_live, + self.time_to_idle, + ) + } +} + +impl CacheBuilder { + /// Sets the max capacity of the cache. + pub fn max_capacity(self, max_capacity: u64) -> Self { + Self { + max_capacity: Some(max_capacity), + ..self + } + } + + /// Sets the initial capacity (number of entries) of the cache. + pub fn initial_capacity(self, number_of_entries: usize) -> Self { + Self { + initial_capacity: Some(number_of_entries), + ..self + } + } + + /// Sets the weigher closure of the cache. + /// + /// The closure should take `&K` and `&V` as the arguments and returns a `u32` + /// representing the relative size of the entry. + pub fn weigher(self, weigher: impl Fn(&K, &V) -> u32 + Send + Sync + 'static) -> Self { + Self { + weigher: Some(Arc::new(weigher)), + ..self + } + } + + /// Sets the time to live of the cache. + /// + /// A cached entry will be expired after the specified duration past from + /// `insert`. + /// + /// # Panics + /// + /// `CacheBuilder::build*` methods will panic if the given `duration` is longer + /// than 1000 years. This is done to protect against overflow when computing key + /// expiration. + pub fn time_to_live(self, duration: Duration) -> Self { + Self { + time_to_live: Some(duration), + ..self + } + } + + /// Sets the time to idle of the cache. + /// + /// A cached entry will be expired after the specified duration past from `get` + /// or `insert`. + /// + /// # Panics + /// + /// `CacheBuilder::build*` methods will panic if the given `duration` is longer + /// than 1000 years. This is done to protect against overflow when computing key + /// expiration. + pub fn time_to_idle(self, duration: Duration) -> Self { + Self { + time_to_idle: Some(duration), + ..self + } + } +} + +#[cfg(test)] +mod tests { + use super::CacheBuilder; + + use std::time::Duration; + + #[test] + fn build_cache() { + // Cache + let cache = CacheBuilder::new(100).build(); + let policy = cache.policy(); + + assert_eq!(policy.max_capacity(), Some(100)); + assert_eq!(policy.time_to_live(), None); + assert_eq!(policy.time_to_idle(), None); + + cache.insert('a', "Alice"); + assert_eq!(cache.get(&'a'), Some("Alice")); + + let cache = CacheBuilder::new(100) + .time_to_live(Duration::from_secs(45 * 60)) + .time_to_idle(Duration::from_secs(15 * 60)) + .build(); + let policy = cache.policy(); + + assert_eq!(policy.max_capacity(), Some(100)); + assert_eq!(policy.time_to_live(), Some(Duration::from_secs(45 * 60))); + assert_eq!(policy.time_to_idle(), Some(Duration::from_secs(15 * 60))); + + cache.insert('a', "Alice"); + assert_eq!(cache.get(&'a'), Some("Alice")); + } + + #[test] + #[should_panic(expected = "time_to_live is longer than 1000 years")] + fn build_cache_too_long_ttl() { + let thousand_years_secs: u64 = 1000 * 365 * 24 * 3600; + let builder: CacheBuilder = CacheBuilder::new(100); + let duration = Duration::from_secs(thousand_years_secs); + builder + .time_to_live(duration + Duration::from_secs(1)) + .build(); + } + + #[test] + #[should_panic(expected = "time_to_idle is longer than 1000 years")] + fn build_cache_too_long_tti() { + let thousand_years_secs: u64 = 1000 * 365 * 24 * 3600; + let builder: CacheBuilder = CacheBuilder::new(100); + let duration = Duration::from_secs(thousand_years_secs); + builder + .time_to_idle(duration + Duration::from_secs(1)) + .build(); + } +} diff --git a/crates/mini-moka-vendored/src/sync/cache.rs b/crates/mini-moka-vendored/src/sync/cache.rs new file mode 100644 index 00000000..a5fd4663 --- /dev/null +++ b/crates/mini-moka-vendored/src/sync/cache.rs @@ -0,0 +1,1120 @@ +use super::{base_cache::BaseCache, CacheBuilder, ConcurrentCacheExt, EntryRef, Iter}; +use crate::{ + common::{ + concurrent::{ + constants::{MAX_SYNC_REPEATS, WRITE_RETRY_INTERVAL_MICROS}, + housekeeper::{Housekeeper, InnerSync}, + Weigher, WriteOp, + }, + time::Instant, + }, + Policy, +}; + +use crossbeam_channel::{Sender, TrySendError}; +use std::{ + borrow::Borrow, + collections::hash_map::RandomState, + fmt, + hash::{BuildHasher, Hash}, + sync::Arc, + time::Duration, +}; + +/// A thread-safe concurrent in-memory cache built upon [`dashmap::DashMap`][dashmap]. +/// +/// The `Cache` uses `DashMap` as the central key-value storage. It performs a +/// best-effort bounding of the map using an entry replacement algorithm to determine +/// which entries to evict when the capacity is exceeded. +/// +/// To use this cache, enable a crate feature called "dash" in your Cargo.toml. +/// Please note that the API of `dash` cache will _be changed very often_ in next few +/// releases as this is yet an experimental component. +/// +/// # Examples +/// +/// Cache entries are manually added using [`insert`](#method.insert) method, and are +/// stored in the cache until either evicted or manually invalidated. +/// +/// Here's an example of reading and updating a cache by using multiple threads: +/// +/// ```rust +/// use mini_moka::sync::Cache; +/// +/// use std::thread; +/// +/// fn value(n: usize) -> String { +/// format!("value {}", n) +/// } +/// +/// const NUM_THREADS: usize = 16; +/// const NUM_KEYS_PER_THREAD: usize = 64; +/// +/// // Create a cache that can store up to 10,000 entries. +/// let cache = Cache::new(10_000); +/// +/// // Spawn threads and read and update the cache simultaneously. +/// let threads: Vec<_> = (0..NUM_THREADS) +/// .map(|i| { +/// // To share the same cache across the threads, clone it. +/// // This is a cheap operation. +/// let my_cache = cache.clone(); +/// let start = i * NUM_KEYS_PER_THREAD; +/// let end = (i + 1) * NUM_KEYS_PER_THREAD; +/// +/// thread::spawn(move || { +/// // Insert 64 entries. (NUM_KEYS_PER_THREAD = 64) +/// for key in start..end { +/// my_cache.insert(key, value(key)); +/// // get() returns Option, a clone of the stored value. +/// assert_eq!(my_cache.get(&key), Some(value(key))); +/// } +/// +/// // Invalidate every 4 element of the inserted entries. +/// for key in (start..end).step_by(4) { +/// my_cache.invalidate(&key); +/// } +/// }) +/// }) +/// .collect(); +/// +/// // Wait for all threads to complete. +/// threads.into_iter().for_each(|t| t.join().expect("Failed")); +/// +/// // Verify the result. +/// for key in 0..(NUM_THREADS * NUM_KEYS_PER_THREAD) { +/// if key % 4 == 0 { +/// assert_eq!(cache.get(&key), None); +/// } else { +/// assert_eq!(cache.get(&key), Some(value(key))); +/// } +/// } +/// ``` +/// +/// # Avoiding to clone the value at `get` +/// +/// The return type of `get` method is `Option` instead of `Option<&V>`. Every +/// time `get` is called for an existing key, it creates a clone of the stored value +/// `V` and returns it. This is because the `Cache` allows concurrent updates from +/// threads so a value stored in the cache can be dropped or replaced at any time by +/// any other thread. `get` cannot return a reference `&V` as it is impossible to +/// guarantee the value outlives the reference. +/// +/// If you want to store values that will be expensive to clone, wrap them by +/// `std::sync::Arc` before storing in a cache. [`Arc`][rustdoc-std-arc] is a +/// thread-safe reference-counted pointer and its `clone()` method is cheap. +/// +/// [rustdoc-std-arc]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html +/// +/// # Size-based Eviction +/// +/// ```rust +/// use std::convert::TryInto; +/// use mini_moka::sync::Cache; +/// +/// // Evict based on the number of entries in the cache. +/// let cache = Cache::builder() +/// // Up to 10,000 entries. +/// .max_capacity(10_000) +/// // Create the cache. +/// .build(); +/// cache.insert(1, "one".to_string()); +/// +/// // Evict based on the byte length of strings in the cache. +/// let cache = Cache::builder() +/// // A weigher closure takes &K and &V and returns a u32 +/// // representing the relative size of the entry. +/// .weigher(|_key, value: &String| -> u32 { +/// value.len().try_into().unwrap_or(u32::MAX) +/// }) +/// // This cache will hold up to 32MiB of values. +/// .max_capacity(32 * 1024 * 1024) +/// .build(); +/// cache.insert(2, "two".to_string()); +/// ``` +/// +/// If your cache should not grow beyond a certain size, use the `max_capacity` +/// method of the [`CacheBuilder`][builder-struct] to set the upper bound. The cache +/// will try to evict entries that have not been used recently or very often. +/// +/// At the cache creation time, a weigher closure can be set by the `weigher` method +/// of the `CacheBuilder`. A weigher closure takes `&K` and `&V` as the arguments and +/// returns a `u32` representing the relative size of the entry: +/// +/// - If the `weigher` is _not_ set, the cache will treat each entry has the same +/// size of `1`. This means the cache will be bounded by the number of entries. +/// - If the `weigher` is set, the cache will call the weigher to calculate the +/// weighted size (relative size) on an entry. This means the cache will be bounded +/// by the total weighted size of entries. +/// +/// Note that weighted sizes are not used when making eviction selections. +/// +/// [builder-struct]: ./struct.CacheBuilder.html +/// +/// # Time-based Expirations +/// +/// `Cache` supports the following expiration policies: +/// +/// - **Time to live**: A cached entry will be expired after the specified duration +/// past from `insert`. +/// - **Time to idle**: A cached entry will be expired after the specified duration +/// past from `get` or `insert`. +/// +/// ```rust +/// use mini_moka::sync::Cache; +/// use std::time::Duration; +/// +/// let cache = Cache::builder() +/// // Time to live (TTL): 30 minutes +/// .time_to_live(Duration::from_secs(30 * 60)) +/// // Time to idle (TTI): 5 minutes +/// .time_to_idle(Duration::from_secs( 5 * 60)) +/// // Create the cache. +/// .build(); +/// +/// // This entry will expire after 5 minutes (TTI) if there is no get(). +/// cache.insert(0, "zero"); +/// +/// // This get() will extend the entry life for another 5 minutes. +/// cache.get(&0); +/// +/// // Even though we keep calling get(), the entry will expire +/// // after 30 minutes (TTL) from the insert(). +/// ``` +/// +/// # Thread Safety +/// +/// All methods provided by the `Cache` are considered thread-safe, and can be safely +/// accessed by multiple concurrent threads. +/// +/// - `Cache` requires trait bounds `Send`, `Sync` and `'static` for `K` +/// (key), `V` (value) and `S` (hasher state). +/// - `Cache` will implement `Send` and `Sync`. +/// +/// # Sharing a cache across threads +/// +/// To share a cache across threads, do one of the followings: +/// +/// - Create a clone of the cache by calling its `clone` method and pass it to other +/// thread. +/// - Wrap the cache by a `sync::OnceCell` or `sync::Lazy` from +/// [once_cell][once-cell-crate] create, and set it to a `static` variable. +/// +/// Cloning is a cheap operation for `Cache` as it only creates thread-safe +/// reference-counted pointers to the internal data structures. +/// +/// [once-cell-crate]: https://crates.io/crates/once_cell +/// +/// # Hashing Algorithm +/// +/// By default, `Cache` uses a hashing algorithm selected to provide resistance +/// against HashDoS attacks. It will be the same one used by +/// `std::collections::HashMap`, which is currently SipHash 1-3. +/// +/// While SipHash's performance is very competitive for medium sized keys, other +/// hashing algorithms will outperform it for small keys such as integers as well as +/// large keys such as long strings. However those algorithms will typically not +/// protect against attacks such as HashDoS. +/// +/// The hashing algorithm can be replaced on a per-`Cache` basis using the +/// [`build_with_hasher`][build-with-hasher-method] method of the +/// `CacheBuilder`. Many alternative algorithms are available on crates.io, such +/// as the [aHash][ahash-crate] crate. +/// +/// [build-with-hasher-method]: ./struct.CacheBuilder.html#method.build_with_hasher +/// [ahash-crate]: https://crates.io/crates/ahash +/// +pub struct Cache { + base: BaseCache, +} + +// TODO: https://github.com/moka-rs/moka/issues/54 +#[allow(clippy::non_send_fields_in_send_ty)] +unsafe impl Send for Cache +where + K: Send + Sync, + V: Send + Sync, + S: Send, +{ +} + +unsafe impl Sync for Cache +where + K: Send + Sync, + V: Send + Sync, + S: Sync, +{ +} + +// NOTE: We cannot do `#[derive(Clone)]` because it will add `Clone` bound to `K`. +impl Clone for Cache { + /// Makes a clone of this shared cache. + /// + /// This operation is cheap as it only creates thread-safe reference counted + /// pointers to the shared internal data structures. + fn clone(&self) -> Self { + Self { + base: self.base.clone(), + } + } +} + +impl fmt::Debug for Cache +where + K: Eq + Hash + fmt::Debug, + V: fmt::Debug, + S: BuildHasher + Clone, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d_map = f.debug_map(); + + for r in self.iter() { + let (k, v) = r.pair(); + d_map.entry(k, v); + } + + d_map.finish() + } +} + +impl Cache +where + K: Hash + Eq + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, +{ + /// Constructs a new `Cache` that will store up to the `max_capacity`. + /// + /// To adjust various configuration knobs such as `initial_capacity` or + /// `time_to_live`, use the [`CacheBuilder`][builder-struct]. + /// + /// [builder-struct]: ./struct.CacheBuilder.html + pub fn new(max_capacity: u64) -> Self { + let build_hasher = RandomState::default(); + Self::with_everything(Some(max_capacity), None, build_hasher, None, None, None) + } + + /// Returns a [`CacheBuilder`][builder-struct], which can builds a `Cache` with + /// various configuration knobs. + /// + /// [builder-struct]: ./struct.CacheBuilder.html + pub fn builder() -> CacheBuilder> { + CacheBuilder::default() + } +} + +impl Cache { + /// Returns a read-only cache policy of this cache. + /// + /// At this time, cache policy cannot be modified after cache creation. + /// A future version may support to modify it. + pub fn policy(&self) -> Policy { + self.base.policy() + } + + /// Returns an approximate number of entries in this cache. + /// + /// The value returned is _an estimate_; the actual count may differ if there are + /// concurrent insertions or removals, or if some entries are pending removal due + /// to expiration. This inaccuracy can be mitigated by performing a `sync()` + /// first. + /// + /// # Example + /// + /// ```rust + /// use mini_moka::sync::Cache; + /// + /// let cache = Cache::new(10); + /// cache.insert('n', "Netherland Dwarf"); + /// cache.insert('l', "Lop Eared"); + /// cache.insert('d', "Dutch"); + /// + /// // Ensure an entry exists. + /// assert!(cache.contains_key(&'n')); + /// + /// // However, followings may print stale number zeros instead of threes. + /// println!("{}", cache.entry_count()); // -> 0 + /// println!("{}", cache.weighted_size()); // -> 0 + /// + /// // To mitigate the inaccuracy, bring `ConcurrentCacheExt` trait to + /// // the scope so we can use `sync` method. + /// use mini_moka::sync::ConcurrentCacheExt; + /// // Call `sync` to run pending internal tasks. + /// cache.sync(); + /// + /// // Followings will print the actual numbers. + /// println!("{}", cache.entry_count()); // -> 3 + /// println!("{}", cache.weighted_size()); // -> 3 + /// ``` + /// + pub fn entry_count(&self) -> u64 { + self.base.entry_count() + } + + /// Returns an approximate total weighted size of entries in this cache. + /// + /// The value returned is _an estimate_; the actual size may differ if there are + /// concurrent insertions or removals, or if some entries are pending removal due + /// to expiration. This inaccuracy can be mitigated by performing a `sync()` + /// first. See [`entry_count`](#method.entry_count) for a sample code. + pub fn weighted_size(&self) -> u64 { + self.base.weighted_size() + } +} + +impl Cache +where + K: Hash + Eq + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, + S: BuildHasher + Clone + Send + Sync + 'static, +{ + pub(crate) fn with_everything( + max_capacity: Option, + initial_capacity: Option, + build_hasher: S, + weigher: Option>, + time_to_live: Option, + time_to_idle: Option, + ) -> Self { + Self { + base: BaseCache::new( + max_capacity, + initial_capacity, + build_hasher, + weigher, + time_to_live, + time_to_idle, + ), + } + } + + /// Returns `true` if the cache contains a value for the key. + /// + /// Unlike the `get` method, this method is not considered a cache read operation, + /// so it does not update the historic popularity estimator or reset the idle + /// timer for the key. + /// + /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq` + /// on the borrowed form _must_ match those for the key type. + pub fn contains_key(&self, key: &Q) -> bool + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.base.contains_key(key) + } + + /// Returns a _clone_ of the value corresponding to the key. + /// + /// If you want to store values that will be expensive to clone, wrap them by + /// `std::sync::Arc` before storing in a cache. [`Arc`][rustdoc-std-arc] is a + /// thread-safe reference-counted pointer and its `clone()` method is cheap. + /// + /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq` + /// on the borrowed form _must_ match those for the key type. + /// + /// [rustdoc-std-arc]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html + pub fn get(&self, key: &Q) -> Option + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.base.get_with_hash(key, self.base.hash(key)) + } + + /// Deprecated, replaced with [`get`](#method.get) + #[doc(hidden)] + #[deprecated(since = "0.8.0", note = "Replaced with `get`")] + pub fn get_if_present(&self, key: &Q) -> Option + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.get(key) + } + + /// Inserts a key-value pair into the cache. + /// + /// If the cache has this key present, the value is updated. + pub fn insert(&self, key: K, value: V) { + let hash = self.base.hash(&key); + let key = Arc::new(key); + self.insert_with_hash(key, hash, value) + } + + pub(crate) fn insert_with_hash(&self, key: Arc, hash: u64, value: V) { + let (op, now) = self.base.do_insert_with_hash(key, hash, value); + let hk = self.base.housekeeper.as_ref(); + Self::schedule_write_op( + self.base.inner.as_ref(), + &self.base.write_op_ch, + op, + now, + hk, + ) + .expect("Failed to insert"); + } + + /// Discards any cached value for the key. + /// + /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq` + /// on the borrowed form _must_ match those for the key type. + pub fn invalidate(&self, key: &Q) + where + Arc: Borrow, + Q: Hash + Eq + ?Sized, + { + if let Some(kv) = self.base.remove_entry(key) { + let op = WriteOp::Remove(kv); + let now = self.base.current_time_from_expiration_clock(); + let hk = self.base.housekeeper.as_ref(); + Self::schedule_write_op( + self.base.inner.as_ref(), + &self.base.write_op_ch, + op, + now, + hk, + ) + .expect("Failed to remove"); + } + } + + /// Discards all cached values. + /// + /// This method returns immediately and a background thread will evict all the + /// cached values inserted before the time when this method was called. It is + /// guaranteed that the `get` method must not return these invalidated values + /// even if they have not been evicted. + /// + /// Like the `invalidate` method, this method does not clear the historic + /// popularity estimator of keys so that it retains the client activities of + /// trying to retrieve an item. + pub fn invalidate_all(&self) { + self.base.invalidate_all(); + } +} + +// Clippy beta 0.1.83 (f41c7ed9889 2024-10-31) warns about unused lifetimes on 'a. +// This seems a false positive. The lifetimes are used in the trait bounds. +// https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes +#[allow(clippy::extra_unused_lifetimes)] +impl<'a, K, V, S> Cache +where + K: 'a + Eq + Hash, + V: 'a, + S: BuildHasher + Clone, +{ + /// Creates an iterator visiting all key-value pairs in arbitrary order. The + /// iterator element type is [`EntryRef<'a, K, V, S>`][moka-entry-ref]. + /// + /// Unlike the `get` method, visiting entries via an iterator do not update the + /// historic popularity estimator or reset idle timers for keys. + /// + /// # Locking behavior + /// + /// This iterator relies on the iterator of [`dashmap::DashMap`][dashmap-iter], + /// which employs read-write locks. May deadlock if the thread holding an + /// iterator attempts to update the cache. + /// + /// [moka-entry-ref]: ./struct.EntryRef.html + /// [dashmap-iter]: + /// + /// # Examples + /// + /// ```rust + /// use mini_moka::sync::Cache; + /// + /// let cache = Cache::new(100); + /// cache.insert("Julia", 14); + /// + /// let mut iter = cache.iter(); + /// let entry_ref = iter.next().unwrap(); + /// assert_eq!(entry_ref.pair(), (&"Julia", &14)); + /// assert_eq!(entry_ref.key(), &"Julia"); + /// assert_eq!(entry_ref.value(), &14); + /// assert_eq!(*entry_ref, 14); + /// + /// assert!(iter.next().is_none()); + /// ``` + /// + pub fn iter(&self) -> Iter<'_, K, V, S> { + self.base.iter() + } +} + +impl ConcurrentCacheExt for Cache +where + K: Hash + Eq + Send + Sync + 'static, + V: Send + Sync + 'static, + S: BuildHasher + Clone + Send + Sync + 'static, +{ + fn sync(&self) { + self.base.inner.sync(MAX_SYNC_REPEATS); + } +} + +impl<'a, K, V, S> IntoIterator for &'a Cache +where + K: 'a + Eq + Hash, + V: 'a, + S: BuildHasher + Clone, +{ + type Item = EntryRef<'a, K, V>; + + type IntoIter = Iter<'a, K, V, S>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +// private methods +impl Cache +where + K: Hash + Eq + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, + S: BuildHasher + Clone + Send + Sync + 'static, +{ + #[inline] + fn schedule_write_op( + inner: &impl InnerSync, + ch: &Sender>, + op: WriteOp, + now: Instant, + housekeeper: Option<&Arc>, + ) -> Result<(), TrySendError>> { + let mut op = op; + + // NOTES: + // - This will block when the channel is full. + // - We are doing a busy-loop here. We were originally calling `ch.send(op)?`, + // but we got a notable performance degradation. + loop { + BaseCache::::apply_reads_writes_if_needed(inner, ch, now, housekeeper); + match ch.try_send(op) { + Ok(()) => break, + Err(TrySendError::Full(op1)) => { + op = op1; + std::thread::sleep(Duration::from_micros(WRITE_RETRY_INTERVAL_MICROS)); + } + Err(e @ TrySendError::Disconnected(_)) => return Err(e), + } + } + Ok(()) + } +} + +// For unit tests. +#[cfg(test)] +impl Cache +where + K: Hash + Eq + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, + S: BuildHasher + Clone + Send + Sync + 'static, +{ + pub(crate) fn is_table_empty(&self) -> bool { + self.entry_count() == 0 + } + + pub(crate) fn reconfigure_for_testing(&mut self) { + self.base.reconfigure_for_testing(); + } + + pub(crate) fn set_expiration_clock(&self, clock: Option) { + self.base.set_expiration_clock(clock); + } +} + +// To see the debug prints, run test as `cargo test -- --nocapture` +#[cfg(test)] +mod tests { + use super::{Cache, ConcurrentCacheExt}; + use crate::common::time::Clock; + + use std::{sync::Arc, time::Duration}; + + #[test] + fn basic_single_thread() { + let mut cache = Cache::new(3); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", "alice"); + cache.insert("b", "bob"); + assert_eq!(cache.get(&"a"), Some("alice")); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert_eq!(cache.get(&"b"), Some("bob")); + cache.sync(); + // counts: a -> 1, b -> 1 + + cache.insert("c", "cindy"); + assert_eq!(cache.get(&"c"), Some("cindy")); + assert!(cache.contains_key(&"c")); + // counts: a -> 1, b -> 1, c -> 1 + cache.sync(); + + assert!(cache.contains_key(&"a")); + assert_eq!(cache.get(&"a"), Some("alice")); + assert_eq!(cache.get(&"b"), Some("bob")); + assert!(cache.contains_key(&"b")); + cache.sync(); + // counts: a -> 2, b -> 2, c -> 1 + + // "d" should not be admitted because its frequency is too low. + cache.insert("d", "david"); // count: d -> 0 + cache.sync(); + assert_eq!(cache.get(&"d"), None); // d -> 1 + assert!(!cache.contains_key(&"d")); + + cache.insert("d", "david"); + cache.sync(); + assert!(!cache.contains_key(&"d")); + assert_eq!(cache.get(&"d"), None); // d -> 2 + + // "d" should be admitted and "c" should be evicted + // because d's frequency is higher than c's. + cache.insert("d", "dennis"); + cache.sync(); + assert_eq!(cache.get(&"a"), Some("alice")); + assert_eq!(cache.get(&"b"), Some("bob")); + assert_eq!(cache.get(&"c"), None); + assert_eq!(cache.get(&"d"), Some("dennis")); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert!(!cache.contains_key(&"c")); + assert!(cache.contains_key(&"d")); + + cache.invalidate(&"b"); + assert_eq!(cache.get(&"b"), None); + assert!(!cache.contains_key(&"b")); + } + + #[test] + fn size_aware_eviction() { + let weigher = |_k: &&str, v: &(&str, u32)| v.1; + + let alice = ("alice", 10); + let bob = ("bob", 15); + let bill = ("bill", 20); + let cindy = ("cindy", 5); + let david = ("david", 15); + let dennis = ("dennis", 15); + + let mut cache = Cache::builder().max_capacity(31).weigher(weigher).build(); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", alice); + cache.insert("b", bob); + assert_eq!(cache.get(&"a"), Some(alice)); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert_eq!(cache.get(&"b"), Some(bob)); + cache.sync(); + // order (LRU -> MRU) and counts: a -> 1, b -> 1 + + cache.insert("c", cindy); + assert_eq!(cache.get(&"c"), Some(cindy)); + assert!(cache.contains_key(&"c")); + // order and counts: a -> 1, b -> 1, c -> 1 + cache.sync(); + + assert!(cache.contains_key(&"a")); + assert_eq!(cache.get(&"a"), Some(alice)); + assert_eq!(cache.get(&"b"), Some(bob)); + assert!(cache.contains_key(&"b")); + cache.sync(); + // order and counts: c -> 1, a -> 2, b -> 2 + + // To enter "d" (weight: 15), it needs to evict "c" (w: 5) and "a" (w: 10). + // "d" must have higher count than 3, which is the aggregated count + // of "a" and "c". + cache.insert("d", david); // count: d -> 0 + cache.sync(); + assert_eq!(cache.get(&"d"), None); // d -> 1 + assert!(!cache.contains_key(&"d")); + + cache.insert("d", david); + cache.sync(); + assert!(!cache.contains_key(&"d")); + assert_eq!(cache.get(&"d"), None); // d -> 2 + + cache.insert("d", david); + cache.sync(); + assert_eq!(cache.get(&"d"), None); // d -> 3 + assert!(!cache.contains_key(&"d")); + + cache.insert("d", david); + cache.sync(); + assert!(!cache.contains_key(&"d")); + assert_eq!(cache.get(&"d"), None); // d -> 4 + + // Finally "d" should be admitted by evicting "c" and "a". + cache.insert("d", dennis); + cache.sync(); + assert_eq!(cache.get(&"a"), None); + assert_eq!(cache.get(&"b"), Some(bob)); + assert_eq!(cache.get(&"c"), None); + assert_eq!(cache.get(&"d"), Some(dennis)); + assert!(!cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert!(!cache.contains_key(&"c")); + assert!(cache.contains_key(&"d")); + + // Update "b" with "bill" (w: 15 -> 20). This should evict "d" (w: 15). + cache.insert("b", bill); + cache.sync(); + assert_eq!(cache.get(&"b"), Some(bill)); + assert_eq!(cache.get(&"d"), None); + assert!(cache.contains_key(&"b")); + assert!(!cache.contains_key(&"d")); + + // Re-add "a" (w: 10) and update "b" with "bob" (w: 20 -> 15). + cache.insert("a", alice); + cache.insert("b", bob); + cache.sync(); + assert_eq!(cache.get(&"a"), Some(alice)); + assert_eq!(cache.get(&"b"), Some(bob)); + assert_eq!(cache.get(&"d"), None); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert!(!cache.contains_key(&"d")); + + // Verify the sizes. + assert_eq!(cache.entry_count(), 2); + assert_eq!(cache.weighted_size(), 25); + } + + #[test] + fn basic_multi_threads() { + let num_threads = 4; + let cache = Cache::new(100); + + // https://rust-lang.github.io/rust-clippy/master/index.html#needless_collect + #[allow(clippy::needless_collect)] + let handles = (0..num_threads) + .map(|id| { + let cache = cache.clone(); + std::thread::spawn(move || { + cache.insert(10, format!("{}-100", id)); + cache.get(&10); + cache.insert(20, format!("{}-200", id)); + cache.invalidate(&10); + }) + }) + .collect::>(); + + handles.into_iter().for_each(|h| h.join().expect("Failed")); + + assert!(cache.get(&10).is_none()); + assert!(cache.get(&20).is_some()); + assert!(!cache.contains_key(&10)); + assert!(cache.contains_key(&20)); + } + + #[test] + fn invalidate_all() { + let mut cache = Cache::new(100); + cache.reconfigure_for_testing(); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", "alice"); + cache.insert("b", "bob"); + cache.insert("c", "cindy"); + assert_eq!(cache.get(&"a"), Some("alice")); + assert_eq!(cache.get(&"b"), Some("bob")); + assert_eq!(cache.get(&"c"), Some("cindy")); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert!(cache.contains_key(&"c")); + + // `cache.sync()` is no longer needed here before invalidating. The last + // modified timestamp of the entries were updated when they were inserted. + // https://github.com/moka-rs/moka/issues/155 + + cache.invalidate_all(); + cache.sync(); + + cache.insert("d", "david"); + cache.sync(); + + assert!(cache.get(&"a").is_none()); + assert!(cache.get(&"b").is_none()); + assert!(cache.get(&"c").is_none()); + assert_eq!(cache.get(&"d"), Some("david")); + assert!(!cache.contains_key(&"a")); + assert!(!cache.contains_key(&"b")); + assert!(!cache.contains_key(&"c")); + assert!(cache.contains_key(&"d")); + } + + #[test] + fn time_to_live() { + let mut cache = Cache::builder() + .max_capacity(100) + .time_to_live(Duration::from_secs(10)) + .build(); + + cache.reconfigure_for_testing(); + + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", "alice"); + cache.sync(); + + mock.increment(Duration::from_secs(5)); // 5 secs from the start. + cache.sync(); + + assert_eq!(cache.get(&"a"), Some("alice")); + assert!(cache.contains_key(&"a")); + + mock.increment(Duration::from_secs(5)); // 10 secs. + assert_eq!(cache.get(&"a"), None); + assert!(!cache.contains_key(&"a")); + + assert_eq!(cache.iter().count(), 0); + + cache.sync(); + assert!(cache.is_table_empty()); + + cache.insert("b", "bob"); + cache.sync(); + + assert_eq!(cache.entry_count(), 1); + + mock.increment(Duration::from_secs(5)); // 15 secs. + cache.sync(); + + assert_eq!(cache.get(&"b"), Some("bob")); + assert!(cache.contains_key(&"b")); + assert_eq!(cache.entry_count(), 1); + + cache.insert("b", "bill"); + cache.sync(); + + mock.increment(Duration::from_secs(5)); // 20 secs + cache.sync(); + + assert_eq!(cache.get(&"b"), Some("bill")); + assert!(cache.contains_key(&"b")); + assert_eq!(cache.entry_count(), 1); + + mock.increment(Duration::from_secs(5)); // 25 secs + assert_eq!(cache.get(&"a"), None); + assert_eq!(cache.get(&"b"), None); + assert!(!cache.contains_key(&"a")); + assert!(!cache.contains_key(&"b")); + + assert_eq!(cache.iter().count(), 0); + + cache.sync(); + assert!(cache.is_table_empty()); + } + + #[test] + fn time_to_idle() { + let mut cache = Cache::builder() + .max_capacity(100) + .time_to_idle(Duration::from_secs(10)) + .build(); + + cache.reconfigure_for_testing(); + + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); + + // Make the cache exterior immutable. + let cache = cache; + + cache.insert("a", "alice"); + cache.sync(); + + mock.increment(Duration::from_secs(5)); // 5 secs from the start. + cache.sync(); + + assert_eq!(cache.get(&"a"), Some("alice")); + + mock.increment(Duration::from_secs(5)); // 10 secs. + cache.sync(); + + cache.insert("b", "bob"); + cache.sync(); + + assert_eq!(cache.entry_count(), 2); + + mock.increment(Duration::from_secs(2)); // 12 secs. + cache.sync(); + + // contains_key does not reset the idle timer for the key. + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + cache.sync(); + + assert_eq!(cache.entry_count(), 2); + + mock.increment(Duration::from_secs(3)); // 15 secs. + assert_eq!(cache.get(&"a"), None); + assert_eq!(cache.get(&"b"), Some("bob")); + assert!(!cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + + assert_eq!(cache.iter().count(), 1); + + cache.sync(); + assert_eq!(cache.entry_count(), 1); + + mock.increment(Duration::from_secs(10)); // 25 secs + assert_eq!(cache.get(&"a"), None); + assert_eq!(cache.get(&"b"), None); + assert!(!cache.contains_key(&"a")); + assert!(!cache.contains_key(&"b")); + + assert_eq!(cache.iter().count(), 0); + + cache.sync(); + assert!(cache.is_table_empty()); + } + + #[test] + fn test_iter() { + const NUM_KEYS: usize = 50; + + fn make_value(key: usize) -> String { + format!("val: {}", key) + } + + let cache = Cache::builder() + .max_capacity(100) + .time_to_idle(Duration::from_secs(10)) + .build(); + + for key in 0..NUM_KEYS { + cache.insert(key, make_value(key)); + } + + let mut key_set = std::collections::HashSet::new(); + + for entry in &cache { + let (key, value) = entry.pair(); + assert_eq!(value, &make_value(*key)); + + key_set.insert(*key); + } + + // Ensure there are no missing or duplicate keys in the iteration. + assert_eq!(key_set.len(), NUM_KEYS); + + // DO NOT REMOVE THE COMMENT FROM THIS BLOCK. + // This block demonstrates how you can write a code to get a deadlock. + // { + // let mut iter = cache.iter(); + // let _ = iter.next(); + + // for key in 0..NUM_KEYS { + // cache.insert(key, make_value(key)); + // println!("{}", key); + // } + + // let _ = iter.next(); + // } + } + + /// Runs 16 threads at the same time and ensures no deadlock occurs. + /// + /// - Eight of the threads will update key-values in the cache. + /// - Eight others will iterate the cache. + /// + #[test] + fn test_iter_multi_threads() { + use std::collections::HashSet; + + const NUM_KEYS: usize = 1024; + const NUM_THREADS: usize = 16; + + fn make_value(key: usize) -> String { + format!("val: {}", key) + } + + let cache = Cache::builder() + .max_capacity(2048) + .time_to_idle(Duration::from_secs(10)) + .build(); + + // Initialize the cache. + for key in 0..NUM_KEYS { + cache.insert(key, make_value(key)); + } + + let rw_lock = Arc::new(std::sync::RwLock::<()>::default()); + let write_lock = rw_lock.write().unwrap(); + + // https://rust-lang.github.io/rust-clippy/master/index.html#needless_collect + #[allow(clippy::needless_collect)] + let handles = (0..NUM_THREADS) + .map(|n| { + let cache = cache.clone(); + let rw_lock = Arc::clone(&rw_lock); + + if n % 2 == 0 { + // This thread will update the cache. + std::thread::spawn(move || { + let read_lock = rw_lock.read().unwrap(); + for key in 0..NUM_KEYS { + // TODO: Update keys in a random order? + cache.insert(key, make_value(key)); + } + std::mem::drop(read_lock); + }) + } else { + // This thread will iterate the cache. + std::thread::spawn(move || { + let read_lock = rw_lock.read().unwrap(); + let mut key_set = HashSet::new(); + for entry in &cache { + let (key, value) = entry.pair(); + assert_eq!(value, &make_value(*key)); + key_set.insert(*key); + } + // Ensure there are no missing or duplicate keys in the iteration. + assert_eq!(key_set.len(), NUM_KEYS); + std::mem::drop(read_lock); + }) + } + }) + .collect::>(); + + // Let these threads to run by releasing the write lock. + std::mem::drop(write_lock); + + handles.into_iter().for_each(|h| h.join().expect("Failed")); + + // Ensure there are no missing or duplicate keys in the iteration. + let key_set = cache.iter().map(|ent| *ent.key()).collect::>(); + assert_eq!(key_set.len(), NUM_KEYS); + } + + #[test] + fn test_debug_format() { + let cache = Cache::new(10); + cache.insert('a', "alice"); + cache.insert('b', "bob"); + cache.insert('c', "cindy"); + + let debug_str = format!("{:?}", cache); + assert!(debug_str.starts_with('{')); + assert!(debug_str.contains(r#"'a': "alice""#)); + assert!(debug_str.contains(r#"'b': "bob""#)); + assert!(debug_str.contains(r#"'c': "cindy""#)); + assert!(debug_str.ends_with('}')); + } +} diff --git a/crates/mini-moka-vendored/src/sync/iter.rs b/crates/mini-moka-vendored/src/sync/iter.rs new file mode 100644 index 00000000..98098193 --- /dev/null +++ b/crates/mini-moka-vendored/src/sync/iter.rs @@ -0,0 +1,64 @@ +use super::{base_cache::BaseCache, mapref::EntryRef}; +use crate::common::concurrent::ValueEntry; + +use std::{ + hash::{BuildHasher, Hash}, + sync::Arc, +}; +use triomphe::Arc as TrioArc; + +pub(crate) type DashMapIter<'a, K, V, S> = + dashmap::iter::Iter<'a, Arc, TrioArc>, S>; + +pub struct Iter<'a, K, V, S> { + cache: &'a BaseCache, + map_iter: DashMapIter<'a, K, V, S>, +} + +impl<'a, K, V, S> Iter<'a, K, V, S> { + pub(crate) fn new(cache: &'a BaseCache, map_iter: DashMapIter<'a, K, V, S>) -> Self { + Self { cache, map_iter } + } +} + +impl<'a, K, V, S> Iterator for Iter<'a, K, V, S> +where + K: Eq + Hash, + S: BuildHasher + Clone, +{ + type Item = EntryRef<'a, K, V>; + + fn next(&mut self) -> Option { + for map_ref in &mut self.map_iter { + if !self.cache.is_expired_entry(map_ref.value()) { + return Some(EntryRef::new(map_ref)); + } + } + + None + } +} + +// Clippy beta 0.1.83 (f41c7ed9889 2024-10-31) warns about unused lifetimes on 'a. +// This seems a false positive. The lifetimes are used in the trait bounds. +// https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes +#[allow(clippy::extra_unused_lifetimes)] +unsafe impl<'a, K, V, S> Send for Iter<'_, K, V, S> +where + K: 'a + Eq + Hash + Send, + V: 'a + Send, + S: 'a + BuildHasher + Clone, +{ +} + +// Clippy beta 0.1.83 (f41c7ed9889 2024-10-31) warns about unused lifetimes on 'a. +// This seems a false positive. The lifetimes are used in the trait bounds. +// https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes +#[allow(clippy::extra_unused_lifetimes)] +unsafe impl<'a, K, V, S> Sync for Iter<'_, K, V, S> +where + K: 'a + Eq + Hash + Sync, + V: 'a + Sync, + S: 'a + BuildHasher + Clone, +{ +} diff --git a/crates/mini-moka-vendored/src/sync/mapref.rs b/crates/mini-moka-vendored/src/sync/mapref.rs new file mode 100644 index 00000000..c59c13f6 --- /dev/null +++ b/crates/mini-moka-vendored/src/sync/mapref.rs @@ -0,0 +1,48 @@ +use crate::common::concurrent::ValueEntry; + +use std::{hash::Hash, sync::Arc}; +use triomphe::Arc as TrioArc; + +type DashMapRef<'a, K, V> = + dashmap::mapref::multiple::RefMulti<'a, Arc, TrioArc>>; + +pub struct EntryRef<'a, K, V>(DashMapRef<'a, K, V>); + +unsafe impl Sync for EntryRef<'_, K, V> +where + K: Eq + Hash + Send + Sync, + V: Send + Sync, +{ +} + +impl<'a, K, V> EntryRef<'a, K, V> +where + K: Eq + Hash, +{ + pub(crate) fn new(map_ref: DashMapRef<'a, K, V>) -> Self { + Self(map_ref) + } + + pub fn key(&self) -> &K { + self.0.key() + } + + pub fn value(&self) -> &V { + &self.0.value().value + } + + pub fn pair(&self) -> (&K, &V) { + (self.key(), self.value()) + } +} + +impl std::ops::Deref for EntryRef<'_, K, V> +where + K: Eq + Hash, +{ + type Target = V; + + fn deref(&self) -> &V { + self.value() + } +} diff --git a/crates/mini-moka-vendored/src/unsync.rs b/crates/mini-moka-vendored/src/unsync.rs new file mode 100644 index 00000000..84e18a0c --- /dev/null +++ b/crates/mini-moka-vendored/src/unsync.rs @@ -0,0 +1,202 @@ +//! Provides a *not* thread-safe cache implementation built upon +//! [`std::collections::HashMap`][std-hashmap]. +//! +//! [std-hashmap]: https://doc.rust-lang.org/std/collections/struct.HashMap.html + +mod builder; +mod cache; +mod deques; +mod iter; + +use std::{ptr::NonNull, rc::Rc}; +use tagptr::TagNonNull; + +pub use builder::CacheBuilder; +pub use cache::Cache; +pub use iter::Iter; + +use crate::common::{deque::DeqNode, time::Instant}; + +pub(crate) type Weigher = Box u32>; + +pub(crate) trait AccessTime { + fn last_accessed(&self) -> Option; + fn set_last_accessed(&mut self, timestamp: Instant); + fn last_modified(&self) -> Option; + fn set_last_modified(&mut self, timestamp: Instant); +} + +pub(crate) struct KeyDate { + pub(crate) key: Rc, + pub(crate) timestamp: Option, +} + +impl KeyDate { + pub(crate) fn new(key: Rc, timestamp: Option) -> Self { + Self { key, timestamp } + } +} + +pub(crate) struct KeyHashDate { + pub(crate) key: Rc, + pub(crate) hash: u64, + pub(crate) timestamp: Option, +} + +impl KeyHashDate { + pub(crate) fn new(key: Rc, hash: u64, timestamp: Option) -> Self { + Self { + key, + hash, + timestamp, + } + } +} + +// DeqNode for an access order queue. +type KeyDeqNodeAo = TagNonNull>, 2>; + +// DeqNode for the write order queue. +type KeyDeqNodeWo = NonNull>>; + +struct EntryInfo { + access_order_q_node: Option>, + write_order_q_node: Option>, + policy_weight: u32, +} + +pub(crate) struct ValueEntry { + pub(crate) value: V, + info: EntryInfo, +} + +impl ValueEntry { + pub(crate) fn new(value: V, policy_weight: u32) -> Self { + Self { + value, + info: EntryInfo { + access_order_q_node: None, + write_order_q_node: None, + policy_weight, + }, + } + } + + #[inline] + pub(crate) fn replace_deq_nodes_with(&mut self, mut other: Self) { + self.info.access_order_q_node = other.info.access_order_q_node.take(); + self.info.write_order_q_node = other.info.write_order_q_node.take(); + } + + #[inline] + pub(crate) fn access_order_q_node(&self) -> Option> { + self.info.access_order_q_node + } + + #[inline] + pub(crate) fn set_access_order_q_node(&mut self, node: Option>) { + self.info.access_order_q_node = node; + } + + #[inline] + pub(crate) fn take_access_order_q_node(&mut self) -> Option> { + self.info.access_order_q_node.take() + } + + #[inline] + pub(crate) fn write_order_q_node(&self) -> Option> { + self.info.write_order_q_node + } + + #[inline] + pub(crate) fn set_write_order_q_node(&mut self, node: Option>) { + self.info.write_order_q_node = node; + } + + #[inline] + pub(crate) fn take_write_order_q_node(&mut self) -> Option> { + self.info.write_order_q_node.take() + } + + #[inline] + pub(crate) fn policy_weight(&self) -> u32 { + self.info.policy_weight + } + + #[inline] + pub(crate) fn set_policy_weight(&mut self, policy_weight: u32) { + self.info.policy_weight = policy_weight; + } +} + +impl AccessTime for ValueEntry { + #[inline] + fn last_accessed(&self) -> Option { + self.access_order_q_node() + .and_then(|node| unsafe { node.as_ref() }.element.timestamp) + } + + #[inline] + fn set_last_accessed(&mut self, timestamp: Instant) { + if let Some(mut node) = self.info.access_order_q_node { + unsafe { node.as_mut() }.set_last_accessed(timestamp); + } + } + + #[inline] + fn last_modified(&self) -> Option { + self.write_order_q_node() + .and_then(|node| unsafe { node.as_ref() }.element.timestamp) + } + + #[inline] + fn set_last_modified(&mut self, timestamp: Instant) { + if let Some(mut node) = self.info.write_order_q_node { + unsafe { node.as_mut() }.set_last_modified(timestamp); + } + } +} + +impl AccessTime for DeqNode> { + #[inline] + fn last_accessed(&self) -> Option { + None + } + + #[inline] + fn set_last_accessed(&mut self, _timestamp: Instant) { + unreachable!(); + } + + #[inline] + fn last_modified(&self) -> Option { + self.element.timestamp + } + + #[inline] + fn set_last_modified(&mut self, timestamp: Instant) { + self.element.timestamp = Some(timestamp); + } +} + +impl AccessTime for DeqNode> { + #[inline] + fn last_accessed(&self) -> Option { + self.element.timestamp + } + + #[inline] + fn set_last_accessed(&mut self, timestamp: Instant) { + self.element.timestamp = Some(timestamp); + } + + #[inline] + fn last_modified(&self) -> Option { + None + } + + #[inline] + fn set_last_modified(&mut self, _timestamp: Instant) { + unreachable!(); + } +} diff --git a/crates/mini-moka-vendored/src/unsync/builder.rs b/crates/mini-moka-vendored/src/unsync/builder.rs new file mode 100644 index 00000000..fe391d43 --- /dev/null +++ b/crates/mini-moka-vendored/src/unsync/builder.rs @@ -0,0 +1,243 @@ +use super::{Cache, Weigher}; +use crate::common::builder_utils; + +use std::{ + collections::hash_map::RandomState, + hash::{BuildHasher, Hash}, + marker::PhantomData, + time::Duration, +}; + +/// Builds a [`Cache`][cache-struct] with various configuration knobs. +/// +/// [cache-struct]: ./struct.Cache.html +/// +/// # Examples +/// +/// ```rust +/// use mini_moka::unsync::Cache; +/// use std::time::Duration; +/// +/// let mut cache = Cache::builder() +/// // Max 10,000 elements +/// .max_capacity(10_000) +/// // Time to live (TTL): 30 minutes +/// .time_to_live(Duration::from_secs(30 * 60)) +/// // Time to idle (TTI): 5 minutes +/// .time_to_idle(Duration::from_secs( 5 * 60)) +/// // Create the cache. +/// .build(); +/// +/// // This entry will expire after 5 minutes (TTI) if there is no get(). +/// cache.insert(0, "zero"); +/// +/// // This get() will extend the entry life for another 5 minutes. +/// cache.get(&0); +/// +/// // Even though we keep calling get(), the entry will expire +/// // after 30 minutes (TTL) from the insert(). +/// ``` +/// +#[must_use] +pub struct CacheBuilder { + max_capacity: Option, + initial_capacity: Option, + weigher: Option>, + time_to_live: Option, + time_to_idle: Option, + cache_type: PhantomData, +} + +impl Default for CacheBuilder> +where + K: Eq + Hash, +{ + fn default() -> Self { + Self { + max_capacity: None, + initial_capacity: None, + weigher: None, + time_to_live: None, + time_to_idle: None, + cache_type: Default::default(), + } + } +} + +impl CacheBuilder> +where + K: Eq + Hash, +{ + /// Construct a new `CacheBuilder` that will be used to build a `Cache` holding + /// up to `max_capacity` entries. + pub fn new(max_capacity: u64) -> Self { + Self { + max_capacity: Some(max_capacity), + ..Default::default() + } + } + + /// Builds a `Cache`. + /// + /// # Panics + /// + /// Panics if configured with either `time_to_live` or `time_to_idle` higher than + /// 1000 years. This is done to protect against overflow when computing key + /// expiration. + pub fn build(self) -> Cache { + let build_hasher = RandomState::default(); + builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); + Cache::with_everything( + self.max_capacity, + self.initial_capacity, + build_hasher, + self.weigher, + self.time_to_live, + self.time_to_idle, + ) + } + + /// Builds a `Cache`, with the given `hasher`. + /// + /// # Panics + /// + /// Panics if configured with either `time_to_live` or `time_to_idle` higher than + /// 1000 years. This is done to protect against overflow when computing key + /// expiration. + pub fn build_with_hasher(self, hasher: S) -> Cache + where + S: BuildHasher + Clone, + { + builder_utils::ensure_expirations_or_panic(self.time_to_live, self.time_to_idle); + Cache::with_everything( + self.max_capacity, + self.initial_capacity, + hasher, + self.weigher, + self.time_to_live, + self.time_to_idle, + ) + } +} + +impl CacheBuilder { + /// Sets the max capacity of the cache. + pub fn max_capacity(self, max_capacity: u64) -> Self { + Self { + max_capacity: Some(max_capacity), + ..self + } + } + + /// Sets the initial capacity (number of entries) of the cache. + pub fn initial_capacity(self, number_of_entries: usize) -> Self { + Self { + initial_capacity: Some(number_of_entries), + ..self + } + } + + /// Sets the weigher closure of the cache. + /// + /// The closure should take `&K` and `&V` as the arguments and returns a `u32` + /// representing the relative size of the entry. + pub fn weigher(self, weigher: impl FnMut(&K, &V) -> u32 + 'static) -> Self { + Self { + weigher: Some(Box::new(weigher)), + ..self + } + } + + /// Sets the time to live of the cache. + /// + /// A cached entry will be expired after the specified duration past from + /// `insert`. + /// + /// # Panics + /// + /// `CacheBuilder::build*` methods will panic if the given `duration` is longer + /// than 1000 years. This is done to protect against overflow when computing key + /// expiration. + pub fn time_to_live(self, duration: Duration) -> Self { + Self { + time_to_live: Some(duration), + ..self + } + } + + /// Sets the time to idle of the cache. + /// + /// A cached entry will be expired after the specified duration past from `get` + /// or `insert`. + /// + /// # Panics + /// + /// `CacheBuilder::build*` methods will panic if the given `duration` is longer + /// than 1000 years. This is done to protect against overflow when computing key + /// expiration. + pub fn time_to_idle(self, duration: Duration) -> Self { + Self { + time_to_idle: Some(duration), + ..self + } + } +} + +#[cfg(test)] +mod tests { + use super::CacheBuilder; + use std::time::Duration; + use wasm_bindgen_test::wasm_bindgen_test; + + #[test] + #[wasm_bindgen_test] + fn build_cache() { + // Cache + let mut cache = CacheBuilder::new(100).build(); + let policy = cache.policy(); + + assert_eq!(policy.max_capacity(), Some(100)); + assert_eq!(policy.time_to_live(), None); + assert_eq!(policy.time_to_idle(), None); + + cache.insert('a', "Alice"); + assert_eq!(cache.get(&'a'), Some(&"Alice")); + + let mut cache = CacheBuilder::new(100) + .time_to_live(Duration::from_secs(45 * 60)) + .time_to_idle(Duration::from_secs(15 * 60)) + .build(); + let policy = cache.policy(); + + assert_eq!(policy.max_capacity(), Some(100)); + assert_eq!(policy.time_to_live(), Some(Duration::from_secs(45 * 60))); + assert_eq!(policy.time_to_idle(), Some(Duration::from_secs(15 * 60))); + + cache.insert('a', "Alice"); + assert_eq!(cache.get(&'a'), Some(&"Alice")); + } + + #[test] + #[wasm_bindgen_test] + #[should_panic(expected = "time_to_live is longer than 1000 years")] + fn build_cache_too_long_ttl() { + let thousand_years_secs: u64 = 1000 * 365 * 24 * 3600; + let builder: CacheBuilder = CacheBuilder::new(100); + let duration = Duration::from_secs(thousand_years_secs); + builder + .time_to_live(duration + Duration::from_secs(1)) + .build(); + } + + #[test] + #[wasm_bindgen_test] + #[should_panic(expected = "time_to_idle is longer than 1000 years")] + fn build_cache_too_long_tti() { + let thousand_years_secs: u64 = 1000 * 365 * 24 * 3600; + let builder: CacheBuilder = CacheBuilder::new(100); + let duration = Duration::from_secs(thousand_years_secs); + builder + .time_to_idle(duration + Duration::from_secs(1)) + .build(); + } +} diff --git a/crates/mini-moka-vendored/src/unsync/cache.rs b/crates/mini-moka-vendored/src/unsync/cache.rs new file mode 100644 index 00000000..e22102ce --- /dev/null +++ b/crates/mini-moka-vendored/src/unsync/cache.rs @@ -0,0 +1,1468 @@ +use super::{ + deques::Deques, AccessTime, CacheBuilder, Iter, KeyDate, KeyHashDate, ValueEntry, Weigher, +}; +use crate::{ + common::{ + self, + deque::{DeqNode, Deque}, + frequency_sketch::FrequencySketch, + time::{CheckedTimeOps, Clock, Instant}, + CacheRegion, + }, + Policy, +}; + +use smallvec::SmallVec; +use std::{ + borrow::Borrow, + collections::{hash_map::RandomState, HashMap}, + fmt, + hash::{BuildHasher, Hash}, + ptr::NonNull, + rc::Rc, + time::Duration, +}; + +const EVICTION_BATCH_SIZE: usize = 100; + +type CacheStore = std::collections::HashMap, ValueEntry, S>; + +/// An in-memory cache that is _not_ thread-safe. +/// +/// `Cache` utilizes a hash table [`std::collections::HashMap`][std-hashmap] from the +/// standard library for the central key-value storage. `Cache` performs a +/// best-effort bounding of the map using an entry replacement algorithm to determine +/// which entries to evict when the capacity is exceeded. +/// +/// [std-hashmap]: https://doc.rust-lang.org/std/collections/struct.HashMap.html +/// +/// # Characteristic difference between `unsync` and `sync`/`future` caches +/// +/// If you use a cache from a single thread application, `unsync::Cache` may +/// outperform other caches for updates and retrievals because other caches have some +/// overhead on syncing internal data structures between threads. +/// +/// However, other caches may outperform `unsync::Cache` on the same operations when +/// expiration polices are configured on a multi-core system. `unsync::Cache` evicts +/// expired entries as a part of update and retrieval operations while others evict +/// them using a dedicated background thread. +/// +/// # Examples +/// +/// Cache entries are manually added using the insert method, and are stored in the +/// cache until either evicted or manually invalidated. +/// +/// Here's an example of reading and updating a cache by using the main thread: +/// +///```rust +/// use mini_moka::unsync::Cache; +/// +/// const NUM_KEYS: usize = 64; +/// +/// fn value(n: usize) -> String { +/// format!("value {}", n) +/// } +/// +/// // Create a cache that can store up to 10,000 entries. +/// let mut cache = Cache::new(10_000); +/// +/// // Insert 64 entries. +/// for key in 0..NUM_KEYS { +/// cache.insert(key, value(key)); +/// } +/// +/// // Invalidate every 4 element of the inserted entries. +/// for key in (0..NUM_KEYS).step_by(4) { +/// cache.invalidate(&key); +/// } +/// +/// // Verify the result. +/// for key in 0..NUM_KEYS { +/// if key % 4 == 0 { +/// assert_eq!(cache.get(&key), None); +/// } else { +/// assert_eq!(cache.get(&key), Some(&value(key))); +/// } +/// } +/// ``` +/// +/// # Size-based Eviction +/// +/// ```rust +/// use std::convert::TryInto; +/// use mini_moka::unsync::Cache; +/// +/// // Evict based on the number of entries in the cache. +/// let mut cache = Cache::builder() +/// // Up to 10,000 entries. +/// .max_capacity(10_000) +/// // Create the cache. +/// .build(); +/// cache.insert(1, "one".to_string()); +/// +/// // Evict based on the byte length of strings in the cache. +/// let mut cache = Cache::builder() +/// // A weigher closure takes &K and &V and returns a u32 +/// // representing the relative size of the entry. +/// .weigher(|_key, value: &String| -> u32 { +/// value.len().try_into().unwrap_or(u32::MAX) +/// }) +/// // This cache will hold up to 32MiB of values. +/// .max_capacity(32 * 1024 * 1024) +/// .build(); +/// cache.insert(2, "two".to_string()); +/// ``` +/// +/// If your cache should not grow beyond a certain size, use the `max_capacity` +/// method of the [`CacheBuilder`][builder-struct] to set the upper bound. The cache +/// will try to evict entries that have not been used recently or very often. +/// +/// At the cache creation time, a weigher closure can be set by the `weigher` method +/// of the `CacheBuilder`. A weigher closure takes `&K` and `&V` as the arguments and +/// returns a `u32` representing the relative size of the entry: +/// +/// - If the `weigher` is _not_ set, the cache will treat each entry has the same +/// size of `1`. This means the cache will be bounded by the number of entries. +/// - If the `weigher` is set, the cache will call the weigher to calculate the +/// weighted size (relative size) on an entry. This means the cache will be bounded +/// by the total weighted size of entries. +/// +/// Note that weighted sizes are not used when making eviction selections. +/// +/// [builder-struct]: ./struct.CacheBuilder.html +/// +/// # Time-based Expirations +/// +/// `Cache` supports the following expiration policies: +/// +/// - **Time to live**: A cached entry will be expired after the specified duration +/// past from `insert`. +/// - **Time to idle**: A cached entry will be expired after the specified duration +/// past from `get` or `insert`. +/// +/// See the [`CacheBuilder`][builder-struct]'s doc for how to configure a cache +/// with them. +/// +/// [builder-struct]: ./struct.CacheBuilder.html +/// +/// # Hashing Algorithm +/// +/// By default, `Cache` uses a hashing algorithm selected to provide resistance +/// against HashDoS attacks. It will the same one used by +/// `std::collections::HashMap`, which is currently SipHash 1-3. +/// +/// While SipHash's performance is very competitive for medium sized keys, other +/// hashing algorithms will outperform it for small keys such as integers as well as +/// large keys such as long strings. However those algorithms will typically not +/// protect against attacks such as HashDoS. +/// +/// The hashing algorithm can be replaced on a per-`Cache` basis using the +/// [`build_with_hasher`][build-with-hasher-method] method of the +/// `CacheBuilder`. Many alternative algorithms are available on crates.io, such +/// as the [aHash][ahash-crate] crate. +/// +/// [build-with-hasher-method]: ./struct.CacheBuilder.html#method.build_with_hasher +/// [ahash-crate]: https://crates.io/crates/ahash +/// +pub struct Cache { + max_capacity: Option, + entry_count: u64, + weighted_size: u64, + cache: CacheStore, + build_hasher: S, + weigher: Option>, + deques: Deques, + frequency_sketch: FrequencySketch, + frequency_sketch_enabled: bool, + time_to_live: Option, + time_to_idle: Option, + expiration_clock: Option, +} + +impl fmt::Debug for Cache +where + K: fmt::Debug + Eq + Hash, + V: fmt::Debug, + // TODO: Remove these bounds from S. + S: BuildHasher + Clone, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d_map = f.debug_map(); + + for (k, v) in self.iter() { + d_map.entry(&k, &v); + } + + d_map.finish() + } +} + +impl Cache +where + K: Hash + Eq, +{ + /// Constructs a new `Cache` that will store up to the `max_capacity` entries. + /// + /// To adjust various configuration knobs such as `initial_capacity` or + /// `time_to_live`, use the [`CacheBuilder`][builder-struct]. + /// + /// [builder-struct]: ./struct.CacheBuilder.html + pub fn new(max_capacity: u64) -> Self { + let build_hasher = RandomState::default(); + Self::with_everything(Some(max_capacity), None, build_hasher, None, None, None) + } + + /// Returns a [`CacheBuilder`][builder-struct], which can builds a `Cache` with + /// various configuration knobs. + /// + /// [builder-struct]: ./struct.CacheBuilder.html + pub fn builder() -> CacheBuilder> { + CacheBuilder::default() + } +} + +// +// public +// +impl Cache { + /// Returns a read-only cache policy of this cache. + /// + /// At this time, cache policy cannot be modified after cache creation. + /// A future version may support to modify it. + pub fn policy(&self) -> Policy { + Policy::new(self.max_capacity, self.time_to_live, self.time_to_idle) + } + + /// Returns the number of entries in this cache. + /// + /// # Example + /// + /// ```rust + /// use mini_moka::unsync::Cache; + /// + /// let mut cache = Cache::new(10); + /// cache.insert('n', "Netherland Dwarf"); + /// cache.insert('l', "Lop Eared"); + /// cache.insert('d', "Dutch"); + /// + /// // Ensure an entry exists. + /// assert!(cache.contains_key(&'n')); + /// + /// // Followings will print the actual numbers. + /// println!("{}", cache.entry_count()); // -> 3 + /// println!("{}", cache.weighted_size()); // -> 3 + /// ``` + /// + pub fn entry_count(&self) -> u64 { + self.entry_count + } + + /// Returns the total weighted size of entries in this cache. + /// + /// See [`entry_count`](#method.entry_count) for a sample code. + pub fn weighted_size(&self) -> u64 { + self.weighted_size + } +} + +impl Cache +where + K: Hash + Eq, + S: BuildHasher + Clone, +{ + pub(crate) fn with_everything( + max_capacity: Option, + initial_capacity: Option, + build_hasher: S, + weigher: Option>, + time_to_live: Option, + time_to_idle: Option, + ) -> Self { + let cache = HashMap::with_capacity_and_hasher( + initial_capacity.unwrap_or_default(), + build_hasher.clone(), + ); + + Self { + max_capacity, + entry_count: 0, + weighted_size: 0, + cache, + build_hasher, + weigher, + deques: Default::default(), + frequency_sketch: Default::default(), + frequency_sketch_enabled: false, + time_to_live, + time_to_idle, + expiration_clock: None, + } + } + + /// Returns `true` if the cache contains a value for the key. + /// + /// Unlike the `get` method, this method is not considered a cache read operation, + /// so it does not update the historic popularity estimator or reset the idle + /// timer for the key. + /// + /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq` + /// on the borrowed form _must_ match those for the key type. + pub fn contains_key(&mut self, key: &Q) -> bool + where + Rc: Borrow, + Q: Hash + Eq + ?Sized, + { + let timestamp = self.evict_expired_if_needed(); + self.evict_lru_entries(); + + match (self.cache.get(key), timestamp) { + // Value not found. + (None, _) => false, + // Value found, no expiry. + (Some(_), None) => true, + // Value found, check if expired. + (Some(entry), Some(ts)) => { + !Self::is_expired_entry_wo(&self.time_to_live, entry, ts) + && !Self::is_expired_entry_ao(&self.time_to_idle, entry, ts) + } + } + } + + /// Returns an immutable reference of the value corresponding to the key. + /// + /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq` + /// on the borrowed form _must_ match those for the key type. + pub fn get(&mut self, key: &Q) -> Option<&V> + where + Rc: Borrow, + Q: Hash + Eq + ?Sized, + { + let timestamp = self.evict_expired_if_needed(); + self.evict_lru_entries(); + self.frequency_sketch.increment(self.hash(key)); + + match (self.cache.get_mut(key), timestamp, &mut self.deques) { + // Value not found. + (None, _, _) => None, + // Value found, no expiry. + (Some(entry), None, deqs) => { + Self::record_hit(deqs, entry, None); + Some(&entry.value) + } + // Value found, check if expired. + (Some(entry), Some(ts), deqs) => { + if Self::is_expired_entry_wo(&self.time_to_live, entry, ts) + || Self::is_expired_entry_ao(&self.time_to_idle, entry, ts) + { + None + } else { + Self::record_hit(deqs, entry, timestamp); + Some(&entry.value) + } + } + } + } + + pub(crate) fn is_expired_entry(&self, entry: &ValueEntry) -> bool { + let now = self.current_time_from_expiration_clock(); + Self::is_expired_entry_wo(&self.time_to_live, entry, now) + || Self::is_expired_entry_ao(&self.time_to_idle, entry, now) + } + + /// Inserts a key-value pair into the cache. + /// + /// If the cache has this key present, the value is updated. + pub fn insert(&mut self, key: K, value: V) { + let timestamp = self.evict_expired_if_needed(); + self.evict_lru_entries(); + let policy_weight = weigh(&mut self.weigher, &key, &value); + let key = Rc::new(key); + let entry = ValueEntry::new(value, policy_weight); + + if let Some(old_entry) = self.cache.insert(Rc::clone(&key), entry) { + self.handle_update(key, timestamp, policy_weight, old_entry); + } else { + let hash = self.hash(&key); + self.handle_insert(key, hash, policy_weight, timestamp); + } + } + + /// Discards any cached value for the key. + /// + /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq` + /// on the borrowed form _must_ match those for the key type. + pub fn invalidate(&mut self, key: &Q) + where + Rc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.evict_expired_if_needed(); + self.evict_lru_entries(); + + if let Some(mut entry) = self.cache.remove(key) { + let weight = entry.policy_weight(); + self.deques.unlink_ao(&mut entry); + Deques::unlink_wo(&mut self.deques.write_order, &mut entry); + self.saturating_sub_from_total_weight(weight as u64); + } + } + + /// Discards any cached value for the key, returning the cached value. + /// + /// The key may be any borrowed form of the cache's key type, but `Hash` and `Eq` + /// on the borrowed form _must_ match those for the key type. + pub fn remove(&mut self, key: &Q) -> Option + where + Rc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.evict_expired_if_needed(); + self.evict_lru_entries(); + + if let Some(mut entry) = self.cache.remove(key) { + let weight = entry.policy_weight(); + self.deques.unlink_ao(&mut entry); + crate::unsync::deques::Deques::unlink_wo(&mut self.deques.write_order, &mut entry); + self.saturating_sub_from_total_weight(weight as u64); + Some(entry.value) + } else { + None + } + } + + /// Discards all cached values. + /// + /// Like the `invalidate` method, this method does not clear the historic + /// popularity estimator of keys so that it retains the client activities of + /// trying to retrieve an item. + pub fn invalidate_all(&mut self) { + self.cache.clear(); + self.deques.clear(); + self.weighted_size = 0; + } + + /// Discards cached values that satisfy a predicate. + /// + /// `invalidate_entries_if` takes a closure that returns `true` or `false`. + /// `invalidate_entries_if` will apply the closure to each cached value, + /// and if the closure returns `true`, the value will be invalidated. + /// + /// Like the `invalidate` method, this method does not clear the historic + /// popularity estimator of keys so that it retains the client activities of + /// trying to retrieve an item. + // ----------------------------------------------------------------------- + // (The followings are not doc comments) + // We need this #[allow(...)] to avoid a false Clippy warning about needless + // collect to create keys_to_invalidate. + // clippy 0.1.52 (9a1dfd2dc5c 2021-04-30) in Rust 1.52.0-beta.7 + #[allow(clippy::needless_collect)] + pub fn invalidate_entries_if(&mut self, mut predicate: impl FnMut(&K, &V) -> bool) { + let Self { cache, deques, .. } = self; + + // Since we can't do cache.iter() and cache.remove() at the same time, + // invalidation needs to run in two steps: + // 1. Examine all entries in this cache and collect keys to invalidate. + // 2. Remove entries for the keys. + + let keys_to_invalidate = cache + .iter() + .filter(|(key, entry)| (predicate)(key, &entry.value)) + .map(|(key, _)| Rc::clone(key)) + .collect::>(); + + let mut invalidated = 0u64; + + keys_to_invalidate.into_iter().for_each(|k| { + if let Some(mut entry) = cache.remove(&k) { + let weight = entry.policy_weight(); + deques.unlink_ao(&mut entry); + Deques::unlink_wo(&mut deques.write_order, &mut entry); + invalidated = invalidated.saturating_sub(weight as u64); + } + }); + self.saturating_sub_from_total_weight(invalidated); + } + + /// Creates an iterator visiting all key-value pairs in arbitrary order. The + /// iterator element type is `(&K, &V)`. + /// + /// Unlike the `get` method, visiting entries via an iterator do not update the + /// historic popularity estimator or reset idle timers for keys. + /// + /// # Examples + /// + /// ```rust + /// use mini_moka::unsync::Cache; + /// + /// let mut cache = Cache::new(100); + /// cache.insert("Julia", 14); + /// + /// let mut iter = cache.iter(); + /// let (k, v) = iter.next().unwrap(); // (&K, &V) + /// assert_eq!(k, &"Julia"); + /// assert_eq!(v, &14); + /// + /// assert!(iter.next().is_none()); + /// ``` + /// + pub fn iter(&self) -> Iter<'_, K, V, S> { + Iter::new(self, self.cache.iter()) + } +} + +// +// private +// +impl Cache +where + K: Hash + Eq, + S: BuildHasher + Clone, +{ + #[inline] + fn hash(&self, key: &Q) -> u64 + where + Rc: Borrow, + Q: Hash + Eq + ?Sized, + { + self.build_hasher.hash_one(key) + } + + #[inline] + fn has_expiry(&self) -> bool { + self.time_to_live.is_some() || self.time_to_idle.is_some() + } + + #[inline] + fn evict_expired_if_needed(&mut self) -> Option { + if self.has_expiry() { + let ts = self.current_time_from_expiration_clock(); + self.evict_expired(ts); + Some(ts) + } else { + None + } + } + + #[inline] + fn current_time_from_expiration_clock(&self) -> Instant { + if let Some(clock) = &self.expiration_clock { + Instant::new(clock.now()) + } else { + Instant::now() + } + } + + #[inline] + fn is_expired_entry_ao( + time_to_idle: &Option, + entry: &impl AccessTime, + now: Instant, + ) -> bool { + if let (Some(ts), Some(tti)) = (entry.last_accessed(), time_to_idle) { + let checked_add = ts.checked_add(*tti); + if checked_add.is_none() { + panic!("ttl overflow") + } + return checked_add.unwrap() <= now; + } + false + } + + #[inline] + fn is_expired_entry_wo( + time_to_live: &Option, + entry: &impl AccessTime, + now: Instant, + ) -> bool { + if let (Some(ts), Some(ttl)) = (entry.last_modified(), time_to_live) { + let checked_add = ts.checked_add(*ttl); + if checked_add.is_none() { + panic!("ttl overflow") + } + return checked_add.unwrap() <= now; + } + false + } + + fn record_hit(deques: &mut Deques, entry: &mut ValueEntry, ts: Option) { + if let Some(ts) = ts { + entry.set_last_accessed(ts); + } + deques.move_to_back_ao(entry) + } + + fn has_enough_capacity(&self, candidate_weight: u32, ws: u64) -> bool { + self.max_capacity + .map(|limit| ws + candidate_weight as u64 <= limit) + .unwrap_or(true) + } + + fn weights_to_evict(&self) -> u64 { + self.max_capacity + .map(|limit| self.weighted_size.saturating_sub(limit)) + .unwrap_or_default() + } + + #[inline] + fn should_enable_frequency_sketch(&self) -> bool { + if self.frequency_sketch_enabled { + false + } else if let Some(max_cap) = self.max_capacity { + self.weighted_size >= max_cap / 2 + } else { + false + } + } + + #[inline] + fn enable_frequency_sketch(&mut self) { + if let Some(max_cap) = self.max_capacity { + let cap = if self.weigher.is_none() { + max_cap + } else { + (self.entry_count as f64 * (self.weighted_size as f64 / max_cap as f64)) as u64 + }; + self.do_enable_frequency_sketch(cap); + } + } + + #[cfg(test)] + fn enable_frequency_sketch_for_testing(&mut self) { + if let Some(max_cap) = self.max_capacity { + self.do_enable_frequency_sketch(max_cap); + } + } + + #[inline] + fn do_enable_frequency_sketch(&mut self, cache_capacity: u64) { + let skt_capacity = common::sketch_capacity(cache_capacity); + self.frequency_sketch.ensure_capacity(skt_capacity); + self.frequency_sketch_enabled = true; + } + + fn saturating_add_to_total_weight(&mut self, weight: u64) { + let total = &mut self.weighted_size; + *total = total.saturating_add(weight); + } + + fn saturating_sub_from_total_weight(&mut self, weight: u64) { + let total = &mut self.weighted_size; + *total = total.saturating_sub(weight); + } + + #[inline] + fn handle_insert( + &mut self, + key: Rc, + hash: u64, + policy_weight: u32, + timestamp: Option, + ) { + let has_free_space = self.has_enough_capacity(policy_weight, self.weighted_size); + let (cache, deqs, freq) = (&mut self.cache, &mut self.deques, &self.frequency_sketch); + + if has_free_space { + // Add the candidate to the deque. + let key = Rc::clone(&key); + let entry = cache.get_mut(&key).unwrap(); + deqs.push_back_ao( + CacheRegion::MainProbation, + KeyHashDate::new(Rc::clone(&key), hash, timestamp), + entry, + ); + if self.time_to_live.is_some() { + deqs.push_back_wo(KeyDate::new(key, timestamp), entry); + } + self.entry_count += 1; + self.saturating_add_to_total_weight(policy_weight as u64); + + if self.should_enable_frequency_sketch() { + self.enable_frequency_sketch(); + } + + return; + } + + if let Some(max) = self.max_capacity { + if policy_weight as u64 > max { + // The candidate is too big to fit in the cache. Reject it. + cache.remove(&Rc::clone(&key)); + return; + } + } + + let mut candidate = EntrySizeAndFrequency::new(policy_weight as u64); + candidate.add_frequency(freq, hash); + + match Self::admit(&candidate, cache, deqs, freq, &mut self.weigher) { + AdmissionResult::Admitted { + victim_nodes, + victims_weight, + } => { + // Remove the victims from the cache (hash map) and deque. + for victim in victim_nodes { + // Remove the victim from the hash map. + let mut vic_entry = cache + .remove(unsafe { &victim.as_ref().element.key }) + .expect("Cannot remove a victim from the hash map"); + // And then remove the victim from the deques. + deqs.unlink_ao(&mut vic_entry); + Deques::unlink_wo(&mut deqs.write_order, &mut vic_entry); + self.entry_count -= 1; + } + + // Add the candidate to the deque. + let entry = cache.get_mut(&key).unwrap(); + let key = Rc::clone(&key); + deqs.push_back_ao( + CacheRegion::MainProbation, + KeyHashDate::new(Rc::clone(&key), hash, timestamp), + entry, + ); + if self.time_to_live.is_some() { + deqs.push_back_wo(KeyDate::new(key, timestamp), entry); + } + + self.entry_count += 1; + Self::saturating_sub_from_total_weight(self, victims_weight); + Self::saturating_add_to_total_weight(self, policy_weight as u64); + + if self.should_enable_frequency_sketch() { + self.enable_frequency_sketch(); + } + } + AdmissionResult::Rejected => { + // Remove the candidate from the cache. + cache.remove(&key); + } + } + } + + /// Performs size-aware admission explained in the paper: + /// [Lightweight Robust Size Aware Cache Management][size-aware-cache-paper] + /// by Gil Einziger, Ohad Eytan, Roy Friedman, Ben Manes. + /// + /// [size-aware-cache-paper]: https://arxiv.org/abs/2105.08770 + /// + /// There are some modifications in this implementation: + /// - To admit to the main space, candidate's frequency must be higher than + /// the aggregated frequencies of the potential victims. (In the paper, + /// `>=` operator is used rather than `>`) The `>` operator will do a better + /// job to prevent the main space from polluting. + /// - When a candidate is rejected, the potential victims will stay at the LRU + /// position of the probation access-order queue. (In the paper, they will be + /// promoted (to the MRU position?) to force the eviction policy to select a + /// different set of victims for the next candidate). We may implement the + /// paper's behavior later? + /// + #[inline] + fn admit( + candidate: &EntrySizeAndFrequency, + cache: &CacheStore, + deqs: &Deques, + freq: &FrequencySketch, + weigher: &mut Option>, + ) -> AdmissionResult { + let mut victims = EntrySizeAndFrequency::default(); + let mut victim_nodes = SmallVec::default(); + + // Get first potential victim at the LRU position. + let mut next_victim = deqs.probation.peek_front_ptr(); + + // Aggregate potential victims. + while victims.weight < candidate.weight { + if candidate.freq < victims.freq { + break; + } + if let Some(victim) = next_victim.take() { + next_victim = DeqNode::next_node_ptr(victim); + let vic_elem = &unsafe { victim.as_ref() }.element; + + let vic_entry = cache + .get(&vic_elem.key) + .expect("Cannot get an victim entry"); + victims.add_policy_weight(vic_elem.key.as_ref(), &vic_entry.value, weigher); + victims.add_frequency(freq, vic_elem.hash); + victim_nodes.push(victim); + } else { + // No more potential victims. + break; + } + } + + // Admit or reject the candidate. + + // TODO: Implement some randomness to mitigate hash DoS attack. + // See Caffeine's implementation. + + if victims.weight >= candidate.weight && candidate.freq > victims.freq { + AdmissionResult::Admitted { + victim_nodes, + victims_weight: victims.weight, + } + } else { + AdmissionResult::Rejected + } + } + + fn handle_update( + &mut self, + key: Rc, + timestamp: Option, + policy_weight: u32, + old_entry: ValueEntry, + ) { + let old_policy_weight = old_entry.policy_weight(); + + let entry = self.cache.get_mut(&key).unwrap(); + entry.replace_deq_nodes_with(old_entry); + if let Some(ts) = timestamp { + entry.set_last_accessed(ts); + entry.set_last_modified(ts); + } + entry.set_policy_weight(policy_weight); + + let deqs = &mut self.deques; + deqs.move_to_back_ao(entry); + if self.time_to_live.is_some() { + deqs.move_to_back_wo(entry); + } + + self.saturating_sub_from_total_weight(old_policy_weight as u64); + self.saturating_add_to_total_weight(policy_weight as u64); + } + + fn evict_expired(&mut self, now: Instant) { + if self.time_to_live.is_some() { + let (count, weight) = self.remove_expired_wo(EVICTION_BATCH_SIZE, now); + self.entry_count -= count; + self.saturating_sub_from_total_weight(weight); + } + + if self.time_to_idle.is_some() { + let deqs = &mut self.deques; + let (window, probation, protected, wo, cache, time_to_idle) = ( + &mut deqs.window, + &mut deqs.probation, + &mut deqs.protected, + &mut deqs.write_order, + &mut self.cache, + &self.time_to_idle, + ); + + let mut rm_expired_ao = |name, deq| { + Self::remove_expired_ao( + name, + deq, + wo, + cache, + time_to_idle, + EVICTION_BATCH_SIZE, + now, + ) + }; + + let (count1, weight1) = rm_expired_ao("window", window); + let (count2, weight2) = rm_expired_ao("probation", probation); + let (count3, weight3) = rm_expired_ao("protected", protected); + + self.entry_count -= count1 + count2 + count3; + self.saturating_sub_from_total_weight(weight1); + self.saturating_sub_from_total_weight(weight2); + self.saturating_sub_from_total_weight(weight3); + } + } + + // Returns (u64, u64) where (evicted_entry_count, evicted_policy_weight). + #[inline] + fn remove_expired_ao( + deq_name: &str, + deq: &mut Deque>, + write_order_deq: &mut Deque>, + cache: &mut CacheStore, + time_to_idle: &Option, + batch_size: usize, + now: Instant, + ) -> (u64, u64) { + let mut evicted_entry_count = 0u64; + let mut evicted_policy_weight = 0u64; + + for _ in 0..batch_size { + let key = deq + .peek_front() + .and_then(|node| { + if Self::is_expired_entry_ao(time_to_idle, node, now) { + Some(Some(Rc::clone(&node.element.key))) + } else { + None + } + }) + .unwrap_or_default(); + + if key.is_none() { + break; + } + + let key = key.unwrap(); + + if let Some(mut entry) = cache.remove(&key) { + let weight = entry.policy_weight(); + Deques::unlink_ao_from_deque(deq_name, deq, &mut entry); + Deques::unlink_wo(write_order_deq, &mut entry); + evicted_entry_count += 1; + evicted_policy_weight = evicted_policy_weight.saturating_add(weight as u64); + } else { + deq.pop_front(); + } + } + + (evicted_entry_count, evicted_policy_weight) + } + + // Returns (u64, u64) where (evicted_entry_count, evicted_policy_weight). + #[inline] + fn remove_expired_wo(&mut self, batch_size: usize, now: Instant) -> (u64, u64) { + let mut evicted_entry_count = 0u64; + let mut evicted_policy_weight = 0u64; + let time_to_live = &self.time_to_live; + + for _ in 0..batch_size { + let key = self + .deques + .write_order + .peek_front() + .and_then(|node| { + if Self::is_expired_entry_wo(time_to_live, node, now) { + Some(Some(Rc::clone(&node.element.key))) + } else { + None + } + }) + .unwrap_or_default(); + + if key.is_none() { + break; + } + + let key = key.unwrap(); + + if let Some(mut entry) = self.cache.remove(&key) { + let weight = entry.policy_weight(); + self.deques.unlink_ao(&mut entry); + Deques::unlink_wo(&mut self.deques.write_order, &mut entry); + evicted_entry_count += 1; + evicted_policy_weight = evicted_policy_weight.saturating_sub(weight as u64); + } else { + self.deques.write_order.pop_front(); + } + } + + (evicted_entry_count, evicted_policy_weight) + } + + #[inline] + fn evict_lru_entries(&mut self) { + const DEQ_NAME: &str = "probation"; + + let weights_to_evict = self.weights_to_evict(); + let mut evicted_count = 0u64; + let mut evicted_policy_weight = 0u64; + + { + let deqs = &mut self.deques; + let (probation, wo, cache) = + (&mut deqs.probation, &mut deqs.write_order, &mut self.cache); + + for _ in 0..EVICTION_BATCH_SIZE { + if evicted_policy_weight >= weights_to_evict { + break; + } + + // clippy::map_clone will give us a false positive warning here. + // Version: clippy 0.1.77 (f2048098a1c 2024-02-09) in Rust 1.77.0-beta.2 + #[allow(clippy::map_clone)] + let key = probation + .peek_front() + .map(|node| Rc::clone(&node.element.key)); + + if key.is_none() { + break; + } + let key = key.unwrap(); + + if let Some(mut entry) = cache.remove(&key) { + let weight = entry.policy_weight(); + Deques::unlink_ao_from_deque(DEQ_NAME, probation, &mut entry); + Deques::unlink_wo(wo, &mut entry); + evicted_count += 1; + evicted_policy_weight = evicted_policy_weight.saturating_add(weight as u64); + } else { + probation.pop_front(); + } + } + } + + self.entry_count -= evicted_count; + self.saturating_sub_from_total_weight(evicted_policy_weight); + } +} + +// +// for testing +// +#[cfg(test)] +impl Cache +where + K: Hash + Eq, + S: BuildHasher + Clone, +{ + fn set_expiration_clock(&mut self, clock: Option) { + self.expiration_clock = clock; + } +} + +#[derive(Default)] +struct EntrySizeAndFrequency { + weight: u64, + freq: u32, +} + +impl EntrySizeAndFrequency { + fn new(policy_weight: u64) -> Self { + Self { + weight: policy_weight, + ..Default::default() + } + } + + fn add_policy_weight(&mut self, key: &K, value: &V, weigher: &mut Option>) { + self.weight += weigh(weigher, key, value) as u64; + } + + fn add_frequency(&mut self, freq: &FrequencySketch, hash: u64) { + self.freq += freq.frequency(hash) as u32; + } +} + +// Access-Order Queue Node +type AoqNode = NonNull>>; + +enum AdmissionResult { + Admitted { + victim_nodes: SmallVec<[AoqNode; 8]>, + victims_weight: u64, + }, + Rejected, +} + +// +// private free-standing functions +// +#[inline] +fn weigh(weigher: &mut Option>, key: &K, value: &V) -> u32 { + weigher.as_mut().map(|w| w(key, value)).unwrap_or(1) +} + +// To see the debug prints, run test as `cargo test -- --nocapture` +#[cfg(test)] +mod tests { + use wasm_bindgen_test::wasm_bindgen_test; + + use super::Cache; + use crate::common::time::Clock; + + use std::time::Duration; + + #[test] + #[wasm_bindgen_test] + fn basic_single_thread() { + let mut cache = Cache::new(3); + cache.enable_frequency_sketch_for_testing(); + + cache.insert("a", "alice"); + cache.insert("b", "bob"); + assert_eq!(cache.get(&"a"), Some(&"alice")); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert_eq!(cache.get(&"b"), Some(&"bob")); + // counts: a -> 1, b -> 1 + + cache.insert("c", "cindy"); + assert_eq!(cache.get(&"c"), Some(&"cindy")); + assert!(cache.contains_key(&"c")); + // counts: a -> 1, b -> 1, c -> 1 + + assert!(cache.contains_key(&"a")); + assert_eq!(cache.get(&"a"), Some(&"alice")); + assert_eq!(cache.get(&"b"), Some(&"bob")); + assert!(cache.contains_key(&"b")); + // counts: a -> 2, b -> 2, c -> 1 + + // "d" should not be admitted because its frequency is too low. + cache.insert("d", "david"); // count: d -> 0 + assert_eq!(cache.get(&"d"), None); // d -> 1 + assert!(!cache.contains_key(&"d")); + + cache.insert("d", "david"); + assert!(!cache.contains_key(&"d")); + assert_eq!(cache.get(&"d"), None); // d -> 2 + + // "d" should be admitted and "c" should be evicted + // because d's frequency is higher than c's. + cache.insert("d", "dennis"); + assert_eq!(cache.get(&"a"), Some(&"alice")); + assert_eq!(cache.get(&"b"), Some(&"bob")); + assert_eq!(cache.get(&"c"), None); + assert_eq!(cache.get(&"d"), Some(&"dennis")); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert!(!cache.contains_key(&"c")); + assert!(cache.contains_key(&"d")); + + cache.invalidate(&"b"); + assert_eq!(cache.get(&"b"), None); + assert!(!cache.contains_key(&"b")); + } + + #[test] + #[wasm_bindgen_test] + fn size_aware_eviction() { + let weigher = |_k: &&str, v: &(&str, u32)| v.1; + + let alice = ("alice", 10); + let bob = ("bob", 15); + let bill = ("bill", 20); + let cindy = ("cindy", 5); + let david = ("david", 15); + let dennis = ("dennis", 15); + + let mut cache = Cache::builder().max_capacity(31).weigher(weigher).build(); + cache.enable_frequency_sketch_for_testing(); + + cache.insert("a", alice); + cache.insert("b", bob); + assert_eq!(cache.get(&"a"), Some(&alice)); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert_eq!(cache.get(&"b"), Some(&bob)); + // order (LRU -> MRU) and counts: a -> 1, b -> 1 + + cache.insert("c", cindy); + assert_eq!(cache.get(&"c"), Some(&cindy)); + assert!(cache.contains_key(&"c")); + // order and counts: a -> 1, b -> 1, c -> 1 + + assert!(cache.contains_key(&"a")); + assert_eq!(cache.get(&"a"), Some(&alice)); + assert_eq!(cache.get(&"b"), Some(&bob)); + assert!(cache.contains_key(&"b")); + // order and counts: c -> 1, a -> 2, b -> 2 + + // To enter "d" (weight: 15), it needs to evict "c" (w: 5) and "a" (w: 10). + // "d" must have higher count than 3, which is the aggregated count + // of "a" and "c". + cache.insert("d", david); // count: d -> 0 + assert_eq!(cache.get(&"d"), None); // d -> 1 + assert!(!cache.contains_key(&"d")); + + cache.insert("d", david); + assert!(!cache.contains_key(&"d")); + assert_eq!(cache.get(&"d"), None); // d -> 2 + + cache.insert("d", david); + assert_eq!(cache.get(&"d"), None); // d -> 3 + assert!(!cache.contains_key(&"d")); + + cache.insert("d", david); + assert!(!cache.contains_key(&"d")); + assert_eq!(cache.get(&"d"), None); // d -> 4 + + // Finally "d" should be admitted by evicting "c" and "a". + cache.insert("d", dennis); + assert_eq!(cache.get(&"a"), None); + assert_eq!(cache.get(&"b"), Some(&bob)); + assert_eq!(cache.get(&"c"), None); + assert_eq!(cache.get(&"d"), Some(&dennis)); + assert!(!cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert!(!cache.contains_key(&"c")); + assert!(cache.contains_key(&"d")); + + // Update "b" with "bill" (w: 15 -> 20). This should evict "d" (w: 15). + cache.insert("b", bill); + assert_eq!(cache.get(&"b"), Some(&bill)); + assert_eq!(cache.get(&"d"), None); + assert!(cache.contains_key(&"b")); + assert!(!cache.contains_key(&"d")); + + // Re-add "a" (w: 10) and update "b" with "bob" (w: 20 -> 15). + cache.insert("a", alice); + cache.insert("b", bob); + assert_eq!(cache.get(&"a"), Some(&alice)); + assert_eq!(cache.get(&"b"), Some(&bob)); + assert_eq!(cache.get(&"d"), None); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert!(!cache.contains_key(&"d")); + + // Verify the sizes. + assert_eq!(cache.entry_count(), 2); + assert_eq!(cache.weighted_size(), 25); + } + + #[test] + #[wasm_bindgen_test] + fn invalidate_all() { + let mut cache = Cache::new(100); + cache.enable_frequency_sketch_for_testing(); + + cache.insert("a", "alice"); + cache.insert("b", "bob"); + cache.insert("c", "cindy"); + assert_eq!(cache.get(&"a"), Some(&"alice")); + assert_eq!(cache.get(&"b"), Some(&"bob")); + assert_eq!(cache.get(&"c"), Some(&"cindy")); + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert!(cache.contains_key(&"c")); + + cache.invalidate_all(); + + cache.insert("d", "david"); + + assert!(cache.get(&"a").is_none()); + assert!(cache.get(&"b").is_none()); + assert!(cache.get(&"c").is_none()); + assert_eq!(cache.get(&"d"), Some(&"david")); + assert!(!cache.contains_key(&"a")); + assert!(!cache.contains_key(&"b")); + assert!(!cache.contains_key(&"c")); + assert!(cache.contains_key(&"d")); + } + + #[test] + #[wasm_bindgen_test] + fn invalidate_entries_if() { + use std::collections::HashSet; + + let mut cache = Cache::new(100); + cache.enable_frequency_sketch_for_testing(); + + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); + + cache.insert(0, "alice"); + cache.insert(1, "bob"); + cache.insert(2, "alex"); + + mock.increment(Duration::from_secs(5)); // 5 secs from the start. + + assert_eq!(cache.get(&0), Some(&"alice")); + assert_eq!(cache.get(&1), Some(&"bob")); + assert_eq!(cache.get(&2), Some(&"alex")); + assert!(cache.contains_key(&0)); + assert!(cache.contains_key(&1)); + assert!(cache.contains_key(&2)); + + let names = ["alice", "alex"].iter().cloned().collect::>(); + cache.invalidate_entries_if(move |_k, &v| names.contains(v)); + + mock.increment(Duration::from_secs(5)); // 10 secs from the start. + + cache.insert(3, "alice"); + + assert!(cache.get(&0).is_none()); + assert!(cache.get(&2).is_none()); + assert_eq!(cache.get(&1), Some(&"bob")); + // This should survive as it was inserted after calling invalidate_entries_if. + assert_eq!(cache.get(&3), Some(&"alice")); + + assert!(!cache.contains_key(&0)); + assert!(cache.contains_key(&1)); + assert!(!cache.contains_key(&2)); + assert!(cache.contains_key(&3)); + + assert_eq!(cache.cache.len(), 2); + + mock.increment(Duration::from_secs(5)); // 15 secs from the start. + + cache.invalidate_entries_if(|_k, &v| v == "alice"); + cache.invalidate_entries_if(|_k, &v| v == "bob"); + + assert!(cache.get(&1).is_none()); + assert!(cache.get(&3).is_none()); + + assert!(!cache.contains_key(&1)); + assert!(!cache.contains_key(&3)); + + assert_eq!(cache.cache.len(), 0); + } + + #[test] + #[wasm_bindgen_test] + fn time_to_live() { + let mut cache = Cache::builder() + .max_capacity(100) + .time_to_live(Duration::from_secs(10)) + .build(); + cache.enable_frequency_sketch_for_testing(); + + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); + + cache.insert("a", "alice"); + + mock.increment(Duration::from_secs(5)); // 5 secs from the start. + + assert_eq!(cache.get(&"a"), Some(&"alice")); + assert!(cache.contains_key(&"a")); + + mock.increment(Duration::from_secs(5)); // 10 secs. + + assert_eq!(cache.get(&"a"), None); + assert!(!cache.contains_key(&"a")); + assert_eq!(cache.iter().count(), 0); + assert!(cache.cache.is_empty()); + + cache.insert("b", "bob"); + + assert_eq!(cache.cache.len(), 1); + + mock.increment(Duration::from_secs(5)); // 15 secs. + + assert_eq!(cache.get(&"b"), Some(&"bob")); + assert!(cache.contains_key(&"b")); + assert_eq!(cache.cache.len(), 1); + + cache.insert("b", "bill"); + + mock.increment(Duration::from_secs(5)); // 20 secs + + assert_eq!(cache.get(&"b"), Some(&"bill")); + assert!(cache.contains_key(&"b")); + assert_eq!(cache.cache.len(), 1); + + mock.increment(Duration::from_secs(5)); // 25 secs + + assert_eq!(cache.get(&"a"), None); + assert_eq!(cache.get(&"b"), None); + assert!(!cache.contains_key(&"a")); + assert!(!cache.contains_key(&"b")); + assert_eq!(cache.iter().count(), 0); + assert!(cache.cache.is_empty()); + } + + #[test] + #[wasm_bindgen_test] + fn time_to_idle() { + let mut cache = Cache::builder() + .max_capacity(100) + .time_to_idle(Duration::from_secs(10)) + .build(); + cache.enable_frequency_sketch_for_testing(); + + let (clock, mock) = Clock::mock(); + cache.set_expiration_clock(Some(clock)); + + cache.insert("a", "alice"); + + mock.increment(Duration::from_secs(5)); // 5 secs from the start. + + assert_eq!(cache.get(&"a"), Some(&"alice")); + + mock.increment(Duration::from_secs(5)); // 10 secs. + + cache.insert("b", "bob"); + + assert_eq!(cache.cache.len(), 2); + + mock.increment(Duration::from_secs(2)); // 12 secs. + + // contains_key does not reset the idle timer for the key. + assert!(cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + + assert_eq!(cache.cache.len(), 2); + + mock.increment(Duration::from_secs(3)); // 15 secs. + + assert_eq!(cache.get(&"a"), None); + assert_eq!(cache.get(&"b"), Some(&"bob")); + assert!(!cache.contains_key(&"a")); + assert!(cache.contains_key(&"b")); + assert_eq!(cache.iter().count(), 1); + assert_eq!(cache.cache.len(), 1); + + mock.increment(Duration::from_secs(10)); // 25 secs + + assert_eq!(cache.get(&"a"), None); + assert_eq!(cache.get(&"b"), None); + assert!(!cache.contains_key(&"a")); + assert!(!cache.contains_key(&"b")); + assert_eq!(cache.iter().count(), 0); + assert!(cache.cache.is_empty()); + } + + #[cfg_attr(target_pointer_width = "16", ignore)] + #[test] + #[wasm_bindgen_test] + fn test_skt_capacity_will_not_overflow() { + // power of two + let pot = |exp| 2u64.pow(exp); + + let ensure_sketch_len = |max_capacity, len, name| { + let mut cache = Cache::::new(max_capacity); + cache.enable_frequency_sketch_for_testing(); + assert_eq!(cache.frequency_sketch.table_len(), len as usize, "{}", name); + }; + + if cfg!(target_pointer_width = "32") { + let pot24 = pot(24); + let pot16 = pot(16); + ensure_sketch_len(0, 128, "0"); + ensure_sketch_len(128, 128, "128"); + ensure_sketch_len(pot16, pot16, "pot16"); + // due to ceiling to next_power_of_two + ensure_sketch_len(pot16 + 1, pot(17), "pot16 + 1"); + // due to ceiling to next_power_of_two + ensure_sketch_len(pot24 - 1, pot24, "pot24 - 1"); + ensure_sketch_len(pot24, pot24, "pot24"); + ensure_sketch_len(pot(27), pot24, "pot(27)"); + ensure_sketch_len(u32::MAX as u64, pot24, "u32::MAX"); + } else { + // target_pointer_width: 64 or larger. + let pot30 = pot(30); + let pot16 = pot(16); + ensure_sketch_len(0, 128, "0"); + ensure_sketch_len(128, 128, "128"); + ensure_sketch_len(pot16, pot16, "pot16"); + // due to ceiling to next_power_of_two + ensure_sketch_len(pot16 + 1, pot(17), "pot16 + 1"); + + // The following tests will allocate large memory (~8GiB). + // Skip when running on Circle CI. + if !cfg!(circleci) { + // due to ceiling to next_power_of_two + ensure_sketch_len(pot30 - 1, pot30, "pot30- 1"); + ensure_sketch_len(pot30, pot30, "pot30"); + ensure_sketch_len(u64::MAX, pot30, "u64::MAX"); + } + }; + } + + #[test] + #[wasm_bindgen_test] + fn test_debug_format() { + let mut cache = Cache::new(10); + cache.insert('a', "alice"); + cache.insert('b', "bob"); + cache.insert('c', "cindy"); + + let debug_str = format!("{:?}", cache); + assert!(debug_str.starts_with('{')); + assert!(debug_str.contains(r#"'a': "alice""#)); + assert!(debug_str.contains(r#"'b': "bob""#)); + assert!(debug_str.contains(r#"'c': "cindy""#)); + assert!(debug_str.ends_with('}')); + } +} diff --git a/crates/mini-moka-vendored/src/unsync/deques.rs b/crates/mini-moka-vendored/src/unsync/deques.rs new file mode 100644 index 00000000..8f928358 --- /dev/null +++ b/crates/mini-moka-vendored/src/unsync/deques.rs @@ -0,0 +1,157 @@ +use super::{KeyDate, KeyHashDate, ValueEntry}; +use crate::common::{ + deque::{DeqNode, Deque}, + CacheRegion, +}; + +use std::ptr::NonNull; +use tagptr::TagNonNull; + +pub(crate) struct Deques { + pub(crate) window: Deque>, // Not used yet. + pub(crate) probation: Deque>, + pub(crate) protected: Deque>, // Not used yet. + pub(crate) write_order: Deque>, +} + +impl Default for Deques { + fn default() -> Self { + Self { + window: Deque::new(CacheRegion::Window), + probation: Deque::new(CacheRegion::MainProbation), + protected: Deque::new(CacheRegion::MainProtected), + write_order: Deque::new(CacheRegion::Other), + } + } +} + +impl Deques { + pub(crate) fn clear(&mut self) { + self.window = Deque::new(CacheRegion::Window); + self.probation = Deque::new(CacheRegion::MainProbation); + self.protected = Deque::new(CacheRegion::MainProtected); + self.write_order = Deque::new(CacheRegion::Other); + } + + pub(crate) fn push_back_ao( + &mut self, + region: CacheRegion, + kh: KeyHashDate, + entry: &mut ValueEntry, + ) { + let node = Box::new(DeqNode::new(kh)); + let node = match region { + CacheRegion::Window => self.window.push_back(node), + CacheRegion::MainProbation => self.probation.push_back(node), + CacheRegion::MainProtected => self.protected.push_back(node), + CacheRegion::Other => unreachable!(), + }; + let tagged_node = TagNonNull::compose(node, region as usize); + entry.set_access_order_q_node(Some(tagged_node)); + } + + pub(crate) fn push_back_wo(&mut self, kh: KeyDate, entry: &mut ValueEntry) { + let node = Box::new(DeqNode::new(kh)); + let node = self.write_order.push_back(node); + entry.set_write_order_q_node(Some(node)); + } + + pub(crate) fn move_to_back_ao(&mut self, entry: &ValueEntry) { + if let Some(tagged_node) = entry.access_order_q_node() { + let (node, tag) = tagged_node.decompose(); + let p = unsafe { node.as_ref() }; + match tag.into() { + CacheRegion::Window if self.window.contains(p) => { + unsafe { self.window.move_to_back(node) }; + } + CacheRegion::MainProbation if self.probation.contains(p) => { + unsafe { self.probation.move_to_back(node) }; + } + CacheRegion::MainProtected if self.protected.contains(p) => { + unsafe { self.protected.move_to_back(node) }; + } + _ => unreachable!(), + } + } + } + + pub(crate) fn move_to_back_wo(&mut self, entry: &ValueEntry) { + let node = entry.write_order_q_node().unwrap(); + let p = unsafe { node.as_ref() }; + if self.write_order.contains(p) { + unsafe { self.write_order.move_to_back(node) }; + } + } + + pub(crate) fn unlink_ao(&mut self, entry: &mut ValueEntry) { + if let Some(node) = entry.take_access_order_q_node() { + self.unlink_node_ao(node); + } + } + + pub(crate) fn unlink_ao_from_deque( + deq_name: &str, + deq: &mut Deque>, + entry: &mut ValueEntry, + ) { + if let Some(node) = entry.take_access_order_q_node() { + unsafe { Self::unlink_node_ao_from_deque(deq_name, deq, node) }; + } + } + + pub(crate) fn unlink_wo(deq: &mut Deque>, entry: &mut ValueEntry) { + if let Some(node) = entry.take_write_order_q_node() { + Self::unlink_node_wo(deq, node); + } + } + + pub(crate) fn unlink_node_ao(&mut self, tagged_node: TagNonNull>, 2>) { + unsafe { + match tagged_node.decompose_tag().into() { + CacheRegion::Window => { + Self::unlink_node_ao_from_deque("window", &mut self.window, tagged_node) + } + CacheRegion::MainProbation => { + Self::unlink_node_ao_from_deque("probation", &mut self.probation, tagged_node) + } + CacheRegion::MainProtected => { + Self::unlink_node_ao_from_deque("protected", &mut self.protected, tagged_node) + } + _ => unreachable!(), + } + } + } + + unsafe fn unlink_node_ao_from_deque( + deq_name: &str, + deq: &mut Deque>, + tagged_node: TagNonNull>, 2>, + ) { + let (node, tag) = tagged_node.decompose(); + if deq.region() == tag && deq.contains(node.as_ref()) { + // https://github.com/moka-rs/moka/issues/64 + deq.unlink_and_drop(node); + } else { + panic!( + "unlink_node - node is not a member of {} deque. {:?}", + deq_name, + node.as_ref() + ) + } + } + + pub(crate) fn unlink_node_wo(deq: &mut Deque>, node: NonNull>>) { + unsafe { + let p = node.as_ref(); + if deq.contains(p) { + // https://github.com/moka-rs/moka/issues/64 + deq.unlink_and_drop(node); + } else { + panic!( + "unlink_node - node is not a member of write_order deque. {:?}", + p + ) + } + } + } +} diff --git a/crates/mini-moka-vendored/src/unsync/iter.rs b/crates/mini-moka-vendored/src/unsync/iter.rs new file mode 100644 index 00000000..49a2aea4 --- /dev/null +++ b/crates/mini-moka-vendored/src/unsync/iter.rs @@ -0,0 +1,36 @@ +use super::{Cache, ValueEntry}; + +use std::{ + hash::{BuildHasher, Hash}, + rc::Rc, +}; + +type HashMapIter<'i, K, V> = std::collections::hash_map::Iter<'i, Rc, ValueEntry>; + +pub struct Iter<'i, K, V, S> { + cache: &'i Cache, + iter: HashMapIter<'i, K, V>, +} + +impl<'i, K, V, S> Iter<'i, K, V, S> { + pub(crate) fn new(cache: &'i Cache, iter: HashMapIter<'i, K, V>) -> Self { + Self { cache, iter } + } +} + +impl<'i, K, V, S> Iterator for Iter<'i, K, V, S> +where + K: Hash + Eq, + S: BuildHasher + Clone, +{ + type Item = (&'i K, &'i V); + + fn next(&mut self) -> Option { + for (k, entry) in self.iter.by_ref() { + if !self.cache.is_expired_entry(entry) { + return Some((k, &entry.value)); + } + } + None + } +} diff --git a/crates/mini-moka-vendored/tests/compile_tests/sync/clone/sync_cache_clone.rs b/crates/mini-moka-vendored/tests/compile_tests/sync/clone/sync_cache_clone.rs new file mode 100644 index 00000000..aafcea95 --- /dev/null +++ b/crates/mini-moka-vendored/tests/compile_tests/sync/clone/sync_cache_clone.rs @@ -0,0 +1,64 @@ +// https://github.com/moka-rs/moka/issues/131 + +use std::{collections::hash_map::DefaultHasher, hash::BuildHasher, sync::Arc}; + +use mini_moka::sync::Cache; + +fn main() { + f1_fail(); + f2_pass(); + f3_fail(); + f4_pass(); +} + +const CAP: u64 = 100; + +fn f1_fail() { + // This should fail because V is not Clone. + let _cache: Cache = Cache::new(CAP); +} + +fn f2_pass() { + let cache: Cache> = Cache::new(CAP); + let _ = cache.clone(); +} + +fn f3_fail() { + // This should fail because S is not Clone. + let _cache: Cache, _> = Cache::builder().build_with_hasher(MyBuildHasher1); +} + +fn f4_pass() { + let cache: Cache, _> = Cache::builder().build_with_hasher(MyBuildHasher2); + let _ = cache.clone(); +} + +// MyKey is not Clone. +#[derive(Hash, PartialEq, Eq)] +pub struct MyKey(i32); + +// MyValue is not Clone. +pub struct MyValue(i32); + +// MyBuildHasher1 is not Clone. +pub struct MyBuildHasher1; + +impl BuildHasher for MyBuildHasher1 { + type Hasher = DefaultHasher; + + fn build_hasher(&self) -> Self::Hasher { + unimplemented!() + } +} + +// MyBuildHasher1 is Clone. +#[derive(Clone)] +pub struct MyBuildHasher2; + +impl BuildHasher for MyBuildHasher2 { + type Hasher = DefaultHasher; + + fn build_hasher(&self) -> Self::Hasher { + unimplemented!() + } +} diff --git a/crates/mini-moka-vendored/tests/compile_tests/sync/clone/sync_cache_clone.stderr b/crates/mini-moka-vendored/tests/compile_tests/sync/clone/sync_cache_clone.stderr new file mode 100644 index 00000000..a2fac756 --- /dev/null +++ b/crates/mini-moka-vendored/tests/compile_tests/sync/clone/sync_cache_clone.stderr @@ -0,0 +1,33 @@ +error[E0277]: the trait bound `MyValue: Clone` is not satisfied + --> tests/compile_tests/sync/clone/sync_cache_clone.rs:18:41 + | +18 | let _cache: Cache = Cache::new(CAP); + | ^^^^^^^^^^ the trait `Clone` is not implemented for `MyValue` + | +note: required by a bound in `mini_moka::sync::Cache::::new` + --> src/sync/cache.rs + | + | V: Clone + Send + Sync + 'static, + | ^^^^^ required by this bound in `mini_moka::sync::Cache::::new` +help: consider annotating `MyValue` with `#[derive(Clone)]` + | +41 | #[derive(Clone)] + | + +error[E0277]: the trait bound `MyBuildHasher1: Clone` is not satisfied + --> tests/compile_tests/sync/clone/sync_cache_clone.rs:28:84 + | +28 | let _cache: Cache, _> = Cache::builder().build_with_hasher(MyBuildHasher1); + | ----------------- ^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `MyBuildHasher1` + | | + | required by a bound introduced by this call + | +note: required by a bound in `mini_moka::sync::CacheBuilder::>::build_with_hasher` + --> src/sync/builder.rs + | + | S: BuildHasher + Clone + Send + Sync + 'static, + | ^^^^^ required by this bound in `mini_moka::sync::CacheBuilder::>::build_with_hasher` +help: consider annotating `MyBuildHasher1` with `#[derive(Clone)]` + | +44 | #[derive(Clone)] + | diff --git a/crates/mini-moka-vendored/tests/skeptic.rs b/crates/mini-moka-vendored/tests/skeptic.rs new file mode 100644 index 00000000..6b54ee21 --- /dev/null +++ b/crates/mini-moka-vendored/tests/skeptic.rs @@ -0,0 +1,2 @@ +#[cfg(skeptic)] +include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs"));