Page MenuHomePhabricator

TRANSACTION_API.md
No OneTemporary

TRANSACTION_API.md

# Transaction API Documentation
## Overview
The store library provides a flexible and composable transaction system that allows atomic operations across multiple stores. The API has been redesigned to be more generic and extensible, supporting both built-in stores (RangeStore, NamespaceStore) and custom user-defined stores.
## Key Features
- **Composable Transactions**: Mix and match different stores in a single transaction
- **Type Safety**: Store-specific transaction contexts prevent API misuse
- **Extensibility**: Easy integration of custom stores via the `TransactionProvider` trait
- **Atomic Operations**: All-or-nothing semantics with automatic rollback on errors
- **Backward Compatibility**: Legacy API remains available
## Core Concepts
### TransactionProvider Trait
Any store that wants to participate in transactions must implement the `TransactionProvider` trait:
```rust
pub trait TransactionProvider {
fn transaction_trees(&self) -> Vec<&sled::Tree>;
}
```
This trait tells the transaction system which sled trees need to be included in the transaction.
### Transaction Contexts
Each store type has its own transaction context that provides type-safe access to store operations within a transaction:
- `RangeTransactionContext` - for RangeStore operations
- `NamespaceTransactionContext` - for NamespaceStore operations
- Custom contexts for user-defined stores
## API Reference
### Basic Transaction API
#### Transaction
The basic `Transaction` struct supports the core built-in stores:
```rust
let transaction = Transaction::new()
.with_range_store(&range_store)
.with_namespace_store(&namespace_store)
.with_tree(&metadata_tree);
let result = transaction.execute(|ctx| {
let ip_bit = ctx.use_range().assign("ip_pool", "192.168.1.100")?;
let reserved = ctx.use_namespace().reserve("users", "alice", "data")?;
if let Some(tree) = ctx.tree(0) {
tree.insert("last_operation", "user_assignment")?;
}
Ok((ip_bit, reserved))
})?;
```
#### TransactionContext Methods
- `use_range()` - Returns `RangeTransactionContext` for range operations
- `use_namespace()` - Returns `NamespaceTransactionContext` for namespace operations
- `tree(index)` - Access additional trees by index
- `raw_tree(index)` - Direct access to transactional trees
- `all_trees()` - Access to all transactional trees
### Extended Transaction API
#### ExtendedTransaction
The extended API supports custom stores with type identification:
```rust
let transaction = ExtendedTransaction::new()
.with_range_store(&range_store)
.with_namespace_store(&namespace_store)
.with_custom_store(&audit_store, "audit")
.with_tree(&config_tree);
let result = transaction.execute(|ctx| {
let ip_bit = ctx.use_range().assign("ip_pool", "192.168.1.100")?;
let reserved = ctx.use_namespace().reserve("users", "alice", "data")?;
// Access custom store trees by type name
if let Some(trees) = ctx.custom_store_trees("audit") {
// Use audit store trees directly
}
Ok((ip_bit, reserved))
})?;
```
#### ExtendedTransactionContext Methods
- `use_range()` - Returns `RangeTransactionContext`
- `use_namespace()` - Returns `NamespaceTransactionContext`
- `custom_store_trees(type_name)` - Get trees for a custom store by type name
- `tree(index)` - Access additional trees by index
- `raw_tree(index)` - Direct access to transactional trees
- `store_map()` - Get mapping of store types to tree indices
### Store-Specific Transaction Contexts
#### RangeTransactionContext
```rust
// Within a transaction
let range_ctx = ctx.use_range();
// Assign a value to a range
let bit_position = range_ctx.assign("ip_pool", "192.168.1.100")?;
// Get assignment details
let value = range_ctx.get("ip_pool", bit_position)?;
```
#### NamespaceTransactionContext
```rust
// Within a transaction
let ns_ctx = ctx.use_namespace();
// Reserve a key
let reserved = ns_ctx.reserve("users", "alice", "user_data")?;
// Get a value
let value = ns_ctx.get("users", "alice")?;
// Remove a key
let removed = ns_ctx.remove("users", "alice")?;
```
## Creating Custom Stores
### Step 1: Implement TransactionProvider
```rust
pub struct MyStore {
data: sled::Tree,
metadata: sled::Tree,
}
impl TransactionProvider for MyStore {
fn transaction_trees(&self) -> Vec<&sled::Tree> {
vec![&self.data, &self.metadata]
}
}
```
### Step 2: Create Transaction Methods
Add methods to your store that work within transactions:
```rust
impl MyStore {
pub fn set_in_transaction(
&self,
data: &sled::transaction::TransactionalTree,
metadata: &sled::transaction::TransactionalTree,
key: &str,
value: &str,
) -> sled::transaction::ConflictableTransactionResult<(), ()> {
data.insert(key, value)?;
metadata.insert("last_modified", &timestamp().to_be_bytes())?;
Ok(())
}
}
```
### Step 3: Create Helper Functions
For the basic Transaction API, create helper functions:
```rust
pub fn use_my_store_in_transaction<'a, 'ctx>(
ctx: &'ctx TransactionContext<'a, 'ctx>,
store: &'a MyStore,
base_index: usize,
) -> MyTransactionHelper<'a, 'ctx> {
MyTransactionHelper {
store,
data: ctx.raw_tree(base_index).unwrap(),
metadata: ctx.raw_tree(base_index + 1).unwrap(),
}
}
```
For the extended API, use the custom store trees feature:
```rust
pub fn use_my_store_extended<'a, 'ctx>(
ctx: &'ctx ExtendedTransactionContext<'a, 'ctx>,
store: &'a MyStore,
) -> Option<MyTransactionHelper<'a, 'ctx>> {
let trees = ctx.custom_store_trees("my_store")?;
Some(MyTransactionHelper {
store,
data: trees[0],
metadata: trees[1],
})
}
```
### Step 4: Use in Transactions
```rust
// With ExtendedTransaction
let transaction = ExtendedTransaction::new()
.with_custom_store(&my_store, "my_store");
transaction.execute(|ctx| {
let helper = use_my_store_extended(ctx, &my_store)
.ok_or_else(|| Error::StoreError(sled::Error::Unsupported("Store not available".to_string())))?;
helper.set("key", "value")?;
Ok(())
})?;
```
## Error Handling
All transaction operations follow Rust's `Result` pattern. When any operation within a transaction returns an error, the entire transaction is automatically rolled back.
### Common Error Types
- `Error::StoreError(sled::Error)` - Underlying sled database errors
- `Error::UndefinedRange(String)` - Range not defined
- `Error::RangeFull(String)` - Range has no available slots
- `Error::UndefinedNamespace(String)` - Namespace not defined
- `Error::NamespaceKeyReserved(String, String)` - Key already exists in namespace
### Error Handling Best Practices
```rust
let result = transaction.execute(|ctx| {
// Operations that might fail
let bit = ctx.use_range().assign("pool", "value")
.map_err(|e| {
log::error!("Failed to assign range: {}", e);
e
})?;
Ok(bit)
});
match result {
Ok(bit) => println!("Successfully assigned bit: {}", bit),
Err(Error::RangeFull(pool)) => {
println!("Pool {} is full, need to expand", pool);
}
Err(e) => {
log::error!("Transaction failed: {}", e);
}
}
```
## Migration Guide
### From CombinedTransaction to Transaction
**Old API:**
```rust
let combined = CombinedTransaction::new(
&range_store,
&namespace_store,
vec![&extra_tree],
);
combined.execute(|ctx| {
ctx.assign_range("pool", "value")?;
ctx.reserve_namespace("users", "key", "value")?;
ctx.additional_tree(0)?.insert("data", "value")?;
Ok(())
})?;
```
**New API:**
```rust
let transaction = Transaction::new()
.with_range_store(&range_store)
.with_namespace_store(&namespace_store)
.with_tree(&extra_tree);
transaction.execute(|ctx| {
ctx.use_range().assign("pool", "value")?;
ctx.use_namespace().reserve("users", "key", "value")?;
ctx.tree(0)?.insert("data", "value")?;
Ok(())
})?;
```
### Benefits of Migration
1. **Better Type Safety**: Store-specific contexts prevent mixing operations
2. **Clearer API**: Methods are namespaced by store type
3. **Extensibility**: Easy to add custom stores
4. **Composability**: Mix and match stores as needed
## Performance Considerations
- **Transaction Scope**: Keep transactions as small as possible
- **Tree Count**: More trees in a transaction may impact performance
- **Contention**: Concurrent transactions on the same trees will retry
- **Memory Usage**: Large transactions hold more data in memory
## Best Practices
1. **Single Responsibility**: Each transaction should have a clear, single purpose
2. **Error Handling**: Always handle specific error types appropriately
3. **Testing**: Write tests that include rollback scenarios
4. **Documentation**: Document transaction semantics for custom stores
5. **Logging**: Add appropriate logging for transaction operations
## Examples
See the `examples/` directory for complete working examples:
- `simple_custom_store.rs` - Basic custom store integration
- `extended_custom_store.rs` - Advanced patterns (with lifetime constraints)
- `custom_store.rs` - Conceptual example (demonstration only)
## Legacy Support
The original `CombinedTransaction` and `CombinedTransactionContext` types are still available for backward compatibility. However, new code should use the updated API for better composability and type safety.

File Metadata

Mime Type
text/plain
Expires
Mon, Jun 9, 11:33 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
47643
Default Alt Text
TRANSACTION_API.md (9 KB)

Event Timeline