Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support weight-based eviction #24

Merged
merged 45 commits into from
Dec 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
cf00496
Update the CI to run cargo test in debug mode
tatsuya6502 Aug 9, 2021
76bb8a4
Support weight-based (cost-based) eviction and unbound cache
tatsuya6502 Aug 9, 2021
35a789e
Fix the CI on MSRV 1.45.2
tatsuya6502 Aug 9, 2021
aa583ad
Temporary disable the CI for MSRV 1.45.2
tatsuya6502 Aug 15, 2021
2082338
Remove unnecessary hash calculation from an internal method:
tatsuya6502 Aug 15, 2021
6a5444e
Add some source code comments
tatsuya6502 Aug 15, 2021
2835b94
Support weight-based (cost-based) eviction and unbound cache
tatsuya6502 Aug 15, 2021
427e67f
Support cost-based eviction
tatsuya6502 Aug 15, 2021
9d0e702
Size-aware cache management
tatsuya6502 Aug 15, 2021
a6a2c0d
Size-aware cache management
tatsuya6502 Aug 15, 2021
e261968
Minor refactoring
tatsuya6502 Aug 15, 2021
7e75972
Fix typos in source code comments
tatsuya6502 Aug 15, 2021
dd35403
Size-aware cache management
tatsuya6502 Aug 15, 2021
45ee2a9
Size-aware cache management
tatsuya6502 Aug 16, 2021
f133967
Size-aware cache management
tatsuya6502 Aug 16, 2021
8fec64c
Size-aware cache management
tatsuya6502 Aug 16, 2021
c24ce64
Rename an internal type
tatsuya6502 Aug 25, 2021
7e01c98
Update the change log
tatsuya6502 Aug 25, 2021
6dba09b
Fix typos (weigher)
tatsuya6502 Aug 25, 2021
5de5924
Size-aware cache management
tatsuya6502 Aug 25, 2021
c1d6fbd
Add cargo clean step to the CI to avoid Skeptic to fail
tatsuya6502 Aug 25, 2021
b93ad50
Cosmetic changes in the README
tatsuya6502 Aug 25, 2021
2c7ca64
Size-aware cache management
tatsuya6502 Dec 18, 2021
4c2ac94
Temporary disable Clippy on Rust 1.58 beta on CI
tatsuya6502 Dec 12, 2021
bd41c6d
Merge branch 'master' into weight-based-eviction
tatsuya6502 Dec 18, 2021
4404e6f
Size-aware cache management
tatsuya6502 Dec 18, 2021
ed804d7
Merge branch 'master' into weight-based-eviction
tatsuya6502 Dec 25, 2021
af92346
Size-aware cache management
tatsuya6502 Dec 25, 2021
261e1ae
Size-aware cache management
tatsuya6502 Dec 25, 2021
841da56
Size-aware cache management
tatsuya6502 Dec 25, 2021
4481394
Size-aware cache management
tatsuya6502 Dec 25, 2021
a8da880
Size-aware cache management
tatsuya6502 Dec 25, 2021
684f9b6
Size-aware cache management
tatsuya6502 Dec 26, 2021
61fcc5f
Size-aware cache management
tatsuya6502 Dec 26, 2021
e0b771d
Size-aware cache management
tatsuya6502 Dec 26, 2021
b5c53d7
Merge branch 'master' into weight-based-eviction
tatsuya6502 Dec 29, 2021
23985d4
Size-aware cache management
tatsuya6502 Dec 29, 2021
9c8d50d
Size-aware cache management
tatsuya6502 Dec 30, 2021
887c5bf
Size-aware cache management
tatsuya6502 Dec 30, 2021
9bca183
Size-aware cache management
tatsuya6502 Dec 30, 2021
554e6ac
Size-aware cache management
tatsuya6502 Dec 31, 2021
56f860e
Update the CHANGELOG
tatsuya6502 Dec 31, 2021
97aec97
Bump the version to v0.7.0
tatsuya6502 Dec 31, 2021
18a3e71
Update the copyright year
tatsuya6502 Dec 31, 2021
b77f505
Size-aware cache management
tatsuya6502 Dec 31, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ jobs:
RUSTFLAGS: '--cfg skeptic'

- name: Run tests (future)
uses: actions-rs/cargo@v1
if: ${{ matrix.rust != '1.45.2' }}
with:
command: test
args: --features future

- name: Run tests (release, no features)
uses: actions-rs/cargo@v1
with:
command: test
args: --release

