Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 18 additions & 9 deletions crates/ember-core/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
//! of the key. Each shard is an independent tokio task — no locks on
//! the hot path.

use std::hash::{Hash, Hasher};

use crate::dropper::DropHandle;
use crate::error::ShardError;
use crate::keyspace::ShardConfig;
Expand Down Expand Up @@ -197,14 +195,25 @@ impl Engine {

/// Pure function: maps a key to a shard index.
///
/// Uses ahash (AHash) for fast, non-cryptographic hashing. ~3x faster
/// than SipHash for short keys. Deterministic within a single process —
/// that's all we need for local sharding. Shard routing is trusted
/// internal logic so DoS-resistant hashing is unnecessary here.
/// Uses FNV-1a hashing for deterministic shard routing across restarts.
/// This is critical for AOF/snapshot recovery — keys must hash to the
/// same shard on every startup, otherwise recovered data lands in the
/// wrong shard.
///
/// FNV-1a is simple, fast for short keys, and completely deterministic
/// (no per-process randomization). Shard routing is trusted internal
/// logic so DoS-resistant hashing is unnecessary here.
fn shard_index(key: &str, shard_count: usize) -> usize {
let mut hasher = ahash::AHasher::default();
key.hash(&mut hasher);
(hasher.finish() as usize) % shard_count
// FNV-1a 64-bit
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;

let mut hash = FNV_OFFSET;
for byte in key.as_bytes() {
hash ^= *byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
(hash as usize) % shard_count
}

#[cfg(test)]
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ harness = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "io-util", "time"] }
bytes = { workspace = true }
ember-protocol = { workspace = true }
ember-server = { path = "../../crates/ember-server", default-features = false }
prost-reflect = { workspace = true }
tempfile = "3"

[features]
protobuf = []
protobuf = ["ember-server/protobuf"]
8 changes: 6 additions & 2 deletions tests/integration/src/basic_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use ember_protocol::Frame;

use crate::helpers::TestServer;
use crate::helpers::{ServerOptions, TestServer};

#[tokio::test]
async fn ping_pong() {
Expand Down Expand Up @@ -206,7 +206,11 @@ async fn type_command() {

#[tokio::test]
async fn rename() {
let server = TestServer::start();
// use a single shard so RENAME doesn't hit cross-shard errors
let server = TestServer::start_with(ServerOptions {
shards: Some(1),
..Default::default()
});
let mut c = server.connect().await;

c.ok(&["SET", "old", "value"]).await;
Expand Down
4 changes: 3 additions & 1 deletion tests/integration/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct ServerOptions {
pub cluster_bootstrap: bool,
/// Enable protobuf value storage.
pub protobuf: bool,
/// Number of shards (defaults to 2 for test coverage).
pub shards: Option<usize>,
/// Use concurrent (DashMap) mode instead of sharded channels.
pub concurrent: bool,
}
Expand All @@ -54,7 +56,7 @@ impl TestServer {
let mut cmd = Command::new(&binary);
cmd.arg("--port").arg(port.to_string());
cmd.arg("--host").arg("127.0.0.1");
cmd.arg("--shards").arg("2");
cmd.arg("--shards").arg(opts.shards.unwrap_or(2).to_string());
// suppress tracing output in tests
cmd.env("RUST_LOG", "error");

Expand Down
Loading