Expand description
A concurrent, in-memory, in-process, Redis-like storage for Rust.
Any bitcode-encodable and bitcode-decodable type can be stored using this store, without being locked to a single type.
Turbostore uses scc::HashMap under the hood, so the performance stays consistent under
load. This means, though, that data structures are locked on access (e.g. accessing two keys of
a single hashmap will lock). Locks are released as soon as possible for maximum performance.
§Installation
To install it, just run cargo add turbostore bitcode. The crate has no features and is
async-only. You need to install bitcode because of the Encode and Decode derive macros.
A sync API may be added in the future since the underlying APIs used by this crate provide both sync and async APIs in their majority.
§Usage
If you prefer to jump to the reference section, go to TurboStore. The API is simple and the
operation names will be familiar if you’ve used Redis in the past.
§Creating Keys
Keys are of a single hashable type. String works by default, but it’s prone to typos,
introduces storage overhead, and it requires you to implement your own key formatting, so if
you want more type safety in your keys (and less storage requirements, therefore), create a key
enum as follows:
use std::hash::Hash;
// All derives are required to be able to use this type as a key.
#[derive(Hash, PartialEq, Eq)]
enum Key {
// An example of using the store for user data store.
UserCache(i32),
}§Creating the TurboStore
To create a store using the key you just created, create a new instance of TurboStore<K>
using TurboStore::new.
use turbostore::TurboStore;
let store: TurboStore<String> = TurboStore::new();Note: This example uses String as a key type for brevity.
§Preallocating Memory
If you already know you’ll have a certain amount of usage, you can preallocate space with
TurboStore::with_capacity.
use turbostore::{TurboStore, Duration};
let store: TurboStore<String> = TurboStore::with_capacity(2);
store.set("key1".into(), &"value1".to_string(), Duration::minutes(1)).await;
store.set("key2".into(), &"value2".to_string(), Duration::minutes(1)).await;
store.set("key3".into(), &"value3".to_string(), Duration::minutes(1)).await;Each structure (KV, map, set, deque) gets its own independent capacity — so setting 3 gives 3 slots per type. This means that if you set the capacity to 3, you’ll have preallocated space for 3 KV pairs, 3 hash maps, 3 hash sets, and 3 deques.
§Creating Storable Data Structures
Storable data structures are easy to implement. They must derive bitcode::Encode to be set,
and bitcode::Decode to be retrieved.
use turbostore::{Encode, Decode};
#[derive(Encode, Decode)]
struct UserCache {
id: i32,
name: String,
}§TTL
This store was mainly intended to be used as a temporary, fast-access cache. It’s not limited to doing only that, but many of the design choices are made based on that purpose. This library may be generalized later.
TTLs are all set based on chrono::Duration and relative to the time of the execution of the
function. Eviction is done both lazily and with manual calls to TurboStore::evict. This can be
called in loop in a background task to periodically clean up expired keys. For example:
use std::sync::Arc;
use tokio::time::Duration;
use turbostore::TurboStore;
let store: Arc<TurboStore<()>> = Arc::new(TurboStore::new());
let thread_store = store.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(2)).await;
thread_store.evict().await;
}
});Turbostore is runtime-agnostic, so this can also be implemented using async-std.
Not calling TurboStore::evict periodically will result in expired data staying in memory
until it’s next attempted to be accessed. When the data is next attempted to be accessed after
being expired, TurboStore will automatically clean that value up.
§Data Structures
Turbostore currently supports 3 different data structures, plus a normal KV pair storage.
The supported data structures are:
- KV Pairs - Normal key-value pairs.
- Hash Maps - Namespaced key-value pairs.
- Hash Sets - Lists of unique items.
- Deques - Lists with O(1) operations on the first and last items.
§KV Pairs
Turbostore supports the basic get, set, remove, pop, and other common operations on key-value pairs.
The available operations on key-value pairs are:
TurboStore::set- Set a new KV pair.TurboStore::get- Get a KV pair’s value.TurboStore::rem- Remove a KV pair.TurboStore::pop- Remove a KV pair and return its value.TurboStore::incr- Increment a value by x.TurboStore::decr- Decrement a value by x.TurboStore::expire- Update the TTL of a KV pair.TurboStore::exists- Checks whether a key is set.
§Hash Maps
Turbostore supports storing sub-hash-maps inside the store.
The available operations on hash maps are:
TurboStore::hset- Add a KV pair to the hash map.TurboStore::hget- Get a value from a KV pair in the hash map.TurboStore::hrem- Remove a KV pair from the hash map.TurboStore::hpop- Remove and return a KV pair from the hash map.TurboStore::hexists- Check whether a key is set in the hash map.TurboStore::hexpire- Set the TTL of a KV pair in the hash map.TurboStore::hlen- Count the amount of KV pairs in the hash map.TurboStore::hclear- Empty the hash map.
TTLs are per KV pair in a hash map, and KV pairs are expired individually.
§Hash Sets
Hash sets are lists of unique items.
The available operations on hash sets are:
TurboStore::sadd- Add an item to the set.TurboStore::srem- Remove an item from the set.TurboStore::slen- Count the amount of items in the set.TurboStore::sclear- Empty the set.TurboStore::sttl- Get the expiry date and time of an item in the set.TurboStore::scontains- Check whether an item is in the set.TurboStore::sall- Get all items in the set.
As with hash maps, hash set items have per-item TTLs and expire individually.
§Deques
Deques are lists optimized for O(1) operations at both ends.
The available operations on deques are:
TurboStore::dappend- Append an item to the deque.TurboStore::dprepend- Prepend an item to the deque.TurboStore::dfpop- Remove and return the first item of the deque.TurboStore::dbpop- Remove and return the last item of the deque.TurboStore::dpop- Remove and return an item of the deque by index.TurboStore::dfrem- Remove the first item of the deque.TurboStore::dbrem- Remove the last item of the deque.TurboStore::drem- Remove an item of the deque by index.TurboStore::dfpeek- Get the first item of the deque.TurboStore::dbpeek- Get the last item of the deque.TurboStore::dpeek- Get an item of the deque by index.TurboStore::dlen- Count the amount of items in the deque.TurboStore::expire- Update the expiration time of an item in the deque.TurboStore::dclear- Empty the deque.TurboStore::dmap- Overwrite each item of the deque.TurboStore::dall- Get all items in the deque.TurboStore::dftruncate- Keep only the first x items in the deque.TurboStore::dbtruncate- Keep only the last x items in the deque.
Note that rem operations are faster than pop operations due to the lack of need to decode
the removed value.
Items in deques have each their individual TTLs and expire individually.
§The Value Wrapper
Value is a wrapper that holds your data plus metadata like the expiry time. Currently, that
metadata is the expiry time of the value, which can be accessed with Value::expires_at.
§Example
This example shows how to set and get a value from the store. Operations with other data structures and operations follow a similar encoding/decoding style, so it’s easy to work with them. If you’ve worked with Redis in the past, you’ll find this crate familiar.
use std::hash::Hash;
use turbostore::{TurboStore, Value, Duration, Encode, Decode};
#[derive(Hash, PartialEq, Eq)]
enum Key {
User(i32),
}
#[derive(Debug, Encode, Decode, PartialEq, Eq, Clone)]
struct User {
id: i32,
name: String,
email: String,
}
let store: TurboStore<Key> = TurboStore::new();
let user = User {
id: 1,
name: "John Doe".into(),
email: "john@example.com".into()
};
store.set(
Key::User(1),
&user,
Duration::minutes(15)
).await;
// The first `Option` returns whether the key existed.
// The `Result` is the result of decoding the value. Since types are dynamic-ish, setting the
// wrong value will fail.
// The `Value` is TurboStore's internal wrapper over values, which stores metadata (e.g. expiry
// time).
let stored_user: Value<User> = store.get::<User>(&Key::User(1)).await.unwrap().unwrap();
assert_eq!(*stored_user, user);§Roadmap
Distribution both sharded and replicated is planned to be supported. Pipelines are also planned, since running multiple operations is currently neither atomic across ops nor as efficient as it could be (e.g. setting two items in a set fetches the set twice, once per op).
Contributions are welcome, whether it’s feedback, bug reports, or pull requests. Check out our GitHub repository for the source code and issue tracker.
Structs§
- Date
Time - ISO 8601 combined date and time with time zone.
- Turbo
Store - A concurrent, in-memory, in-process, Redis-like storage for Rust.
- Utc
- The UTC time zone. This is the most efficient time zone when you don’t need the local time. It is also used as an offset (which is also a dummy type).
- Value
- A TurboStore value.
Enums§
Traits§
- Decode
- A type which can be decoded from bytes with
decode. - Decode
Owned - A type which can be decoded without borrowing any bytes from the input.
- Encode
- A type which can be encoded to bytes with
encode.