Skip to content

Commit

Permalink
Merge pull request #24 from moka-rs/weight-based-eviction
Browse files Browse the repository at this point in the history
Support weight-based eviction
  • Loading branch information
tatsuya6502 authored Dec 31, 2021
2 parents d1d68cf + b77f505 commit 582e03f
Show file tree
Hide file tree
Showing 22 changed files with 2,009 additions and 539 deletions.
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

0 comments on commit 582e03f

Please sign in to comment.