KV Store
KV Store in Shizuku provides a high-level abstraction over NATS JetStream’s Key-Value store. It offers features like atomic operations, distributed locking, and structured data storage.
For more information, see API Reference
Basic Operations
The KV Store functionality is built around the KeyValue
trait, which provides basic operations for storing and retrieving data. Here’s how to use it with dynamic keys:
use shizuku::kv::KeyValue;
// Read a value with dynamic keylet value = MyDynamicValue::read_from(&store, key).await?;
// Write a valuemy_value.write_to_anyway(&store).await?;
// Atomic write with version checkmy_value.write_to_atomically(&store, revision).await?;
// Delete a valueMyDynamicValue::delete_anyway(&store, key).await?;
Static Key Values
For values that always use the same key, you should implement the StaticKeyIndexedValue
trait instead of using dynamic keys. This is the recommended approach for most use cases:
use shizuku::kv::{KeyValue, StaticKeyIndexedValue};
// Define a type with a static keystruct MyConfig { // your fields here}
impl StaticKeyIndexedValue for MyConfig { fn key() -> String { "app.config".to_string() }}
// Now you can use the KV operationslet value = MyConfig::read_from(&store, MyConfig::key()).await?;
// Write a valuemy_config.write_to_anyway(&store).await?;
// Atomic write with version checkmy_config.write_to_atomically(&store, revision).await?;
// Delete a valueMyConfig::delete_anyway(&store, MyConfig::key()).await?;
This approach is particularly useful for configuration-like values that have fixed keys.
Distributed Read-Write Lock
Shizuku provides a distributed read-write lock implementation through DistroRwLock
. This lock follows these principles:
- Multiple readers can access the resource simultaneously
- Only one writer can access the resource at a time
- Writers have priority - new read requests are blocked when a writer is waiting
Basic Lock Usage
use shizuku::kv::rw_lock::DistroRwLock;
// Acquire read lockDistroRwLock::acquire_read(&store, "my-resource").await?;// ... perform read operations ...DistroRwLock::release_read(&store, "my-resource").await?;
// Acquire write lockDistroRwLock::acquire_write(&store, "my-resource").await?;// ... perform write operations ...DistroRwLock::release_write(&store, "my-resource").await?;
RAII Lock Wrappers
For safer lock handling, Shizuku provides RAII-style wrappers that automatically acquire and release locks. Since these wrappers are lightweight (containing only an Arc<P>
, &'static Store
, and a key), they can be created on demand:
use shizuku::kv::rw_lock::{LockedResourceReadProcessor, LockedResourceWriteProcessor};
// Create read-locked processor on the flylet result = LockedResourceReadProcessor::new( Arc::new(my_processor), &store, "my-resource").process(input).await?;
// Create write-locked processor on the flylet result = LockedResourceWriteProcessor::new( Arc::new(my_processor), &store, "my-resource").process(input).await?;
Watching for Changes
You can watch for changes to values in the KV store:
use shizuku::kv::KeyValueRead;
let watch = MyValue::watch(&store, key).await?;while let Some(entry) = watch.next().await { // Handle updated value}
Error Handling
The KV store operations can produce several types of errors:
KvReadError
: Errors during read operationsKvWriteError
: Errors during write operationsWithLockProcessError
: Errors when using lock processorsDistroRwLockError
: Errors specific to distributed lock operations
Always handle these errors appropriately in your application code.
Best Practices
- Use RAII lock wrappers (
LockedResourceReadProcessor
andLockedResourceWriteProcessor
) instead of manual lock acquisition/release when possible - Implement
StaticKeyIndexedValue
for configuration-like values that have fixed keys - Use atomic operations when you need to ensure consistency
- Consider using the watch functionality for reactive updates to stored values
- Keep lock holding times as short as possible to prevent contention
Security Note
The KV Store does not encrypt data by default. If you need to store sensitive information (more sensitive than database passwords), implement your own encryption before storing the data.
Configuration Storage Example
KV Store is ideal for storing application configuration like database credentials. Here’s a recommended pattern:
use shizuku::kv::{KeyValue, StaticKeyIndexedValue};use serde::{Serialize, Deserialize};use shizuku::JsonByteDes;
#[derive(Serialize, Deserialize, JsonByteDes)]struct DatabaseConfig { url: String, username: String, password: String, max_connections: u32,}
impl StaticKeyIndexedValue for DatabaseConfig { fn key() -> String { "config.database".to_string() }}
async fn initialize_database(store: &Store) -> Result<DatabasePool, Error> { // Read config from KV store let config = match DatabaseConfig::read_from(store, DatabaseConfig::key()).await? { Some(config) => config, None => return Err(Error::ConfigNotFound), };
// Initialize database connection pool DatabasePool::connect(&config.url) .with_credentials(&config.username, &config.password) .with_max_connections(config.max_connections) .build() .await}
Since KV Store is distributed, you can update the configuration on any node and all other nodes will receive the updates. While KV Store doesn’t encrypt data by default, it’s secure enough for database credentials. For more sensitive data, implement your own encryption.