diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5b19f1..3a348e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,25 +1,61 @@ -name: Rust +name: Rust CI on: push: - branches: [ "main" ] + branches: [ "main", "dev" ] pull_request: - branches: [ "main" ] + branches: [ "main", "dev" ] env: CARGO_TERM_COLOR: always jobs: - build: + # Check formatting + fmt: + name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + # Static analysis + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: swatinem/rust-cache@v2 + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + # Multi-feature testing matrix test: - needs: build + name: Test + needs: [ fmt, clippy ] + runs-on: ubuntu-latest + strategy: + matrix: + feature_flags: [ "--all-features", "--no-default-features" ] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: swatinem/rust-cache@v2 + - name: Run tests + run: cargo test ${{ matrix.feature_flags }} --verbose + + # Ensure benchmarks still compile + check-benches: + name: Check Benchmarks runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Test - run: cargo test --verbose \ No newline at end of file + - uses: dtolnay/rust-toolchain@stable + - uses: swatinem/rust-cache@v2 + - name: Build benchmarks + run: cargo check --benches --all-features diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a0bc75d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Release-plz + +on: + push: + branches: + - main + +permissions: + id-token: write + contents: write + pull-requests: write + +jobs: + release-plz: + name: Release-plz + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Run release-plz + uses: MarcoIeni/release-plz-action@v0.5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e593f65..5144c34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,25 +3,35 @@ version = 4 [[package]] -name = "addr2line" -version = "0.24.2" +name = "ahash" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "gimli", + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", ] [[package]] -name = "adler2" -version = "2.0.0" +name = "aho-corasick" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] [[package]] -name = "android-tzdata" -version = "0.1.1" +name = "alloca" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] [[package]] name = "android_system_properties" @@ -33,66 +43,69 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.4.0" +name = "anes" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] -name = "backtrace" -version = "0.3.75" +name = "anstyle" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.23" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -100,6 +113,58 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -107,16 +172,122 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "gimli" -version = "0.31.1" +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -136,11 +307,26 @@ dependencies = [ "cc", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -148,50 +334,40 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "miniz_oxide" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" -dependencies = [ - "adler2", -] +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mio" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -203,26 +379,33 @@ dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -230,15 +413,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link", ] [[package]] @@ -247,59 +430,191 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] -name = "rustc-demangle" -version = "0.1.24" +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustycache" version = "1.0.0" dependencies = [ + "ahash", "chrono", + "criterion", + "parking_lot", "tokio", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shlex" version = "1.3.0" @@ -308,47 +623,57 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.9" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.60.2", ] [[package]] name = "syn" -version = "2.0.101" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" -version = "1.45.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", "bytes", "libc", "mio", @@ -357,14 +682,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -373,47 +698,59 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.100" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" +name = "wasm-bindgen" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -421,31 +758,72 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" -version = "0.61.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -456,9 +834,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -467,9 +845,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -478,43 +856,53 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ + "windows-link", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", @@ -527,48 +915,80 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml index f0f8551..55d6e94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,33 @@ [package] name = "rustycache" version = "1.0.0" -rust-version = "1.86" +rust-version = "1.93" authors = ["Q300Z"] description = "A simple and easy-to-use caching library for Rust." -license-file = "LICENSE" +license = "MIT" readme = "README.md" repository = "https://github.com/Q300Z/rustycache" -homepage = "https://github.com/Q300Z/rustycache" -keywords = ["cache", "ttl","LRU", "LFU", "FIFO"] +keywords = ["cache", "ttl", "LRU", "LFU", "FIFO"] categories = ["caching"] - edition = "2024" [dependencies] chrono = "0.4" -tokio = { version = "1", features = ["full"] } \ No newline at end of file +tokio = { version = "1", features = ["full"], optional = true } +parking_lot = "0.12" +ahash = "0.8.12" + +[features] +default = ["async"] +async = ["dep:tokio"] + +[dev-dependencies] +criterion = { version = "0.8.2", features = ["async_tokio"] } + +[[bench]] +name = "cache_benchmarks" +harness = false + +[[bench]] +name = "contention_benchmarks" +harness = false \ No newline at end of file diff --git a/README.md b/README.md index 4ce83d7..f33074c 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,109 @@ -# RustyCache Rust Library +# RustyCache + ![Rust](https://img.shields.io/badge/Rust-lang-000000.svg?style=flat&logo=rust) -[![Rust](https://github.com/Q300Z/easycache/actions/workflows/ci.yml/badge.svg)](https://github.com/Q300Z/easycache/actions/workflows/ci.yml) [![Crates.io](https://img.shields.io/crates/v/rustycache.svg)](https://crates.io/crates/rustycache) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -A generic, thread-safe, asynchronous cache library in Rust implementing multiple cache eviction strategies with TTL and background cleaning. +**RustyCache** is an ultra-high-performance, sharded, and thread-safe caching library for Rust. Engineered for extreme +concurrency, it features index-based O(1) eviction algorithms and zero-cost abstractions. -## Features +## ๐Ÿš€ Performance & Optimizations -- Supports multiple cache eviction strategies: - - **LFU** (Least Frequently Used) - - **FIFO** (First In First Out) -- Thread-safe with `Arc>` -- Time-to-live (TTL) expiration on entries -- Background cleaner task using Tokio async runtime -- Generic over keys and values (with necessary trait bounds) -- Simple trait-based `CacheStrategy` interface for easy extension +RustyCache is optimized for modern CPU architectures and high-throughput requirements: -## Usage +- **Index-based Arena (O(1))**: LRU and FIFO strategies use a `Vec`-based arena with `usize` indices for the doubly + linked list. This **eliminates key cloning** during priority updates, drastically reducing CPU overhead and memory + pressure. +- **Sharded Locking**: Internal partitioning (sharding) minimizes lock contention, allowing linear scaling with CPU core + counts. +- **Static Dispatch**: A fully generic architecture removes dynamic dispatch (`Box`) overhead, enabling deep + compiler inlining. +- **Fast Hashing**: Powered by **AHash**, the most efficient non-cryptographic hasher available for Rust. +- **Optimized Mutexes**: Uses **Parking Lot** for fast, compact, and non-poisoning synchronization primitives. -### Add dependency +## โœจ Features -Add this crate to your `Cargo.toml`: +- **Multiple Strategies**: + - `LRU` (Least Recently Used) + - `LFU` (Least Frequently Used) + - `FIFO` (First In First Out) +- **Time-To-Live (TTL)**: Automatic entry expiration. +- **Hybrid Async/Sync**: + - **Async Mode**: Background worker task for proactive expiration cleaning (requires `tokio`). + - **Sync Mode**: Zero-dependency, passive expiration for ultra-low-overhead environments. +- **Thread-Safe**: Designed for safe concurrent access. +- **Generic**: Supports any key `K` and value `V` that implement `Clone + Hash + Eq`. + +## ๐Ÿ“ฆ Installation + +Add to your `Cargo.toml`: ```toml -rustycache = { path = "path/to/rustycache" } -``` -Or if published on crates.io, replace with version: -```toml +[dependencies] +# Default: async feature enabled rustycache = "1.0" + +# For pure synchronous environments (no tokio) +# rustycache = { version = "1.0", default-features = false } ``` -### Example: Using LFU Cache + +## ๐Ÿ›  Usage + +### Asynchronous Mode (Default) + ```rust -use easycache::LFUCache; +use rustycache::rustycache::Rustycache; use std::time::Duration; #[tokio::main] async fn main() { - // Create LFU cache with capacity 100, TTL 60 seconds, cleaner interval 10 seconds - let mut cache = Easycache::new(100, Duration::from_secs(60), Duration::from_secs(10), easycache::strategy::StrategyType::LFU); - - // Put some values - cache.put("key1".to_string(), "value1".to_string()); - cache.put("key2".to_string(), "value2".to_string()); - - // Get a value - if let Some(val) = cache.get(&"key1".to_string()) { - println!("Got: {}", val); - } else { - println!("Key expired or not found"); - } + // 16 shards, 10k capacity, 5m TTL, 60s cleanup interval + let cache = Rustycache::lru(16, 10000, Duration::from_secs(300), Duration::from_secs(60)); + + cache.put("key".to_string(), "value".to_string()); + let val = cache.get(&"key".to_string()); } ``` -### Example: Using FIFO Cache + +### Synchronous Mode + ```rust -use easycache::FIFOCache; +use rustycache::rustycache::Rustycache; use std::time::Duration; -#[tokio::main] -async fn main() { - // Create FIFO cache with capacity 50, TTL 120 seconds, cleaner interval 15 seconds - let mut cache = Easycache::new(50, Duration::from_secs(120), Duration::from_secs(15), easycache::strategy::StrategyType::FIFO); - - // Put some values - cache.put("foo".to_string(), 123); - cache.put("bar".to_string(), 456); - - // Get a value - if let Some(val) = cache.get(&"foo".to_string()) { - println!("Got: {}", val); - } else { - println!("Key expired or not found"); - } +fn main() { + let cache = Rustycache::lru_sync(8, 1000, Duration::from_secs(60)); + cache.put("key".to_string(), 42); + assert_eq!(cache.get(&"key".to_string()), Some(42)); } ``` -## Testing -To run the tests, use the following command: + +## ๐Ÿ“Š Benchmarks + +*Results measured on 10,000 elements with 16 shards.* + +| Operation | Strategy | Latency | Complexity | +|:--------------|:---------|:------------|:-----------| +| **Get (Hit)** | LRU | **~128 ns** | **O(1)** | +| **Get (Hit)** | FIFO | **~117 ns** | **O(1)** | +| **Get (Hit)** | LFU | **~205 ns** | O(log N) | + +### Throughput (Scaling) + +RustyCache scales exceptionally well under heavy thread contention: + +- **1 Thread**: ~8.0 Million ops/sec +- **8 Threads**: **~23.0 Million ops/sec** (on 8-core machine) + +*Note: For large keys (e.g., 4KB), performance is dominated by hashing (~700ns), regardless of the strategy.* + +## ๐Ÿงช Testing ```bash cargo test +cargo test --no-default-features ``` -Tests cover cache insertion, eviction, TTL expiration, concurrency safety, and cleanup logic. -## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file + +## ๐Ÿ“œ License + +MIT License - see [LICENSE](LICENSE) file for details. diff --git a/benches/cache_benchmarks.rs b/benches/cache_benchmarks.rs new file mode 100644 index 0000000..c4f8c0c --- /dev/null +++ b/benches/cache_benchmarks.rs @@ -0,0 +1,145 @@ +use criterion::{Criterion, criterion_group, criterion_main}; +use rustycache::rustycache::Rustycache; +use std::hint::black_box; +use std::time::Duration; + +fn bench_lru(c: &mut Criterion) { + let cap = 10000; + let ttl = Duration::from_secs(60); + let interval = Duration::from_secs(60); + let rt = tokio::runtime::Runtime::new().unwrap(); + + let mut group = c.benchmark_group("Cache_LRU_Sharded"); + + group.bench_function("put", |b| { + let cache = rt.block_on(async { Rustycache::lru(16, cap, ttl, interval) }); + let mut i = 0; + b.iter(|| { + cache.put(black_box(i.to_string()), black_box(i.to_string())); + i += 1; + }); + }); + + group.bench_function("get", |b| { + let cache = rt.block_on(async { + let c = Rustycache::lru(16, cap, ttl, interval); + for i in 0..cap { + c.put(i.to_string(), i.to_string()); + } + c + }); + let mut i = 0; + b.iter(|| { + cache.get(black_box(&(i % cap).to_string())); + i += 1; + }); + }); + + group.finish(); +} + +fn bench_fifo(c: &mut Criterion) { + let cap = 10000; + let ttl = Duration::from_secs(60); + let interval = Duration::from_secs(60); + let rt = tokio::runtime::Runtime::new().unwrap(); + + let mut group = c.benchmark_group("Cache_FIFO_Sharded"); + + group.bench_function("put", |b| { + let cache = rt.block_on(async { Rustycache::fifo(16, cap, ttl, interval) }); + let mut i = 0; + b.iter(|| { + cache.put(black_box(i.to_string()), black_box(i.to_string())); + i += 1; + }); + }); + + group.bench_function("get", |b| { + let cache = rt.block_on(async { + let c = Rustycache::fifo(16, cap, ttl, interval); + for i in 0..cap { + c.put(i.to_string(), i.to_string()); + } + c + }); + let mut i = 0; + b.iter(|| { + cache.get(black_box(&(i % cap).to_string())); + i += 1; + }); + }); + + group.finish(); +} + +fn bench_lfu(c: &mut Criterion) { + let cap = 10000; + let ttl = Duration::from_secs(60); + let interval = Duration::from_secs(60); + let rt = tokio::runtime::Runtime::new().unwrap(); + + let mut group = c.benchmark_group("Cache_LFU_Sharded"); + + group.bench_function("put", |b| { + let cache = rt.block_on(async { Rustycache::lfu(16, cap, ttl, interval) }); + let mut i = 0; + b.iter(|| { + cache.put(black_box(i.to_string()), black_box(i.to_string())); + i += 1; + }); + }); + + group.bench_function("get", |b| { + let cache = rt.block_on(async { + let c = Rustycache::lfu(16, cap, ttl, interval); + for i in 0..cap { + c.put(i.to_string(), i.to_string()); + } + c + }); + let mut i = 0; + b.iter(|| { + cache.get(black_box(&(i % cap).to_string())); + i += 1; + }); + }); + + group.finish(); +} + +fn bench_key_sizes(c: &mut Criterion) { + let cap = 1000; + let ttl = Duration::from_secs(60); + let interval = Duration::from_secs(60); + let rt = tokio::runtime::Runtime::new().unwrap(); + + let mut group = c.benchmark_group("Key_Sizes"); + + for size in [16, 256, 4096] { + let key = "a".repeat(size); + + group.bench_with_input(format!("put_{}_bytes", size), &key, |b, k| { + let cache = rt.block_on(async { Rustycache::lru(1, cap, ttl, interval) }); + b.iter(|| { + cache.put(black_box(k.clone()), black_box("value".to_string())); + }); + }); + + group.bench_with_input(format!("get_{}_bytes", size), &key, |b, k| { + let cache = rt.block_on(async { + let c = Rustycache::lru(1, cap, ttl, interval); + c.put(k.clone(), "value".to_string()); + c + }); + b.iter(|| { + cache.get(black_box(k)); + }); + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_lru, bench_fifo, bench_lfu, bench_key_sizes); +criterion_main!(benches); diff --git a/benches/contention_benchmarks.rs b/benches/contention_benchmarks.rs new file mode 100644 index 0000000..b2c97a8 --- /dev/null +++ b/benches/contention_benchmarks.rs @@ -0,0 +1,85 @@ +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use rustycache::rustycache::Rustycache; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; + +fn bench_contention(c: &mut Criterion) { + let cap = 10000; + let ttl = Duration::from_secs(60); + let interval = Duration::from_secs(60); + let total_ops = 100_000; + let rt = tokio::runtime::Runtime::new().unwrap(); + + for thread_count in [1, 4, 8, 16] { + let mut group = c.benchmark_group(format!("Contention_{}_threads", thread_count)); + group.throughput(Throughput::Elements(total_ops as u64)); + + // put benchmark + group.bench_function("sharded_lru_put", |b| { + b.iter_custom(|iters| { + let mut elapsed = Duration::ZERO; + for _ in 0..iters { + let cache = + Arc::new(rt.block_on(async { Rustycache::lru(16, cap, ttl, interval) })); + let ops_per_thread = total_ops / thread_count; + let mut handles = vec![]; + + let start = Instant::now(); + for t in 0..thread_count { + let c_clone = Arc::clone(&cache); + handles.push(thread::spawn(move || { + for i in 0..ops_per_thread { + c_clone.put(format!("{}-{}", t, i), "value".to_string()); + } + })); + } + for h in handles { + h.join().unwrap(); + } + elapsed += start.elapsed(); + } + elapsed + }); + }); + + // get benchmark (pre-filled) + group.bench_function("sharded_lru_get", |b| { + let cache = Arc::new(rt.block_on(async { + let c = Rustycache::lru(16, cap, ttl, interval); + for i in 0..cap { + c.put(i.to_string(), "value".to_string()); + } + c + })); + + b.iter_custom(|iters| { + let mut elapsed = Duration::ZERO; + for _ in 0..iters { + let ops_per_thread = total_ops / thread_count; + let mut handles = vec![]; + + let start = Instant::now(); + for _ in 0..thread_count { + let c_clone = Arc::clone(&cache); + handles.push(thread::spawn(move || { + for i in 0..ops_per_thread { + c_clone.get(&(i % cap).to_string()); + } + })); + } + for h in handles { + h.join().unwrap(); + } + elapsed += start.elapsed(); + } + elapsed + }); + }); + + group.finish(); + } +} + +criterion_group!(benches, bench_contention); +criterion_main!(benches); diff --git a/examples/simple_async.rs b/examples/simple_async.rs new file mode 100644 index 0000000..d920463 --- /dev/null +++ b/examples/simple_async.rs @@ -0,0 +1,43 @@ +#[cfg(feature = "async")] +use rustycache::rustycache::Rustycache; +#[cfg(feature = "async")] +use std::time::Duration; +#[cfg(feature = "async")] +use tokio::time::sleep; + +#[cfg(feature = "async")] +#[tokio::main] +async fn main() { + println!("--- Example: Asynchronous Mode ---"); + + // Create an LRU cache with: + // - 8 shards (high concurrency) + // - 1000 total capacity + // - 2 seconds TTL (short for demonstration) + // - 1 second cleaner interval + let cache = Rustycache::lru(8, 1000, Duration::from_secs(2), Duration::from_secs(1)); + + // Inserting data + cache.put("api_result_1".to_string(), "Result A"); + println!("Inserted key 'api_result_1'"); + + // Verify it exists + if cache.contains("api_result_1") { + println!("Key is present in cache."); + } + + println!("Waiting for 3 seconds (longer than TTL)..."); + sleep(Duration::from_secs(3)).await; + + // The background cleaner should have removed the entry + if cache.get("api_result_1").is_none() { + println!("Key has been automatically removed by the background cleaner."); + } + + println!("Cache length: {}", cache.len()); +} + +#[cfg(not(feature = "async"))] +fn main() { + println!("This example requires the 'async' feature."); +} diff --git a/examples/simple_sync.rs b/examples/simple_sync.rs new file mode 100644 index 0000000..19a4476 --- /dev/null +++ b/examples/simple_sync.rs @@ -0,0 +1,26 @@ +use rustycache::rustycache::Rustycache; +use std::time::Duration; + +fn main() { + println!("--- Example: Synchronous Mode ---"); + + // Create a FIFO cache with 4 shards, total capacity of 100, and 1 minute TTL. + // In sync mode, expiration is handled passively when calling `get`. + let cache = Rustycache::fifo_sync(4, 100, Duration::from_secs(60)); + + // Inserting data + cache.put("session_1".to_string(), "user_data_A"); + cache.put("session_2".to_string(), "user_data_B"); + + // Retrieval using &str (thanks to Borrow support, no need to create a String) + if let Some(data) = cache.get("session_1") { + println!("Retrieved session_1: {}", data); + } + + // Check size + println!("Cache length: {}", cache.len()); + + // Clear the cache + cache.clear(); + println!("Cache length after clear: {}", cache.len()); +} diff --git a/src/lib.rs b/src/lib.rs index acdc44d..c749ba2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,2 @@ - pub mod rustycache; pub mod strategy; diff --git a/src/rustycache.rs b/src/rustycache.rs index e6bbfca..9d0b84b 100644 --- a/src/rustycache.rs +++ b/src/rustycache.rs @@ -1,63 +1,169 @@ -use std::time::Duration; -use crate::strategy::{CacheStrategy, StrategyType}; +use crate::strategy::CacheStrategy; use crate::strategy::fifo::FIFOCache; use crate::strategy::lfu::LFUCache; use crate::strategy::lru::LRUCache; +use ahash::RandomState; +use std::borrow::Borrow; +use std::hash::Hash; +use std::time::Duration; -pub struct Rustycache { - inner: Box>, +/// A high-performance, sharded, and thread-safe cache. +pub struct Rustycache { + shards: Vec, + hasher: RandomState, + _phantom: std::marker::PhantomData<(K, V)>, } -impl Rustycache +impl Rustycache where - K: 'static + Send + Sync + Clone + Eq + std::hash::Hash, + K: 'static + Send + Sync + Clone + Eq + Hash, V: 'static + Send + Sync + Clone, + S: CacheStrategy, { - pub fn new(cap: usize, ttl: Duration, clean_interval: Duration, strat: StrategyType) -> Self { - let inner: Box> = match strat { - StrategyType::LRU => Box::new(LRUCache::new(cap, ttl, clean_interval)), - StrategyType::FIFO => Box::new(FIFOCache::new(cap, ttl, clean_interval)), - StrategyType::LFU => Box::new(LFUCache::new(cap, ttl, clean_interval)), - }; + pub fn new(num_shards: usize, shard_factory: impl Fn() -> S) -> Self { + let mut shards = Vec::with_capacity(num_shards); + for _ in 0..num_shards { + shards.push(shard_factory()); + } - inner.start_cleaner(clean_interval); - - Rustycache { inner } + Rustycache { + shards, + hasher: RandomState::new(), + _phantom: std::marker::PhantomData, + } } - pub fn put(&mut self, key: K, value: V) { - self.inner.put(key, value) + #[inline] + fn get_shard(&self, key: &Q) -> &S + where + K: Borrow, + { + let hash = self.hasher.hash_one(key); + &self.shards[(hash as usize) % self.shards.len()] } - pub fn get(&mut self, key: &K) -> Option { - self.inner.get(key) + #[inline] + pub fn put(&self, key: K, value: V) { + let hash = self.hasher.hash_one(&key); + self.shards[(hash as usize) % self.shards.len()].put(key, value); } - pub fn remove(&mut self, key: &K) { - self.inner.remove(key) + #[inline] + pub fn get(&self, key: &Q) -> Option + where + K: Borrow, + { + self.get_shard(key).get(key) } - pub fn contains(&self, key: &K) -> bool { - self.inner.contains(key) + #[inline] + pub fn remove(&self, key: &Q) + where + K: Borrow, + { + self.get_shard(key).remove(key) } - pub fn stop_cleaner(&self) { - self.inner.stop_cleaner() + #[inline] + pub fn contains(&self, key: &Q) -> bool + where + K: Borrow, + { + self.get_shard(key).contains(key) } - pub fn start_cleaner(&self, interval: Duration) { - self.inner.start_cleaner(interval) - } - + #[inline] pub fn len(&self) -> usize { - self.inner.len() + self.shards.iter().map(|s| s.len()).sum() } - + + #[inline] pub fn is_empty(&self) -> bool { - self.inner.is_empty() + self.shards.iter().all(|s| s.is_empty()) + } + + #[inline] + pub fn clear(&self) { + for shard in &self.shards { + shard.clear(); + } + } +} + +impl Rustycache> +where + K: 'static + Send + Sync + Clone + Eq + Hash, + V: 'static + Send + Sync + Clone, +{ + #[cfg(feature = "async")] + pub fn lru( + num_shards: usize, + capacity: usize, + ttl: Duration, + clean_interval: Duration, + ) -> Self { + Self::new(num_shards, move || { + let shard = LRUCache::new(capacity / num_shards + 1, ttl, clean_interval); + shard.start_cleaner(clean_interval); + shard + }) + } + + pub fn lru_sync(num_shards: usize, capacity: usize, ttl: Duration) -> Self { + Self::new(num_shards, move || { + LRUCache::new(capacity / num_shards + 1, ttl, Duration::from_secs(0)) + }) } - - pub fn clear(&mut self) { - self.inner.clear() +} + +impl Rustycache> +where + K: 'static + Send + Sync + Clone + Eq + Hash, + V: 'static + Send + Sync + Clone, +{ + #[cfg(feature = "async")] + pub fn fifo( + num_shards: usize, + capacity: usize, + ttl: Duration, + clean_interval: Duration, + ) -> Self { + Self::new(num_shards, move || { + let shard = FIFOCache::new(capacity / num_shards + 1, ttl, clean_interval); + shard.start_cleaner(clean_interval); + shard + }) + } + + pub fn fifo_sync(num_shards: usize, capacity: usize, ttl: Duration) -> Self { + Self::new(num_shards, move || { + FIFOCache::new(capacity / num_shards + 1, ttl, Duration::from_secs(0)) + }) + } +} + +impl Rustycache> +where + K: 'static + Send + Sync + Clone + Eq + Hash, + V: 'static + Send + Sync + Clone, +{ + #[cfg(feature = "async")] + pub fn lfu( + num_shards: usize, + capacity: usize, + ttl: Duration, + clean_interval: Duration, + ) -> Self { + Self::new(num_shards, move || { + let shard = LFUCache::new(capacity / num_shards + 1, ttl, clean_interval); + shard.start_cleaner(clean_interval); + shard + }) + } + + pub fn lfu_sync(num_shards: usize, capacity: usize, ttl: Duration) -> Self { + Self::new(num_shards, move || { + LFUCache::new(capacity / num_shards + 1, ttl, Duration::from_secs(0)) + }) } } diff --git a/src/strategy/fifo.rs b/src/strategy/fifo.rs index 123a268..0a9877a 100644 --- a/src/strategy/fifo.rs +++ b/src/strategy/fifo.rs @@ -1,19 +1,36 @@ -use chrono::{DateTime, Utc}; -use std::collections::{HashMap, VecDeque}; +use ahash::AHashMap as HashMap; +use parking_lot::RwLock; +use std::borrow::Borrow; use std::hash::Hash; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; +use crate::strategy::CacheStrategy; +use chrono::{DateTime, Utc}; +#[cfg(feature = "async")] use tokio::sync::Notify; +#[cfg(feature = "async")] use tokio::task; +#[cfg(feature = "async")] use tokio::time::sleep; -use crate::strategy::CacheStrategy; -struct CacheEntry { +struct Node { + key: K, value: V, expires_at: DateTime, + prev: Option, + next: Option, +} + +struct FIFOState { + map: HashMap, + nodes: Vec>>, + free_indices: Vec, + head: Option, + tail: Option, } +#[derive(Clone)] pub struct FIFOCache where K: Eq + Hash + Clone + Send + Sync + 'static, @@ -21,8 +38,8 @@ where { capacity: usize, ttl: Duration, - map: Arc>>>, - order: Arc>>, + state: Arc>>, + #[cfg(feature = "async")] notify_stop: Arc, } @@ -31,17 +48,61 @@ where K: Eq + Hash + Clone + Send + Sync + 'static, V: Clone + Send + Sync + 'static, { - pub fn new(capacity: usize, ttl: Duration, clean_interval: Duration) -> Self { - let cache = FIFOCache { + pub fn new(capacity: usize, ttl: Duration, _clean_interval: Duration) -> Self { + FIFOCache { capacity, ttl, - map: Arc::new(Mutex::new(HashMap::new())), - order: Arc::new(Mutex::new(VecDeque::new())), + state: Arc::new(RwLock::new(FIFOState { + map: HashMap::default(), + nodes: Vec::with_capacity(capacity), + free_indices: Vec::new(), + head: None, + tail: None, + })), + #[cfg(feature = "async")] notify_stop: Arc::new(Notify::new()), + } + } + + fn detach_node(state: &mut FIFOState, node_idx: u32) { + let (prev, next) = { + let node = state.nodes[node_idx as usize].as_ref().unwrap(); + (node.prev, node.next) }; - cache.start_cleaner(clean_interval); - cache + if let Some(p) = prev { + state.nodes[p as usize].as_mut().unwrap().next = next; + } else { + state.head = next; + } + + if let Some(n) = next { + state.nodes[n as usize].as_mut().unwrap().prev = prev; + } else { + state.tail = prev; + } + } + + fn push_back(state: &mut FIFOState, node_idx: u32) { + let old_tail = state.tail; + if let Some(ot) = old_tail { + state.nodes[ot as usize].as_mut().unwrap().next = Some(node_idx); + } else { + state.head = Some(node_idx); + } + + let node = state.nodes[node_idx as usize].as_mut().unwrap(); + node.next = None; + node.prev = old_tail; + state.tail = Some(node_idx); + } + + fn remove_node_internal(state: &mut FIFOState, node_idx: u32) { + Self::detach_node(state, node_idx); + if let Some(node) = state.nodes[node_idx as usize].take() { + state.map.remove(&node.key); + state.free_indices.push(node_idx); + } } } @@ -50,76 +111,130 @@ where K: Eq + Hash + Clone + Send + Sync + 'static, V: Clone + Send + Sync + 'static, { - fn put(&mut self, key: K, value: V) { - let mut map = self.map.lock().unwrap(); - let mut order = self.order.lock().unwrap(); - - if map.contains_key(&key) { - return; // FIFO ne met pas ร  jour les valeurs existantes + #[inline] + fn put(&self, key: K, value: V) { + let mut state = self.state.write(); + if state.map.contains_key(&key) { + return; } - if order.len() >= self.capacity { - if let Some(oldest) = order.pop_front() { - map.remove(&oldest); - } + if state.map.len() >= self.capacity + && let Some(oldest_idx) = state.head + { + Self::remove_node_internal(&mut state, oldest_idx); } - order.push_back(key.clone()); - map.insert( - key, - CacheEntry { + let expires_at = Utc::now() + chrono::Duration::from_std(self.ttl).unwrap(); + + let node_idx = if let Some(idx) = state.free_indices.pop() { + state.nodes[idx as usize] = Some(Node { + key: key.clone(), + value, + expires_at, + prev: None, + next: None, + }); + idx + } else { + let idx = state.nodes.len() as u32; + state.nodes.push(Some(Node { + key: key.clone(), value, - expires_at: Utc::now() + chrono::Duration::from_std(self.ttl).unwrap(), - }, - ); + expires_at, + prev: None, + next: None, + })); + idx + }; + + state.map.insert(key, node_idx); + Self::push_back(&mut state, node_idx); } - fn get(&mut self, key: &K) -> Option { - let map = self.map.lock().unwrap(); - if let Some(entry) = map.get(key) { - if entry.expires_at > Utc::now() { - return Some(entry.value.clone()); + #[inline] + fn get(&self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + // Try read lock + { + let state = self.state.read(); + if let Some(&node_idx) = state.map.get(key) { + let node = state.nodes[node_idx as usize].as_ref().unwrap(); + if node.expires_at > Utc::now() { + return Some(node.value.clone()); + } + } else { + return None; + } + } + + // Handle expiration + let mut state = self.state.write(); + if let Some(&node_idx) = state.map.get(key) { + if state.nodes[node_idx as usize].as_ref().unwrap().expires_at <= Utc::now() { + Self::remove_node_internal(&mut state, node_idx); } else { - drop(map); // release before relocking - let mut map = self.map.lock().unwrap(); - let mut order = self.order.lock().unwrap(); - map.remove(key); - order.retain(|k| k != key); + return Some( + state.nodes[node_idx as usize] + .as_ref() + .unwrap() + .value + .clone(), + ); } } None } - fn remove(&mut self, key: &K) { - let mut map = self.map.lock().unwrap(); - let mut order = self.order.lock().unwrap(); - map.remove(key); - order.retain(|k| k != key); + #[inline] + fn remove(&self, key: &Q) + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let mut state = self.state.write(); + if let Some(&node_idx) = state.map.get(key) { + Self::remove_node_internal(&mut state, node_idx); + } } - fn contains(&self, key: &K) -> bool { - let map = self.map.lock().unwrap(); - map.contains_key(key) + #[inline] + fn contains(&self, key: &Q) -> bool + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let state = self.state.read(); + state.map.contains_key(key) } + #[inline] fn len(&self) -> usize { - let map = self.map.lock().unwrap(); - map.len() + let state = self.state.read(); + state.map.len() } + + #[inline] fn is_empty(&self) -> bool { - let map = self.map.lock().unwrap(); - map.is_empty() + let state = self.state.read(); + state.map.is_empty() } - fn clear(&mut self) { - let mut map = self.map.lock().unwrap(); - let mut order = self.order.lock().unwrap(); - map.clear(); - order.clear(); + + #[inline] + fn clear(&self) { + let mut state = self.state.write(); + state.map.clear(); + state.nodes.clear(); + state.free_indices.clear(); + state.head = None; + state.tail = None; } + #[cfg(feature = "async")] fn start_cleaner(&self, clean_interval: Duration) { - let map = Arc::clone(&self.map); - let order = Arc::clone(&self.order); + let state_clone = Arc::clone(&self.state); let notify = Arc::clone(&self.notify_stop); task::spawn(async move { @@ -127,14 +242,18 @@ where tokio::select! { _ = sleep(clean_interval) => { let now = Utc::now(); - let mut map = map.lock().unwrap(); - let mut order = order.lock().unwrap(); + let mut state = state_clone.write(); + let mut indices_to_remove = Vec::new(); - order.retain(|key| { - map.get(key).map_or(false, |entry| entry.expires_at > now) - }); + for (_, &idx) in state.map.iter() { + if let Some(node) = &state.nodes[idx as usize] && node.expires_at <= now { + indices_to_remove.push(idx); + } + } - map.retain(|_key, entry| entry.expires_at > now); + for idx in indices_to_remove { + Self::remove_node_internal(&mut state, idx); + } } _ = notify.notified() => { break; @@ -144,7 +263,19 @@ where }); } + #[cfg(feature = "async")] fn stop_cleaner(&self) { self.notify_stop.notify_waiters(); } } + +#[cfg(feature = "async")] +impl Drop for FIFOCache +where + K: Eq + Hash + Clone + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, +{ + fn drop(&mut self) { + self.stop_cleaner(); + } +} diff --git a/src/strategy/lfu.rs b/src/strategy/lfu.rs index a11569a..b4cbb63 100644 --- a/src/strategy/lfu.rs +++ b/src/strategy/lfu.rs @@ -1,20 +1,34 @@ -use chrono::{DateTime, Utc}; -use std::collections::{BTreeMap, HashMap, HashSet}; +use ahash::AHashMap as HashMap; +use ahash::AHashSet as HashSet; +use parking_lot::RwLock; +use std::borrow::Borrow; +use std::collections::BTreeMap; use std::hash::Hash; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; +use crate::strategy::CacheStrategy; +use chrono::{DateTime, Utc}; +#[cfg(feature = "async")] use tokio::sync::Notify; +#[cfg(feature = "async")] use tokio::task; +#[cfg(feature = "async")] use tokio::time::sleep; -use crate::strategy::CacheStrategy; -struct CacheEntry { +struct CacheEntry { + key: K, value: V, expires_at: DateTime, frequency: usize, } +struct LFUState { + map: HashMap>, + freq_map: BTreeMap>, +} + +#[derive(Clone)] pub struct LFUCache where K: Eq + Hash + Clone + Send + Sync + 'static, @@ -22,8 +36,8 @@ where { capacity: usize, ttl: Duration, - map: Arc>>>, - freq_map: Arc>>>, + state: Arc>>, + #[cfg(feature = "async")] notify_stop: Arc, } @@ -32,53 +46,31 @@ where K: Eq + Hash + Clone + Send + Sync + 'static, V: Clone + Send + Sync + 'static, { - pub fn new(capacity: usize, ttl: Duration, clean_interval: Duration) -> Self { - let cache = LFUCache { + pub fn new(capacity: usize, ttl: Duration, _clean_interval: Duration) -> Self { + LFUCache { capacity, ttl, - map: Arc::new(Mutex::new(HashMap::>::new())), - freq_map: Arc::new(Mutex::new(BTreeMap::new())), + state: Arc::new(RwLock::new(LFUState { + map: HashMap::default(), + freq_map: BTreeMap::new(), + })), + #[cfg(feature = "async")] notify_stop: Arc::new(Notify::new()), - }; - - let map_clone = Arc::clone(&cache.map); - let freq_map_clone = Arc::clone(&cache.freq_map); - let notify_clone = Arc::clone(&cache.notify_stop); - - task::spawn(async move { - loop { - tokio::select! { - _ = sleep(clean_interval) => { - let now = Utc::now(); - let mut map = map_clone.lock().unwrap(); - let mut freq_map = freq_map_clone.lock().unwrap(); - let keys_to_remove: Vec = map.iter() - .filter_map(|(k, v)| { - if v.expires_at <= now { - Some(k.clone()) - } else { - None - } - }) - .collect(); + } + } - for key in keys_to_remove { - if let Some(entry) = map.remove(&key) { - if let Some(set) = freq_map.get_mut(&entry.frequency) { - set.remove(&key); - if set.is_empty() { - freq_map.remove(&entry.frequency); - } - } - } - } - }, - _ = notify_clone.notified() => break, - } + fn remove_entry_internal(key: &Q, freq: usize, state: &mut LFUState) + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + state.map.remove(key); + if let Some(set) = state.freq_map.get_mut(&freq) { + set.remove(key); + if set.is_empty() { + state.freq_map.remove(&freq); } - }); - - cache + } } } @@ -87,123 +79,148 @@ where K: Eq + Hash + Clone + Send + Sync + 'static, V: Clone + Send + Sync + 'static, { - fn put(&mut self, key: K, value: V) { - let mut map = self.map.lock().unwrap(); - let mut freq_map = self.freq_map.lock().unwrap(); + #[inline] + fn put(&self, key: K, value: V) { + let mut state = self.state.write(); - if let Some(entry) = map.get_mut(&key) { + if let Some(entry) = state.map.get_mut(&key) { entry.value = value; entry.expires_at = Utc::now() + chrono::Duration::from_std(self.ttl).unwrap(); return; } - if map.len() >= self.capacity { - if let Some((&min_freq, keys)) = freq_map.iter_mut().next() { - if let Some(k) = keys.iter().next().cloned() { - keys.remove(&k); - if keys.is_empty() { - freq_map.remove(&min_freq); - } - map.remove(&k); - } + if state.map.len() >= self.capacity { + let to_remove = if let Some((&min_freq, keys)) = state.freq_map.iter().next() { + keys.iter().next().cloned().map(|k| (k, min_freq)) + } else { + None + }; + + if let Some((k, freq)) = to_remove { + Self::remove_entry_internal(&k, freq, &mut state); } } - map.insert(key.clone(), CacheEntry { - value, - expires_at: Utc::now() + chrono::Duration::from_std(self.ttl).unwrap(), - frequency: 1, - }); - - freq_map.entry(1).or_insert_with(HashSet::new).insert(key); + state.map.insert( + key.clone(), + CacheEntry { + key: key.clone(), + value, + expires_at: Utc::now() + chrono::Duration::from_std(self.ttl).unwrap(), + frequency: 1, + }, + ); + + state.freq_map.entry(1).or_default().insert(key); } - fn get(&mut self, key: &K) -> Option { - let mut map = self.map.lock().unwrap(); - let mut freq_map = self.freq_map.lock().unwrap(); + #[inline] + fn get(&self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let mut state = self.state.write(); - if let Some(entry) = map.get_mut(key) { + let (old_freq, new_freq, val, k_clone) = if let Some(entry) = state.map.get_mut(key) { + let freq = entry.frequency; if entry.expires_at <= Utc::now() { - let freq = entry.frequency; - map.remove(key); - if let Some(set) = freq_map.get_mut(&freq) { - set.remove(key); - if set.is_empty() { - freq_map.remove(&freq); - } - } + Self::remove_entry_internal(key, freq, &mut state); return None; } - let old_freq = entry.frequency; entry.frequency += 1; + ( + freq, + entry.frequency, + entry.value.clone(), + entry.key.clone(), + ) + } else { + return None; + }; - if let Some(set) = freq_map.get_mut(&old_freq) { - set.remove(key); - if set.is_empty() { - freq_map.remove(&old_freq); - } + // Update freq_map + if let Some(set) = state.freq_map.get_mut(&old_freq) { + set.remove(key); + if set.is_empty() { + state.freq_map.remove(&old_freq); } - - freq_map - .entry(entry.frequency) - .or_insert_with(HashSet::new) - .insert(key.clone()); - - return Some(entry.value.clone()); } - None - } + state.freq_map.entry(new_freq).or_default().insert(k_clone); - fn remove(&mut self, key: &K) { - let mut map = self.map.lock().unwrap(); - let mut freq_map = self.freq_map.lock().unwrap(); + Some(val) + } - if let Some(entry) = map.remove(key) { - if let Some(set) = freq_map.get_mut(&entry.frequency) { - set.remove(key); - if set.is_empty() { - freq_map.remove(&entry.frequency); - } - } + #[inline] + fn remove(&self, key: &Q) + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let mut state = self.state.write(); + + if let Some(entry) = state.map.get(key) { + let freq = entry.frequency; + Self::remove_entry_internal(key, freq, &mut state); } } - fn contains(&self, key: &K) -> bool { - let map = self.map.lock().unwrap(); - map.contains_key(key) + #[inline] + fn contains(&self, key: &Q) -> bool + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let state = self.state.read(); + state.map.contains_key(key) } + #[inline] fn len(&self) -> usize { - let map = self.map.lock().unwrap(); - map.len() + let state = self.state.read(); + state.map.len() } + #[inline] fn is_empty(&self) -> bool { - let map = self.map.lock().unwrap(); - map.is_empty() + let state = self.state.read(); + state.map.is_empty() } - fn clear(&mut self) { - let mut map = self.map.lock().unwrap(); - let mut freq_map = self.freq_map.lock().unwrap(); - map.clear(); - freq_map.clear(); + #[inline] + fn clear(&self) { + let mut state = self.state.write(); + state.map.clear(); + state.freq_map.clear(); } + #[cfg(feature = "async")] fn start_cleaner(&self, clean_interval: Duration) { - let map = Arc::clone(&self.map); - let notify = Arc::clone(&self.notify_stop); + let state_clone = Arc::clone(&self.state); + let notify_clone = Arc::clone(&self.notify_stop); task::spawn(async move { loop { tokio::select! { _ = sleep(clean_interval) => { let now = Utc::now(); - let mut map = map.lock().unwrap(); + let mut state = state_clone.write(); + + let keys_to_remove: Vec<(K, usize)> = state.map.iter() + .filter_map(|(k, v)| { + if v.expires_at <= now { + Some((k.clone(), v.frequency)) + } else { + None + } + }) + .collect(); - map.retain(|_key, entry| entry.expires_at > now); + for (key, freq) in keys_to_remove { + Self::remove_entry_internal(&key, freq, &mut state); + } } - _ = notify.notified() => { + _ = notify_clone.notified() => { break; } } @@ -211,7 +228,19 @@ where }); } + #[cfg(feature = "async")] fn stop_cleaner(&self) { self.notify_stop.notify_waiters(); } } + +#[cfg(feature = "async")] +impl Drop for LFUCache +where + K: Eq + Hash + Clone + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, +{ + fn drop(&mut self) { + self.stop_cleaner(); + } +} diff --git a/src/strategy/lru.rs b/src/strategy/lru.rs index 5a90437..6be870b 100644 --- a/src/strategy/lru.rs +++ b/src/strategy/lru.rs @@ -1,19 +1,38 @@ -use std::collections::{HashMap, VecDeque}; +use ahash::AHashMap as HashMap; +use parking_lot::{Mutex, RwLock}; +use std::borrow::Borrow; use std::hash::Hash; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; +use crate::strategy::CacheStrategy; use chrono::{DateTime, Utc}; +#[cfg(feature = "async")] use tokio::sync::Notify; +#[cfg(feature = "async")] use tokio::task; +#[cfg(feature = "async")] use tokio::time::sleep; -use crate::strategy::CacheStrategy; -struct CacheEntry { +const BATCH_SIZE: usize = 64; + +struct Node { + key: K, value: V, expires_at: DateTime, + prev: Option, + next: Option, } +struct LRUState { + map: HashMap, + nodes: Vec>>, + free_indices: Vec, + head: Option, + tail: Option, +} + +#[derive(Clone)] pub struct LRUCache where K: Eq + Hash + Clone + Send + Sync + 'static, @@ -21,8 +40,9 @@ where { capacity: usize, ttl: Duration, - map: Arc>>>, - order: Arc>>, + state: Arc>>, + access_buffer: Arc>>, + #[cfg(feature = "async")] notify_stop: Arc, } @@ -31,17 +51,74 @@ where K: Eq + Hash + Clone + Send + 'static + Sync, V: Clone + Send + 'static + Sync, { - pub fn new(capacity: usize, ttl: Duration, clean_interval: Duration) -> Self { - let cache = LRUCache { + pub fn new(capacity: usize, ttl: Duration, _clean_interval: Duration) -> Self { + LRUCache { capacity, ttl, - map: Arc::new(Mutex::new(HashMap::new())), - order: Arc::new(Mutex::new(VecDeque::new())), + state: Arc::new(RwLock::new(LRUState { + map: HashMap::default(), + nodes: Vec::with_capacity(capacity), + free_indices: Vec::new(), + head: None, + tail: None, + })), + access_buffer: Arc::new(Mutex::new(Vec::with_capacity(BATCH_SIZE))), + #[cfg(feature = "async")] notify_stop: Arc::new(Notify::new()), + } + } + + fn detach_node(state: &mut LRUState, node_idx: u32) { + let (prev, next) = { + let node = state.nodes[node_idx as usize].as_ref().unwrap(); + (node.prev, node.next) }; - cache.start_cleaner(clean_interval); - cache + if let Some(p) = prev { + state.nodes[p as usize].as_mut().unwrap().next = next; + } else { + state.head = next; + } + + if let Some(n) = next { + state.nodes[n as usize].as_mut().unwrap().prev = prev; + } else { + state.tail = prev; + } + } + + fn push_front(state: &mut LRUState, node_idx: u32) { + let old_head = state.head; + if let Some(oh) = old_head { + state.nodes[oh as usize].as_mut().unwrap().prev = Some(node_idx); + } else { + state.tail = Some(node_idx); + } + + let node = state.nodes[node_idx as usize].as_mut().unwrap(); + node.prev = None; + node.next = old_head; + state.head = Some(node_idx); + } + + fn remove_node_internal(state: &mut LRUState, node_idx: u32) { + Self::detach_node(state, node_idx); + if let Some(node) = state.nodes[node_idx as usize].take() { + state.map.remove(&node.key); + state.free_indices.push(node_idx); + } + } + + fn apply_access_batch(&self, state: &mut LRUState) { + let mut buffer = self.access_buffer.lock(); + for &node_idx in buffer.iter() { + // Verify node still exists and wasn't removed/expired in the meantime + if node_idx < state.nodes.len() as u32 && state.nodes[node_idx as usize].is_some() { + Self::detach_node(state, node_idx); + Self::push_front(state, node_idx); + } + } + buffer.clear(); } } @@ -50,78 +127,154 @@ where K: Eq + Hash + Clone + Send + Sync + 'static, V: Clone + Send + Sync + 'static, { - fn put(&mut self, key: K, value: V) { - let mut map = self.map.lock().unwrap(); - let mut order = self.order.lock().unwrap(); + #[inline] + fn put(&self, key: K, value: V) { + let mut state = self.state.write(); - if map.contains_key(&key) { - order.retain(|k| k != &key); - } + // Always flush batch on put to maintain order before potential eviction + self.apply_access_batch(&mut state); + + let expires_at = Utc::now() + chrono::Duration::from_std(self.ttl).unwrap(); - if order.len() >= self.capacity { - if let Some(oldest) = order.pop_back() { - map.remove(&oldest); + if let Some(&node_idx) = state.map.get(&key) { + Self::detach_node(&mut state, node_idx); + let node = state.nodes[node_idx as usize].as_mut().unwrap(); + node.value = value; + node.expires_at = expires_at; + Self::push_front(&mut state, node_idx); + } else { + if state.map.len() >= self.capacity + && let Some(oldest_idx) = state.tail + { + Self::remove_node_internal(&mut state, oldest_idx); } - } - order.push_front(key.clone()); - map.insert( - key, - CacheEntry { - value, - expires_at: Utc::now() + chrono::Duration::from_std(self.ttl).unwrap(), - }, - ); + let node_idx = if let Some(idx) = state.free_indices.pop() { + state.nodes[idx as usize] = Some(Node { + key: key.clone(), + value, + expires_at, + prev: None, + next: None, + }); + idx + } else { + let idx = state.nodes.len() as u32; + state.nodes.push(Some(Node { + key: key.clone(), + value, + expires_at, + prev: None, + next: None, + })); + idx + }; + + state.map.insert(key, node_idx); + Self::push_front(&mut state, node_idx); + } } - fn get(&mut self, key: &K) -> Option { - let mut map = self.map.lock().unwrap(); - let mut order = self.order.lock().unwrap(); + #[inline] + fn get(&self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + // Try read lock first + { + let state = self.state.read(); + if let Some(&node_idx) = state.map.get(key) { + let node = state.nodes[node_idx as usize].as_ref().unwrap(); + if node.expires_at > Utc::now() { + let val = node.value.clone(); + + // Record access in buffer + let mut buffer = self.access_buffer.lock(); + buffer.push(node_idx); + let should_flush = buffer.len() >= BATCH_SIZE; + drop(buffer); + + if should_flush { + drop(state); // Must release read lock before taking write lock + let mut state = self.state.write(); + self.apply_access_batch(&mut state); + } - if let Some(entry) = map.get(key) { - if entry.expires_at > Utc::now() { - order.retain(|k| k != key); - order.push_front(key.clone()); - return Some(entry.value.clone()); + return Some(val); + } } else { - map.remove(key); - order.retain(|k| k != key); + return None; } } + // Handle expiration (requires write lock) + let mut state = self.state.write(); + if let Some(&node_idx) = state.map.get(key) { + if state.nodes[node_idx as usize].as_ref().unwrap().expires_at <= Utc::now() { + Self::remove_node_internal(&mut state, node_idx); + } else { + // Was not expired after all (race condition), just return it + return Some( + state.nodes[node_idx as usize] + .as_ref() + .unwrap() + .value + .clone(), + ); + } + } None } - fn remove(&mut self, key: &K) { - let mut map = self.map.lock().unwrap(); - let mut order = self.order.lock().unwrap(); - map.remove(key); - order.retain(|k| k != key); + #[inline] + fn remove(&self, key: &Q) + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let mut state = self.state.write(); + if let Some(&node_idx) = state.map.get(key) { + Self::remove_node_internal(&mut state, node_idx); + } } - fn contains(&self, key: &K) -> bool { - let map = self.map.lock().unwrap(); - map.contains_key(key) + #[inline] + fn contains(&self, key: &Q) -> bool + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + let state = self.state.read(); + state.map.contains_key(key) } + #[inline] fn len(&self) -> usize { - let map = self.map.lock().unwrap(); - map.len() + let state = self.state.read(); + state.map.len() } + + #[inline] fn is_empty(&self) -> bool { - let map = self.map.lock().unwrap(); - map.is_empty() + let state = self.state.read(); + state.map.is_empty() } - fn clear(&mut self) { - let mut map = self.map.lock().unwrap(); - let mut order = self.order.lock().unwrap(); - map.clear(); - order.clear(); + + #[inline] + fn clear(&self) { + let mut state = self.state.write(); + state.map.clear(); + state.nodes.clear(); + state.free_indices.clear(); + state.head = None; + state.tail = None; + self.access_buffer.lock().clear(); } + #[cfg(feature = "async")] fn start_cleaner(&self, clean_interval: Duration) { - let map = Arc::clone(&self.map); - let order = Arc::clone(&self.order); + let state_clone = Arc::clone(&self.state); let notify = Arc::clone(&self.notify_stop); task::spawn(async move { @@ -129,21 +282,18 @@ where tokio::select! { _ = sleep(clean_interval) => { let now = Utc::now(); - let mut map = map.lock().unwrap(); - let mut order = order.lock().unwrap(); - - order.retain(|key| { - if let Some(entry) = map.get(key) { - if entry.expires_at > now { - true - } else { - map.remove(key); - false - } - } else { - false + let mut state = state_clone.write(); + let mut indices_to_remove = Vec::new(); + + for (_, &idx) in state.map.iter() { + if let Some(node) = &state.nodes[idx as usize] && node.expires_at <= now { + indices_to_remove.push(idx); } - }); + } + + for idx in indices_to_remove { + Self::remove_node_internal(&mut state, idx); + } } _ = notify.notified() => { break; @@ -153,7 +303,19 @@ where }); } + #[cfg(feature = "async")] fn stop_cleaner(&self) { self.notify_stop.notify_waiters(); } } + +#[cfg(feature = "async")] +impl Drop for LRUCache +where + K: Eq + Hash + Clone + Send + Sync + 'static, + V: Clone + Send + Sync + 'static, +{ + fn drop(&mut self) { + self.stop_cleaner(); + } +} diff --git a/src/strategy/mod.rs b/src/strategy/mod.rs index 225e236..0b72aa7 100644 --- a/src/strategy/mod.rs +++ b/src/strategy/mod.rs @@ -2,22 +2,54 @@ pub mod fifo; pub mod lfu; pub mod lru; +use std::borrow::Borrow; +use std::hash::Hash; use std::time::Duration; +/// Common interface for all caching strategies. +/// +/// This trait defines the core operations that any cache strategy must implement, +/// such as putting, getting, and removing entries. pub trait CacheStrategy: Send + Sync { - fn put(&mut self, key: K, value: V); - fn get(&mut self, key: &K) -> Option; - fn remove(&mut self, key: &K); - fn contains(&self, key: &K) -> bool; + /// Inserts a key-value pair into the cache. + fn put(&self, key: K, value: V); + + /// Retrieves a value from the cache by its key. + /// + /// Supports [Borrow] lookups for zero-allocation searches. + fn get(&self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq + ?Sized; + + /// Removes an entry from the cache. + fn remove(&self, key: &Q) + where + K: Borrow, + Q: Hash + Eq + ?Sized; + + /// Checks if a key exists in the cache. + fn contains(&self, key: &Q) -> bool + where + K: Borrow, + Q: Hash + Eq + ?Sized; + + /// Returns the number of entries in the cache. fn len(&self) -> usize; + + /// Returns true if the cache is empty. fn is_empty(&self) -> bool; - fn clear(&mut self); + + /// Clears all entries from the cache. + fn clear(&self); + + /// Starts the background task for cleaning expired entries. + /// + /// Requires the `async` feature and a running Tokio runtime. + #[cfg(feature = "async")] fn start_cleaner(&self, interval: Duration); - fn stop_cleaner(&self); -} -pub enum StrategyType { - LRU, - FIFO, - LFU, + /// Stops the background task. + #[cfg(feature = "async")] + fn stop_cleaner(&self); } diff --git a/tests/concurrency_tests.rs b/tests/concurrency_tests.rs new file mode 100644 index 0000000..cbe474c --- /dev/null +++ b/tests/concurrency_tests.rs @@ -0,0 +1,89 @@ +use rustycache::rustycache::Rustycache; +#[cfg(feature = "async")] +use rustycache::strategy::CacheStrategy; +#[cfg(feature = "async")] +use std::sync::Arc; +use std::time::Duration; +#[cfg(feature = "async")] +use tokio::sync::Barrier; + +#[cfg(feature = "async")] +#[tokio::test] +async fn test_concurrent_put_get_lru() { + let capacity = 100; + let cache = Arc::new(Rustycache::lru( + 8, + capacity, + Duration::from_secs(10), + Duration::from_secs(60), + )); + run_concurrency_test(cache).await; +} + +#[cfg(feature = "async")] +#[tokio::test] +async fn test_concurrent_put_get_fifo() { + let capacity = 100; + let cache = Arc::new(Rustycache::fifo( + 8, + capacity, + Duration::from_secs(10), + Duration::from_secs(60), + )); + run_concurrency_test(cache).await; +} + +#[cfg(feature = "async")] +#[tokio::test] +async fn test_concurrent_put_get_lfu() { + let capacity = 100; + let cache = Arc::new(Rustycache::lfu( + 8, + capacity, + Duration::from_secs(10), + Duration::from_secs(60), + )); + run_concurrency_test(cache).await; +} + +#[cfg(feature = "async")] +async fn run_concurrency_test(cache: Arc>) +where + S: CacheStrategy + 'static, +{ + let num_threads = 10; + let ops_per_thread = 1000; + let barrier = Arc::new(Barrier::new(num_threads)); + let mut handles = vec![]; + + for t in 0..num_threads { + let cache_clone = Arc::clone(&cache); + let barrier_clone = Arc::clone(&barrier); + + let handle = tokio::spawn(async move { + barrier_clone.wait().await; + for i in 0..ops_per_thread { + let key = (i % 200).to_string(); + let val = format!("thread-{}-val-{}", t, i); + + if i % 2 == 0 { + cache_clone.put(key, val); + } else { + cache_clone.get(&key); + } + } + }); + handles.push(handle); + } + + for handle in handles { + handle.await.unwrap(); + } +} + +#[test] +fn test_sync_basics() { + let cache = Rustycache::lru_sync(1, 10, Duration::from_secs(60)); + cache.put("a".to_string(), "b".to_string()); + assert_eq!(cache.get(&"a".to_string()), Some("b".to_string())); +} diff --git a/tests/fifo_tests.rs b/tests/fifo_tests.rs index b6d9cda..d76dbf9 100644 --- a/tests/fifo_tests.rs +++ b/tests/fifo_tests.rs @@ -1,16 +1,40 @@ #[cfg(test)] mod fifo_tests { + use rustycache::rustycache::Rustycache; + #[allow(unused_imports)] + use rustycache::strategy::CacheStrategy; + use rustycache::strategy::fifo::FIFOCache; use std::time::Duration; + #[cfg(feature = "async")] use tokio::time::sleep; - use rustycache::rustycache::Rustycache; - fn create_cache(capacity: usize, ttl_secs: u64, clean_interval_secs: u64) -> Rustycache { - Rustycache::new(capacity, Duration::from_secs(ttl_secs), Duration::from_secs(clean_interval_secs), rustycache::strategy::StrategyType::FIFO) + fn create_cache( + capacity: usize, + ttl_secs: u64, + clean_interval_secs: u64, + _start_cleaner: bool, + ) -> Rustycache> { + let ttl = Duration::from_secs(ttl_secs); + let interval = Duration::from_secs(clean_interval_secs); + let s = FIFOCache::new(capacity, ttl, interval); + #[cfg(feature = "async")] + if _start_cleaner { + s.start_cleaner(interval); + } + Rustycache::new(1, move || s.clone()) + } + + #[test] + fn test_put_and_get_sync() { + let cache = create_cache(2, 5, 60, false); + cache.put("key1".to_string(), "value1".to_string()); + assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); } + #[cfg(feature = "async")] #[tokio::test] async fn test_put_and_get_basic() { - let mut cache = create_cache(2, 5, 60); + let cache = create_cache(2, 5, 60, true); cache.put("key1".to_string(), "value1".to_string()); assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); @@ -19,18 +43,20 @@ mod fifo_tests { assert!(!cache.is_empty()); } + #[cfg(feature = "async")] #[tokio::test] async fn test_put_does_not_update_existing_value() { - let mut cache = create_cache(2, 5, 60); + let cache = create_cache(2, 5, 60, true); cache.put("key1".to_string(), "value1".to_string()); cache.put("key1".to_string(), "value2".to_string()); // should be ignored assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); } + #[cfg(feature = "async")] #[tokio::test] async fn test_fifo_eviction_order() { - let mut cache = create_cache(2, 5, 60); + let cache = create_cache(2, 5, 60, true); cache.put("a".to_string(), "A".to_string()); cache.put("b".to_string(), "B".to_string()); cache.put("c".to_string(), "C".to_string()); // should evict "a" @@ -41,9 +67,10 @@ mod fifo_tests { assert_eq!(cache.len(), 2); } + #[cfg(feature = "async")] #[tokio::test] async fn test_expiration_removes_entry() { - let mut cache = create_cache(2, 1, 60); + let cache = create_cache(2, 1, 60, true); cache.put("x".to_string(), "expire_me".to_string()); assert_eq!(cache.get(&"x".to_string()), Some("expire_me".to_string())); @@ -55,9 +82,10 @@ mod fifo_tests { assert!(!cache.contains(&"x".to_string())); } + #[cfg(feature = "async")] #[tokio::test] async fn test_remove_and_clear() { - let mut cache = create_cache(3, 5, 60); + let cache = create_cache(3, 5, 60, true); cache.put("a".to_string(), "1".to_string()); cache.put("b".to_string(), "2".to_string()); cache.put("c".to_string(), "3".to_string()); @@ -71,30 +99,28 @@ mod fifo_tests { assert!(cache.is_empty()); } - #[tokio::test] - async fn test_cleaner_removes_expired() { - let mut cache = create_cache(2, 1, 1); // TTL=1s, cleaner every 1s - cache.put("k1".to_string(), "v1".to_string()); + #[test] - sleep(Duration::from_secs(2)).await; // let entry expire and cleaner run + fn test_fifo_sync_constructor() { + let cache = Rustycache::fifo_sync(1, 10, Duration::from_secs(60)); - assert_eq!(cache.get(&"k1".to_string()), None); - assert_eq!(cache.len(), 0); - } + cache.put("a".to_string(), "b".to_string()); - #[tokio::test] - async fn test_stop_cleaner_does_not_panic() { - let cache = create_cache(2, 1, 1); - tokio::time::sleep(Duration::from_millis(100)).await; - cache.stop_cleaner(); // Just test stop logic without panic - tokio::time::sleep(Duration::from_millis(100)).await; + assert_eq!(cache.get(&"a".to_string()), Some("b".to_string())); } + #[cfg(feature = "async")] #[tokio::test] - async fn test_start_cleaner_does_not_panic() { - let cache = create_cache(2, 1, 1); - tokio::time::sleep(Duration::from_millis(100)).await; - cache.start_cleaner(Duration::from_secs(1)); // just ensure no panic - tokio::time::sleep(Duration::from_millis(100)).await; + + async fn test_explicit_stop_cleaner() { + let cache = create_cache(2, 1, 1, true); + + // Explicitly calling stop_cleaner to cover the code path + + // In our current API, Rustycache doesn't expose stop_cleaner directly, + + // but it's called on Drop. We can force a drop. + + drop(cache); } } diff --git a/tests/lfu_tests.rs b/tests/lfu_tests.rs index 91986c3..2e759b5 100644 --- a/tests/lfu_tests.rs +++ b/tests/lfu_tests.rs @@ -1,99 +1,136 @@ #[cfg(test)] mod lfu_tests { + use rustycache::rustycache::Rustycache; + #[allow(unused_imports)] + use rustycache::strategy::CacheStrategy; + use rustycache::strategy::lfu::LFUCache; use std::time::Duration; + #[cfg(feature = "async")] use tokio::time::sleep; - use rustycache::rustycache::Rustycache; - fn create_cache(capacity: usize, ttl_secs: u64, interval_secs: u64) -> Rustycache { - Rustycache::new(capacity, Duration::from_secs(ttl_secs), Duration::from_secs(interval_secs), rustycache::strategy::StrategyType::LFU) + fn create_cache( + capacity: usize, + ttl_secs: u64, + clean_interval_secs: u64, + _start_cleaner: bool, + ) -> Rustycache> { + let ttl = Duration::from_secs(ttl_secs); + let interval = Duration::from_secs(clean_interval_secs); + let s = LFUCache::new(capacity, ttl, interval); + #[cfg(feature = "async")] + if _start_cleaner { + s.start_cleaner(interval); + } + Rustycache::new(1, move || s.clone()) + } + + #[test] + fn test_put_and_get_sync() { + let cache = create_cache(10, 5, 60, false); + cache.put("key1".to_string(), "value1".to_string()); + assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); } + #[cfg(feature = "async")] #[tokio::test] async fn test_put_and_get_basic() { - let mut cache = create_cache(2, 5, 60); + let cache = create_cache(10, 5, 60, true); cache.put("key1".to_string(), "value1".to_string()); assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); - assert!(cache.contains(&"key1".to_string())); - assert_eq!(cache.len(), 1); } + #[cfg(feature = "async")] #[tokio::test] async fn test_update_value_and_frequency() { - let mut cache = create_cache(2, 5, 60); + let cache = create_cache(10, 5, 60, true); cache.put("key1".to_string(), "value1".to_string()); - assert_eq!(cache.get(&"key1".to_string()), Some("value1".to_string())); - - cache.put("key1".to_string(), "value2".to_string()); // update value resets frequency? + cache.put("key1".to_string(), "value2".to_string()); assert_eq!(cache.get(&"key1".to_string()), Some("value2".to_string())); } + #[cfg(feature = "async")] #[tokio::test] async fn test_lfu_eviction() { - let mut cache = create_cache(2, 5, 60); + let cache = create_cache(2, 5, 60, true); cache.put("a".to_string(), "A".to_string()); cache.put("b".to_string(), "B".to_string()); - cache.get(&"a".to_string()); // freq a = 2 - // b freq = 1, a freq = 2 - cache.put("c".to_string(), "C".to_string()); // should evict 'b' (lowest freq) + // increase frequency of "b" + cache.get(&"b".to_string()); - assert!(cache.get(&"a".to_string()).is_some()); - assert!(cache.get(&"b".to_string()).is_none()); - assert!(cache.get(&"c".to_string()).is_some()); + // should evict "a" since it has lower frequency + cache.put("c".to_string(), "C".to_string()); + + assert!(!cache.contains(&"a".to_string())); + assert!(cache.contains(&"b".to_string())); + assert!(cache.contains(&"c".to_string())); } + #[cfg(feature = "async")] #[tokio::test] async fn test_expiration_behavior() { - let mut cache = create_cache(2, 1, 60); + let cache = create_cache(2, 1, 60, true); cache.put("x".to_string(), "expire_me".to_string()); - - assert_eq!(cache.get(&"x".to_string()), Some("expire_me".to_string())); sleep(Duration::from_secs(2)).await; - assert_eq!(cache.get(&"x".to_string()), None); - assert!(!cache.contains(&"x".to_string())); } + #[cfg(feature = "async")] #[tokio::test] async fn test_remove_and_clear() { - let mut cache = create_cache(3, 5, 60); + let cache = create_cache(3, 5, 60, true); cache.put("a".to_string(), "1".to_string()); - cache.put("b".to_string(), "2".to_string()); - cache.put("c".to_string(), "3".to_string()); - - cache.remove(&"b".to_string()); - assert_eq!(cache.get(&"b".to_string()), None); - assert_eq!(cache.len(), 2); + cache.remove(&"a".to_string()); + assert!(cache.is_empty()); + cache.put("b".to_string(), "2".to_string()); cache.clear(); - assert_eq!(cache.len(), 0); assert!(cache.is_empty()); } + #[cfg(feature = "async")] #[tokio::test] async fn test_cleaner_removes_expired() { - let mut cache = create_cache(2, 1, 1); // TTL = 1s, cleaner every 1s + let cache = create_cache(2, 1, 1, true); cache.put("k1".to_string(), "v1".to_string()); - - sleep(Duration::from_secs(2)).await; // wait expiration + cleaner run - - assert_eq!(cache.get(&"k1".to_string()), None); + sleep(Duration::from_secs(2)).await; assert_eq!(cache.len(), 0); } + #[cfg(feature = "async")] #[tokio::test] - async fn test_stop_cleaner_no_panic() { - let cache = create_cache(2, 1, 1); - tokio::time::sleep(Duration::from_millis(100)).await; - cache.stop_cleaner(); // Just test stop logic without panic - tokio::time::sleep(Duration::from_millis(100)).await; + async fn test_freq_map_cleanup_on_eviction() { + let cache = create_cache(1, 10, 60, true); + cache.put("1".to_string(), "v1".to_string()); + cache.put("2".to_string(), "v2".to_string()); + assert_eq!(cache.get(&"1".to_string()), None); + assert_eq!(cache.get(&"2".to_string()), Some("v2".to_string())); } + #[cfg(feature = "async")] #[tokio::test] - async fn test_start_cleaner_does_not_panic() { - let cache = create_cache(2, 1, 1); - tokio::time::sleep(Duration::from_millis(100)).await; - cache.start_cleaner(Duration::from_secs(1)); // just ensure no panic - tokio::time::sleep(Duration::from_millis(100)).await; + async fn test_freq_map_cleanup_on_get_expiry() { + let cache = create_cache(2, 1, 60, true); + cache.put("1".to_string(), "v1".to_string()); + sleep(Duration::from_secs(2)).await; + assert_eq!(cache.get(&"1".to_string()), None); + } + + #[test] + + fn test_lfu_sync_constructor() { + let cache = Rustycache::lfu_sync(1, 10, Duration::from_secs(60)); + + cache.put("a".to_string(), "b".to_string()); + + assert_eq!(cache.get(&"a".to_string()), Some("b".to_string())); + } + + #[test] + + fn test_explicit_drop() { + let cache = create_cache(2, 1, 1, false); + + drop(cache); } } diff --git a/tests/lru_tests.rs b/tests/lru_tests.rs index f7a761f..e6c1f8e 100644 --- a/tests/lru_tests.rs +++ b/tests/lru_tests.rs @@ -1,16 +1,33 @@ #[cfg(test)] mod lru_tests { + use rustycache::rustycache::Rustycache; + #[allow(unused_imports)] + use rustycache::strategy::CacheStrategy; + use rustycache::strategy::lru::LRUCache; use std::time::Duration; + #[cfg(feature = "async")] use tokio::time::sleep; - use rustycache::rustycache::Rustycache; - fn create_cache(capacity: usize, ttl_secs: u64, interval_secs: u64) -> Rustycache { - Rustycache::new(capacity, Duration::from_secs(ttl_secs), Duration::from_secs(interval_secs), rustycache::strategy::StrategyType::LRU) + fn create_cache( + capacity: usize, + ttl_secs: u64, + interval_secs: u64, + _start_cleaner: bool, + ) -> Rustycache> { + let ttl = Duration::from_secs(ttl_secs); + let interval = Duration::from_secs(interval_secs); + let s = LRUCache::new(capacity, ttl, interval); + #[cfg(feature = "async")] + if _start_cleaner { + s.start_cleaner(interval); + } + Rustycache::new(1, move || s.clone()) } + #[cfg(feature = "async")] #[tokio::test] async fn test_insert_and_get() { - let mut cache = create_cache(2, 5, 60); + let cache = create_cache(2, 5, 60, true); cache.put("a".to_string(), "value1".to_string()); assert_eq!(cache.get(&"a".to_string()), Some("value1".to_string())); @@ -18,9 +35,30 @@ mod lru_tests { assert_eq!(cache.len(), 1); } + #[test] + fn test_insert_and_get_sync() { + let cache = create_cache(2, 5, 60, false); + cache.put("a".to_string(), "value1".to_string()); + assert_eq!(cache.get(&"a".to_string()), Some("value1".to_string())); + } + + #[test] + fn test_borrow_support() { + let cache = create_cache(2, 5, 60, false); + let key = "string_key".to_string(); + cache.put(key, "value".to_string()); + + // Lookup using &str instead of &String + assert_eq!(cache.get("string_key"), Some("value".to_string())); + assert!(cache.contains("string_key")); + cache.remove("string_key"); + assert!(!cache.contains("string_key")); + } + + #[cfg(feature = "async")] #[tokio::test] async fn test_lru_eviction() { - let mut cache = create_cache(2, 5, 60); + let cache = create_cache(2, 5, 60, true); cache.put("a".to_string(), "A".to_string()); cache.put("b".to_string(), "B".to_string()); cache.get(&"a".to_string()); // 'a' becomes recently used @@ -31,9 +69,10 @@ mod lru_tests { assert!(cache.get(&"c".to_string()).is_some()); } + #[cfg(feature = "async")] #[tokio::test] async fn test_expiration_behavior() { - let mut cache = create_cache(2, 1, 60); + let cache = create_cache(2, 1, 60, true); cache.put("x".to_string(), "expire_me".to_string()); assert_eq!(cache.get(&"x".to_string()), Some("expire_me".to_string())); @@ -42,9 +81,10 @@ mod lru_tests { assert_eq!(cache.get(&"x".to_string()), None); } + #[cfg(feature = "async")] #[tokio::test] async fn test_clear_and_remove() { - let mut cache = create_cache(3, 5, 60); + let cache = create_cache(3, 5, 60, true); cache.put("a".to_string(), "1".to_string()); cache.put("b".to_string(), "2".to_string()); cache.put("c".to_string(), "3".to_string()); @@ -58,9 +98,10 @@ mod lru_tests { assert!(cache.is_empty()); } + #[cfg(feature = "async")] #[tokio::test] async fn test_cleaner_removes_expired() { - let mut cache = create_cache(2, 1, 1); // TTL = 1s, cleaner every 1s + let cache = create_cache(2, 1, 1, true); // TTL = 1s, cleaner every 1s cache.put("k1".to_string(), "v1".to_string()); sleep(Duration::from_secs(2)).await; // Let it expire @@ -69,9 +110,10 @@ mod lru_tests { assert_eq!(cache.len(), 0); } + #[cfg(feature = "async")] #[tokio::test] async fn test_eviction_order_preserved() { - let mut cache = create_cache(3, 5, 60); + let cache = create_cache(3, 5, 60, true); cache.put("1".to_string(), "v1".to_string()); cache.put("2".to_string(), "v2".to_string()); cache.put("3".to_string(), "v3".to_string()); @@ -86,19 +128,11 @@ mod lru_tests { assert!(cache.get(&"2".to_string()).is_none()); // Least recently used } - #[tokio::test] - async fn test_stop_cleaner_does_not_panic() { - let cache = create_cache(2, 1, 1); - tokio::time::sleep(Duration::from_millis(100)).await; - cache.stop_cleaner(); // Just test stop logic without panic - tokio::time::sleep(Duration::from_millis(100)).await; - } + #[test] - #[tokio::test] - async fn test_start_cleaner_does_not_panic() { - let cache = create_cache(2, 1, 1); - tokio::time::sleep(Duration::from_millis(100)).await; - cache.start_cleaner(Duration::from_secs(1)); // just ensure no panic - tokio::time::sleep(Duration::from_millis(100)).await; + fn test_explicit_drop() { + let cache = create_cache(2, 1, 1, false); + + drop(cache); } }