- name: Run tests (release, future)
uses: actions-rs/cargo@v1
if: ${{ matrix.rust != '1.45.2' }}
with:
Expand Down
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"deqs",
"Deque",
"Deques",
"Einziger",
"else's",
"Eytan",
"getrandom",
"Hasher",
"Kawano",
Expand All @@ -24,6 +26,7 @@
"MSRV",
"nanos",
"nocapture",
"Ohad",
"peekable",
"preds",
"reqwest",
Expand All @@ -32,6 +35,7 @@
"RUSTFLAGS",
"rustfmt",
"semver",
"smallvec",
"structs",
"Tatsuya",
"thiserror",
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Moka — Change Log

## Version 0.7.0

### Added

- Add support for weight-based (size aware) cache management.
([#24][gh-pull-0024])
- Add support for unbound cache. ([#24][gh-pull-0024])


## Version 0.6.3

### Fixed
Expand Down Expand Up @@ -182,6 +191,7 @@
[gh-pull-0033]: https://github.com/moka-rs/moka/pull/33/
[gh-pull-0030]: https://github.com/moka-rs/moka/pull/30/
[gh-pull-0028]: https://github.com/moka-rs/moka/pull/28/
[gh-pull-0024]: https://github.com/moka-rs/moka/pull/24/
[gh-pull-0023]: https://github.com/moka-rs/moka/pull/23/
[gh-pull-0022]: https://github.com/moka-rs/moka/pull/22/
[gh-pull-0020]: https://github.com/moka-rs/moka/pull/20/
Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "moka"
version = "0.6.3"
version = "0.7.0"
authors = ["Tatsuya Kawano <tatsuya@hibaridb.org>"]
edition = "2018"

Expand Down Expand Up @@ -33,12 +33,14 @@ atomic64 = []

[dependencies]
crossbeam-channel = "0.5"
crossbeam-utils = "0.8"
moka-cht = "0.4.2"
num_cpus = "1.13"
once_cell = "1.7"
parking_lot = "0.11"
quanta = "0.9.3"
scheduled-thread-pool = "0.2"
smallvec = "1.6"
thiserror = "1.0"
uuid = { version = "0.8", features = ["v4"] }

Expand Down
2 changes: 1 addition & 1 deletion LICENSE-APACHE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2020 - 2021 Tatsuya Kawano
Copyright 2020 - 2022 Tatsuya Kawano

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 - 2021 Tatsuya Kawano
Copyright (c) 2020 - 2022 Tatsuya Kawano

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
93 changes: 76 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
[![license][license-badge]](#license)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgh.neting.cc%2Fmoka-rs%2Fmoka.svg?type=shield)](https://app.fossa.com/projects/git%2Bgh.neting.cc%2Fmoka-rs%2Fmoka?ref=badge_shield)

Moka is a fast, concurrent cache library for Rust. Moka is inspired by
[Caffeine][caffeine-git] (Java).
Moka is a fast, concurrent cache library for Rust. Moka is inspired by the
[Caffeine][caffeine-git] library for Java.

Moka provides cache implementations on top of hash maps. They support full
concurrency of retrievals and a high expected concurrency for updates. Moka also
Expand Down Expand Up @@ -42,7 +42,9 @@ algorithm to determine which entries to evict when the capacity is exceeded.
- Synchronous caches that can be shared across OS threads.
- An asynchronous (futures aware) cache that can be accessed inside and outside
of asynchronous contexts.
- Caches are bounded by the maximum number of entries.
- A cache can be bounded by one of the followings:
- The maximum number of entries.
- The total weighted size of entries.
- Maintains good hit rate by using an entry replacement algorithms inspired by
[Caffeine][caffeine-git]:
- Admission to a cache is controlled by the Least Frequently Used (LFU) policy.
Expand All @@ -54,15 +56,15 @@ algorithm to determine which entries to evict when the capacity is exceeded.

## Moka in Production

Moka is powering production services as well as embedded devices like home routers.
Here are some highlights:
Moka is powering production services as well as embedded Linux devices like home
routers. Here are some highlights:

- [crates.io](https://crates.io/): The official crate registry has been using Moka in
its API service to reduce the loads on PostgreSQL. Moka is maintaining
[cache hit rates of ~85%][gh-discussions-51] for the high-traffic download endpoint.
(Moka used: Nov 2021 &mdash; present)
- [aliyundrive-webdav][aliyundrive-webdav-git]: This WebDAV gateway for a cloud drive
may have been deployed in hundreds of home WiFi routers, including inexpensive
may have been deployed in hundreds of home Wi-Fi routers, including inexpensive
models with 32-bit MIPS or ARMv5TE-based SoCs. Moka is used to cache the metadata
of remote files. (Moka used: Aug 2021 &mdash; present)

Expand All @@ -76,23 +78,23 @@ Add this to your `Cargo.toml`:

```toml
[dependencies]
moka = "0.6"
moka = "0.7"
```

To use the asynchronous cache, enable a crate feature called "future".

```toml
[dependencies]
moka = { version = "0.6", features = ["future"] }
moka = { version = "0.7", features = ["future"] }
```


## Example: Synchronous Cache

The thread-safe, synchronous caches are defined in the `sync` module.

Cache entries are manually added using `insert` method, and are stored in the cache
until either evicted or manually invalidated.
Cache entries are manually added using `insert` or `get_or_insert_with` method, and
are stored in the cache until either evicted or manually invalidated.

Here's an example of reading and updating a cache by using multiple threads:

Expand Down Expand Up @@ -152,6 +154,12 @@ fn main() {
}
```

If you want to atomically initialize and insert a value when the key is not present,
you might want to check [the document][doc-sync-cache] for other insertion methods
`get_or_insert_with` and `get_or_try_insert_with`.

[doc-sync-cache]: https://docs.rs/moka/*/moka/sync/struct.Cache.html#method.get_or_insert_with


## Example: Asynchronous Cache

Expand Down Expand Up @@ -179,7 +187,7 @@ Here is a similar program to the previous example, but using asynchronous cache
// Cargo.toml
//
// [dependencies]
// moka = { version = "0.6", features = ["future"] }
// moka = { version = "0.7", features = ["future"] }
// tokio = { version = "1", features = ["rt-multi-thread", "macros" ] }
// futures = "0.3"
Expand Down Expand Up @@ -239,6 +247,12 @@ async fn main() {
}
```

If you want to atomically initialize and insert a value when the key is not present,
you might want to check [the document][doc-future-cache] for other insertion methods
`get_or_insert_with` and `get_or_try_insert_with`.

[doc-future-cache]: https://docs.rs/moka/*/moka/future/struct.Cache.html#method.get_or_insert_with


## Avoiding to clone the value at `get`

Expand Down Expand Up @@ -270,6 +284,34 @@ cache.get(&key);
```


## Example: Bounding a Cache with Weighted Size of Entry

A `weigher` closure can be set at the cache creation time. It will calculate and
return a weighted size (relative size) of an entry. When it is set, a cache tries to
evict entries when the total weighted size exceeds its `max_capacity`.

```rust
use std::convert::TryInto;
use moka::sync::Cache;

fn main() {
let cache = Cache::builder()
// A weigher closure takes &K and &V and returns a u32 representing the
// relative size of the entry. Here, we use the byte length of the value
// String as the size.
.weigher(|_key, value: &String| -> u32 {
value.len().try_into().unwrap_or(u32::MAX)
})
// This cache will hold up to 32MiB of values.
.max_capacity(32 * 1024 * 1024)
.build();
cache.insert(0, "zero".to_string());
}
```

Note that weighted sizes are not used when making eviction selections.


## Example: Expiration Policies

Moka supports the following expiration policies:
Expand All @@ -282,12 +324,11 @@ Moka supports the following expiration policies:
To set them, use the `CacheBuilder`.

```rust
use moka::sync::CacheBuilder;

use moka::sync::Cache;
use std::time::Duration;

fn main() {
let cache = CacheBuilder::new(10_000) // Max 10,000 elements
let cache = Cache::builder()
// Time to live (TTL): 30 minutes
.time_to_live(Duration::from_secs(30 * 60))
// Time to idle (TTI): 5 minutes
Expand Down Expand Up @@ -385,9 +426,9 @@ to the dependency declaration.

```toml:Cargo.toml
[dependencies]
moka = { version = "0.6", default-feautures = false }
moka = { version = "0.7", default-feautures = false }
# Or
moka = { version = "0.6", default-feautures = false, features = ["future"] }
moka = { version = "0.7", default-feautures = false, features = ["future"] }
```

This will make Moka to switch to a fall-back implementation, so it will compile.
Expand Down Expand Up @@ -415,8 +456,18 @@ $ RUSTFLAGS='--cfg skeptic --cfg trybuild' cargo test \
## Road Map

- [x] `async` optimized caches. (`v0.2.0`)
- [ ] Weight based cache management ([#24](https://github.com/moka-rs/moka/pull/24))
- [x] Bounding a cache with weighted size of entry.
(`v0.7.0` via [#24](https://github.com/moka-rs/moka/pull/24))
- [ ] API stabilization. (Smaller core API, shorter names for frequently used
methods)
- e.g.
- `get(&Q)``get_if_present(&Q)`
- `get_or_insert_with(K, F)``get(K, F)`
- `get_or_try_insert_with(K, F)``try_get(K, F)`
- `blocking_insert(K, V)``blocking().insert(K, V)`.
- `time_to_live()``config().time_to_live()`
- [ ] Cache statistics. (Hit rate, etc.)
- [ ] Notifications on eviction, etc.
- [ ] Upgrade TinyLFU to Window TinyLFU.
- [ ] The variable (per-entry) expiration, using a hierarchical timer wheel.

Expand All @@ -426,6 +477,14 @@ $ RUSTFLAGS='--cfg skeptic --cfg trybuild' cargo test \
Moka is named after the [moka pot][moka-pot-wikipedia], a stove-top coffee maker that
brews espresso-like coffee using boiling water pressurized by steam.

This name would imply the following facts and hopes:

- Moka is a part of the Java Caffeine cache family.
- It is written in Rust. (Many moka pots are made of aluminum alloy or stainless
steel. We know they don't rust though)
- It should be fast. ("Espresso" in Italian means express)
- It should be easy to use, like a moka pot.

[moka-pot-wikipedia]: https://en.wikipedia.org/wiki/Moka_pot


Expand Down
9 changes: 0 additions & 9 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,3 @@ pub(crate) mod unsafe_weak_pointer;
pub(crate) mod atomic_time;

pub(crate) mod time;

use time::Instant;

pub(crate) trait AccessTime {
fn last_accessed(&self) -> Option<Instant>;
fn set_last_accessed(&mut self, timestamp: Instant);
fn last_modified(&self) -> Option<Instant>;
fn set_last_modified(&mut self, timestamp: Instant);
}
49 changes: 49 additions & 0 deletions src/common/deque.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ impl<T> DeqNode<T> {
element,
}
}

pub(crate) fn next_node(&self) -> Option<&DeqNode<T>> {
self.next.as_ref().map(|node| unsafe { node.as_ref() })
}
}

/// Cursor is used to remember the current iterating position.
Expand Down Expand Up @@ -650,6 +654,51 @@ mod tests {
assert!((&mut deque).next().is_none());
}

#[test]
fn next_node() {
let mut deque: Deque<String> = Deque::new(MainProbation);

let node1 = DeqNode::new(MainProbation, "a".into());
deque.push_back(Box::new(node1));
let node2 = DeqNode::new(MainProbation, "b".into());
let node2_ptr = deque.push_back(Box::new(node2));
let node3 = DeqNode::new(MainProbation, "c".into());
let node3_ptr = deque.push_back(Box::new(node3));

// -------------------------------------------------------
// First iteration.
// peek_front() -> node1
let node1a = deque.peek_front().unwrap();
assert_eq!(node1a.element, "a".to_string());
let node2a = node1a.next_node().unwrap();
assert_eq!(node2a.element, "b".to_string());
let node3a = node2a.next_node().unwrap();
assert_eq!(node3a.element, "c".to_string());
assert!(node3a.next_node().is_none());

// -------------------------------------------------------
// Iterate after a move_to_back.
// Move "b" to the back. So now "a" -> "c" -> "b".
unsafe { deque.move_to_back(node2_ptr) };
let node1a = deque.peek_front().unwrap();
assert_eq!(node1a.element, "a".to_string());
let node3a = node1a.next_node().unwrap();
assert_eq!(node3a.element, "c".to_string());
let node2a = node3a.next_node().unwrap();
assert_eq!(node2a.element, "b".to_string());
assert!(node2a.next_node().is_none());

// -------------------------------------------------------
// Iterate after an unlink.
// Unlink the second node "c". Now "a" -> "c".
unsafe { deque.unlink(node3_ptr) };
let node1a = deque.peek_front().unwrap();
assert_eq!(node1a.element, "a".to_string());
let node2a = node1a.next_node().unwrap();
assert_eq!(node2a.element, "b".to_string());
assert!(node2a.next_node().is_none());
}

#[test]
fn drop() {
use std::{cell::RefCell, rc::Rc};
Expand Down
Loading