use std::collections::{HashMap, HashSet, VecDeque};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, AtomicU8, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex as StdMutex, RwLock, Weak};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use prost::Message as ProstMessage;
use ractor::rpc::CallResult;
#[cfg(test)]
use ractor::Actor;
use ractor::{ActorRef, ActorRuntime, SpawnErr};
use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::sync::{
mpsc, oneshot, watch, Mutex, Notify, OwnedMutexGuard, OwnedSemaphorePermit,
RwLock as AsyncRwLock, Semaphore,
};
use tokio::time::Instant;
use crate::actor::{
ApplyDurability, ApplyError, ApplyOutcome, FragmentHealth, FragmentShutdownError,
FragmentStartupStorageError, FragmentSupervisor, HlcClock, KvMessage, OpPriority, PoolConfig,
ReplicatedOp, SupervisorArgs, SupervisorMessage, VersionedValue,
};
use crate::network::{BoxStream, Network, TcpNetwork};
use crate::placement::{
validate_placement, FragmentPlacementState, PlacementController, PlacementReport,
};
use crate::proto::{
BatchAck, BatchGet, BatchGetResponse, BatchKeyValue, BatchNack, BatchPut, Envelope,
EnvelopeMsg, ForwardedGet, ForwardedPut, GetResponse, Hello, HelloAuth, KeySummary,
MutationResponse, MutationResult, MutationStatus, Priority as ProtoPriority, ReconcileData,
ReconcileFetch, ReconcileResponse, ReplicationAckLevel, SyncEntry,
};
use crate::replication::{
spawn_node_sender, NodeSender, NodeSenderDrain, NodeSenderSendError, ReplicationAckDurability,
SenderConfig, SenderDeps, SenderMsg, SenderRuntimeOptions, SenderStateReport,
};
use crate::runtime_tasks::RuntimeTasks;
use crate::shard::{fragment_of, FragmentId};
use crate::store::{
KvStore, PrefixDeleteOutcome, PrefixDeleteSupport, PrefixedStore, StoreBinding, StoreIdentity,
};
use crate::ttl_expiry::{
ExpiryDiagnostics, ExpiryPublisher, ExpiryRuntime, TtlExpiryHandler, TtlExpiryOptions,
};
use crate::wire::{
bytes_field_len, check_envelope_budget, check_record_budget, encode_envelope,
message_field_len, preflight_envelope_decode, varint_field_len, BudgetedFrame, BudgetedPeerTx,
NodePeerByteBudget, ReconcileGate, RepeatedFieldSizer, ANTI_ENTROPY_PAGE_BYTES,
MAX_FRAME_BYTES, MAX_RECONCILE_FETCH_KEYS, MAX_RECONCILE_PAGE_ITEMS, MAX_WIRE_BATCH_ITEMS,
MAX_WIRE_ERROR_BYTES, MAX_WIRE_NODE_ID_BYTES, NODE_PEER_QUEUE_BYTE_CAPACITY,
};
pub use crate::wire::PeerWriterDiagnostics;
#[cfg(test)]
mod peer_writer_budget_tests;
mod connection;
use connection::{
fresh_boot_id, handle_connection, send_envelope, try_send_envelope_immediate,
try_send_envelope_until, try_send_envelope_with_admission_timeout, DialIntentState,
PeerChannel, PeerFailureSignals, PeerMap,
};
#[cfg(test)]
use connection::{
fresh_hello, run_bounded_peer_writer, try_send_envelope, validate_peer_hello, ConnContext,
PeerWriterPipeline,
};
mod session;
use session::{
fit_reconcile_response_page, run_reconcile_responder, InboundSession, InboundSessions,
ReconcileResponseJob, ServedReconcileState,
};
mod relay;
use relay::{
cancel_connection_relays, relay_batch, relay_get, relay_put, remove_get_relay,
remove_mutation_relay, send_batch_get_response, send_mutation_response,
send_unavailable_mutation, unavailable_mutation_results, MutationRelayMap, ReforwardCtx,
RelayAdmission, RelayMap,
};
#[cfg(test)]
use relay::{relay_key_storage_bytes, GetRelay};
mod placement;
use placement::{placement_driver, PlacementDriverDeps, BOOTSTRAP_TIMEOUT};
mod lifecycle;
#[cfg(test)]
use lifecycle::spawn_fragment_subtree;
pub use lifecycle::FragmentRouter;
use lifecycle::{
ensure_sender_entry, quiesce_fragment_supervisor, register_fragment_dispatcher_monitor,
spawn_fragment_subtree_registered, OperationGate, OperationPermit, RetiringSenderRegistry,
RouteMonitorRegistrationGuard, SenderEntryDeps, SenderFinalizationRegistry, SenderMap,
SenderReportMap, ShutdownProgress, SupervisorRegistry,
};
mod read_service;
use read_service::{
ReadJob, ReadRequest, ReadRoute, ReadService, READ_KEY_BYTE_CAPACITY, READ_QUEUE_CAPACITY,
READ_WORKERS_PER_PRIORITY,
};
mod embedded_operations;
pub use embedded_operations::EmbeddedOperationDiagnostics;
use embedded_operations::{
EmbeddedOperations, RequestRetention, EMBEDDED_OPERATION_BYTE_CAPACITY,
EMBEDDED_OPERATION_CAPACITY,
};
mod mutation_service;
mod write_concern;
use mutation_service::{
MutationJob, MutationRequest, MutationService, FORWARDED_MUTATION_BYTE_CAPACITY,
FORWARDED_MUTATION_CAPACITY,
};
mod query;
pub(crate) mod recovery;
use recovery::RecoveryContext;
pub use recovery::RecoveryOptions;
pub mod scan;
pub use scan::{ScanError, MAX_SCAN_TOTAL_MATCHES};
pub const WIRE_PROTOCOL_VERSION: u32 = 4;
pub const CAPABILITY_MUTATION_RESULTS: u64 = 1 << 0;
pub const CAPABILITY_DURABILITY_ACKS: u64 = 1 << 1;
pub const CAPABILITY_CORRELATED_RECONCILIATION: u64 = 1 << 2;
pub const CAPABILITY_CHALLENGE_AUTH: u64 = 1 << 3;
pub const CAPABILITY_PAGED_ANTI_ENTROPY: u64 = 1 << 4;
pub const CAPABILITY_BYTE_BOUNDED_BATCHES: u64 = 1 << 5;
pub const CAPABILITY_STABLE_DURABILITY_ACKS: u64 = 1 << 6;
pub const CAPABILITY_SCAN: u64 = 1 << 7;
pub const CAPABILITY_QUERY_PAGES: u64 = 1 << 8;
pub const CAPABILITY_WRITE_CONCERNS: u64 = 1 << 9;
pub const CAPABILITY_RECORD_CONTENT_DIGEST: u64 = 1 << 10;
pub const CAPABILITY_TYPED_UNKNOWN_MUTATION: u64 = 1 << 11;
pub const REQUIRED_WIRE_CAPABILITIES: u64 = CAPABILITY_MUTATION_RESULTS
| CAPABILITY_DURABILITY_ACKS
| CAPABILITY_CORRELATED_RECONCILIATION
| CAPABILITY_CHALLENGE_AUTH
| CAPABILITY_PAGED_ANTI_ENTROPY
| CAPABILITY_BYTE_BOUNDED_BATCHES
| CAPABILITY_STABLE_DURABILITY_ACKS
| CAPABILITY_WRITE_CONCERNS
| CAPABILITY_RECORD_CONTENT_DIGEST
| CAPABILITY_TYPED_UNKNOWN_MUTATION;
const PEER_CHANNEL_DEPTH: usize = 256;
const PEER_CHANNEL_BYTE_CAPACITY: usize = 32 * 1024 * 1024;
const PEER_RESPONSE_BURST_LIMIT: usize = 16;
const PEER_RESPONSE_BYTE_RESERVE: usize = MAX_FRAME_BYTES;
const MIN_PEER_CHANNEL_BYTE_CAPACITY: usize = MAX_FRAME_BYTES + PEER_RESPONSE_BYTE_RESERVE;
const WIRE_BATCH_PAGE_BYTES: usize = ANTI_ENTROPY_PAGE_BYTES;
const MAX_BATCH_GET_REQUEST_KEYS: usize = 256;
const MAX_LOCAL_BATCH_IN_FLIGHT: usize = 1024;
const REPLICATION_CONTROL_CHANNEL_DEPTH: usize = 64;
const INBOUND_CONNECTION_CAPACITY: usize = 1024;
const RECEIVE_BYTE_CAPACITY: usize = 64 * 1024 * 1024;
const RELAY_CAPACITY: usize = 1024;
const RELAY_KEY_BYTE_CAPACITY: usize = 64 * 1024 * 1024;
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
const TASK_COMMAND_CAPACITY: usize = 64;
const HANDSHAKE_CHALLENGE_BYTES: usize = 32;
const RECONCILE_SUMMARY_PAGE_ITEMS: usize = 256;
const RECONCILE_RESPONSE_QUEUE_DEPTH: usize = 1;
const RECONNECT_BASE: Duration = Duration::from_millis(100);
const RECONNECT_MAX: Duration = Duration::from_secs(30);
const REMOTE_RPC_TIMEOUT: Duration = Duration::from_secs(5);
const SOCKET_WRITE_TIMEOUT: Duration = Duration::from_secs(30);
const RECONCILE_DATA_ADMISSION_TIMEOUT: Duration = Duration::from_secs(30);
fn deadline_after(timeout: Duration) -> Instant {
let now = Instant::now();
now.checked_add(timeout).unwrap_or(now)
}
type PendingGets = Arc<StdMutex<HashMap<u64, PendingGet>>>;
type PendingBatchGets = Arc<StdMutex<HashMap<u64, PendingBatchGet>>>;
type PendingMutations = Arc<StdMutex<HashMap<u64, PendingMutation>>>;
type PendingScans = Arc<StdMutex<HashMap<u64, PendingScan>>>;
struct PendingMutation {
expected_peer: String,
expected_keys: Vec<String>,
reply: oneshot::Sender<Vec<MutationResult>>,
}
struct PendingGet {
expected_peer: String,
expected_key: String,
reply: oneshot::Sender<Result<Option<VersionedValue>, NodeError>>,
}
struct PendingBatchGet {
expected_peer: String,
expected_keys: Vec<String>,
reply: oneshot::Sender<Result<Vec<BatchKeyValue>, NodeError>>,
}
struct PendingScan {
expected_peer: String,
reply: oneshot::Sender<Result<Vec<crate::proto::ScanMatchProto>, String>>,
}
struct PendingMutationGuard {
req_id: u64,
pending: PendingMutations,
}
struct PendingGetGuard {
req_id: u64,
pending: PendingGets,
}
impl Drop for PendingGetGuard {
fn drop(&mut self) {
self.pending.lock().unwrap().remove(&self.req_id);
}
}
struct PendingBatchGetGuard {
req_id: u64,
pending: PendingBatchGets,
}
impl Drop for PendingBatchGetGuard {
fn drop(&mut self) {
self.pending.lock().unwrap().remove(&self.req_id);
}
}
impl Drop for PendingMutationGuard {
fn drop(&mut self) {
self.pending.lock().unwrap().remove(&self.req_id);
}
}
const MAX_FORWARD_HOPS: u32 = 4;
#[derive(Debug)]
pub enum NodeError {
Io(std::io::Error),
Codec(String),
Actor(String),
Network(String),
UnknownOutcome(String),
Backpressure(String),
Storage(String),
Superseded {
timestamp_ms: u64,
logical_counter: u64,
node_id: String,
is_tombstone: bool,
},
Expired,
ShuttingDown,
}
impl std::fmt::Display for NodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NodeError::Io(e) => write!(f, "io error: {e}"),
NodeError::Codec(e) => write!(f, "codec error: {e}"),
NodeError::Actor(e) => write!(f, "actor error: {e}"),
NodeError::Network(e) => write!(f, "network error: {e}"),
NodeError::UnknownOutcome(e) => write!(f, "mutation outcome unknown: {e}"),
NodeError::Backpressure(e) => write!(f, "backpressure shed: {e}"),
NodeError::Storage(e) => write!(f, "storage error: {e}"),
NodeError::Superseded {
timestamp_ms,
logical_counter,
node_id,
is_tombstone,
} => write!(
f,
"write was superseded by timestamp={timestamp_ms}:{logical_counter} node_id={node_id} tombstone={is_tombstone}"
),
NodeError::Expired => write!(f, "write expired before it could be applied"),
NodeError::ShuttingDown => write!(f, "node is shutting down"),
}
}
}
fn ttl_expiry_deadline_ms(wall_ms: u64, ttl: Duration) -> Result<u64, NodeError> {
let ttl_ms = u64::try_from(ttl.as_millis())
.map_err(|_| NodeError::Codec("TTL duration exceeds u64 milliseconds".to_string()))?;
wall_ms
.checked_add(ttl_ms)
.filter(|deadline| *deadline != 0)
.ok_or_else(|| {
NodeError::Codec("TTL expiry must fit a nonzero u64 wall-clock deadline".to_string())
})
}
fn priority_to_proto(p: OpPriority) -> i32 {
match p {
OpPriority::Default => ProtoPriority::Default as i32,
OpPriority::High => ProtoPriority::High as i32,
OpPriority::Background => ProtoPriority::Background as i32,
}
}
fn priority_from_proto(v: i32) -> OpPriority {
match v {
x if x == ProtoPriority::High as i32 => OpPriority::High,
x if x == ProtoPriority::Background as i32 => OpPriority::Background,
_ => OpPriority::Default,
}
}
async fn observe_remote_timestamp(
hlc: &Arc<Mutex<HlcClock>>,
version: actordb_protocol::RecordVersion,
message: &str,
) -> bool {
match hlc.lock().await.observe_version(version) {
Ok(()) => true,
Err(error) => {
tracing::warn!("rejecting {message} with invalid timestamp: {error}");
false
}
}
}
async fn call_apply(
actor: &ActorRef<KvMessage>,
op: ReplicatedOp,
priority: OpPriority,
is_replication: bool,
) -> Result<ApplyOutcome, NodeError> {
match actor
.call(
|reply| KvMessage::ApplyOp {
op,
priority,
is_replication,
reply: Some(reply),
},
None,
)
.await
.map_err(|error| NodeError::Actor(error.to_string()))?
{
CallResult::Success(Ok(outcome)) => Ok(outcome),
CallResult::Success(Err(ApplyError::Backpressure)) => Err(NodeError::Backpressure(
"fragment worker pool is at capacity".to_string(),
)),
CallResult::Success(Err(ApplyError::InvalidInput(error))) => Err(NodeError::Codec(error)),
CallResult::Success(Err(error @ ApplyError::VersionConflict { .. })) => {
Err(NodeError::Codec(error.to_string()))
}
CallResult::Success(Err(ApplyError::Storage(error))) => Err(NodeError::Storage(error)),
CallResult::Timeout => Err(NodeError::Actor("apply timed out".to_string())),
CallResult::SenderError => Err(NodeError::Actor("apply reply channel dropped".to_string())),
}
}
async fn flush_fence(
actor: &ActorRef<KvMessage>,
required: ReplicationAckDurability,
) -> Result<ApplyDurability, NodeError> {
let result = match required {
ReplicationAckDurability::StableCommitted => {
actor
.call(
|reply| KvMessage::FlushStable { reply },
Some(Duration::from_secs(30)),
)
.await
}
ReplicationAckDurability::MemoryApplied | ReplicationAckDurability::StoreCommitted => {
actor
.call(
|reply| KvMessage::Flush { reply },
Some(Duration::from_secs(30)),
)
.await
}
};
match result.map_err(|error| NodeError::Actor(error.to_string()))? {
CallResult::Success(Ok(durability)) => Ok(durability),
CallResult::Success(Err(ApplyError::Storage(error))) => Err(NodeError::Storage(error)),
CallResult::Success(Err(ApplyError::Backpressure)) => Err(NodeError::Actor(
"flush fence unexpectedly hit admission control".to_string(),
)),
CallResult::Success(Err(ApplyError::InvalidInput(error))) => Err(NodeError::Actor(
format!("flush fence unexpectedly returned invalid input: {error}"),
)),
CallResult::Success(Err(error @ ApplyError::VersionConflict { .. })) => {
Err(NodeError::Actor(error.to_string()))
}
CallResult::Timeout => Err(NodeError::Actor("flush fence timed out".to_string())),
CallResult::SenderError => Err(NodeError::Actor(
"flush fence reply channel dropped".to_string(),
)),
}
}
async fn enqueue_to_senders(senders: &SenderMap, frag: FragmentId, op: &ReplicatedOp) {
let targets: Vec<NodeSender> = senders
.lock()
.await
.iter()
.filter(|((sender_frag, _), _)| *sender_frag == frag)
.map(|(_, sender)| sender.clone())
.collect();
for sender in targets {
let _ = sender.offer(op);
}
}
async fn sender_task_is_current(
senders: &SenderMap,
key: &(FragmentId, String),
sender: &NodeSender,
) -> bool {
senders
.lock()
.await
.get(key)
.is_some_and(|current| current.same_task(sender))
}
fn truncate_wire_error(mut error: String) -> String {
if error.len() <= MAX_WIRE_ERROR_BYTES {
return error;
}
let mut end = MAX_WIRE_ERROR_BYTES;
while !error.is_char_boundary(end) {
end -= 1;
}
error.truncate(end);
error
}
fn max_mutation_result_encoded_len(key_len: usize) -> usize {
bytes_field_len(1, key_len)
+ varint_field_len(2, MutationStatus::Unavailable as u64)
+ bytes_field_len(3, MAX_WIRE_ERROR_BYTES)
+ varint_field_len(4, u64::MAX)
+ bytes_field_len(5, MAX_WIRE_NODE_ID_BYTES)
+ varint_field_len(6, 1)
+ varint_field_len(7, u64::from(u32::MAX))
}
fn batch_put_base_encoded_len(priority: i32) -> usize {
BatchPut {
puts: Vec::new(),
priority,
req_id: u64::MAX,
}
.encoded_len()
}
fn mutation_response_base_encoded_len() -> usize {
MutationResponse {
req_id: u64::MAX,
results: Vec::new(),
}
.encoded_len()
}
fn batch_get_base_encoded_len(priority: i32) -> usize {
BatchGet {
req_id: u64::MAX,
keys: Vec::new(),
priority,
}
.encoded_len()
}
fn batch_get_response_base_encoded_len(req_id: u64) -> usize {
BatchGetResponse {
req_id,
results: Vec::new(),
}
.encoded_len()
}
fn max_batch_get_non_value_result_encoded_len(key_len: usize) -> usize {
bytes_field_len(1, key_len) + varint_field_len(8, 1)
}
fn batch_put_envelope_encoded_len(payload_len: usize) -> usize {
message_field_len(15, payload_len)
}
fn batch_get_envelope_encoded_len(payload_len: usize) -> usize {
message_field_len(16, payload_len)
}
fn batch_get_response_envelope_encoded_len(payload_len: usize) -> usize {
message_field_len(17, payload_len)
}
fn mutation_response_envelope_encoded_len(payload_len: usize) -> usize {
message_field_len(18, payload_len)
}
fn validate_batch_put_wire_bounds(batch: &BatchPut) -> Result<(), NodeError> {
let request_len = batch_put_envelope_encoded_len(batch.encoded_len());
let mut response_payload_len = mutation_response_base_encoded_len();
for put in &batch.puts {
response_payload_len +=
message_field_len(2, max_mutation_result_encoded_len(put.key.len()));
}
let response_len = mutation_response_envelope_encoded_len(response_payload_len);
if request_len > MAX_FRAME_BYTES || response_len > MAX_FRAME_BYTES {
return Err(NodeError::Codec(format!(
"batch put exceeds the hard request/response frame bound: request={request_len} response={response_len} limit={MAX_FRAME_BYTES}"
)));
}
Ok(())
}
fn mutation_result(
key: String,
request_index: usize,
result: Result<ApplyOutcome, NodeError>,
) -> MutationResult {
let mut wire = MutationResult {
key,
status: MutationStatus::Applied as i32,
error: String::new(),
winner_timestamp_ms: 0,
winner_logical_counter: 0,
winner_node_id: String::new(),
winner_is_tombstone: false,
request_index: request_index as u32,
write_concern: None,
};
match result {
Ok(ApplyOutcome::Applied) => {}
Ok(ApplyOutcome::AlreadyPresent) => {
wire.status = MutationStatus::AlreadyPresent as i32;
}
Ok(ApplyOutcome::Superseded {
timestamp_ms,
logical_counter,
node_id,
is_tombstone,
}) => {
wire.status = MutationStatus::Superseded as i32;
wire.winner_timestamp_ms = timestamp_ms;
wire.winner_logical_counter = logical_counter;
wire.winner_node_id = node_id;
wire.winner_is_tombstone = is_tombstone;
}
Ok(ApplyOutcome::Expired) | Err(NodeError::Expired) => {
wire.status = MutationStatus::Expired as i32;
}
Err(NodeError::Backpressure(error)) => {
wire.status = MutationStatus::Backpressure as i32;
wire.error = error;
}
Err(NodeError::Storage(error)) => {
wire.status = MutationStatus::StorageError as i32;
wire.error = error;
}
Err(NodeError::Superseded {
timestamp_ms,
logical_counter,
node_id,
is_tombstone,
}) => {
wire.status = MutationStatus::Superseded as i32;
wire.winner_timestamp_ms = timestamp_ms;
wire.winner_logical_counter = logical_counter;
wire.winner_node_id = node_id;
wire.winner_is_tombstone = is_tombstone;
}
Err(NodeError::UnknownOutcome(error)) => {
wire.status = MutationStatus::UnknownOutcome as i32;
wire.error = error;
}
Err(NodeError::Network(error)) => {
wire.status = MutationStatus::Unavailable as i32;
wire.error = error;
}
Err(error) => {
wire.status = MutationStatus::ActorError as i32;
wire.error = error.to_string();
}
}
wire.error = truncate_wire_error(wire.error);
wire
}
fn decode_mutation_result(result: MutationResult) -> Result<(), NodeError> {
match MutationStatus::try_from(result.status) {
Ok(MutationStatus::Applied | MutationStatus::AlreadyPresent) => Ok(()),
Ok(MutationStatus::Superseded) => Err(NodeError::Superseded {
timestamp_ms: result.winner_timestamp_ms,
logical_counter: result.winner_logical_counter,
node_id: result.winner_node_id,
is_tombstone: result.winner_is_tombstone,
}),
Ok(MutationStatus::Expired) => Err(NodeError::Expired),
Ok(MutationStatus::Backpressure) => Err(NodeError::Backpressure(result.error)),
Ok(MutationStatus::StorageError) => Err(NodeError::Storage(result.error)),
Ok(MutationStatus::ActorError) => Err(NodeError::Actor(result.error)),
Ok(MutationStatus::Unavailable) => Err(NodeError::Network(result.error)),
Ok(MutationStatus::UnknownOutcome) => Err(NodeError::UnknownOutcome(result.error)),
Ok(MutationStatus::Unknown) => Err(NodeError::Codec(
"peer returned an unspecified mutation status".to_string(),
)),
Err(_) => Err(NodeError::Codec(format!(
"unknown mutation status {}",
result.status
))),
}
}
fn validate_mutation_results(
expected_keys: &[String],
results: &[MutationResult],
) -> Result<(), NodeError> {
if results.len() != expected_keys.len() {
return Err(NodeError::Codec(format!(
"forwarded mutation returned {} results, expected {}",
results.len(),
expected_keys.len()
)));
}
for (request_index, (expected_key, result)) in expected_keys.iter().zip(results).enumerate() {
if result.request_index as usize != request_index || &result.key != expected_key {
return Err(NodeError::Codec(format!(
"forwarded mutation result {request_index} did not match its request"
)));
}
if matches!(
MutationStatus::try_from(result.status),
Ok(MutationStatus::Unknown) | Err(_)
) {
return Err(NodeError::Codec(format!(
"forwarded mutation result {request_index} has unknown status {}",
result.status
)));
}
if result.error.len() > MAX_WIRE_ERROR_BYTES {
return Err(NodeError::Codec(format!(
"forwarded mutation result {request_index} exceeds the wire error limit"
)));
}
match MutationStatus::try_from(result.status) {
Ok(MutationStatus::Superseded) => {
if !result.error.is_empty()
|| result.winner_node_id.is_empty()
|| result.winner_node_id.len() > MAX_WIRE_NODE_ID_BYTES
{
return Err(NodeError::Codec(format!(
"forwarded mutation result {request_index} has invalid superseded fields"
)));
}
}
Ok(
MutationStatus::Backpressure
| MutationStatus::StorageError
| MutationStatus::ActorError
| MutationStatus::Unavailable
| MutationStatus::UnknownOutcome,
) => {
if result.winner_timestamp_ms != 0
|| result.winner_logical_counter != 0
|| !result.winner_node_id.is_empty()
|| result.winner_is_tombstone
{
return Err(NodeError::Codec(format!(
"forwarded mutation result {request_index} has unexpected winner fields"
)));
}
}
Ok(
MutationStatus::Applied | MutationStatus::AlreadyPresent | MutationStatus::Expired,
) => {
if !result.error.is_empty()
|| result.winner_timestamp_ms != 0
|| result.winner_logical_counter != 0
|| !result.winner_node_id.is_empty()
|| result.winner_is_tombstone
{
return Err(NodeError::Codec(format!(
"forwarded mutation result {request_index} has unexpected fields"
)));
}
}
Ok(MutationStatus::Unknown) | Err(_) => unreachable!("rejected above"),
}
}
Ok(())
}
fn validate_batch_get_results(
expected_keys: &[String],
results: &[BatchKeyValue],
) -> Result<(), NodeError> {
if results.len() > expected_keys.len() {
return Err(NodeError::Codec(format!(
"batch get returned {} results, requested at most {}",
results.len(),
expected_keys.len()
)));
}
for (request_index, (expected_key, result)) in expected_keys.iter().zip(results).enumerate() {
if &result.key != expected_key {
return Err(NodeError::Codec(format!(
"batch get result {request_index} did not match its request"
)));
}
if result.shed {
if result.found
|| !result.value.is_empty()
|| result.timestamp_ms != 0
|| result.logical_counter != 0
|| !result.node_id.is_empty()
|| result.expires_at_ms != 0
|| result.is_tombstone
{
return Err(NodeError::Codec(format!(
"batch get result {request_index} has invalid shed fields"
)));
}
} else if result.found {
validate_record_wire_budget(&result.key, result.value.len(), &result.node_id)?;
} else if !result.value.is_empty()
|| result.timestamp_ms != 0
|| result.logical_counter != 0
|| !result.node_id.is_empty()
|| result.expires_at_ms != 0
|| result.is_tombstone
{
return Err(NodeError::Codec(format!(
"batch get result {request_index} has invalid not-found fields"
)));
}
}
Ok(())
}
fn validate_legacy_get_response(
expected_key: &str,
response: &GetResponse,
) -> Result<(), NodeError> {
if response.shed {
if response.found
|| !response.value.is_empty()
|| response.timestamp_ms != 0
|| response.logical_counter != 0
|| !response.node_id.is_empty()
|| response.expires_at_ms != 0
|| response.is_tombstone
{
return Err(NodeError::Codec(
"forwarded get response has invalid shed fields".to_string(),
));
}
} else if response.found {
if response.is_tombstone {
return Err(NodeError::Codec(
"forwarded get response exposed a tombstone".to_string(),
));
}
validate_record_wire_budget(expected_key, response.value.len(), &response.node_id)?;
} else if !response.value.is_empty()
|| response.timestamp_ms != 0
|| response.logical_counter != 0
|| !response.node_id.is_empty()
|| response.expires_at_ms != 0
|| response.is_tombstone
{
return Err(NodeError::Codec(
"forwarded get response has invalid not-found fields".to_string(),
));
}
Ok(())
}
async fn apply_forwarded_mutation(
actor: &ActorRef<KvMessage>,
senders: &SenderMap,
frag: FragmentId,
op: ReplicatedOp,
priority: OpPriority,
request_index: usize,
) -> MutationResult {
let key = op.key.clone();
if let Err(error) = validate_replicated_op(&op) {
return mutation_result(key, request_index, Err(error));
}
let result = match call_apply(actor, op.clone(), priority, false).await {
Err(NodeError::Actor(error)) => Err(NodeError::Network(error)),
result => result,
};
if let Ok(outcome) = &result {
match outcome {
ApplyOutcome::Applied | ApplyOutcome::AlreadyPresent => {
enqueue_to_senders(senders, frag, &op).await;
}
ApplyOutcome::Expired => {
let mut expiry = op;
expiry.value = expiry.value.expiry_tombstone();
enqueue_to_senders(senders, frag, &expiry).await;
}
ApplyOutcome::Superseded { .. } => {}
}
}
mutation_result(key, request_index, result)
}
fn validate_replicated_op(op: &ReplicatedOp) -> Result<(), NodeError> {
validate_record_wire_budget(&op.key, op.value.value.len(), &op.value.node_id)
}
fn validate_record_wire_budget(
key: &str,
value_len: usize,
node_id: &str,
) -> Result<(), NodeError> {
check_record_budget(key.len(), value_len, node_id.len()).map_err(|error| {
NodeError::Codec(format!(
"key/value cannot fit every required peer frame: {error}"
))
})
}
impl std::error::Error for NodeError {}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ReplicationDrainTarget {
fragment: FragmentId,
peer_id: String,
}
impl ReplicationDrainTarget {
pub fn fragment(&self) -> FragmentId {
self.fragment
}
pub fn peer_id(&self) -> &str {
&self.peer_id
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReplicationDrainReport {
targets: Vec<ReplicationDrainTarget>,
durability: ReplicationAckDurability,
}
impl ReplicationDrainReport {
pub fn targets(&self) -> &[ReplicationDrainTarget] {
&self.targets
}
pub fn durability(&self) -> ReplicationAckDurability {
self.durability
}
}
#[derive(Clone, Debug, bon::Builder)]
pub struct ReplicationDrainOptions {
#[builder(default = Duration::from_secs(30))]
timeout: Duration,
}
impl Default for ReplicationDrainOptions {
fn default() -> Self {
Self::builder().build()
}
}
impl ReplicationDrainOptions {
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn timeout(&self) -> Duration {
self.timeout
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ReplicationDrainPhase {
ShutdownCoordination,
PlacementFreeze,
MutationAdmissionDrain,
TargetSnapshot,
SenderPreparation,
PeerReconciliation,
}
impl std::fmt::Display for ReplicationDrainPhase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::ShutdownCoordination => "shutdown coordination",
Self::PlacementFreeze => "placement freeze",
Self::MutationAdmissionDrain => "mutation admission drain",
Self::TargetSnapshot => "target snapshot",
Self::SenderPreparation => "sender preparation",
Self::PeerReconciliation => "peer reconciliation",
};
f.write_str(name)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ReplicationDrainShutdownError {
InvalidOptions(String),
Timeout {
phase: ReplicationDrainPhase,
completed: Vec<ReplicationDrainTarget>,
pending: Vec<ReplicationDrainTarget>,
},
RetryableSender(String),
Unavailable(String),
Local {
error: NodeError,
replication: ReplicationDrainReport,
},
}
impl std::fmt::Display for ReplicationDrainShutdownError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidOptions(error) => write!(f, "invalid replication drain options: {error}"),
Self::Timeout {
phase,
completed,
pending,
} => write!(
f,
"replication drain timed out during {phase}: {} completed, {} pending",
completed.len(),
pending.len()
),
Self::RetryableSender(error) => {
write!(f, "replication drain sender must be retried: {error}")
}
Self::Unavailable(error) => write!(f, "replication drain unavailable: {error}"),
Self::Local { error, .. } => {
write!(f, "replication drained, but local shutdown failed: {error}")
}
}
}
}
impl std::error::Error for ReplicationDrainShutdownError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Local { error, .. } => Some(error),
_ => None,
}
}
}
impl From<std::io::Error> for NodeError {
fn from(e: std::io::Error) -> Self {
NodeError::Io(e)
}
}
impl From<prost::DecodeError> for NodeError {
fn from(e: prost::DecodeError) -> Self {
NodeError::Codec(e.to_string())
}
}
pub struct Node {
node_id: String,
boot_id: u64,
listen_addr: SocketAddr,
router: FragmentRouter,
serving: Arc<RwLock<HashSet<FragmentId>>>,
supervisors: Arc<Mutex<HashMap<FragmentId, ActorRef<SupervisorMessage>>>>,
supervisor_stops: Arc<SupervisorRegistry>,
fragment_map: Arc<RwLock<HashMap<FragmentId, Vec<String>>>>,
placement_states: Arc<Mutex<HashMap<FragmentId, FragmentPlacementState>>>,
placement_controller: Option<Arc<dyn PlacementController>>,
placement_poll_interval: Duration,
members: HashMap<String, SocketAddr>,
pending_gets: PendingGets,
pending_batch_gets: PendingBatchGets,
pending_scans: PendingScans,
pending_query_pages: query::PendingQueryPages,
query_admission: Arc<crate::query::QueryAdmission>,
pending_mutations: PendingMutations,
pending_relays: RelayMap,
pending_mutation_relays: MutationRelayMap,
next_req_id: Arc<AtomicU64>,
peers: PeerMap,
peer_failures: PeerFailureSignals,
next_seq: Arc<Mutex<u64>>,
hlc: Arc<Mutex<HlcClock>>,
senders: SenderMap,
finalizing_senders: Arc<SenderFinalizationRegistry>,
retiring_senders: Arc<RetiringSenderRegistry>,
sender_config: SenderConfig,
pub(crate) write_concerns: Arc<crate::write_concern::WriteConcernTracker>,
runtime_options: NodeRuntimeOptions,
storage_executor: Arc<crate::store::executor::StorageExecutor>,
persistence_budget: Arc<crate::retained::ByteBudget>,
sender_reports: SenderReportMap,
sync_entries_applied: Arc<AtomicU64>,
membership_tx: Option<watch::Sender<u64>>,
cluster_secret: Option<Vec<u8>>,
placement_tasks: Arc<RuntimeTasks>,
tasks: Arc<RuntimeTasks>,
read_service: Arc<ReadService>,
inbound_connections: Arc<Semaphore>,
receive_byte_budget: Arc<Semaphore>,
node_peer_queue_budget: Arc<NodePeerByteBudget>,
relay_admission: Arc<RelayAdmission>,
mutation_service: Arc<MutationService>,
embedded_operations: Arc<EmbeddedOperations>,
dial_intents: Arc<StdMutex<HashMap<SocketAddr, DialIntentState>>>,
dial_intent_changes: watch::Sender<u64>,
peer_changes: watch::Sender<u64>,
operation_gate: Arc<OperationGate>,
shutdown_state: Mutex<ShutdownProgress>,
replication_drain_report: StdMutex<Option<ReplicationDrainReport>>,
network: Arc<dyn Network>,
ttl_expiry: Option<ExpiryRuntime>,
store_binding: StdMutex<Option<StoreBinding>>,
recovery: Option<Arc<RecoveryContext>>,
}
#[derive(Clone, Debug, bon::Builder)]
pub struct NodeRuntimeOptions {
#[builder(default = 256 * 1024 * 1024)]
replication_retained_byte_capacity: usize,
#[builder(default = Duration::from_secs(30))]
local_work_timeout: Duration,
#[builder(skip = crate::retained::ByteBudget::new(256 * 1024 * 1024))]
replication_retention: Arc<crate::retained::ByteBudget>,
#[builder(default = 64 * 1024 * 1024)]
reconciliation_retained_byte_capacity: usize,
#[builder(skip = crate::retained::ByteBudget::new(64 * 1024 * 1024))]
reconciliation_retention: Arc<crate::retained::ByteBudget>,
#[builder(default, into)]
regions: Arc<HashMap<String, String>>,
#[builder(default = PEER_CHANNEL_DEPTH)]
peer_queue_frame_capacity: usize,
#[builder(default = NODE_PEER_QUEUE_BYTE_CAPACITY)]
node_peer_queue_byte_capacity: usize,
#[builder(default = PEER_CHANNEL_BYTE_CAPACITY)]
peer_queue_byte_capacity: usize,
#[builder(default = crate::query::DEFAULT_QUERY_RUNTIME_BYTES)]
query_runtime_byte_capacity: usize,
#[builder(default = REPLICATION_CONTROL_CHANNEL_DEPTH)]
replication_control_message_capacity: usize,
#[builder(default = INBOUND_CONNECTION_CAPACITY)]
inbound_connection_capacity: usize,
#[builder(default = RECEIVE_BYTE_CAPACITY)]
receive_byte_capacity: usize,
#[builder(default = RELAY_CAPACITY)]
relay_capacity: usize,
#[builder(default = RELAY_KEY_BYTE_CAPACITY)]
relay_key_byte_capacity: usize,
#[builder(default = READ_QUEUE_CAPACITY)]
serving_read_queue_capacity: usize,
#[builder(default = READ_KEY_BYTE_CAPACITY)]
serving_read_key_byte_capacity: usize,
#[builder(default = EMBEDDED_OPERATION_CAPACITY)]
embedded_operation_capacity: usize,
#[builder(default = EMBEDDED_OPERATION_BYTE_CAPACITY)]
embedded_operation_byte_capacity: usize,
#[builder(default = FORWARDED_MUTATION_CAPACITY)]
forwarded_mutation_capacity: usize,
#[builder(default = FORWARDED_MUTATION_BYTE_CAPACITY)]
forwarded_mutation_byte_capacity: usize,
#[builder(default = REMOTE_RPC_TIMEOUT)]
queue_admission_timeout: Duration,
#[builder(default = RECONCILE_DATA_ADMISSION_TIMEOUT)]
reconcile_data_admission_timeout: Duration,
#[builder(default = REMOTE_RPC_TIMEOUT)]
forwarded_attempt_timeout: Duration,
#[builder(default = REMOTE_RPC_TIMEOUT)]
connect_timeout: Duration,
#[builder(default = HANDSHAKE_TIMEOUT)]
handshake_timeout: Duration,
#[builder(default = SOCKET_WRITE_TIMEOUT)]
socket_read_timeout: Duration,
#[builder(default = SOCKET_WRITE_TIMEOUT)]
socket_write_timeout: Duration,
#[builder(default = Duration::from_secs(30))]
replication_ack_timeout: Duration,
#[builder(default = Duration::from_secs(5))]
health_timeout: Duration,
write_concern_acknowledgement_cap: Option<u32>,
}
impl Default for NodeRuntimeOptions {
fn default() -> Self {
Self::builder().build()
}
}
impl NodeRuntimeOptions {
pub fn with_node_peer_queue_byte_capacity(mut self, capacity: usize) -> Self {
self.node_peer_queue_byte_capacity = capacity;
self
}
pub fn node_peer_queue_byte_capacity(&self) -> usize {
self.node_peer_queue_byte_capacity
}
pub fn with_query_runtime_byte_capacity(mut self, capacity: usize) -> Self {
self.query_runtime_byte_capacity = capacity;
self
}
pub fn query_runtime_byte_capacity(&self) -> usize {
self.query_runtime_byte_capacity
}
pub fn with_embedded_operation_capacity(mut self, capacity: usize) -> Self {
self.embedded_operation_capacity = capacity;
self
}
pub fn with_embedded_operation_byte_capacity(mut self, capacity: usize) -> Self {
self.embedded_operation_byte_capacity = capacity;
self
}
pub fn embedded_operation_capacity(&self) -> usize {
self.embedded_operation_capacity
}
pub fn embedded_operation_byte_capacity(&self) -> usize {
self.embedded_operation_byte_capacity
}
pub fn with_regions(mut self, regions: HashMap<String, String>) -> Self {
self.regions = Arc::new(regions);
self
}
pub fn regions(&self) -> &HashMap<String, String> {
&self.regions
}
pub fn with_peer_queue_frame_capacity(mut self, capacity: usize) -> Self {
self.peer_queue_frame_capacity = capacity;
self
}
pub fn with_peer_queue_byte_capacity(mut self, capacity: usize) -> Self {
self.peer_queue_byte_capacity = capacity;
self
}
pub fn with_replication_control_message_capacity(mut self, capacity: usize) -> Self {
self.replication_control_message_capacity = capacity;
self
}
pub fn with_inbound_connection_capacity(mut self, capacity: usize) -> Self {
self.inbound_connection_capacity = capacity;
self
}
pub fn with_receive_byte_capacity(mut self, capacity: usize) -> Self {
self.receive_byte_capacity = capacity;
self
}
pub fn with_relay_capacity(mut self, capacity: usize) -> Self {
self.relay_capacity = capacity;
self
}
pub fn with_relay_key_byte_capacity(mut self, capacity: usize) -> Self {
self.relay_key_byte_capacity = capacity;
self
}
pub fn with_serving_read_queue_capacity(mut self, capacity: usize) -> Self {
self.serving_read_queue_capacity = capacity;
self
}
pub fn with_serving_read_key_byte_capacity(mut self, capacity: usize) -> Self {
self.serving_read_key_byte_capacity = capacity;
self
}
pub fn with_forwarded_mutation_capacity(mut self, capacity: usize) -> Self {
self.forwarded_mutation_capacity = capacity;
self
}
pub fn with_forwarded_mutation_byte_capacity(mut self, capacity: usize) -> Self {
self.forwarded_mutation_byte_capacity = capacity;
self
}
pub fn with_queue_admission_timeout(mut self, timeout: Duration) -> Self {
self.queue_admission_timeout = timeout;
self
}
pub fn with_reconcile_data_admission_timeout(mut self, timeout: Duration) -> Self {
self.reconcile_data_admission_timeout = timeout;
self
}
pub fn with_forwarded_attempt_timeout(mut self, timeout: Duration) -> Self {
self.forwarded_attempt_timeout = timeout;
self
}
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = timeout;
self
}
pub fn with_handshake_timeout(mut self, timeout: Duration) -> Self {
self.handshake_timeout = timeout;
self
}
pub fn with_socket_read_timeout(mut self, timeout: Duration) -> Self {
self.socket_read_timeout = timeout;
self
}
pub fn with_socket_write_timeout(mut self, timeout: Duration) -> Self {
self.socket_write_timeout = timeout;
self
}
pub fn with_replication_ack_timeout(mut self, timeout: Duration) -> Self {
self.replication_ack_timeout = timeout;
self
}
pub fn with_health_timeout(mut self, timeout: Duration) -> Self {
self.health_timeout = timeout;
self
}
pub fn with_write_concern_acknowledgement_cap(mut self, cap: u32) -> Self {
self.write_concern_acknowledgement_cap = Some(cap);
self
}
pub fn without_write_concern_acknowledgement_cap(mut self) -> Self {
self.write_concern_acknowledgement_cap = None;
self
}
pub fn write_concern_acknowledgement_cap(&self) -> Option<u32> {
self.write_concern_acknowledgement_cap
}
pub fn peer_queue_frame_capacity(&self) -> usize {
self.peer_queue_frame_capacity
}
pub fn peer_queue_byte_capacity(&self) -> usize {
self.peer_queue_byte_capacity
}
pub fn replication_control_message_capacity(&self) -> usize {
self.replication_control_message_capacity
}
pub fn inbound_connection_capacity(&self) -> usize {
self.inbound_connection_capacity
}
pub fn receive_byte_capacity(&self) -> usize {
self.receive_byte_capacity
}
pub fn relay_capacity(&self) -> usize {
self.relay_capacity
}
pub fn relay_key_byte_capacity(&self) -> usize {
self.relay_key_byte_capacity
}
pub fn serving_read_queue_capacity(&self) -> usize {
self.serving_read_queue_capacity
}
pub fn serving_read_key_byte_capacity(&self) -> usize {
self.serving_read_key_byte_capacity
}
pub fn forwarded_mutation_capacity(&self) -> usize {
self.forwarded_mutation_capacity
}
pub fn forwarded_mutation_byte_capacity(&self) -> usize {
self.forwarded_mutation_byte_capacity
}
pub fn queue_admission_timeout(&self) -> Duration {
self.queue_admission_timeout
}
pub fn reconcile_data_admission_timeout(&self) -> Duration {
self.reconcile_data_admission_timeout
}
pub fn forwarded_attempt_timeout(&self) -> Duration {
self.forwarded_attempt_timeout
}
pub fn connect_timeout(&self) -> Duration {
self.connect_timeout
}
pub fn handshake_timeout(&self) -> Duration {
self.handshake_timeout
}
pub fn socket_read_timeout(&self) -> Duration {
self.socket_read_timeout
}
pub fn socket_write_timeout(&self) -> Duration {
self.socket_write_timeout
}
pub fn replication_ack_timeout(&self) -> Duration {
self.replication_ack_timeout
}
pub fn health_timeout(&self) -> Duration {
self.health_timeout
}
pub fn reconciliation_retained_byte_capacity(&self) -> usize {
self.reconciliation_retained_byte_capacity
}
pub fn local_work_timeout(&self) -> Duration {
self.local_work_timeout
}
pub fn with_local_work_timeout(mut self, timeout: Duration) -> Self {
self.local_work_timeout = timeout;
self
}
fn sender_options(&self) -> SenderRuntimeOptions {
SenderRuntimeOptions::default()
.with_aggregate_retention(self.replication_retention.clone())
.with_reconciliation_result_budget(self.reconciliation_retention.clone())
.with_local_work_timeout(self.local_work_timeout)
.with_ack_timeout(self.replication_ack_timeout)
.with_queue_admission_timeout(self.queue_admission_timeout)
.with_control_channel_capacity(self.replication_control_message_capacity)
}
pub fn replication_retained_bytes(&self) -> usize {
self.replication_retention.used()
}
fn validate(&self) -> Result<(), NodeError> {
if self.reconciliation_retained_byte_capacity < 4 * MAX_FRAME_BYTES {
return Err(NodeError::Actor(
"reconciliation result allowance must be at least 64 MiB".into(),
));
}
if self.local_work_timeout.is_zero()
|| Instant::now()
.checked_add(self.local_work_timeout)
.is_none()
{
return Err(NodeError::Actor(
"local work timeout must be positive and representable".into(),
));
}
if self.replication_retained_byte_capacity == 0 {
return Err(NodeError::Actor(
"node replication byte allowance must be nonzero".into(),
));
}
for (node_id, region) in self.regions.iter() {
if node_id.is_empty() || node_id.len() > MAX_WIRE_NODE_ID_BYTES {
return Err(NodeError::Actor(format!(
"region map node_id must contain 1 to {MAX_WIRE_NODE_ID_BYTES} bytes"
)));
}
if region.is_empty() || region.len() > MAX_WIRE_NODE_ID_BYTES {
return Err(NodeError::Actor(format!(
"region label must contain 1 to {MAX_WIRE_NODE_ID_BYTES} bytes"
)));
}
}
if self.write_concern_acknowledgement_cap == Some(0) {
return Err(NodeError::Actor(
"write concern acknowledgement cap must be positive".to_string(),
));
}
for (name, capacity) in [
(
"query runtime byte capacity",
self.query_runtime_byte_capacity,
),
(
"embedded operation capacity",
self.embedded_operation_capacity,
),
(
"embedded operation byte capacity",
self.embedded_operation_byte_capacity,
),
("relay capacity", self.relay_capacity),
("relay key byte capacity", self.relay_key_byte_capacity),
(
"forwarded mutation capacity",
self.forwarded_mutation_capacity,
),
(
"forwarded mutation byte capacity",
self.forwarded_mutation_byte_capacity,
),
] {
if capacity == 0 || capacity > Semaphore::MAX_PERMITS {
return Err(NodeError::Actor(format!(
"{name} must be non-zero and at most {}",
Semaphore::MAX_PERMITS
)));
}
}
if self.embedded_operation_byte_capacity > u32::MAX as usize {
return Err(NodeError::Actor(
"embedded operation byte capacity exceeds u32 permit range".into(),
));
}
if self.serving_read_queue_capacity == 0
|| self
.serving_read_queue_capacity
.checked_add(READ_WORKERS_PER_PRIORITY)
.is_none_or(|capacity| capacity > Semaphore::MAX_PERMITS)
{
return Err(NodeError::Actor(
"serving read queue capacity is invalid".to_string(),
));
}
if self.serving_read_key_byte_capacity < MAX_FRAME_BYTES
|| self.serving_read_key_byte_capacity > Semaphore::MAX_PERMITS
{
return Err(NodeError::Actor(format!(
"serving read key byte capacity must be between {MAX_FRAME_BYTES} and {}",
Semaphore::MAX_PERMITS,
)));
}
if self.peer_queue_frame_capacity < 2 {
return Err(NodeError::Actor(
"peer queue frame capacity must be at least 2".to_string(),
));
}
if self.node_peer_queue_byte_capacity < 2 * MAX_FRAME_BYTES
|| self.node_peer_queue_byte_capacity > u32::MAX as usize
|| self.node_peer_queue_byte_capacity > Semaphore::MAX_PERMITS
{
return Err(NodeError::Actor("node peer queue byte capacity must fit two maximum frames and the u32/semaphore permit range".into()));
}
if self.peer_queue_byte_capacity < MIN_PEER_CHANNEL_BYTE_CAPACITY {
return Err(NodeError::Actor(format!(
"peer queue byte capacity must be at least {MIN_PEER_CHANNEL_BYTE_CAPACITY}"
)));
}
if self.replication_control_message_capacity == 0 {
return Err(NodeError::Actor(
"replication control message capacity must be non-zero".to_string(),
));
}
if self.inbound_connection_capacity == 0 {
return Err(NodeError::Actor(
"inbound connection capacity must be non-zero".to_string(),
));
}
if self.receive_byte_capacity < MAX_FRAME_BYTES {
return Err(NodeError::Actor(format!(
"receive byte capacity must be at least {MAX_FRAME_BYTES}"
)));
}
if self.receive_byte_capacity > tokio::sync::Semaphore::MAX_PERMITS {
return Err(NodeError::Actor(format!(
"receive byte capacity exceeds maximum {}",
tokio::sync::Semaphore::MAX_PERMITS
)));
}
if self.peer_queue_frame_capacity > tokio::sync::Semaphore::MAX_PERMITS {
return Err(NodeError::Actor(format!(
"peer queue frame capacity exceeds maximum {}",
tokio::sync::Semaphore::MAX_PERMITS
)));
}
if self.peer_queue_byte_capacity > tokio::sync::Semaphore::MAX_PERMITS {
return Err(NodeError::Actor(format!(
"peer queue byte capacity exceeds maximum {}",
tokio::sync::Semaphore::MAX_PERMITS
)));
}
if self.replication_control_message_capacity > tokio::sync::Semaphore::MAX_PERMITS {
return Err(NodeError::Actor(format!(
"replication control message capacity exceeds maximum {}",
tokio::sync::Semaphore::MAX_PERMITS
)));
}
if self.inbound_connection_capacity > tokio::sync::Semaphore::MAX_PERMITS {
return Err(NodeError::Actor(format!(
"inbound connection capacity exceeds maximum {}",
tokio::sync::Semaphore::MAX_PERMITS
)));
}
if self.queue_admission_timeout.is_zero()
|| self.reconcile_data_admission_timeout.is_zero()
|| self.forwarded_attempt_timeout.is_zero()
|| self.connect_timeout.is_zero()
|| self.handshake_timeout.is_zero()
|| self.socket_read_timeout.is_zero()
|| self.socket_write_timeout.is_zero()
|| self.replication_ack_timeout.is_zero()
{
return Err(NodeError::Actor(
"runtime transport and replication timeouts must be non-zero".to_string(),
));
}
let now = Instant::now();
if [
self.queue_admission_timeout,
self.reconcile_data_admission_timeout,
self.forwarded_attempt_timeout,
self.connect_timeout,
self.handshake_timeout,
self.socket_read_timeout,
self.socket_write_timeout,
self.replication_ack_timeout,
]
.iter()
.any(|timeout| now.checked_add(*timeout).is_none())
{
return Err(NodeError::Actor(
"runtime transport and replication timeouts must be representable".to_string(),
));
}
if self.health_timeout.is_zero()
|| Instant::now().checked_add(self.health_timeout).is_none()
{
return Err(NodeError::Actor(
"health timeout must be non-zero and representable".to_string(),
));
}
Ok(())
}
}
#[derive(Clone, bon::Builder)]
#[non_exhaustive]
pub struct NodeConfig {
pub recovery: Option<RecoveryOptions>,
pub ttl_expiry_handler: Option<Arc<dyn TtlExpiryHandler>>,
#[builder(default)]
pub ttl_expiry_options: TtlExpiryOptions,
pub store: Option<Arc<dyn KvStore>>,
#[builder(default = Arc::new(TcpNetwork))]
pub network: Arc<dyn Network>,
#[builder(default = crate::actor::DEFAULT_FLUSH_BATCH_SIZE)]
pub flush_batch_size: usize,
#[builder(default)]
pub persistence: crate::store::PersistenceConfig,
#[builder(default = crate::actor::DEFAULT_FLUSH_INTERVAL)]
pub flush_interval: Duration,
#[builder(default = 1)]
pub fragment_count: usize,
#[builder(default)]
pub fragment_map: HashMap<FragmentId, Vec<String>>,
#[builder(default)]
pub members: HashMap<String, SocketAddr>,
#[builder(default)]
pub default_pool: PoolConfig,
#[builder(default = PoolConfig::high_priority_defaults())]
pub high_pool: PoolConfig,
#[builder(default = PoolConfig::background_defaults())]
pub background_pool: PoolConfig,
#[builder(default = crate::actor::DEFAULT_CLEAN_INTERVAL)]
pub clean_interval: Duration,
#[builder(default = crate::actor::DEFAULT_SNAPSHOT_INTERVAL)]
pub snapshot_interval: Duration,
#[doc(hidden)]
#[builder(default)]
pub clock_skew_ms: i64,
#[builder(default = Duration::from_secs(300))]
pub max_clock_skew: Duration,
pub cluster_secret: Option<Vec<u8>>,
#[builder(default)]
pub replication: SenderConfig,
pub placement_controller: Option<Arc<dyn PlacementController>>,
#[builder(default = Duration::from_secs(1))]
pub placement_poll_interval: Duration,
#[builder(default = Duration::from_secs(10))]
pub placement_drain_warn_interval: Duration,
}
impl Default for NodeConfig {
fn default() -> Self {
Self::builder().build()
}
}
#[cfg(feature = "rocksdb")]
impl NodeConfig {
pub fn with_rocksdb(
path: impl AsRef<std::path::Path>,
) -> Result<Self, crate::store::StoreError> {
Self::with_rocksdb_options(path, crate::store::RocksDbOptions::default())
}
pub fn with_rocksdb_options(
path: impl AsRef<std::path::Path>,
options: crate::store::RocksDbOptions,
) -> Result<Self, crate::store::StoreError> {
Ok(Self::builder()
.store(Arc::new(crate::store::RocksDbStore::open_with_options(
path, options,
)?))
.build())
}
}
impl Node {
pub fn peer_writer_diagnostics(&self) -> PeerWriterDiagnostics {
self.node_peer_queue_budget.diagnostics()
}
pub fn embedded_operation_diagnostics(&self) -> EmbeddedOperationDiagnostics {
self.embedded_operations.diagnostics()
}
pub fn replication_retained_bytes(&self) -> usize {
self.runtime_options.replication_retention.used()
}
pub fn reconciliation_retained_bytes(&self) -> usize {
self.runtime_options.reconciliation_retention.used()
}
pub fn persistence_retained_bytes(&self) -> usize {
self.persistence_budget.used()
}
pub fn active_storage_jobs(&self) -> usize {
self.storage_executor.active()
}
fn fragment_actor(&self, key: &str) -> Option<ActorRef<KvMessage>> {
let frag = fragment_of(key, self.router.fragment_count);
if self.serving.read().unwrap().contains(&frag) {
self.router.route(key)
} else {
None
}
}
async fn ensure_sender(&self, fragment: FragmentId, peer: &str) {
ensure_sender_entry(
fragment,
peer,
SenderEntryDeps {
config: &self.sender_config,
runtime: &self
.runtime_options
.sender_options()
.with_write_concerns(self.write_concerns.clone())
.with_recovery(self.recovery.clone()),
router: &self.router,
hlc: &self.hlc,
senders: &self.senders,
finalizing_senders: &self.finalizing_senders,
retiring_senders: &self.retiring_senders,
reports: &self.sender_reports,
peer_failures: &self.peer_failures,
operation_gate: &self.operation_gate,
allow_quiescing: false,
},
)
.await;
}
fn replica_order(&self, frag: FragmentId) -> Vec<String> {
let map = self.fragment_map.read().unwrap();
if map.is_empty() {
return Vec::new(); }
let Some(replicas) = map.get(&frag) else {
return Vec::new();
};
let serves = replicas.iter().any(|id| id == &self.node_id)
&& self.serving.read().unwrap().contains(&frag);
let mut order: Vec<String> = replicas
.iter()
.filter(|id| *id != &self.node_id)
.cloned()
.collect();
if serves {
order.insert(0, self.node_id.clone());
}
order
}
async fn peer_sender(&self, node_id: &str) -> Option<PeerChannel> {
self.peers.lock().await.get(node_id).cloned()
}
pub async fn put(&self, key: String, value: Vec<u8>) -> Result<(), NodeError> {
let _operation = self.admit_operation()?;
let retention =
self.embedded_operations
.admit(embedded_operations::single_request_bytes(
&key,
Some(&value),
))?;
self.put_with_ttl_and_priority(key, value, None, OpPriority::Default, retention)
.await
}
pub async fn put_with_priority(
&self,
key: String,
value: Vec<u8>,
priority: OpPriority,
) -> Result<(), NodeError> {
let _operation = self.admit_operation()?;
let retention =
self.embedded_operations
.admit(embedded_operations::single_request_bytes(
&key,
Some(&value),
))?;
self.put_with_ttl_and_priority(key, value, None, priority, retention)
.await
}
pub async fn put_with_options(
&self,
key: String,
value: Vec<u8>,
ttl: Option<Duration>,
priority: OpPriority,
) -> Result<(), NodeError> {
let _operation = self.admit_operation()?;
let retention =
self.embedded_operations
.admit(embedded_operations::single_request_bytes(
&key,
Some(&value),
))?;
self.put_with_ttl_and_priority(key, value, ttl, priority, retention)
.await
}
async fn put_with_ttl_and_priority(
&self,
key: String,
value: Vec<u8>,
ttl: Option<Duration>,
priority: OpPriority,
retention: Arc<RequestRetention>,
) -> Result<(), NodeError> {
let expires_at_ms = ttl
.map(|ttl| ttl_expiry_deadline_ms(crate::actor::now_ms(), ttl))
.transpose()?;
let seq = {
let mut guard = self.next_seq.lock().await;
*guard += 1;
*guard
};
let now = self
.hlc
.lock()
.await
.tick()
.map_err(|error| NodeError::Codec(error.to_string()))?;
let mut vv = VersionedValue::new_version(value, now, self.node_id.clone());
vv.expires_at_ms = expires_at_ms;
let op = ReplicatedOp {
key,
value: vv,
seq,
boot_id: self.boot_id,
};
self.apply_local_with_priority(op, priority, retention)
.await
}
pub async fn put_with_ttl(
&self,
key: String,
value: Vec<u8>,
ttl: Option<Duration>,
) -> Result<(), NodeError> {
let _operation = self.admit_operation()?;
let retention =
self.embedded_operations
.admit(embedded_operations::single_request_bytes(
&key,
Some(&value),
))?;
self.put_with_ttl_and_priority(key, value, ttl, OpPriority::Default, retention)
.await
}
pub async fn delete(&self, key: String) -> Result<(), NodeError> {
self.delete_with_priority(key, OpPriority::Default).await
}
pub async fn delete_with_priority(
&self,
key: String,
priority: OpPriority,
) -> Result<(), NodeError> {
let _operation = self.admit_operation()?;
let retention = self
.embedded_operations
.admit(embedded_operations::single_request_bytes(&key, None))?;
let seq = {
let mut guard = self.next_seq.lock().await;
*guard += 1;
*guard
};
let now = self
.hlc
.lock()
.await
.tick()
.map_err(|error| NodeError::Codec(error.to_string()))?;
let op = ReplicatedOp {
key: key.clone(),
value: VersionedValue::tombstone_version(now, self.node_id.clone()),
seq,
boot_id: self.boot_id,
};
self.apply_local_with_priority(op, priority, retention)
.await
}
async fn apply_local_with_priority(
&self,
op: ReplicatedOp,
priority: OpPriority,
retention: Arc<RequestRetention>,
) -> Result<(), NodeError> {
self.apply_routed_with_concern(op, priority, None, retention)
.await
.map(|_| ())
.map_err(crate::write_concern::WriteConcernError::into_node_error)
}
pub async fn get(&self, key: String) -> Result<Option<VersionedValue>, NodeError> {
let _operation = self.admit_operation()?;
let retention = self
.embedded_operations
.admit(embedded_operations::single_request_bytes(&key, None))?;
self.get_with_priority_admitted(key, OpPriority::Default, retention)
.await
}
pub async fn get_with_priority(
&self,
key: String,
priority: OpPriority,
) -> Result<Option<VersionedValue>, NodeError> {
let _operation = self.admit_operation()?;
let retention = self
.embedded_operations
.admit(embedded_operations::single_request_bytes(&key, None))?;
self.get_with_priority_admitted(key, priority, retention)
.await
}
async fn get_with_priority_admitted(
&self,
key: String,
priority: OpPriority,
retention: Arc<RequestRetention>,
) -> Result<Option<VersionedValue>, NodeError> {
if let Some(actor) = self.fragment_actor(&key) {
let ownership: Arc<dyn Send + Sync> = Arc::new((retention, self.admit_operation()?));
let res = actor
.call(
|reply| {
KvMessage::GetBatchRetained(crate::actor::RetainedBatchRead::new(
vec![key],
0,
MAX_FRAME_BYTES,
reply,
priority,
ownership,
))
},
Some(self.runtime_options.local_work_timeout()),
)
.await
.map_err(|error| NodeError::Actor(error.to_string()))?;
return match res {
CallResult::Success(mut page) if page.entries.len() == 1 => page
.entries
.remove(0)
.result
.map_err(NodeError::Backpressure),
CallResult::Success(_) => {
Err(NodeError::Actor("point read result cardinality".into()))
}
CallResult::Timeout => Err(NodeError::Actor("get timed out".into())),
CallResult::SenderError => {
Err(NodeError::Actor("get reply channel dropped".into()))
}
};
}
let frag = fragment_of(&key, self.router.fragment_count);
let replicas = self.replica_order(frag);
if replicas.is_empty() {
return Err(NodeError::Actor(format!("no replica for fragment {frag}")));
}
let wire_priority = priority_to_proto(priority);
check_envelope_budget(
&Envelope {
msg: Some(EnvelopeMsg::ForwardedGet(ForwardedGet {
key: key.clone(),
req_id: u64::MAX,
priority: wire_priority,
hops: MAX_FORWARD_HOPS,
})),
},
MAX_FRAME_BYTES,
)
.map_err(|error| {
NodeError::Codec(format!("get key cannot fit its request frame: {error}"))
})?;
let mut tried = Vec::with_capacity(replicas.len());
for owner in replicas {
tried.push(owner.clone());
let Some(tx) = self.peer_sender(&owner).await else {
continue;
};
let req_id = self.next_req_id.fetch_add(1, Ordering::Relaxed);
let (resp_tx, resp_rx) = oneshot::channel();
self.pending_gets.lock().unwrap().insert(
req_id,
PendingGet {
expected_peer: owner,
expected_key: key.clone(),
reply: resp_tx,
},
);
let _pending_guard = PendingGetGuard {
req_id,
pending: self.pending_gets.clone(),
};
let attempt_deadline = deadline_after(self.runtime_options.forwarded_attempt_timeout);
if !try_send_envelope_until(
&tx,
Envelope {
msg: Some(EnvelopeMsg::ForwardedGet(ForwardedGet {
key: key.clone(),
req_id,
priority: wire_priority,
hops: 0,
})),
},
attempt_deadline,
)
.await
{
continue;
}
let response = tokio::time::timeout_at(attempt_deadline, resp_rx).await;
match response {
Ok(Ok(Ok(value))) => return Ok(value),
Ok(Ok(Err(error @ NodeError::Backpressure(_)))) => return Err(error),
Ok(Ok(Err(_))) | Ok(Err(_)) | Err(_) => continue,
}
}
Err(NodeError::Network(format!(
"no replica answered get for fragment {frag} (tried {})",
tried.join(",")
)))
}
pub async fn get_all(&self) -> Result<HashMap<String, VersionedValue>, NodeError> {
let _operation = self.admit_operation()?;
let retention = self
.embedded_operations
.admit(std::mem::size_of::<KvMessage>())?;
let ownership: Arc<dyn Send + Sync> = Arc::new((retention, self.admit_operation()?));
let all = tokio::time::timeout(
self.runtime_options.local_work_timeout(),
self.router.get_all_retained(Some(ownership)),
)
.await
.map_err(|_| NodeError::Actor("get_all local work timed out".into()))??;
Ok(all.into_iter().filter(|(_, v)| !v.is_tombstone).collect())
}
pub async fn put_multi_with_priority(
&self,
kvs: Vec<(String, Vec<u8>)>,
priority: OpPriority,
) -> Vec<Result<(), NodeError>> {
let _operation = match self.admit_operation() {
Ok(operation) => operation,
Err(_) => {
return kvs
.into_iter()
.map(|_| Err(NodeError::ShuttingDown))
.collect()
}
};
let retention = match self
.embedded_operations
.admit(embedded_operations::batch_put_request_bytes(&kvs))
{
Ok(retention) => retention,
Err(error) => return embedded_operations::reject_batch(kvs, error),
};
self.put_multi_with_priority_admitted(kvs, priority, retention)
.await
}
async fn put_multi_with_priority_admitted(
&self,
kvs: Vec<(String, Vec<u8>)>,
priority: OpPriority,
retention: Arc<RequestRetention>,
) -> Vec<Result<(), NodeError>> {
let mut local: Vec<(usize, String, Vec<u8>)> = Vec::new();
let mut remote: HashMap<FragmentId, Vec<(usize, String, Vec<u8>)>> = HashMap::new();
let mut results: Vec<Option<Result<(), NodeError>>> =
(0..kvs.len()).map(|_| None).collect();
for (i, (key, value)) in kvs.into_iter().enumerate() {
if self.fragment_actor(&key).is_some() {
local.push((i, key, value));
} else {
let frag = fragment_of(&key, self.router.fragment_count);
if self.replica_order(frag).is_empty() {
results[i] = Some(Err(NodeError::Actor(format!(
"no replica for fragment {frag}"
))));
} else {
remote.entry(frag).or_default().push((i, key, value));
}
}
}
let mut local = local.into_iter();
loop {
let chunk: Vec<_> = local.by_ref().take(MAX_LOCAL_BATCH_IN_FLIGHT).collect();
if chunk.is_empty() {
break;
}
let local_futs = chunk.into_iter().map(|(i, key, value)| {
let this = self;
let retention = retention.clone();
async move {
let r = this
.put_with_ttl_and_priority(key, value, None, priority, retention)
.await;
(i, r)
}
});
for (i, r) in futures::future::join_all(local_futs).await {
results[i] = Some(r);
}
}
let wire_priority = priority_to_proto(priority);
for (frag, items) in remote {
let mut remaining = VecDeque::new();
for (index, key, value) in items {
if let Err(error) = validate_record_wire_budget(&key, value.len(), &self.node_id) {
results[index] = Some(Err(error));
continue;
}
let seq = {
let mut guard = self.next_seq.lock().await;
*guard += 1;
*guard
};
let version = match self.hlc.lock().await.tick() {
Ok(version) => version,
Err(error) => {
results[index] = Some(Err(NodeError::Codec(error.to_string())));
continue;
}
};
remaining.push_back((
index,
ForwardedPut {
key,
value,
timestamp_ms: version.timestamp_ms,
logical_counter: version.logical_counter,
node_id: self.node_id.clone(),
seq,
boot_id: self.boot_id,
expires_at_ms: 0,
is_tombstone: false,
priority: wire_priority,
hops: 0,
req_id: 0,
write_concern: None,
},
));
}
while !remaining.is_empty() {
let mut chunk = Vec::new();
let mut request_payload_len = batch_put_base_encoded_len(wire_priority);
let mut response_payload_len = mutation_response_base_encoded_len();
while chunk.len() < MAX_WIRE_BATCH_ITEMS {
let Some((index, put)) = remaining.pop_front() else {
break;
};
let next_request_payload =
request_payload_len + message_field_len(1, put.encoded_len());
let next_response_payload = response_payload_len
+ message_field_len(2, max_mutation_result_encoded_len(put.key.len()));
let request_len = batch_put_envelope_encoded_len(next_request_payload);
let response_len =
mutation_response_envelope_encoded_len(next_response_payload);
if !chunk.is_empty()
&& (request_len > WIRE_BATCH_PAGE_BYTES
|| response_len > WIRE_BATCH_PAGE_BYTES)
{
remaining.push_front((index, put));
break;
}
if request_len > MAX_FRAME_BYTES || response_len > MAX_FRAME_BYTES {
results[index] = Some(Err(NodeError::Codec(format!(
"batch put item cannot fit its request/response frames: request={request_len} response={response_len} limit={MAX_FRAME_BYTES}"
))));
continue;
}
request_payload_len = next_request_payload;
response_payload_len = next_response_payload;
chunk.push((index, put));
}
if chunk.is_empty() {
continue;
}
let mut unresolved = chunk;
let mut tried: Vec<String> = Vec::new();
for owner in self.replica_order(frag) {
tried.push(owner.clone());
let Some(tx) = self.peer_sender(&owner).await else {
continue;
};
let expected_keys: Vec<String> =
unresolved.iter().map(|(_, put)| put.key.clone()).collect();
let req_id = self.next_req_id.fetch_add(1, Ordering::Relaxed);
let (resp_tx, resp_rx) = oneshot::channel();
self.pending_mutations.lock().unwrap().insert(
req_id,
PendingMutation {
expected_peer: owner.clone(),
expected_keys: expected_keys.clone(),
reply: resp_tx,
},
);
let _pending_guard = PendingMutationGuard {
req_id,
pending: self.pending_mutations.clone(),
};
let attempt_deadline =
deadline_after(self.runtime_options.forwarded_attempt_timeout);
if !try_send_envelope_until(
&tx,
Envelope {
msg: Some(EnvelopeMsg::BatchPut(BatchPut {
puts: unresolved.iter().map(|(_, put)| put.clone()).collect(),
priority: wire_priority,
req_id,
})),
},
attempt_deadline,
)
.await
{
continue;
}
let response = tokio::time::timeout_at(attempt_deadline, resp_rx).await;
match response {
Ok(Ok(wire_results))
if validate_mutation_results(&expected_keys, &wire_results).is_ok() =>
{
let mut unavailable = Vec::new();
for ((index, put), result) in unresolved.into_iter().zip(wire_results) {
if MutationStatus::try_from(result.status)
== Ok(MutationStatus::Unavailable)
{
unavailable.push((index, put));
} else {
results[index] = Some(decode_mutation_result(result));
}
}
unresolved = unavailable;
if unresolved.is_empty() {
break;
}
}
_ => {}
}
}
for (index, _) in unresolved {
results[index] = Some(Err(NodeError::Network(format!(
"no replica acknowledged batch put for fragment {frag} (tried {})",
tried.join(",")
))));
}
}
}
results.into_iter().map(|r| r.unwrap()).collect()
}
pub async fn put_multi(&self, kvs: Vec<(String, Vec<u8>)>) -> Vec<Result<(), NodeError>> {
let _operation = match self.admit_operation() {
Ok(operation) => operation,
Err(_) => {
return kvs
.into_iter()
.map(|_| Err(NodeError::ShuttingDown))
.collect()
}
};
let retention = match self
.embedded_operations
.admit(embedded_operations::batch_put_request_bytes(&kvs))
{
Ok(retention) => retention,
Err(error) => return embedded_operations::reject_batch(kvs, error),
};
self.put_multi_with_priority_admitted(kvs, OpPriority::Background, retention)
.await
}
pub async fn get_multi_with_priority(
&self,
keys: Vec<String>,
priority: OpPriority,
) -> Vec<Result<Option<VersionedValue>, NodeError>> {
let _operation = match self.admit_operation() {
Ok(operation) => operation,
Err(_) => {
return keys
.into_iter()
.map(|_| Err(NodeError::ShuttingDown))
.collect()
}
};
let retention = match self
.embedded_operations
.admit(embedded_operations::batch_get_request_bytes(&keys))
{
Ok(retention) => retention,
Err(error) => return embedded_operations::reject_batch(keys, error),
};
self.get_multi_with_priority_admitted(keys, priority, retention)
.await
}
async fn get_multi_with_priority_admitted(
&self,
keys: Vec<String>,
priority: OpPriority,
retention: Arc<RequestRetention>,
) -> Vec<Result<Option<VersionedValue>, NodeError>> {
let mut local: Vec<(usize, String)> = Vec::new();
let mut remote: HashMap<FragmentId, Vec<(usize, String)>> = HashMap::new();
let mut results: Vec<Option<Result<Option<VersionedValue>, NodeError>>> =
(0..keys.len()).map(|_| None).collect();
for (i, key) in keys.into_iter().enumerate() {
if self.fragment_actor(&key).is_some() {
local.push((i, key));
} else {
let frag = fragment_of(&key, self.router.fragment_count);
if self.replica_order(frag).is_empty() {
results[i] = Some(Err(NodeError::Actor(format!(
"no replica for fragment {frag}"
))));
} else {
remote.entry(frag).or_default().push((i, key));
}
}
}
let mut local = local.into_iter();
loop {
let chunk: Vec<_> = local.by_ref().take(MAX_LOCAL_BATCH_IN_FLIGHT).collect();
if chunk.is_empty() {
break;
}
let local_futs = chunk.into_iter().map(|(i, key)| {
let this = self;
let retention = retention.clone();
async move {
let r = this
.get_with_priority_admitted(key, priority, retention)
.await;
(i, r)
}
});
for (i, r) in futures::future::join_all(local_futs).await {
results[i] = Some(r);
}
}
let wire_priority = priority_to_proto(priority);
let singleton_response_base = batch_get_response_base_encoded_len(u64::MAX);
for (frag, items) in remote {
let mut remaining: VecDeque<_> = items.into_iter().collect();
while !remaining.is_empty() {
let mut chunk = Vec::new();
let mut request_payload_len = batch_get_base_encoded_len(wire_priority);
while chunk.len() < MAX_BATCH_GET_REQUEST_KEYS {
let Some((index, key)) = remaining.pop_front() else {
break;
};
let next_request_payload =
request_payload_len + message_field_len(2, key.len());
let request_len = batch_get_envelope_encoded_len(next_request_payload);
let missing_result_len = max_batch_get_non_value_result_encoded_len(key.len());
let response_payload_len =
singleton_response_base + message_field_len(2, missing_result_len);
let response_len =
batch_get_response_envelope_encoded_len(response_payload_len);
if !chunk.is_empty() && request_len > WIRE_BATCH_PAGE_BYTES {
remaining.push_front((index, key));
break;
}
if request_len > MAX_FRAME_BYTES || response_len > MAX_FRAME_BYTES {
results[index] = Some(Err(NodeError::Codec(format!(
"batch get key cannot fit its request/response frames: request={request_len} response={response_len} limit={MAX_FRAME_BYTES}"
))));
continue;
}
request_payload_len = next_request_payload;
chunk.push((index, key));
}
if chunk.is_empty() {
continue;
}
let mut offset = 0usize;
let mut tried = Vec::new();
for owner in self.replica_order(frag) {
if offset == chunk.len() {
break;
}
tried.push(owner.clone());
let Some(tx) = self.peer_sender(&owner).await else {
continue;
};
let attempt_deadline =
deadline_after(self.runtime_options.forwarded_attempt_timeout);
while offset < chunk.len() && Instant::now() < attempt_deadline {
let keys: Vec<String> =
chunk[offset..].iter().map(|(_, key)| key.clone()).collect();
let req_id = self.next_req_id.fetch_add(1, Ordering::Relaxed);
let (resp_tx, resp_rx) = oneshot::channel();
self.pending_batch_gets.lock().unwrap().insert(
req_id,
PendingBatchGet {
expected_peer: owner.clone(),
expected_keys: keys.clone(),
reply: resp_tx,
},
);
let _pending_guard = PendingBatchGetGuard {
req_id,
pending: self.pending_batch_gets.clone(),
};
if !try_send_envelope_until(
&tx,
Envelope {
msg: Some(EnvelopeMsg::BatchGet(BatchGet {
req_id,
keys: keys.clone(),
priority: wire_priority,
})),
},
attempt_deadline,
)
.await
{
break;
}
let response = tokio::time::timeout_at(attempt_deadline, resp_rx).await;
let Ok(Ok(Ok(kvs))) = response else {
break;
};
if kvs.is_empty() {
break;
}
let page_len = kvs.len();
for ((index, key), kv) in chunk[offset..offset + page_len].iter().zip(kvs) {
let result = if kv.shed {
Err(NodeError::Backpressure(format!(
"batch get for key {key} shed by remote"
)))
} else if kv.found {
let mut value = VersionedValue::new_version(
kv.value,
actordb_protocol::RecordVersion::new(
kv.timestamp_ms,
kv.logical_counter,
),
kv.node_id,
);
value.expires_at_ms =
(kv.expires_at_ms != 0).then_some(kv.expires_at_ms);
value.is_tombstone = kv.is_tombstone;
Ok(if value.is_tombstone {
None
} else {
Some(value)
})
} else {
Ok(None)
};
results[*index] = Some(result);
}
offset += page_len;
}
}
if offset < chunk.len() {
for (index, _) in &chunk[offset..] {
results[*index] = Some(Err(NodeError::Network(format!(
"no replica answered batch get for fragment {frag} (tried {})",
tried.join(",")
))));
}
}
}
}
results.into_iter().map(|r| r.unwrap()).collect()
}
pub async fn get_multi(
&self,
keys: Vec<String>,
) -> Vec<Result<Option<VersionedValue>, NodeError>> {
let _operation = match self.admit_operation() {
Ok(operation) => operation,
Err(_) => {
return keys
.into_iter()
.map(|_| Err(NodeError::ShuttingDown))
.collect()
}
};
let retention = match self
.embedded_operations
.admit(embedded_operations::batch_get_request_bytes(&keys))
{
Ok(retention) => retention,
Err(error) => return embedded_operations::reject_batch(keys, error),
};
self.get_multi_with_priority_admitted(keys, OpPriority::Background, retention)
.await
}
pub fn id(&self) -> &str {
&self.node_id
}
#[doc(hidden)]
pub async fn sender_report(
&self,
fragment: FragmentId,
peer: &str,
) -> Option<SenderStateReport> {
let key = (fragment, peer.to_string());
loop {
let report = self.sender_reports.lock().await.get(&key).cloned()?;
let snapshot = report.lock().await.clone();
let reports = self.sender_reports.lock().await;
match reports.get(&key) {
Some(current) if Arc::ptr_eq(current, &report) => return Some(snapshot),
Some(_) => continue,
None => return None,
}
}
}
pub fn addr(&self) -> SocketAddr {
self.listen_addr
}
pub async fn peer_count(&self) -> usize {
self.peers.lock().await.len()
}
pub fn sync_entries_applied(&self) -> u64 {
self.sync_entries_applied.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests {
use super::test_support::*;
pub(super) use super::test_support::{
complete_test_handshake, read_test_envelope, test_connection_context, test_peer_channel,
write_test_envelope,
};
use super::*;
use crate::store::StoreError;
use ractor::ActorProcessingErr;
#[tokio::test]
async fn embedded_get_has_an_explicit_local_actor_wait_deadline() {
let node = Node::start_with_options(
"local-deadline".into(),
"127.0.0.1:0".parse().unwrap(),
NodeConfig::default(),
NodeRuntimeOptions::builder()
.local_work_timeout(Duration::from_millis(20))
.build(),
)
.await
.unwrap();
let original = node.router.route_for_fragment(FragmentId(0)).unwrap();
let (stalled, handle) = StalledLocalActor::spawn(None, StalledLocalActor, ())
.await
.unwrap();
node.router.insert(FragmentId(0), stalled.clone());
let result = tokio::time::timeout(Duration::from_secs(2), node.get("key".into()))
.await
.unwrap();
assert!(matches!(result,Err(NodeError::Actor(message)) if message.contains("timed out")));
node.router.insert(FragmentId(0), original);
stalled.stop(None);
handle.await.unwrap();
node.shutdown().await.unwrap();
}
#[test]
fn ttl_expiry_deadline_preserves_millisecond_precision_and_exact_boundaries() {
assert_eq!(ttl_expiry_deadline_ms(1000, Duration::ZERO).unwrap(), 1000);
assert_eq!(
ttl_expiry_deadline_ms(1000, Duration::from_nanos(999_999)).unwrap(),
1000
);
assert_eq!(
ttl_expiry_deadline_ms(1000, Duration::from_nanos(1_999_999)).unwrap(),
1001
);
assert_eq!(
ttl_expiry_deadline_ms(1000, Duration::from_millis(u64::MAX - 1000)).unwrap(),
u64::MAX
);
assert_eq!(
ttl_expiry_deadline_ms(u64::MAX, Duration::ZERO).unwrap(),
u64::MAX
);
assert_eq!(
ttl_expiry_deadline_ms(0, Duration::from_millis(1)).unwrap(),
1
);
}
#[test]
fn ttl_expiry_deadline_rejects_truncation_overflow_and_absent_expiry_sentinel() {
let wrapping = Duration::new(18_446_744_073_709_556, 616_000_000);
assert_eq!(wrapping.as_millis(), u64::MAX as u128 + 5001);
for ttl in [wrapping, Duration::MAX, Duration::from_millis(u64::MAX)] {
assert!(matches!(
ttl_expiry_deadline_ms(1000, ttl),
Err(NodeError::Codec(_))
));
}
assert!(matches!(
ttl_expiry_deadline_ms(u64::MAX, Duration::from_millis(1)),
Err(NodeError::Codec(_))
));
for ttl in [Duration::ZERO, Duration::from_nanos(999_999)] {
assert!(matches!(
ttl_expiry_deadline_ms(0, ttl),
Err(NodeError::Codec(_))
));
}
}
#[tokio::test]
async fn invalid_ttl_rejects_before_sequence_clock_or_store_and_node_stays_healthy() {
let store = Arc::new(TtlRecordingStore::default());
let node = Node::start(
"ttl-invalid".into(),
"127.0.0.1:0".parse().unwrap(),
ttl_store_config(&store),
)
.await
.unwrap();
let sequence = node.next_seq.lock().await;
let mut clock = node.hlc.lock().await;
let future = crate::actor::now_ms() + 120_000;
clock.seed_persisted(future).unwrap();
let next_request = node.next_req_id.load(Ordering::Relaxed);
for ttl in [
Duration::MAX,
Duration::from_millis(u64::MAX),
Duration::new(18_446_744_073_709_556, 616_000_000),
] {
assert!(matches!(
tokio::time::timeout(
Duration::from_secs(2),
node.put_with_ttl("invalid".into(), b"value".to_vec(), Some(ttl))
)
.await
.unwrap(),
Err(NodeError::Codec(_))
));
}
assert_eq!(*sequence, 0);
assert!(clock.tick().unwrap() > actordb_protocol::RecordVersion::from(future));
assert_eq!(node.next_req_id.load(Ordering::Relaxed), next_request);
drop(clock);
drop(sequence);
assert!(node.get_all().await.unwrap().is_empty());
assert!(store.load().unwrap().is_empty());
assert_eq!(store.writes.load(Ordering::SeqCst), 0);
node.put("healthy".into(), b"ok".to_vec()).await.unwrap();
assert_eq!(
node.get("healthy".into()).await.unwrap().unwrap().value,
b"ok"
);
assert_eq!(store.writes.load(Ordering::SeqCst), 1);
node.shutdown().await.unwrap();
assert!(matches!(
node.put_with_ttl("after-shutdown".into(), Vec::new(), Some(Duration::MAX))
.await,
Err(NodeError::ShuttingDown)
));
}
#[tokio::test]
async fn local_ttl_uses_wall_time_and_retains_future_hlc_expiry_provenance_on_restart() {
let store = Arc::new(TtlRecordingStore::default());
let config = ttl_store_config(&store);
let node = Node::start(
"ttl-local".into(),
"127.0.0.1:0".parse().unwrap(),
config.clone(),
)
.await
.unwrap();
let future = crate::actor::now_ms() + 120_000;
node.hlc.lock().await.seed_persisted(future).unwrap();
node.put_with_ttl("permanent".into(), b"value".to_vec(), None)
.await
.unwrap();
let permanent = node.get("permanent".into()).await.unwrap().unwrap();
assert_eq!(
permanent.version(),
actordb_protocol::RecordVersion::new(future, 1)
);
assert_eq!(permanent.expires_at_ms, None);
let before = crate::actor::now_ms();
node.put_with_ttl(
"expiring".into(),
b"value".to_vec(),
Some(Duration::from_secs(60)),
)
.await
.unwrap();
let after = crate::actor::now_ms();
let expiring = node.get("expiring".into()).await.unwrap().unwrap();
assert_eq!(
expiring.version(),
actordb_protocol::RecordVersion::new(future, 2)
);
assert!((before + 60_000..=after + 60_000).contains(&expiring.expires_at_ms.unwrap()));
assert!(expiring.expires_at_ms.unwrap() < expiring.timestamp_ms);
let mut markers = Vec::new();
for (index, ttl) in [Duration::ZERO, Duration::from_nanos(999_999)]
.into_iter()
.enumerate()
{
let key = format!("immediate-{index}");
let before = crate::actor::now_ms();
assert!(matches!(
node.put_with_ttl(key.clone(), b"value".to_vec(), Some(ttl))
.await,
Err(NodeError::Expired)
));
let after = crate::actor::now_ms();
assert!(node.get(key.clone()).await.unwrap().is_none());
let marker = node.router.get_all().await.unwrap().remove(&key).unwrap();
assert!(marker.is_tombstone);
assert_eq!(
marker.version(),
actordb_protocol::RecordVersion::new(future, 3 + index as u64)
);
assert_eq!(marker.node_id, "ttl-local");
assert!((before..=after).contains(&marker.expires_at_ms.unwrap()));
markers.push((key, marker));
}
node.shutdown().await.unwrap();
let restarted = Node::start("ttl-local".into(), "127.0.0.1:0".parse().unwrap(), config)
.await
.unwrap();
let values = restarted.router.get_all().await.unwrap();
assert_eq!(values["permanent"], permanent);
assert_eq!(values["expiring"], expiring);
for (key, marker) in markers {
assert_eq!(values[&key], marker);
assert!(restarted.get(key).await.unwrap().is_none());
}
restarted.shutdown().await.unwrap();
}
#[tokio::test]
async fn forwarded_ttl_preserves_origin_wall_deadline_and_hlc_stamp() {
let store = Arc::new(TtlRecordingStore::default());
let placement = HashMap::from([(FragmentId(0), vec!["ttl-owner".to_string()])]);
let owner = Node::start(
"ttl-owner".into(),
"127.0.0.1:0".parse().unwrap(),
NodeConfig {
fragment_map: placement.clone(),
..ttl_store_config(&store)
},
)
.await
.unwrap();
let caller = Node::start(
"ttl-caller".into(),
"127.0.0.1:0".parse().unwrap(),
NodeConfig {
fragment_map: placement,
..Default::default()
},
)
.await
.unwrap();
caller.connect(owner.addr()).await.unwrap();
tokio::time::timeout(Duration::from_secs(2), async {
while caller.peer_sender("ttl-owner").await.is_none()
|| owner.peer_sender("ttl-caller").await.is_none()
{
tokio::task::yield_now().await;
}
})
.await
.unwrap();
let future = crate::actor::now_ms() + 120_000;
let mut clock = caller.hlc.lock().await;
clock.seed_persisted(future).unwrap();
let next_request = caller.next_req_id.load(Ordering::Relaxed);
assert!(matches!(
tokio::time::timeout(
Duration::from_secs(2),
caller.put_with_ttl(
"invalid-remote".into(),
b"value".to_vec(),
Some(Duration::MAX)
)
)
.await
.unwrap(),
Err(NodeError::Codec(_))
));
assert_eq!(*caller.next_seq.lock().await, 0);
assert_eq!(caller.next_req_id.load(Ordering::Relaxed), next_request);
assert_eq!(store.writes.load(Ordering::SeqCst), 0);
drop(clock);
assert!(owner.get_all().await.unwrap().is_empty());
assert!(store.load().unwrap().is_empty());
let before = crate::actor::now_ms();
caller
.put_with_ttl(
"forwarded".into(),
b"value".to_vec(),
Some(Duration::from_secs(60)),
)
.await
.unwrap();
let after = crate::actor::now_ms();
let value = owner.get("forwarded".into()).await.unwrap().unwrap();
assert_eq!(
value.version(),
actordb_protocol::RecordVersion::new(future, 1)
);
assert_eq!(value.node_id, "ttl-caller");
assert!((before + 60_000..=after + 60_000).contains(&value.expires_at_ms.unwrap()));
assert!(value.expires_at_ms.unwrap() < value.timestamp_ms);
let before = crate::actor::now_ms();
assert!(matches!(
caller
.put_with_ttl(
"immediate-remote".into(),
b"value".to_vec(),
Some(Duration::ZERO)
)
.await,
Err(NodeError::Expired)
));
let after = crate::actor::now_ms();
let marker = owner
.router
.get_all()
.await
.unwrap()
.remove("immediate-remote")
.unwrap();
assert!(marker.is_tombstone);
assert_eq!(marker.node_id, "ttl-caller");
assert_eq!(
marker.version(),
actordb_protocol::RecordVersion::new(future, 2)
);
assert!((before..=after).contains(&marker.expires_at_ms.unwrap()));
caller.shutdown().await.unwrap();
owner.shutdown().await.unwrap();
let persisted = store.load().unwrap();
assert!(persisted.iter().any(|(_, stored)| stored == &value));
assert!(persisted.iter().any(|(_, stored)| stored == &marker));
}
#[tokio::test]
async fn ttl_deadline_includes_waiting_for_the_origin_clock() {
let node = Node::start(
"ttl-wait".into(),
"127.0.0.1:0".parse().unwrap(),
NodeConfig::default(),
)
.await
.unwrap();
let clock = node.hlc.lock().await;
let mut request = Box::pin(node.put_with_ttl(
"waited".into(),
b"value".to_vec(),
Some(Duration::from_millis(5)),
));
assert!(futures::poll!(request.as_mut()).is_pending());
tokio::time::sleep(Duration::from_millis(20)).await;
drop(clock);
assert!(matches!(request.await, Err(NodeError::Expired)));
assert!(node.get("waited".into()).await.unwrap().is_none());
let marker = node
.router
.get_all()
.await
.unwrap()
.remove("waited")
.unwrap();
assert!(marker.is_tombstone);
assert!(marker.expires_at_ms.unwrap() < marker.timestamp_ms);
node.shutdown().await.unwrap();
}
#[test]
fn mutation_responses_require_exact_cardinality_order_and_indices() {
let expected = vec!["a".to_string(), "b".to_string()];
assert!(validate_mutation_results(&expected, &[result("a", 0), result("b", 1)]).is_ok());
assert!(validate_mutation_results(&expected, &[result("a", 0)]).is_err());
assert!(validate_mutation_results(&expected, &[result("b", 0), result("a", 1)]).is_err());
assert!(validate_mutation_results(&expected, &[result("a", 1), result("b", 0)]).is_err());
}
#[test]
fn batch_get_prefix_validation_is_positional_and_preserves_duplicates() {
fn missing(key: &str) -> BatchKeyValue {
BatchKeyValue {
key: key.to_string(),
found: false,
value: Vec::new(),
timestamp_ms: 0,
logical_counter: 0,
node_id: String::new(),
expires_at_ms: 0,
is_tombstone: false,
shed: false,
}
}
let expected = vec!["dup".to_string(), "missing".to_string(), "dup".to_string()];
assert!(validate_batch_get_results(&expected, &[]).is_ok());
assert!(
validate_batch_get_results(&expected, &[missing("dup"), missing("missing")]).is_ok()
);
assert!(
validate_batch_get_results(&expected, &[missing("missing"), missing("dup")]).is_err()
);
assert!(validate_batch_get_results(
&expected,
&[
missing("dup"),
missing("missing"),
missing("dup"),
missing("extra"),
]
)
.is_err());
}
#[test]
fn aggregate_batch_length_accounting_matches_prost() {
let puts = vec![
ForwardedPut {
key: String::new(),
value: Vec::new(),
timestamp_ms: 1,
logical_counter: 0,
node_id: "node".to_string(),
seq: 1,
boot_id: 1,
expires_at_ms: 0,
is_tombstone: false,
priority: ProtoPriority::Default as i32,
hops: 0,
req_id: 0,
write_concern: None,
},
ForwardedPut {
key: "k".repeat(127),
value: vec![7; 128],
timestamp_ms: u64::MAX,
logical_counter: 0,
node_id: "node".to_string(),
seq: u64::MAX,
boot_id: u64::MAX,
expires_at_ms: u64::MAX,
is_tombstone: true,
priority: ProtoPriority::Default as i32,
hops: 0,
req_id: 0,
write_concern: None,
},
];
let priority = ProtoPriority::Default as i32;
let put_payload = batch_put_base_encoded_len(priority)
+ puts
.iter()
.map(|put| message_field_len(1, put.encoded_len()))
.sum::<usize>();
let put_envelope = Envelope {
msg: Some(EnvelopeMsg::BatchPut(BatchPut {
puts,
priority,
req_id: u64::MAX,
})),
};
assert_eq!(
batch_put_envelope_encoded_len(put_payload),
put_envelope.encoded_len()
);
let keys = vec![String::new(), "k".repeat(127), "k".repeat(128)];
let get_payload = batch_get_base_encoded_len(priority)
+ keys
.iter()
.map(|key| message_field_len(2, key.len()))
.sum::<usize>();
let get_envelope = Envelope {
msg: Some(EnvelopeMsg::BatchGet(BatchGet {
req_id: u64::MAX,
keys,
priority,
})),
};
assert_eq!(
batch_get_envelope_encoded_len(get_payload),
get_envelope.encoded_len()
);
let shed = BatchKeyValue {
key: "shed".repeat(32),
found: false,
value: Vec::new(),
timestamp_ms: 0,
logical_counter: 0,
node_id: String::new(),
expires_at_ms: 0,
is_tombstone: false,
shed: true,
};
assert_eq!(
max_batch_get_non_value_result_encoded_len(shed.key.len()),
shed.encoded_len()
);
let shed_payload = batch_get_response_base_encoded_len(u64::MAX)
+ message_field_len(2, shed.encoded_len());
let shed_envelope = Envelope {
msg: Some(EnvelopeMsg::BatchGetResponse(BatchGetResponse {
req_id: u64::MAX,
results: vec![shed],
})),
};
assert_eq!(
batch_get_response_envelope_encoded_len(shed_payload),
shed_envelope.encoded_len()
);
let key = "result".to_string();
let worst = MutationResult {
key: key.clone(),
status: MutationStatus::Unavailable as i32,
error: "e".repeat(MAX_WIRE_ERROR_BYTES),
winner_timestamp_ms: u64::MAX,
winner_logical_counter: 0,
winner_node_id: "n".repeat(MAX_WIRE_NODE_ID_BYTES),
winner_is_tombstone: true,
request_index: u32::MAX,
write_concern: None,
};
assert_eq!(
max_mutation_result_encoded_len(key.len()),
worst.encoded_len()
);
let response_payload =
mutation_response_base_encoded_len() + message_field_len(2, worst.encoded_len());
let response = Envelope {
msg: Some(EnvelopeMsg::MutationResponse(MutationResponse {
req_id: u64::MAX,
results: vec![worst],
})),
};
assert_eq!(
mutation_response_envelope_encoded_len(response_payload),
response.encoded_len()
);
}
#[test]
fn batch_get_single_key_preflight_reserves_the_shed_bit_at_hard_boundary() {
let priority = ProtoPriority::Default as i32;
let fits = |key_len: usize| {
let request_payload =
batch_get_base_encoded_len(priority) + message_field_len(2, key_len);
let response_payload = batch_get_response_base_encoded_len(u64::MAX)
+ message_field_len(2, max_batch_get_non_value_result_encoded_len(key_len));
batch_get_envelope_encoded_len(request_payload) <= MAX_FRAME_BYTES
&& batch_get_response_envelope_encoded_len(response_payload) <= MAX_FRAME_BYTES
};
let mut low = 0usize;
let mut high = MAX_FRAME_BYTES;
while low < high {
let midpoint = low + (high - low).div_ceil(2);
if fits(midpoint) {
low = midpoint;
} else {
high = midpoint - 1;
}
}
assert!(fits(low));
assert!(!fits(low + 1));
}
#[test]
fn wire_errors_are_utf8_safely_bounded() {
let truncated = truncate_wire_error("🦀".repeat(MAX_WIRE_ERROR_BYTES));
assert!(truncated.len() <= MAX_WIRE_ERROR_BYTES);
assert!(truncated.is_char_boundary(truncated.len()));
assert_eq!(truncate_wire_error("short".to_string()), "short");
}
#[test]
fn runtime_regions_validate_utf8_byte_lengths() {
for (node_id, region) in [
(String::new(), "west".to_string()),
("n".repeat(256), "west".to_string()),
("node".to_string(), String::new()),
("node".to_string(), "r".repeat(256)),
("node".to_string(), "🦀".repeat(64)),
] {
let options =
NodeRuntimeOptions::default().with_regions(HashMap::from([(node_id, region)]));
assert!(options.validate().is_err());
}
let options = NodeRuntimeOptions::builder()
.regions(HashMap::from([("n".repeat(255), "r".repeat(255))]))
.build();
assert!(options.validate().is_ok());
assert!(NodeRuntimeOptions::default().validate().is_ok());
}
#[tokio::test]
async fn invalid_region_config_fails_before_bind() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let options = NodeRuntimeOptions::default()
.with_regions(HashMap::from([("node".to_string(), String::new())]));
let result = Node::start_with_options(
"node".to_string(),
listener.local_addr().unwrap(),
NodeConfig::default(),
options,
)
.await;
assert!(matches!(result, Err(NodeError::Actor(error)) if error.contains("region label")));
}
#[test]
fn runtime_options_reject_zero_write_concern_acknowledgement_cap() {
assert!(NodeRuntimeOptions::default()
.with_write_concern_acknowledgement_cap(0)
.validate()
.is_err());
assert!(NodeRuntimeOptions::default()
.with_write_concern_acknowledgement_cap(0)
.without_write_concern_acknowledgement_cap()
.validate()
.is_ok());
}
#[test]
fn runtime_options_reject_unrepresentable_limits() {
for capacity in [0, Semaphore::MAX_PERMITS.saturating_add(1)] {
assert!(NodeRuntimeOptions::default()
.with_relay_capacity(capacity)
.validate()
.is_err());
assert!(NodeRuntimeOptions::default()
.with_relay_key_byte_capacity(capacity)
.validate()
.is_err());
assert!(NodeRuntimeOptions::default()
.with_forwarded_mutation_capacity(capacity)
.validate()
.is_err());
assert!(NodeRuntimeOptions::default()
.with_forwarded_mutation_byte_capacity(capacity)
.validate()
.is_err());
}
assert!(NodeRuntimeOptions::default()
.with_relay_capacity(1)
.with_relay_key_byte_capacity(1)
.validate()
.is_ok());
for options in [
NodeRuntimeOptions::default().with_serving_read_queue_capacity(0),
NodeRuntimeOptions::default().with_serving_read_queue_capacity(usize::MAX),
NodeRuntimeOptions::default().with_serving_read_key_byte_capacity(MAX_FRAME_BYTES - 1),
NodeRuntimeOptions::default().with_serving_read_key_byte_capacity(usize::MAX),
] {
assert!(options.validate().is_err());
}
assert!(NodeRuntimeOptions::default()
.with_forwarded_mutation_capacity(1)
.with_forwarded_mutation_byte_capacity(1)
.validate()
.is_ok());
assert!(NodeRuntimeOptions::default()
.with_socket_write_timeout(Duration::MAX)
.validate()
.is_err());
assert!(NodeRuntimeOptions::default()
.with_peer_queue_frame_capacity(tokio::sync::Semaphore::MAX_PERMITS.saturating_add(1))
.validate()
.is_err());
assert!(NodeRuntimeOptions::default()
.with_peer_queue_frame_capacity(1)
.validate()
.is_err());
assert!(NodeRuntimeOptions::default()
.with_peer_queue_byte_capacity(MIN_PEER_CHANNEL_BYTE_CAPACITY - 1)
.validate()
.is_err());
assert!(NodeRuntimeOptions::default()
.with_peer_queue_byte_capacity(MIN_PEER_CHANNEL_BYTE_CAPACITY)
.validate()
.is_ok());
assert!(NodeRuntimeOptions::default()
.with_replication_control_message_capacity(0)
.validate()
.is_err());
assert!(NodeRuntimeOptions::default()
.with_inbound_connection_capacity(0)
.validate()
.is_err());
for capacity in [0, MAX_FRAME_BYTES - 1, Semaphore::MAX_PERMITS + 1] {
assert!(NodeRuntimeOptions::default()
.with_receive_byte_capacity(capacity)
.validate()
.is_err());
}
assert!(NodeRuntimeOptions::default()
.with_receive_byte_capacity(MAX_FRAME_BYTES)
.validate()
.is_ok());
assert!(NodeRuntimeOptions::default()
.with_replication_control_message_capacity(
tokio::sync::Semaphore::MAX_PERMITS.saturating_add(1),
)
.validate()
.is_err());
assert!(
NodeRuntimeOptions::default()
.with_inbound_connection_capacity(
tokio::sync::Semaphore::MAX_PERMITS.saturating_add(1),
)
.validate()
.is_err()
);
for options in [
NodeRuntimeOptions::default().with_connect_timeout(Duration::ZERO),
NodeRuntimeOptions::default().with_handshake_timeout(Duration::ZERO),
NodeRuntimeOptions::default().with_socket_read_timeout(Duration::ZERO),
NodeRuntimeOptions::default().with_reconcile_data_admission_timeout(Duration::ZERO),
] {
assert!(options.validate().is_err());
}
for timeout in [Duration::ZERO, Duration::MAX] {
assert!(NodeRuntimeOptions::default()
.with_health_timeout(timeout)
.validate()
.is_err());
}
}
#[test]
fn clock_only_v4_peer_is_rejected_before_digest_reconciliation() {
let mut hello = Hello {
node_id: "peer".into(),
auth_tag: Vec::new(),
min_protocol_version: WIRE_PROTOCOL_VERSION,
max_protocol_version: WIRE_PROTOCOL_VERSION,
capabilities: REQUIRED_WIRE_CAPABILITIES,
challenge: vec![1; HANDSHAKE_CHALLENGE_BYTES],
};
assert_eq!(validate_peer_hello("local", Some("peer"), &hello), Ok(()));
hello.capabilities &= !CAPABILITY_RECORD_CONTENT_DIGEST;
assert_eq!(
validate_peer_hello("local", Some("peer"), &hello),
Err("peer omitted required wire capabilities")
);
}
#[test]
fn v4_peer_without_typed_unknown_outcomes_is_rejected_at_hello() {
let mut hello = Hello {
node_id: "peer".into(),
auth_tag: Vec::new(),
min_protocol_version: WIRE_PROTOCOL_VERSION,
max_protocol_version: WIRE_PROTOCOL_VERSION,
capabilities: REQUIRED_WIRE_CAPABILITIES,
challenge: vec![1; HANDSHAKE_CHALLENGE_BYTES],
};
assert_eq!(validate_peer_hello("local", Some("peer"), &hello), Ok(()));
hello.capabilities &= !CAPABILITY_TYPED_UNKNOWN_MUTATION;
assert_eq!(
validate_peer_hello("local", Some("peer"), &hello),
Err("peer omitted required wire capabilities")
);
}
#[tokio::test]
async fn batch_actor_timeout_returns_unavailable_without_stalling_shared_connection() {
let fragment = FragmentId(0);
let router = FragmentRouter::new(2, 0);
let started = Arc::new(Notify::new());
let release = Arc::new(Notify::new());
let (actor, _actor_handle) = ractor::Actor::spawn(
None,
BlockingPageActor,
BlockingPageState {
started: started.clone(),
release: release.clone(),
},
)
.await
.unwrap();
router.insert(fragment, actor.clone());
let peers: PeerMap = Arc::new(Mutex::new(HashMap::new()));
let (mut ctx, runtime_tasks) = test_connection_context(
router,
Arc::new(RwLock::new(HashSet::from([fragment]))),
Arc::new(RwLock::new(HashMap::new())),
true,
Arc::new(Mutex::new(HashMap::new())),
Arc::new(Mutex::new(HashMap::new())),
peers.clone(),
)
.await;
ctx.runtime_options =
NodeRuntimeOptions::default().with_forwarded_attempt_timeout(Duration::from_millis(50));
let (mut client, server) = tokio::io::duplex(4096);
let connection = tokio::spawn(handle_connection(ctx, Box::pin(server), false, None, None));
complete_test_handshake(&mut client, "peer").await;
let key_for_fragment = |fragment| {
(0..1000)
.map(|i| format!("batch-timeout-{i}"))
.find(|key| fragment_of(key, 2) == fragment)
.unwrap()
};
write_test_envelope(
&mut client,
Envelope {
msg: Some(EnvelopeMsg::BatchGet(BatchGet {
req_id: 41,
keys: vec![key_for_fragment(fragment)],
priority: ProtoPriority::Default as i32,
})),
},
)
.await;
tokio::time::timeout(Duration::from_secs(1), started.notified())
.await
.expect("batch read did not reach the blocked actor");
let response =
tokio::time::timeout(Duration::from_secs(1), read_test_envelope(&mut client))
.await
.expect("batch actor deadline did not release the connection reader");
assert!(matches!(
response.msg,
Some(EnvelopeMsg::BatchGetResponse(BatchGetResponse {
req_id: 41,
results,
})) if results.is_empty()
));
write_test_envelope(
&mut client,
Envelope {
msg: Some(EnvelopeMsg::BatchGet(BatchGet {
req_id: 42,
keys: vec![key_for_fragment(FragmentId(1))],
priority: ProtoPriority::High as i32,
})),
},
)
.await;
let probe = tokio::time::timeout(Duration::from_secs(1), read_test_envelope(&mut client))
.await
.expect("shared connection stalled after the batch actor timeout");
assert!(matches!(
probe.msg,
Some(EnvelopeMsg::BatchGetResponse(BatchGetResponse {
req_id: 42,
results,
})) if results.is_empty()
));
assert!(peers.lock().await.contains_key("peer"));
release.notify_one();
actor.stop_and_wait(None, None).await.unwrap();
drop(client);
tokio::time::timeout(Duration::from_secs(1), connection)
.await
.expect("connection did not stop after its stream closed")
.unwrap();
runtime_tasks.shutdown().await;
}
#[tokio::test]
async fn dynamic_mutations_at_routing_boundaries_fail_closed() {
let fragment = FragmentId(0);
let router = FragmentRouter::new(1, 0);
let serving = Arc::new(RwLock::new(HashSet::new()));
let peers: PeerMap = Arc::new(Mutex::new(HashMap::new()));
let (ctx, runtime_tasks) = test_connection_context(
router,
serving.clone(),
Arc::new(RwLock::new(HashMap::new())),
true,
Arc::new(Mutex::new(HashMap::new())),
Arc::new(Mutex::new(HashMap::new())),
peers.clone(),
)
.await;
let (mut client, server) = tokio::io::duplex(4096);
let connection = tokio::spawn(handle_connection(ctx, Box::pin(server), false, None, None));
complete_test_handshake(&mut client, "peer").await;
write_test_envelope(
&mut client,
Envelope {
msg: Some(EnvelopeMsg::ForwardedPut(ForwardedPut {
key: "hop-limit".to_string(),
value: b"value".to_vec(),
timestamp_ms: 1,
logical_counter: 0,
node_id: "peer".to_string(),
seq: 1,
boot_id: 1,
expires_at_ms: 0,
is_tombstone: false,
priority: ProtoPriority::Default as i32,
hops: MAX_FORWARD_HOPS,
req_id: 11,
write_concern: None,
})),
},
)
.await;
let hop_envelope = read_test_envelope(&mut client).await;
assert!(matches!(
hop_envelope.msg,
Some(EnvelopeMsg::MutationResponse(ref response))
if response.req_id == 11
&& response.results.len() == 1
&& response.results[0].status == MutationStatus::Unavailable as i32
));
serving.write().unwrap().insert(fragment);
write_test_envelope(
&mut client,
Envelope {
msg: Some(EnvelopeMsg::BatchPut(BatchPut {
puts: vec![ForwardedPut {
key: "missing-actor".to_string(),
value: b"value".to_vec(),
timestamp_ms: 2,
logical_counter: 0,
node_id: "peer".to_string(),
seq: 2,
boot_id: 1,
expires_at_ms: 0,
is_tombstone: false,
priority: ProtoPriority::Default as i32,
hops: 0,
req_id: 12,
write_concern: None,
}],
priority: ProtoPriority::Default as i32,
req_id: 12,
})),
},
)
.await;
let batch_envelope = read_test_envelope(&mut client).await;
assert!(matches!(
batch_envelope.msg,
Some(EnvelopeMsg::MutationResponse(ref response))
if response.req_id == 12
&& response.results.len() == 1
&& response.results[0].status == MutationStatus::Unavailable as i32
));
write_test_envelope(
&mut client,
Envelope {
msg: Some(EnvelopeMsg::ForwardedPut(ForwardedPut {
key: "missing-single-actor".to_string(),
value: b"value".to_vec(),
timestamp_ms: 3,
logical_counter: 0,
node_id: "peer".to_string(),
seq: 3,
boot_id: 1,
expires_at_ms: 0,
is_tombstone: false,
priority: ProtoPriority::Default as i32,
hops: 0,
req_id: 13,
write_concern: None,
})),
},
)
.await;
let single_envelope = read_test_envelope(&mut client).await;
assert!(matches!(
single_envelope.msg,
Some(EnvelopeMsg::MutationResponse(ref response))
if response.req_id == 13
&& response.results.len() == 1
&& response.results[0].status == MutationStatus::Unavailable as i32
));
write_test_envelope(
&mut client,
Envelope {
msg: Some(EnvelopeMsg::ForwardedGet(ForwardedGet {
key: "missing-read-actor".to_string(),
req_id: 14,
priority: ProtoPriority::Default as i32,
hops: 0,
})),
},
)
.await;
write_test_envelope(
&mut client,
Envelope {
msg: Some(EnvelopeMsg::BatchGet(BatchGet {
req_id: 15,
keys: vec!["missing-read-actor".to_string()],
priority: ProtoPriority::Default as i32,
})),
},
)
.await;
assert!(matches!(
read_test_envelope(&mut client).await.msg,
Some(EnvelopeMsg::BatchGetResponse(BatchGetResponse {
req_id: 15,
results,
})) if results.is_empty()
));
drop(client);
tokio::time::timeout(Duration::from_secs(1), connection)
.await
.expect("connection did not close after its stream was dropped")
.unwrap();
assert!(peers.lock().await.is_empty());
runtime_tasks.shutdown().await;
}
#[test]
fn unspecified_wire_statuses_are_never_success() {
let unknown = MutationResult {
status: MutationStatus::Unknown as i32,
..result("key", 0)
};
assert!(
validate_mutation_results(&["key".to_string()], std::slice::from_ref(&unknown))
.is_err()
);
assert!(matches!(
decode_mutation_result(unknown),
Err(NodeError::Codec(_))
));
assert_eq!(
ReplicationAckLevel::try_from(0),
Ok(ReplicationAckLevel::Unknown)
);
}
#[tokio::test]
async fn stopped_forwarded_mutation_actor_is_unavailable() {
let fragment = FragmentId(0);
let hlc = Arc::new(Mutex::new(HlcClock::new()));
let (supervisor, actor) = spawn_fragment_subtree(
"stopped-forwarded-actor",
fragment,
&NodeConfig::default(),
&hlc,
)
.await
.unwrap();
let _ = supervisor.stop_and_wait(None, None).await;
let senders: SenderMap = Arc::new(Mutex::new(HashMap::new()));
let result = apply_forwarded_mutation(
&actor,
&senders,
fragment,
ReplicatedOp {
key: "key".to_string(),
value: VersionedValue::new(b"value".to_vec(), 1, "origin".to_string()),
seq: 1,
boot_id: 1,
},
OpPriority::Default,
0,
)
.await;
assert_eq!(
MutationStatus::try_from(result.status),
Ok(MutationStatus::Unavailable)
);
}
#[test]
fn legacy_get_response_shapes_fail_closed() {
let not_found = GetResponse {
req_id: 1,
found: false,
value: Vec::new(),
timestamp_ms: 0,
logical_counter: 0,
node_id: String::new(),
expires_at_ms: 0,
is_tombstone: false,
shed: false,
};
assert!(validate_legacy_get_response("key", ¬_found).is_ok());
let found = GetResponse {
found: true,
value: b"value".to_vec(),
timestamp_ms: 7,
logical_counter: 0,
node_id: "owner".to_string(),
..not_found.clone()
};
assert!(validate_legacy_get_response("key", &found).is_ok());
let shed = GetResponse {
shed: true,
..not_found.clone()
};
assert!(validate_legacy_get_response("key", &shed).is_ok());
for malformed in [
GetResponse {
shed: true,
found: true,
..not_found.clone()
},
GetResponse {
found: true,
node_id: String::new(),
..not_found.clone()
},
GetResponse {
value: b"ghost".to_vec(),
..not_found.clone()
},
GetResponse {
is_tombstone: true,
..not_found
},
] {
assert!(validate_legacy_get_response("key", &malformed).is_err());
}
}
#[test]
fn dropping_a_pending_mutation_waiter_removes_its_registry_entry() {
let pending: PendingMutations = Arc::new(StdMutex::new(HashMap::new()));
let (reply, _response) = oneshot::channel();
pending.lock().unwrap().insert(
7,
PendingMutation {
expected_peer: "peer".to_string(),
expected_keys: vec!["key".to_string()],
reply,
},
);
{
let _guard = PendingMutationGuard {
req_id: 7,
pending: pending.clone(),
};
}
assert!(pending.lock().unwrap().is_empty());
}
#[test]
fn dropping_pending_read_waiters_removes_their_registry_entries() {
let pending_gets: PendingGets = Arc::new(StdMutex::new(HashMap::new()));
let (reply, _response) = oneshot::channel();
pending_gets.lock().unwrap().insert(
3,
PendingGet {
expected_peer: "peer".to_string(),
expected_key: "key".to_string(),
reply,
},
);
{
let _guard = PendingGetGuard {
req_id: 3,
pending: pending_gets.clone(),
};
}
assert!(pending_gets.lock().unwrap().is_empty());
let pending_batch_gets: PendingBatchGets = Arc::new(StdMutex::new(HashMap::new()));
let (batch_reply, _batch_response) = oneshot::channel();
pending_batch_gets.lock().unwrap().insert(
4,
PendingBatchGet {
expected_peer: "peer".to_string(),
expected_keys: vec!["key".to_string()],
reply: batch_reply,
},
);
{
let _guard = PendingBatchGetGuard {
req_id: 4,
pending: pending_batch_gets.clone(),
};
}
assert!(pending_batch_gets.lock().unwrap().is_empty());
}
struct StalledLocalActor;
impl Actor for StalledLocalActor {
type Msg = KvMessage;
type State = Vec<KvMessage>;
type Arguments = ();
async fn pre_start(
&self,
_: ActorRef<KvMessage>,
_: (),
) -> Result<Self::State, ActorProcessingErr> {
Ok(Vec::new())
}
async fn handle(
&self,
_: ActorRef<KvMessage>,
message: KvMessage,
state: &mut Self::State,
) -> Result<(), ActorProcessingErr> {
state.push(message);
Ok(())
}
}
#[derive(Default)]
struct TtlRecordingStore {
values: StdMutex<HashMap<Vec<u8>, VersionedValue>>,
writes: AtomicUsize,
}
impl KvStore for TtlRecordingStore {
fn load(&self) -> Result<Vec<(Vec<u8>, VersionedValue)>, StoreError> {
Ok(self
.values
.lock()
.unwrap()
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect())
}
fn write_batch(&self, writes: &[(Vec<u8>, VersionedValue)]) -> Result<(), StoreError> {
self.writes.fetch_add(1, Ordering::SeqCst);
self.values.lock().unwrap().extend(writes.iter().cloned());
Ok(())
}
}
fn ttl_store_config(store: &Arc<TtlRecordingStore>) -> NodeConfig {
NodeConfig {
store: Some(store.clone()),
flush_batch_size: 1,
flush_interval: Duration::from_secs(3600),
clean_interval: Duration::from_secs(3600),
snapshot_interval: Duration::from_secs(3600),
..Default::default()
}
}
}