Crate turbostore

Crate turbostore 

Source
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:

§Hash Maps

Turbostore supports storing sub-hash-maps inside the store.

The available operations on hash maps are:

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:

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:

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§

DateTime
ISO 8601 combined date and time with time zone.
TurboStore
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§

Error

Traits§

Decode
A type which can be decoded from bytes with decode.
DecodeOwned
A type which can be decoded without borrowing any bytes from the input.
Encode
A type which can be encoded to bytes with encode.

Type Aliases§

Duration
Alias of TimeDelta.

Derive Macros§

Decode
Encode