#![cfg(feature = "noxu")]
use std::sync::Arc;
use dyniak::proto::pb::{MessageCode, RpbContent, RpbGetReq, RpbGetResp, RpbPutReq, RpbPutResp};
use dyniak::server::serve_pbc;
use dyniak::vclock::VClock;
use dynomite::embed::Datastore;
use prost::Message;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
async fn send_frame(stream: &mut TcpStream, code: u8, body: &[u8]) {
let len = u32::try_from(body.len() + 1).expect("frame length fits u32");
stream.write_all(&len.to_be_bytes()).await.expect("len");
stream.write_all(&[code]).await.expect("code");
stream.write_all(body).await.expect("body");
}
async fn recv_frame(stream: &mut TcpStream) -> (u8, Vec<u8>) {
let mut len_buf = [0u8; 4];
stream.read_exact(&mut len_buf).await.expect("len");
let len = u32::from_be_bytes(len_buf) as usize;
let mut buf = vec![0u8; len];
stream.read_exact(&mut buf).await.expect("frame");
let code = buf[0];
(code, buf[1..].to_vec())
}
#[tokio::test]
async fn put_returns_vclock_get_returns_it_and_writes_advance_it() {
let dir = tempfile::tempdir().expect("tempdir");
let ds: Arc<dyn Datastore> =
Arc::new(dyniak::datastore::NoxuDatastore::open_transactional(dir.path()).expect("noxu"));
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("addr");
let server = tokio::spawn(async move {
let _ = serve_pbc(listener, ds).await;
});
let mut c = TcpStream::connect(addr).await.expect("connect");
let put1 = RpbPutReq {
bucket: b"cart".to_vec(),
key: Some(b"k1".to_vec()),
content: Some(RpbContent {
value: b"v1".to_vec(),
..RpbContent::default()
}),
..RpbPutReq::default()
};
send_frame(&mut c, MessageCode::PutReq.as_u8(), &put1.encode_to_vec()).await;
let (code, body) = recv_frame(&mut c).await;
assert_eq!(code, MessageCode::PutResp.as_u8());
let resp1 = RpbPutResp::decode(body.as_slice()).expect("put resp");
let vclock1 = resp1.vclock.expect("put returns a vclock");
assert!(!vclock1.is_empty(), "first write has a non-empty context");
let clock1 = VClock::decode(&vclock1);
let get = RpbGetReq {
bucket: b"cart".to_vec(),
key: b"k1".to_vec(),
..RpbGetReq::default()
};
send_frame(&mut c, MessageCode::GetReq.as_u8(), &get.encode_to_vec()).await;
let (code, body) = recv_frame(&mut c).await;
assert_eq!(code, MessageCode::GetResp.as_u8());
let gresp = RpbGetResp::decode(body.as_slice()).expect("get resp");
assert_eq!(
gresp.vclock.as_deref(),
Some(vclock1.as_slice()),
"GET returns the stored context"
);
let put2 = RpbPutReq {
bucket: b"cart".to_vec(),
key: Some(b"k1".to_vec()),
vclock: Some(vclock1.clone()),
content: Some(RpbContent {
value: b"v2".to_vec(),
..RpbContent::default()
}),
..RpbPutReq::default()
};
send_frame(&mut c, MessageCode::PutReq.as_u8(), &put2.encode_to_vec()).await;
let (_code, body) = recv_frame(&mut c).await;
let resp2 = RpbPutResp::decode(body.as_slice()).expect("put resp 2");
let vclock2 = resp2.vclock.expect("put 2 returns a vclock");
let clock2 = VClock::decode(&vclock2);
assert_eq!(
clock2.partial_cmp(&clock1),
Some(std::cmp::Ordering::Greater),
"the second write's context strictly dominates the first"
);
server.abort();
let _ = server.await;
}
#[derive(Debug)]
struct NoopOutbound;
impl dyniak::router::PeerOutbound for NoopOutbound {
fn dispatch(
&self,
_peer_idx: u32,
_op: dyniak::router::PeerOp,
) -> dynomite::embed::hooks::BoxFuture<'_, ()> {
Box::pin(async {})
}
}
#[tokio::test]
async fn concurrent_writes_surface_as_siblings_under_allow_mult() {
use dyniak::bucket_props::{BucketProps, BucketPropsRegistry};
use dyniak::replication::{RingPoint, RingView};
use dyniak::router::{BucketRouter, RoutingHooks};
use dyniak::server::serve_pbc_with_routing;
use dynomite::cluster::admin_rpc::NoopClusterAdmin;
use dynomite::hashkit::HashType;
let dir = tempfile::tempdir().expect("tempdir");
let ds: Arc<dyn Datastore> =
Arc::new(dyniak::datastore::NoxuDatastore::open_transactional(dir.path()).expect("noxu"));
let registry = Arc::new(BucketPropsRegistry::new_riak_defaults());
registry.set(
b"",
b"cart",
BucketProps {
allow_mult: Some(true),
..BucketProps::default()
},
);
let span = u64::from(u32::MAX);
let pts: Vec<RingPoint> = (0..3u32)
.map(|i| RingPoint::new(u64::from(i) * span / 3, i, "dc1", "r1"))
.collect();
let router = Arc::new(BucketRouter::new(
registry,
Arc::new(RingView::new(pts)),
HashType::Murmur,
));
let hooks = RoutingHooks {
router,
outbound: Arc::new(NoopOutbound) as Arc<dyn dyniak::router::PeerOutbound>,
local_actor: dyniak::datatypes::ActorId::new("dc1", "local"),
local_peer_idx: 0,
precommit: None,
postcommit: None,
};
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("addr");
let admin = Arc::new(NoopClusterAdmin);
let server = tokio::spawn(async move {
let _ = serve_pbc_with_routing(listener, ds, admin, hooks).await;
});
let mut c = TcpStream::connect(addr).await.expect("connect");
for v in [b"red".as_slice(), b"blue".as_slice()] {
let put = RpbPutReq {
bucket: b"cart".to_vec(),
key: Some(b"k".to_vec()),
content: Some(RpbContent {
value: v.to_vec(),
..RpbContent::default()
}),
..RpbPutReq::default()
};
send_frame(&mut c, MessageCode::PutReq.as_u8(), &put.encode_to_vec()).await;
let (code, _) = recv_frame(&mut c).await;
assert_eq!(code, MessageCode::PutResp.as_u8());
}
let get = RpbGetReq {
bucket: b"cart".to_vec(),
key: b"k".to_vec(),
..RpbGetReq::default()
};
send_frame(&mut c, MessageCode::GetReq.as_u8(), &get.encode_to_vec()).await;
let (code, body) = recv_frame(&mut c).await;
assert_eq!(code, MessageCode::GetResp.as_u8());
let resp = RpbGetResp::decode(body.as_slice()).expect("get resp");
assert_eq!(
resp.content.len(),
2,
"two concurrent writes surface as two siblings under allow_mult"
);
let values: std::collections::BTreeSet<Vec<u8>> =
resp.content.iter().map(|c| c.value.clone()).collect();
assert!(values.contains(b"red".as_slice()));
assert!(values.contains(b"blue".as_slice()));
server.abort();
let _ = server.await;
}
#[derive(Clone)]
struct PerPeerStores {
stores: std::sync::Arc<std::collections::HashMap<u32, Arc<dyniak::datastore::NoxuDatastore>>>,
}
impl std::fmt::Debug for PerPeerStores {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PerPeerStores").finish_non_exhaustive()
}
}
impl dyniak::router::PeerOutbound for PerPeerStores {
fn dispatch(
&self,
peer_idx: u32,
op: dyniak::router::PeerOp,
) -> dynomite::embed::hooks::BoxFuture<'_, ()> {
Box::pin(async move {
if let dyniak::router::PeerOp::RepairPut {
bucket,
key,
storage,
..
} = op
{
if let Some(ds) = self.stores.get(&peer_idx) {
let _ = ds.put_object(&bucket, &key, &storage, &[]);
}
}
})
}
fn request(
&self,
peer_idx: u32,
op: dyniak::router::PeerOp,
) -> dynomite::embed::hooks::BoxFuture<'_, Option<Vec<u8>>> {
Box::pin(async move {
let dyniak::router::PeerOp::Get { bucket, key, .. } = op else {
return None;
};
let ds = self.stores.get(&peer_idx)?;
match ds.get_object(&bucket, &key) {
Ok(Some(b)) => Some(b),
_ => Some(Vec::new()),
}
})
}
}
#[tokio::test]
async fn coordinated_read_merges_sibling_sets_across_replicas() {
use dyniak::bucket_props::{BucketProps, BucketPropsRegistry};
use dyniak::proto::http::object::{HttpObject, SiblingSet};
use dyniak::replication::{RingPoint, RingView};
use dyniak::router::{BucketRouter, RoutingHooks};
use dynomite::hashkit::HashType;
let dirs: Vec<tempfile::TempDir> = (0..3).map(|_| tempfile::tempdir().unwrap()).collect();
let mut map = std::collections::HashMap::new();
for (i, d) in dirs.iter().enumerate() {
let ds = dyniak::datastore::NoxuDatastore::open_transactional(d.path()).expect("noxu");
map.insert(u32::try_from(i).expect("peer index fits u32"), Arc::new(ds));
}
let stores = PerPeerStores {
stores: std::sync::Arc::new(map),
};
let mk = |val: &[u8], actor: &[u8]| -> Vec<u8> {
let ctx = dyniak::vclock::VClock::decode(&[]);
let mut ctx = ctx;
ctx.advance(actor);
SiblingSet::single(HttpObject {
value: val.to_vec(),
context: ctx.encode(),
..HttpObject::default()
})
.to_storage_bytes()
};
stores.stores[&1]
.put_object(b"cart", b"k", &mk(b"red", b"n1"), &[])
.expect("seed n1");
stores.stores[&2]
.put_object(b"cart", b"k", &mk(b"blue", b"n2"), &[])
.expect("seed n2");
let coord_ds: Arc<dyn Datastore> = stores.stores[&0].clone();
let registry = Arc::new(BucketPropsRegistry::new_riak_defaults());
registry.set(
b"",
b"cart",
BucketProps {
allow_mult: Some(true),
n_val: Some(3),
..BucketProps::default()
},
);
let span = u64::from(u32::MAX);
let pts: Vec<RingPoint> = (0..3u32)
.map(|i| RingPoint::new(u64::from(i) * span / 3, i, "dc1", "r1"))
.collect();
let router = Arc::new(BucketRouter::new(
registry,
Arc::new(RingView::new(pts)),
HashType::Murmur,
));
let hooks = RoutingHooks {
router,
outbound: Arc::new(stores.clone()) as Arc<dyn dyniak::router::PeerOutbound>,
local_actor: dyniak::datatypes::ActorId::new("dc1", "n0"),
local_peer_idx: 0,
precommit: None,
postcommit: None,
};
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("addr");
let admin = Arc::new(dynomite::cluster::admin_rpc::NoopClusterAdmin);
let server = tokio::spawn(async move {
let _ = dyniak::server::serve_pbc_with_routing(listener, coord_ds, admin, hooks).await;
});
let mut c = TcpStream::connect(addr).await.expect("connect");
let get = RpbGetReq {
bucket: b"cart".to_vec(),
key: b"k".to_vec(),
..RpbGetReq::default()
};
send_frame(&mut c, MessageCode::GetReq.as_u8(), &get.encode_to_vec()).await;
let (code, body) = recv_frame(&mut c).await;
assert_eq!(code, MessageCode::GetResp.as_u8());
let resp = RpbGetResp::decode(body.as_slice()).expect("get resp");
let values: std::collections::BTreeSet<Vec<u8>> =
resp.content.iter().map(|c| c.value.clone()).collect();
assert_eq!(
resp.content.len(),
2,
"coordinated read merges the two replicas' concurrent siblings"
);
assert!(values.contains(b"red".as_slice()));
assert!(values.contains(b"blue".as_slice()));
server.abort();
let _ = server.await;
}