Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
d8bd658
feat #43: When a Raft leader is elected, it should first send an empt…
JoshuaChi Apr 19, 2025
8fb91b3
Feature/59 benchmark (#72)
JoshuaChi Apr 20, 2025
2f97f6c
chore(ci) #73: prepare release for v0.1.2
JoshuaChi Apr 20, 2025
1756873
Merge branch 'main' into develop
JoshuaChi Apr 22, 2025
e53ba82
feat #79: Snapshot feature, Learner join cluster feature and develope…
JoshuaChi Sep 3, 2025
729e100
feat #79: optimize v0.1.3 CHANGELOG
JoshuaChi Sep 3, 2025
cebfdbb
Merge branch 'main' into develop
JoshuaChi Sep 3, 2025
0a057c8
fix #133: Example project: File/RocksDB storage engines can fail to s…
JoshuaChi Sep 7, 2025
bd44015
fix #133: fix fmt
JoshuaChi Sep 7, 2025
265c250
fix #133: fix fmt
JoshuaChi Sep 7, 2025
2e88aae
fix #133: fix fmt
JoshuaChi Sep 7, 2025
598f011
fix #133: update REAMDE
JoshuaChi Sep 7, 2025
68de4c7
feat #135: Enable tokio via three-nodes-cluster example
JoshuaChi Sep 17, 2025
3fe4cdc
feat #135: Enable tokio_metrics via three-nodes-cluster example
JoshuaChi Sep 17, 2025
f7911c1
feat #135: Enable tokio_metrics via three-nodes-cluster example
JoshuaChi Sep 18, 2025
4e30171
feat #138: v0.1.4 - target 100K throughput
JoshuaChi Oct 13, 2025
b03a606
fix #145: fix Bytes compatible bugs in examples folder
JoshuaChi Oct 14, 2025
f3f3430
fix #150: type issue
JoshuaChi Oct 14, 2025
32cc365
fix #151: fix mmap bug
JoshuaChi Oct 14, 2025
0713943
fix #152: Update README by introduce new feature from v0.1.4
JoshuaChi Oct 15, 2025
ff1e251
fix #153: fix code review feedback
JoshuaChi Oct 15, 2025
8659663
fix #154: code review feedback
JoshuaChi Oct 15, 2025
901008f
fix #155: fix code review feedback
JoshuaChi Oct 15, 2025
a3c314d
fix #156: fix code review feedback
JoshuaChi Oct 15, 2025
58f97dc
fix #157: fix code review feedback
JoshuaChi Oct 15, 2025
f145325
fix #158, #159: fix code review feedback
JoshuaChi Oct 16, 2025
7e0d96e
fix #164: fix code review bug
JoshuaChi Oct 16, 2025
b3e119e
fix #161: fix code review feedback
JoshuaChi Oct 16, 2025
9ea5116
fix #164: fix doc issues
JoshuaChi Oct 16, 2025
2169613
Merge branch 'main' into develop
JoshuaChi Oct 18, 2025
be93d34
feat #167: workspace structure (#175)
JoshuaChi Nov 5, 2025
d9636d1
feat #172: Feature ttl support (#176)
JoshuaChi Nov 12, 2025
2569af0
fix #172: format code to pass rustfmt checks
JoshuaChi Nov 12, 2025
e1143d0
fix #172: Replace tokio-tar with astral-tokio-tar to resolve CVE-2025…
JoshuaChi Nov 13, 2025
803400a
fix #172: Resolve Tokio runtime nesting issue in TTL benchmark
JoshuaChi Nov 13, 2025
69279ee
fix #172: Resolve clippy unused_unit warnings and improve Makefile be…
JoshuaChi Nov 13, 2025
56f1410
feat #174: Implement lock-free Watch mechanism for real-time key moni…
JoshuaChi Nov 25, 2025
6c90b57
feat #179: Implement single-node cluster support for dynamic expansio…
JoshuaChi Nov 26, 2025
97fab24
feat #182: Add EmbeddedEngine API with leader election notification &…
JoshuaChi Dec 1, 2025
ec736ab
doc #185: Add service discovery pattern guide for DNS/registry use cases
JoshuaChi Dec 3, 2025
c653330
doc #187: Restructure examples documentation to match folder structure
JoshuaChi Dec 3, 2025
635b3cd
doc #187: Fix broken benchmark documentation link in three-nodes-clus…
JoshuaChi Dec 3, 2025
5ed3544
docs #192: add comprehensive read consistency documentation
JoshuaChi Dec 4, 2025
e0b4142
feat(bench) #193: add --key-space parameter for hot-key testing
JoshuaChi Dec 5, 2025
9220e58
perf(watch) #194: Skip protobuf decoding when no active watchers exist
JoshuaChi Dec 5, 2025
0312010
docs(bench) #195: Update v0.2.0 benchmark report with accurate v0.1.4…
JoshuaChi Dec 5, 2025
208df8b
feat #196: Implement Watch feature with gRPC streaming and examples
JoshuaChi Dec 11, 2025
6d8ce99
docs #204: Prepare v0.2.0 release documentation
JoshuaChi Dec 11, 2025
2381550
fix #204: Suppress rustls-pemfile unmaintained warning in dependency …
JoshuaChi Dec 11, 2025
4163f9a
fix #204: Fix flaky test and update documentation for v0.2.0
JoshuaChi Dec 11, 2025
9123484
fix #205: Enable graceful shutdown during node startup phase
JoshuaChi Dec 12, 2025
0f94d9e
docs #204: Fix documentation links and optimize workspace doc generation
JoshuaChi Dec 12, 2025
52e1e74
doc #204: Simplify feature flags and add use-case guidance
JoshuaChi Dec 13, 2025
f7b0418
chore #204: Prepare v0.2.0 release with workspace cleanup
JoshuaChi Dec 13, 2025
793ab4c
style #204: Fix import ordering to comply with rustfmt stable
JoshuaChi Dec 13, 2025
faa62b3
fix #204: Address code review feedback for v0.2.0 release
JoshuaChi Dec 13, 2025
1b55a6f
perf #208: eliminate redundant async calls in leader write hot path
JoshuaChi Dec 13, 2025
86a1278
fix #204: update both fields in update_cluster_metadata to prevent st…
JoshuaChi Dec 13, 2025
bcf2239
fix #204: Increase election RPC timeout to fix flaky tests
JoshuaChi Dec 13, 2025
f601479
ci #204: Support docs as alias for doc in commit message validation
JoshuaChi Dec 14, 2025
b8eb206
docs #204: Prepare v0.2.0 for HN launch - simplify docs and set expec…
JoshuaChi Dec 14, 2025
8204c5c
docs #204: Fix documentation links and add integration mode quick sta…
JoshuaChi Dec 16, 2025
6e0c610
refactor(raft) #209: Architecture review - consolidate v0.2.0 improve…
JoshuaChi Dec 31, 2025
84c1539
doc #224: Fix rustdoc links and improve documentation generation
JoshuaChi Dec 31, 2025
40d7c47
doc #224: Fix markdown table formatting and typo in documentation
JoshuaChi Dec 31, 2025
c4a6e1f
doc #225: Prepare multi-crate publishing documentation and configurat…
JoshuaChi Jan 1, 2026
ea370fe
chore #227: Merge branch 'main' into develop
JoshuaChi Jan 1, 2026
0db60ee
doc #227: fix working directory and version requirements
JoshuaChi Jan 1, 2026
1d42cb4
chore #228: Merge branch 'main' into develop
JoshuaChi Jan 1, 2026
dcae76c
fix #228: Critical: Inconsistent reads in single-node mode (#229)
JoshuaChi Jan 1, 2026
4b8e0cb
perf #228: Optimize wait_applied with fast path and comprehensive tests
JoshuaChi Jan 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 42 additions & 73 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,120 +29,89 @@ with pluggable storage backends, built-in observability, and tokio runtime suppo
- **Tunable Persistence**: DiskFirst for durability or MemFirst for lower latency
- **Flexible Read Consistency**: Three-tier model (Linearizable/Lease-Based/Eventual)
- **Pluggable Storage**: Custom backends supported (RocksDB, Sled, Raw File)
- **Observability**: Built-in metrics, structured logging, distributed tracing
- **Extensible Design**: Business logic decoupled from protocol layer

---

## Quick Start

### Installation
## Quick Start (Embedded Mode)

Add d-engine to your `Cargo.toml`:

```toml
# Default: Embedded mode with RocksDB (recommended for most use cases)
d-engine = "0.2"
```

# Standalone mode: Connect to existing d-engine cluster
d-engine = { version = "0.2", features = ["client"], default-features = false }
Example code:

# Custom storage backend (disable RocksDB)
d-engine = { version = "0.2", features = ["server"], default-features = false }
```
```rust
use d_engine::prelude::*;
use std::time::Duration;

### When to Use
#[tokio::main]
async fn main() {
let engine = EmbeddedEngine::start().await.unwrap();
engine.wait_ready(Duration::from_secs(5)).await.unwrap();

#### Embedded Mode (Default) - Rust Applications
let client = engine.client();
client.put(b"hello".to_vec(), b"world".to_vec()).await.unwrap();
let value = client.get_linearizable(b"hello".to_vec()).await.unwrap();

```toml
d-engine = "0.2"
println!("Retrieved: {}", String::from_utf8_lossy(&value.unwrap()));
engine.stop().await.unwrap();
}
```

**Use when**: Building Rust services needing distributed coordination
**Examples**: Database HA coordinators, distributed schedulers, control plane services
**Why**: Zero-overhead (<0.1ms), single binary, type-safe

❌ **Don't use**: For non-Rust applications (use Standalone mode instead)
**→ Full example:** [examples/quick-start-embedded](examples/quick-start-embedded/README.md)

---

#### Standalone Mode - Non-Rust Applications
## Integration Modes

### Embedded Mode - In-Process Deployment

```toml
d-engine = { version = "0.2", features = ["client"], default-features = false }
d-engine = "0.2"
```

**Use when**: Go/Python/Java apps need distributed KV
**Examples**: Microservices requiring distributed KV, multi-language deployments
**Why**: Language-agnostic gRPC API, optimized for write-heavy workloads
**Use when**: Building Rust applications that need distributed coordination
**Why**: Zero-overhead (<0.1ms), single binary, zero network cost

\*Benchmarks show 51% higher write throughput vs etcd in test environments. Production results vary by hardware. See benches/ for methodology.
**→ Examples:**

❌ **Don't use**: For Rust apps (embedded mode has zero overhead)
- [Quick Start Embedded](examples/quick-start-embedded/README.md) - Minimal setup
- [Service Discovery Embedded](examples/service-discovery-embedded/README.md) - Watch API with sub-millisecond latency

---

#### Custom Storage - Advanced Use Cases
### Standalone Mode - Separate Service Deployment

```toml
d-engine = { version = "0.2", features = ["server"], default-features = false }
d-engine = { version = "0.2", features = ["client"], default-features = false }
```

**Use when**: Specific storage requirements (Sled, memory-only, cloud storage)
**Examples**: Embedded devices, testing environments
**See**: [Custom Storage Guide](https://docs.rs/d-engine/latest/d_engine/docs/server_guide/index.html#implementing-custom-storage-engines) | [Sled Example](examples/sled-cluster)
**Use when**: Application and d-engine run as separate processes
**Why**: Language-agnostic (Go/Python/Java/Rust), independent scaling, easier operations

---

## Basic Usage (Single-Node Mode)

```rust
use d_engine::prelude::*;
use std::time::Duration;
> **Performance note**: Benchmark shows 45% higher write throughput vs etcd 3.5 in high-concurrency tests (M2 Mac single machine vs etcd on 3 GCE instances). See [benches/](benches/d-engine-bench/reports/v0.2.0/) for methodology and hardware details.

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Start embedded engine with RocksDB (auto-creates directories)
let engine = EmbeddedEngine::with_rocksdb("./data", None).await?;
**Note**: Rust apps can use both modes - embedded for performance, standalone for operational flexibility

// Wait for leader election (single-node: instant)
engine.wait_ready(Duration::from_secs(5)).await?;
**→ Example:** [Quick Start Standalone (Go client)](examples/quick-start-standalone/README.md)

// Get KV client (zero-overhead, in-process)
let client = engine.client();
---

// Store and retrieve data
client.put(b"hello".to_vec(), b"world".to_vec()).await?;
## Advanced: Custom Storage Backends

if let Some(value) = client.get(b"hello".to_vec()).await? {
println!("Retrieved: hello = {}", String::from_utf8_lossy(&value));
}
For specific storage requirements (Sled, memory-only, cloud storage):

// Graceful shutdown
engine.stop().await?;
Ok(())
}
```toml
d-engine = { version = "0.2", features = ["server"], default-features = false }
```

## **Custom Storage Implementations**

d-engine provides flexible storage abstraction layers. Implement your own storage engines and state machines by implementing the respective traits:

- **Custom Storage Engines**: See [Implementing Custom Storage Engines](https://docs.rs/d-engine/latest/d_engine/docs/server_guide/index.html#implementing-custom-storage-engines)
- **Custom State Machines**: See [Implementing Custom State Machines](https://docs.rs/d-engine/latest/d_engine/docs/server_guide/index.html#implementing-custom-state-machines)

Note: Single-node deployment is supported for development and low-traffic production. For high availability, you can dynamically expand to a 3-node cluster with zero downtime.

---

## Architecture

**Hybrid threading model**: Single-threaded Raft core + async I/O layer
Implement custom storage engines and state machines by implementing the respective traits:

- **Consensus logic**: Dedicated event loop (eliminates lock contention)
- **Network/Storage**: Tokio's async runtime for efficient I/O multiplexing
- **Design goal**: Maximize single CPU core performance while minimizing resource overhead
- **Custom Storage Engines**: [Implementation Guide](https://docs.rs/d-engine/latest/d_engine/docs/server_guide/index.html#implementing-custom-storage-engines)
- **Custom State Machines**: [Implementation Guide](https://docs.rs/d-engine/latest/d_engine/docs/server_guide/index.html#implementing-custom-state-machines)
- **Example**: [Sled Storage Backend](examples/sled-cluster)

---

Expand Down
4 changes: 4 additions & 0 deletions config/base/raft.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ max_buffered_entries = 10000
default_policy = "LinearizableRead"
lease_duration_ms = 250
allow_client_override = true
# Timeout to wait for state machine to catch up with commit index (milliseconds)
# Used by LinearizableRead to ensure reads reflect all committed writes
# Typical apply latency: <1ms on local SSD. Default: 10ms (safe buffer)
state_machine_sync_timeout_ms = 10


# Granular RPC compression configuration
Expand Down
13 changes: 13 additions & 0 deletions d-engine-core/src/config/raft.rs
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,14 @@ pub struct ReadConsistencyConfig {
/// When false, all reads use the cluster's default_policy setting.
#[serde(default = "default_allow_client_override")]
pub allow_client_override: bool,

/// Timeout in milliseconds to wait for state machine to catch up with commit index
///
/// Used by LinearizableRead to ensure the state machine has applied all committed
/// entries before serving reads. Typical apply latency is <1ms on local SSD.
/// Default: 10ms (safe buffer for single-node local deployments)
#[serde(default = "default_state_machine_sync_timeout_ms")]
pub state_machine_sync_timeout_ms: u64,
}

impl Default for ReadConsistencyConfig {
Expand All @@ -957,6 +965,7 @@ impl Default for ReadConsistencyConfig {
default_policy: ReadConsistencyPolicy::default(),
lease_duration_ms: default_lease_duration_ms(),
allow_client_override: default_allow_client_override(),
state_machine_sync_timeout_ms: default_state_machine_sync_timeout_ms(),
}
}
}
Expand All @@ -971,6 +980,10 @@ fn default_allow_client_override() -> bool {
true
}

fn default_state_machine_sync_timeout_ms() -> u64 {
10 // 10ms is safe for typical <1ms apply latency on local SSD
}

impl ReadConsistencyConfig {
fn validate(&self) -> Result<()> {
// Validate read consistency configuration
Expand Down
20 changes: 15 additions & 5 deletions d-engine-core/src/raft_role/leader_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -911,10 +911,13 @@ impl<T: TypeConfig> RaftRoleState for LeaderState<T> {
Err(tonic::Status::failed_precondition(
"enforce_quorum_consensus failed".to_string(),
))
} else if let Err(e) = self.ensure_state_machine_upto_commit_index(
&ctx.handlers.state_machine_handler,
last_applied_index,
) {
} else if let Err(e) = self
.ensure_state_machine_upto_commit_index(
&ctx.handlers.state_machine_handler,
last_applied_index,
)
.await
{
warn!(
"ensure_state_machine_upto_commit_index failed for linear read request"
);
Expand Down Expand Up @@ -1842,7 +1845,7 @@ impl<T: TypeConfig> LeaderState<T> {
(false, current_commit_index)
}

pub fn ensure_state_machine_upto_commit_index(
pub async fn ensure_state_machine_upto_commit_index(
&self,
state_machine_handler: &Arc<SMHOF<T>>,
last_applied: u64,
Expand All @@ -1856,6 +1859,13 @@ impl<T: TypeConfig> LeaderState<T> {
if last_applied < commit_index {
state_machine_handler.update_pending(commit_index);

// Wait for state machine to catch up
// This ensures linearizable reads see all committed writes
let timeout_ms = self.node_config.raft.read_consistency.state_machine_sync_timeout_ms;
state_machine_handler
.wait_applied(commit_index, std::time::Duration::from_millis(timeout_ms))
.await?;

debug!("ensure_state_machine_upto_commit_index success");
}
Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ where
/// Temporary lock when snapshot is generated (to prevent concurrent snapshot generation)
snapshot_lock: RwLock<()>,

/// Watch channel to notify when last_applied advances
/// Used for linearizable reads to wait until state machine catches up with commit_index
applied_notify_tx: tokio::sync::watch::Sender<u64>,
applied_notify_rx: tokio::sync::watch::Receiver<u64>,

/// Broadcast channel for watch event notifications (fire-and-forget)
/// StateMachine sends events here without blocking on watchers
#[cfg(feature = "watch")]
Expand Down Expand Up @@ -155,6 +160,44 @@ where
}
}

async fn wait_applied(
&self,
target_index: u64,
timeout: Duration,
) -> Result<()> {
let mut rx = self.applied_notify_rx.clone();

// Fast path: check if already applied (avoid timeout future overhead)
{
let current = *rx.borrow();
if current >= target_index {
return Ok(());
}
}

// Slow path: need to wait for notification
tokio::time::timeout(timeout, async {
loop {
rx.changed()
.await
.map_err(|_| crate::Error::Fatal("apply notify channel closed".into()))?;

let current = *rx.borrow();
if current >= target_index {
return Ok(());
}
}
})
.await
.map_err(|_| {
let current_applied = *rx.borrow();
crate::Error::Fatal(format!(
"Timeout waiting for state machine to apply index {target_index} \
(timeout: {timeout:?}, current_applied: {current_applied})"
))
})?
}

async fn apply_chunk(
&self,
chunk: Vec<Entry>,
Expand Down Expand Up @@ -210,6 +253,10 @@ where
// Efficiently obtain the maximum index: directly get the index of the last entry
if let Some(idx) = last_index {
self.last_applied.store(idx, Ordering::Release);
// Notify waiters that last_applied has advanced
if let Err(e) = self.applied_notify_tx.send(idx) {
debug_assert!(false, "apply notify send failed: {e:?}");
}
}

metrics::counter!(
Expand Down Expand Up @@ -764,6 +811,9 @@ where
tokio::sync::broadcast::Sender<d_engine_proto::client::WatchResponse>,
>,
) -> Self {
let (applied_notify_tx, applied_notify_rx) =
tokio::sync::watch::channel(last_applied_index);

Self {
node_id,
last_applied: AtomicU64::new(last_applied_index),
Expand All @@ -779,6 +829,8 @@ where

snapshot_lock: RwLock::new(()),
snapshot_in_progress: AtomicBool::new(false),
applied_notify_tx,
applied_notify_rx,
#[cfg(feature = "watch")]
watch_event_tx,
}
Expand Down Expand Up @@ -1194,6 +1246,16 @@ where
pub fn snapshot_in_progress(&self) -> bool {
self.snapshot_in_progress.load(Ordering::Acquire)
}

/// Test helper: simulate state machine applying to target index
#[cfg(test)]
pub fn test_simulate_apply(
&self,
target_index: u64,
) {
self.last_applied.store(target_index, Ordering::Release);
let _ = self.applied_notify_tx.send(target_index);
}
}

/// Helper function to create zero-copy Bytes from memory map
Expand Down
14 changes: 14 additions & 0 deletions d-engine-core/src/state_machine_handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub use snapshot_policy::*;
mod default_state_machine_handler_test;
#[cfg(test)]
mod snapshot_assembler_test;
#[cfg(test)]
mod wait_applied_test;

use std::sync::Arc;

Expand Down Expand Up @@ -78,6 +80,18 @@ where
new_commit: u64,
);

/// Waits until the state machine has applied entries up to the target index.
/// Returns error if timeout is reached before the target is applied.
///
/// This is used to ensure linearizable reads: after leader confirms a log entry
/// is committed (via quorum), we must wait for the state machine to apply it
/// before reading to guarantee the read reflects all committed writes.
async fn wait_applied(
&self,
target_index: u64,
timeout: std::time::Duration,
) -> Result<()>;

/// Applies a batch of committed log entries to the state machine
async fn apply_chunk(
&self,
Expand Down
Loading
Loading