use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use mongreldb_types::errors::ErrorCategory;
use mongreldb_types::hlc::HlcTimestamp;
use mongreldb_types::ids::{MetadataVersion, RaftGroupId, TableId, TabletId};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::meta::MetaRejectionReason;
use crate::node::ClusterError;
use crate::tablet::{
Bound, Key, PartitionBounds, ReplicaDescriptor, ReplicaRole, RoutingError, TabletDescriptor,
TabletError, TabletLayout, TabletState,
};
#[derive(Debug, thiserror::Error)]
pub enum SplitError {
#[error(transparent)]
Tablet(#[from] TabletError),
#[error(transparent)]
Fault(#[from] mongreldb_fault::Fault),
#[error(transparent)]
MetaPlane(#[from] MetaRejectionReason),
#[error(transparent)]
TabletData(#[from] TabletDataError),
#[error("source tablet {tablet} is in state {state}, expected Active")]
SourceNotActive {
tablet: TabletId,
state: TabletState,
},
#[error(
"cannot derive a deterministic midpoint split key from unbounded bounds; \
supply an explicit split key"
)]
UnboundedMidpoint,
#[error("no key lies strictly between the bounds' endpoints; the tablet cannot be split")]
UnsplittableBounds,
#[error("split key {key} does not partition the source bounds into two non-empty halves")]
InvalidSplitKey {
key: Key,
},
#[error("invalid split plan: {0}")]
InvalidPlan(String),
#[error("applied key {0} lies outside the source partition")]
KeyOutsideSource(Key),
#[error("source tablet {tablet} is retained by {pins} old-generation pin(s)")]
SourceRetained {
tablet: TabletId,
pins: usize,
},
#[error("cannot abort the split of tablet {tablet}: it already reached phase {phase}")]
CannotAbort {
tablet: TabletId,
phase: SplitPhase,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum TabletDataError {
#[error("keyspace operation failed: {0}")]
Keyspace(String),
#[error("no staged build in progress")]
NoStagedBuild,
#[error("child state sink failed: {0}")]
Sink(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SplitPhase {
Started,
MarkedSplitting,
ChildrenCreated,
SnapshotPinned,
ChildrenBuilt,
CaughtUp,
Published,
SourceRetired,
}
impl SplitPhase {
pub fn hook_name(self) -> Option<&'static str> {
Some(match self {
Self::Started => return None,
Self::MarkedSplitting => "tablet.split.phase.1",
Self::ChildrenCreated => "tablet.split.phase.2",
Self::SnapshotPinned => "tablet.split.phase.3",
Self::ChildrenBuilt => "tablet.split.phase.4",
Self::CaughtUp => "tablet.split.phase.5",
Self::Published => "tablet.split.phase.6",
Self::SourceRetired => "tablet.split.phase.7",
})
}
}
impl fmt::Display for SplitPhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::Started => "Started",
Self::MarkedSplitting => "MarkedSplitting",
Self::ChildrenCreated => "ChildrenCreated",
Self::SnapshotPinned => "SnapshotPinned",
Self::ChildrenBuilt => "ChildrenBuilt",
Self::CaughtUp => "CaughtUp",
Self::Published => "Published",
Self::SourceRetired => "SourceRetired",
};
f.write_str(name)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SplitKeySelection {
Midpoint,
Explicit(Key),
}
fn bound_key(bound: &Bound<Key>) -> Option<&Key> {
match bound {
Bound::Unbounded => None,
Bound::Included(key) | Bound::Excluded(key) => Some(key),
}
}
pub fn midpoint_key(low: &Key, high: &Key) -> Option<Key> {
let (low, high) = (low.as_bytes(), high.as_bytes());
debug_assert!(low < high, "midpoint requires ordered endpoints");
midpoint_bytes(low, high).map(Key::from_bytes)
}
fn midpoint_bytes(low: &[u8], high: &[u8]) -> Option<Vec<u8>> {
let mut index = 0;
while index < low.len() && index < high.len() && low[index] == high[index] {
index += 1;
}
if index == low.len() {
if high.len() == index + 1 && high[index] == 0 {
return None;
}
let mut mid = low.to_vec();
mid.push(high[index] / 2);
return Some(mid);
}
let (low_byte, high_byte) = (low[index], high[index]);
if high_byte >= low_byte + 2 {
let mut mid = low[..index].to_vec();
mid.push(low_byte + (high_byte - low_byte) / 2);
Some(mid)
} else {
let mut mid = low.to_vec();
mid.push(0x80);
Some(mid)
}
}
fn choose_split_key(
bounds: &PartitionBounds,
selection: &SplitKeySelection,
) -> Result<Key, SplitError> {
match selection {
SplitKeySelection::Explicit(key) => {
if bounds.split_at(key).is_none() {
return Err(SplitError::InvalidSplitKey { key: key.clone() });
}
Ok(key.clone())
}
SplitKeySelection::Midpoint => {
let (Some(low), Some(high)) = (bound_key(&bounds.low), bound_key(&bounds.high)) else {
return Err(SplitError::UnboundedMidpoint);
};
midpoint_key(low, high).ok_or(SplitError::UnsplittableBounds)
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ChildAllocation {
pub tablet_id: TabletId,
pub raft_group_id: RaftGroupId,
pub replicas: Vec<ReplicaDescriptor>,
}
#[derive(Clone, Debug)]
pub struct ChildPlan {
pub bounds: PartitionBounds,
pub layout: TabletLayout,
pub replicas: Vec<ReplicaDescriptor>,
}
impl ChildPlan {
pub fn descriptor(
&self,
table_id: TableId,
generation: u64,
state: TabletState,
role: ReplicaRole,
) -> TabletDescriptor {
TabletDescriptor {
tablet_id: self.layout.tablet_id(),
database_id: mongreldb_types::ids::DatabaseId::ZERO,
table_id,
raft_group_id: self.layout.raft_group_id(),
partition: self.bounds.clone(),
replicas: self
.replicas
.iter()
.map(|replica| ReplicaDescriptor { role, ..*replica })
.collect(),
leader_hint: None,
generation,
state,
}
}
pub fn progress(&self) -> ChildProgress {
ChildProgress {
node_data: self.layout.node_data().to_path_buf(),
tablet_id: self.layout.tablet_id(),
raft_group_id: self.layout.raft_group_id(),
bounds: self.bounds.clone(),
replicas: self.replicas.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ChildProgress {
pub node_data: PathBuf,
pub tablet_id: TabletId,
pub raft_group_id: RaftGroupId,
pub bounds: PartitionBounds,
pub replicas: Vec<ReplicaDescriptor>,
}
impl ChildProgress {
pub fn plan(&self) -> ChildPlan {
ChildPlan {
bounds: self.bounds.clone(),
layout: TabletLayout::new(self.node_data.clone(), self.tablet_id, self.raft_group_id),
replicas: self.replicas.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct SplitPlan {
pub source: TabletDescriptor,
pub children: [ChildPlan; 2],
pub split_key: Key,
pub split_ts: HlcTimestamp,
}
impl SplitPlan {
pub fn validate(&self) -> Result<(), SplitError> {
self.source.validate()?;
let (lower, upper) = self
.source
.partition
.split_at(&self.split_key)
.ok_or_else(|| SplitError::InvalidSplitKey {
key: self.split_key.clone(),
})?;
if self.children[0].bounds != lower || self.children[1].bounds != upper {
return Err(SplitError::InvalidPlan(
"child bounds are not the source bounds split at the split key".to_owned(),
));
}
let child_ids = [
(
self.children[0].layout.tablet_id(),
self.children[0].layout.raft_group_id(),
),
(
self.children[1].layout.tablet_id(),
self.children[1].layout.raft_group_id(),
),
];
if child_ids[0] == child_ids[1] || child_ids[0].0 == child_ids[1].0 {
return Err(SplitError::InvalidPlan(
"child tablet and raft group ids must be distinct".to_owned(),
));
}
if child_ids.iter().any(|ids| ids.0 == self.source.tablet_id) {
return Err(SplitError::InvalidPlan(
"child tablet ids must differ from the source's".to_owned(),
));
}
if child_ids
.iter()
.any(|ids| ids.1 == self.source.raft_group_id)
{
return Err(SplitError::InvalidPlan(
"child raft group ids must differ from the source's".to_owned(),
));
}
for descriptor in self.child_descriptors() {
descriptor.validate()?;
}
Ok(())
}
pub fn child_descriptors(&self) -> [TabletDescriptor; 2] {
let generation = self
.source
.generation
.checked_add(1)
.expect("descriptor generation overflows u64");
self.children.clone().map(|child| {
child.descriptor(
self.source.table_id,
generation,
TabletState::Creating,
ReplicaRole::Learner,
)
})
}
}
#[derive(Clone, Debug)]
pub struct TabletSplitPlanner {
node_data: PathBuf,
}
impl TabletSplitPlanner {
pub fn new(node_data: impl Into<PathBuf>) -> Self {
Self {
node_data: node_data.into(),
}
}
pub fn plan(
&self,
source: &TabletDescriptor,
selection: SplitKeySelection,
split_ts: HlcTimestamp,
allocations: [ChildAllocation; 2],
) -> Result<SplitPlan, SplitError> {
if source.state != TabletState::Active {
return Err(SplitError::SourceNotActive {
tablet: source.tablet_id,
state: source.state,
});
}
source.validate()?;
let split_key = choose_split_key(&source.partition, &selection)?;
let (lower, upper) =
source
.partition
.split_at(&split_key)
.ok_or_else(|| SplitError::InvalidSplitKey {
key: split_key.clone(),
})?;
let [lower_alloc, upper_alloc] = allocations;
let child = |bounds: PartitionBounds, alloc: ChildAllocation| ChildPlan {
bounds,
layout: TabletLayout::new(self.node_data.clone(), alloc.tablet_id, alloc.raft_group_id),
replicas: alloc.replicas,
};
let plan = SplitPlan {
source: source.clone(),
children: [child(lower, lower_alloc), child(upper, upper_alloc)],
split_key,
split_ts,
};
plan.validate()?;
Ok(plan)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SplitPublishCommand {
pub source: TabletDescriptor,
pub children: [TabletDescriptor; 2],
pub split_key: Key,
pub split_ts: HlcTimestamp,
}
impl SplitPublishCommand {
pub fn from_plan(plan: &SplitPlan) -> Result<Self, SplitError> {
let marked = plan.source.published_transition(TabletState::Splitting)?;
let source = marked.published_transition(TabletState::Retiring)?;
let mut children = plan.child_descriptors();
for child in &mut children {
*child = child.published_transition(TabletState::Active)?;
for replica in &mut child.replicas {
replica.role = ReplicaRole::Voter;
}
}
let command = Self {
source,
children,
split_key: plan.split_key.clone(),
split_ts: plan.split_ts,
};
command.validate()?;
Ok(command)
}
pub fn publish_generation(&self) -> u64 {
self.source.generation
}
pub fn validate(&self) -> Result<(), SplitError> {
if self.source.state != TabletState::Retiring {
return Err(SplitError::InvalidPlan(format!(
"published source must be Retiring, is {}",
self.source.state
)));
}
let generation = self.source.generation;
for child in &self.children {
if child.state != TabletState::Active {
return Err(SplitError::InvalidPlan(format!(
"published child {} must be Active, is {}",
child.tablet_id, child.state
)));
}
if child.generation != generation {
return Err(SplitError::InvalidPlan(
"publication assigns one generation to all descriptors".to_owned(),
));
}
if child.table_id != self.source.table_id {
return Err(SplitError::InvalidPlan(
"children and source name different tables".to_owned(),
));
}
child.validate()?;
}
self.source.validate()?;
let (lower, upper) = self
.source
.partition
.split_at(&self.split_key)
.ok_or_else(|| SplitError::InvalidSplitKey {
key: self.split_key.clone(),
})?;
if self.children[0].partition != lower || self.children[1].partition != upper {
return Err(SplitError::InvalidPlan(
"published child bounds are not the source bounds split at the split key"
.to_owned(),
));
}
if self.children[0].tablet_id == self.children[1].tablet_id
|| self
.children
.iter()
.any(|c| c.tablet_id == self.source.tablet_id)
{
return Err(SplitError::InvalidPlan(
"publication tablet ids must be distinct".to_owned(),
));
}
Ok(())
}
}
pub const SPLIT_PROGRESS_FILENAME: &str = "split.json";
pub const SPLIT_PROGRESS_FORMAT_VERSION: u32 = 1;
pub const MIN_SUPPORTED_SPLIT_PROGRESS_FORMAT_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SplitProgress {
pub source: TabletDescriptor,
pub split_key: Key,
pub split_ts: HlcTimestamp,
pub children: [ChildProgress; 2],
pub phase: SplitPhase,
}
impl SplitProgress {
pub fn from_plan(plan: &SplitPlan, phase: SplitPhase) -> Self {
Self {
source: plan.source.clone(),
split_key: plan.split_key.clone(),
split_ts: plan.split_ts,
children: plan.children.clone().map(|child| child.progress()),
phase,
}
}
pub fn plan(&self) -> SplitPlan {
SplitPlan {
source: self.source.clone(),
children: self.children.clone().map(|child| child.plan()),
split_key: self.split_key.clone(),
split_ts: self.split_ts,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct SplitProgressFile {
format_version: u32,
checksum: String,
progress: SplitProgress,
}
impl SplitProgressFile {
fn envelope(progress: &SplitProgress) -> Result<Self, SplitError> {
Ok(Self {
format_version: SPLIT_PROGRESS_FORMAT_VERSION,
checksum: progress_checksum(progress).map_err(meta_io)?,
progress: progress.clone(),
})
}
}
fn progress_checksum(progress: &SplitProgress) -> Result<String, ClusterError> {
let bytes = serde_json::to_vec(progress).map_err(|error| ClusterError::CorruptMetadata {
file: SPLIT_PROGRESS_FILENAME,
detail: format!("encode: {error}"),
})?;
Ok(hex_encode(&Sha256::digest(&bytes)))
}
fn meta_io(error: ClusterError) -> SplitError {
SplitError::Tablet(TabletError::Metadata(error))
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
pub trait TabletMetaPlane {
fn set_tablet(&mut self, descriptor: &TabletDescriptor) -> Result<(), MetaRejectionReason>;
fn tablet(&self, tablet_id: TabletId) -> Option<TabletDescriptor>;
fn remove_tablet(
&mut self,
tablet_id: TabletId,
generation: u64,
) -> Result<(), MetaRejectionReason>;
fn publish_split(&mut self, command: &SplitPublishCommand) -> Result<(), MetaRejectionReason> {
command
.validate()
.map_err(|error| MetaRejectionReason::Invalid {
reason: error.to_string(),
})?;
for child in &command.children {
self.set_tablet(child)?;
}
self.set_tablet(&command.source)
}
}
#[derive(Clone, Default)]
pub struct InMemoryMetaPlane {
tablets: Arc<Mutex<BTreeMap<TabletId, TabletDescriptor>>>,
}
impl InMemoryMetaPlane {
pub fn new() -> Self {
Self::default()
}
pub fn descriptors(&self) -> Vec<TabletDescriptor> {
self.tablets
.lock()
.expect("meta plane lock poisoned")
.values()
.cloned()
.collect()
}
}
impl TabletMetaPlane for InMemoryMetaPlane {
fn set_tablet(&mut self, descriptor: &TabletDescriptor) -> Result<(), MetaRejectionReason> {
descriptor
.validate()
.map_err(|error| MetaRejectionReason::Invalid {
reason: error.to_string(),
})?;
let mut tablets = self.tablets.lock().expect("meta plane lock poisoned");
match tablets.get(&descriptor.tablet_id) {
Some(existing) => {
if descriptor.generation > existing.generation {
tablets.insert(descriptor.tablet_id, descriptor.clone());
Ok(())
} else if descriptor.generation == existing.generation {
if existing == descriptor {
Ok(())
} else {
Err(MetaRejectionReason::Conflict {
resource: format!("tablet {}", descriptor.tablet_id),
reason: "generation already used for different content".to_owned(),
})
}
} else {
Err(MetaRejectionReason::StaleWrite {
resource: format!("tablet {}", descriptor.tablet_id),
current: MetadataVersion(existing.generation),
attempted: MetadataVersion(descriptor.generation),
})
}
}
None => {
tablets.insert(descriptor.tablet_id, descriptor.clone());
Ok(())
}
}
}
fn tablet(&self, tablet_id: TabletId) -> Option<TabletDescriptor> {
self.tablets
.lock()
.expect("meta plane lock poisoned")
.get(&tablet_id)
.cloned()
}
fn remove_tablet(
&mut self,
tablet_id: TabletId,
generation: u64,
) -> Result<(), MetaRejectionReason> {
let mut tablets = self.tablets.lock().expect("meta plane lock poisoned");
match tablets.get(&tablet_id) {
None => Ok(()),
Some(existing) => {
if generation >= existing.generation {
tablets.remove(&tablet_id);
Ok(())
} else {
Err(MetaRejectionReason::StaleWrite {
resource: format!("tablet {tablet_id}"),
current: MetadataVersion(existing.generation),
attempted: MetadataVersion(generation),
})
}
}
}
}
}
pub trait SnapshotPin: Send {
fn pinned_at(&self) -> HlcTimestamp;
}
pub type RecordStream<'a> = Box<dyn Iterator<Item = (Key, Vec<u8>)> + 'a>;
pub trait TabletKeyspace {
fn pin_snapshot(&mut self, ts: HlcTimestamp) -> Result<Box<dyn SnapshotPin>, TabletDataError>;
fn snapshot_at(&self, ts: HlcTimestamp) -> Result<RecordStream<'_>, TabletDataError>;
fn deltas_after(&self, ts: HlcTimestamp) -> Result<RecordStream<'_>, TabletDataError>;
}
pub trait ChildStateSink {
fn begin_build(&mut self) -> Result<(), TabletDataError>;
fn stage(&mut self, key: &Key, value: &[u8]) -> Result<(), TabletDataError>;
fn install_staged(&mut self) -> Result<(), TabletDataError>;
fn apply_delta(&mut self, key: &Key, value: &[u8]) -> Result<(), TabletDataError>;
}
#[derive(Clone, Default)]
pub struct MapKeyspace {
state: Arc<Mutex<MapKeyspaceState>>,
}
#[derive(Default)]
struct MapKeyspaceState {
rows: BTreeMap<Key, Vec<(HlcTimestamp, Vec<u8>)>>,
pins: usize,
}
impl MapKeyspace {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&self, key: Key, ts: HlcTimestamp, value: Vec<u8>) {
let mut state = self.state.lock().expect("keyspace lock poisoned");
let chain = state.rows.entry(key).or_default();
chain.push((ts, value));
chain.sort_by_key(|(version, _)| *version);
}
pub fn pin_count(&self) -> usize {
self.state.lock().expect("keyspace lock poisoned").pins
}
pub fn rows_at(&self, ts: HlcTimestamp) -> BTreeMap<Key, Vec<u8>> {
let state = self.state.lock().expect("keyspace lock poisoned");
state
.rows
.iter()
.filter_map(|(key, chain)| {
let visible = chain.iter().rfind(|(version, _)| *version <= ts)?;
Some((key.clone(), visible.1.clone()))
})
.collect()
}
}
impl TabletKeyspace for MapKeyspace {
fn pin_snapshot(&mut self, ts: HlcTimestamp) -> Result<Box<dyn SnapshotPin>, TabletDataError> {
self.state.lock().expect("keyspace lock poisoned").pins += 1;
Ok(Box::new(MapSnapshotPin {
ts,
state: self.state.clone(),
}))
}
fn snapshot_at(&self, ts: HlcTimestamp) -> Result<RecordStream<'_>, TabletDataError> {
Ok(Box::new(self.rows_at(ts).into_iter()))
}
fn deltas_after(&self, ts: HlcTimestamp) -> Result<RecordStream<'_>, TabletDataError> {
let state = self.state.lock().expect("keyspace lock poisoned");
let mut deltas: Vec<(HlcTimestamp, Key, Vec<u8>)> = Vec::new();
for (key, chain) in &state.rows {
for (version, value) in chain {
if *version > ts {
deltas.push((*version, key.clone(), value.clone()));
}
}
}
deltas.sort();
Ok(Box::new(
deltas.into_iter().map(|(_, key, value)| (key, value)),
))
}
}
struct MapSnapshotPin {
ts: HlcTimestamp,
state: Arc<Mutex<MapKeyspaceState>>,
}
impl SnapshotPin for MapSnapshotPin {
fn pinned_at(&self) -> HlcTimestamp {
self.ts
}
}
impl Drop for MapSnapshotPin {
fn drop(&mut self) {
self.state.lock().expect("keyspace lock poisoned").pins -= 1;
}
}
#[derive(Clone, Default)]
pub struct MapChildSink {
state: Arc<Mutex<MapChildSinkState>>,
}
#[derive(Default)]
struct MapChildSinkState {
staged: Option<BTreeMap<Key, Vec<u8>>>,
installed: BTreeMap<Key, Vec<u8>>,
}
impl MapChildSink {
pub fn new() -> Self {
Self::default()
}
pub fn rows(&self) -> BTreeMap<Key, Vec<u8>> {
self.state
.lock()
.expect("child sink lock poisoned")
.installed
.clone()
}
}
impl ChildStateSink for MapChildSink {
fn begin_build(&mut self) -> Result<(), TabletDataError> {
self.state.lock().expect("child sink lock poisoned").staged = Some(BTreeMap::new());
Ok(())
}
fn stage(&mut self, key: &Key, value: &[u8]) -> Result<(), TabletDataError> {
let mut state = self.state.lock().expect("child sink lock poisoned");
let staged = state
.staged
.as_mut()
.ok_or(TabletDataError::NoStagedBuild)?;
staged.insert(key.clone(), value.to_vec());
Ok(())
}
fn install_staged(&mut self) -> Result<(), TabletDataError> {
let mut state = self.state.lock().expect("child sink lock poisoned");
let staged = state.staged.take().ok_or(TabletDataError::NoStagedBuild)?;
state.installed = staged;
Ok(())
}
fn apply_delta(&mut self, key: &Key, value: &[u8]) -> Result<(), TabletDataError> {
let mut state = self.state.lock().expect("child sink lock poisoned");
state.installed.insert(key.clone(), value.to_vec());
Ok(())
}
}
#[derive(Debug)]
pub struct SourceRetentionGuard {
source: TabletId,
retired_generation: u64,
inner: Mutex<RetentionInner>,
}
#[derive(Debug, Default)]
struct RetentionInner {
next_pin: u64,
pins: BTreeMap<u64, u64>,
}
impl SourceRetentionGuard {
pub fn new(source: TabletId, retired_generation: u64) -> Self {
Self {
source,
retired_generation,
inner: Mutex::new(RetentionInner::default()),
}
}
pub fn source(&self) -> TabletId {
self.source
}
pub fn retired_generation(&self) -> u64 {
self.retired_generation
}
pub fn pin(&self, used_generation: u64) -> u64 {
let mut inner = self.inner.lock().expect("retention lock poisoned");
let pin = inner.next_pin;
inner.next_pin += 1;
inner.pins.insert(pin, used_generation);
pin
}
pub fn unpin(&self, pin: u64) -> bool {
self.inner
.lock()
.expect("retention lock poisoned")
.pins
.remove(&pin)
.is_some()
}
pub fn pin_count(&self) -> usize {
self.inner
.lock()
.expect("retention lock poisoned")
.pins
.len()
}
pub fn old_generation_pins(&self) -> usize {
self.inner
.lock()
.expect("retention lock poisoned")
.pins
.values()
.filter(|generation| **generation < self.retired_generation)
.count()
}
pub fn ready_for_removal(&self) -> bool {
self.old_generation_pins() == 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RetryGuidance {
AwaitSplitPublish {
tablet_id: TabletId,
},
RefreshAndReroute {
tablet_id: TabletId,
},
RefreshAndRetry {
tablet_id: TabletId,
},
}
impl RetryGuidance {
pub fn category(&self) -> ErrorCategory {
match self {
Self::AwaitSplitPublish { .. } => ErrorCategory::TabletSplitting,
Self::RefreshAndReroute { .. } => ErrorCategory::TabletMoved,
Self::RefreshAndRetry { .. } => ErrorCategory::StaleMetadata,
}
}
pub fn failure(&self) -> crate::routing::Failure {
crate::routing::Failure::new(self.category())
}
}
pub fn retry_guidance(error: &RoutingError) -> RetryGuidance {
match *error {
RoutingError::TabletSplit { tablet_id, .. } => {
RetryGuidance::AwaitSplitPublish { tablet_id }
}
RoutingError::TabletMoved { tablet_id, .. } => {
RetryGuidance::RefreshAndReroute { tablet_id }
}
RoutingError::StaleMetadata { tablet_id, .. } => {
RetryGuidance::RefreshAndRetry { tablet_id }
}
}
}
pub struct SplitExecutor<M, K, S> {
progress: SplitProgress,
source_layout: TabletLayout,
meta: M,
keyspace: K,
sinks: [S; 2],
snapshot_pin: Option<Box<dyn SnapshotPin>>,
retention: Option<SourceRetentionGuard>,
}
impl<M: TabletMetaPlane, K: TabletKeyspace, S: ChildStateSink> SplitExecutor<M, K, S> {
pub fn begin(
plan: SplitPlan,
source_layout: TabletLayout,
meta: M,
keyspace: K,
sinks: [S; 2],
) -> Result<Self, SplitError> {
plan.validate()?;
if plan.source.state != TabletState::Active {
return Err(SplitError::SourceNotActive {
tablet: plan.source.tablet_id,
state: plan.source.state,
});
}
if source_layout.tablet_id() != plan.source.tablet_id
|| source_layout.raft_group_id() != plan.source.raft_group_id
{
return Err(TabletError::TabletMismatch {
path: source_layout.tablet_dir(),
expected: source_layout.tablet_id(),
found: plan.source.tablet_id,
expected_group: source_layout.raft_group_id(),
found_group: plan.source.raft_group_id,
}
.into());
}
source_layout.validate()?;
let executor = Self {
progress: SplitProgress::from_plan(&plan, SplitPhase::Started),
source_layout,
meta,
keyspace,
sinks,
snapshot_pin: None,
retention: None,
};
executor.persist_progress()?;
Ok(executor)
}
pub fn resume(
source_layout: TabletLayout,
meta: M,
keyspace: K,
sinks: [S; 2],
) -> Result<Option<Self>, SplitError> {
let Some(progress) = load_progress(&source_layout)? else {
return Ok(None);
};
progress.plan().validate()?;
Ok(Some(Self {
progress,
source_layout,
meta,
keyspace,
sinks,
snapshot_pin: None,
retention: None,
}))
}
pub fn phase(&self) -> SplitPhase {
self.progress.phase
}
pub fn progress(&self) -> &SplitProgress {
&self.progress
}
pub fn plan(&self) -> SplitPlan {
self.progress.plan()
}
pub fn retention(&self) -> Option<&SourceRetentionGuard> {
self.retention.as_ref()
}
pub fn meta(&self) -> &M {
&self.meta
}
pub fn keyspace(&self) -> &K {
&self.keyspace
}
pub fn sinks(&self) -> &[S; 2] {
&self.sinks
}
pub fn step(&mut self) -> Result<SplitPhase, SplitError> {
use SplitPhase::{
CaughtUp, ChildrenBuilt, ChildrenCreated, MarkedSplitting, Published, SnapshotPinned,
SourceRetired, Started,
};
let next = match self.progress.phase {
Started => {
self.mark_source_splitting()?;
MarkedSplitting
}
MarkedSplitting => {
self.create_children()?;
ChildrenCreated
}
ChildrenCreated => {
self.pin_source_snapshot()?;
SnapshotPinned
}
SnapshotPinned => {
self.build_children()?;
ChildrenBuilt
}
ChildrenBuilt => {
self.catch_up_children()?;
CaughtUp
}
CaughtUp => {
self.publish_children()?;
Published
}
Published => {
self.remove_source()?;
SourceRetired
}
SourceRetired => SourceRetired,
};
self.progress.phase = next;
if next != SourceRetired {
self.persist_progress()?;
}
if let Some(hook) = next.hook_name() {
mongreldb_fault::inject(hook)?;
}
Ok(next)
}
pub fn run(&mut self) -> Result<(), SplitError> {
while self.progress.phase != SplitPhase::SourceRetired {
self.step()?;
}
Ok(())
}
pub fn run_until(&mut self, phase: SplitPhase) -> Result<(), SplitError> {
while self.progress.phase != phase {
self.step()?;
}
Ok(())
}
fn mark_source_splitting(&mut self) -> Result<(), SplitError> {
let marked = self
.progress
.source
.published_transition(TabletState::Splitting)?;
self.meta.set_tablet(&marked)?;
self.source_layout.store_metadata(&marked)?;
Ok(())
}
fn create_children(&mut self) -> Result<(), SplitError> {
let plan = self.plan();
for (descriptor, child) in plan.child_descriptors().iter().zip(plan.children.iter()) {
child.layout.create(descriptor)?;
self.meta.set_tablet(descriptor)?;
}
Ok(())
}
fn pin_source_snapshot(&mut self) -> Result<(), SplitError> {
self.ensure_snapshot_pin()
}
fn ensure_snapshot_pin(&mut self) -> Result<(), SplitError> {
if self.snapshot_pin.is_none() {
self.snapshot_pin = Some(self.keyspace.pin_snapshot(self.progress.split_ts)?);
}
Ok(())
}
fn build_children(&mut self) -> Result<(), SplitError> {
self.ensure_snapshot_pin()?;
for sink in &mut self.sinks {
sink.begin_build()?;
}
let plan = self.plan();
let snapshot = self.keyspace.snapshot_at(self.progress.split_ts)?;
for (key, value) in snapshot {
let index = route_child(&plan, &key)?;
self.sinks[index].stage(&key, &value)?;
}
for sink in &mut self.sinks {
sink.install_staged()?;
}
Ok(())
}
fn catch_up_children(&mut self) -> Result<(), SplitError> {
self.ensure_snapshot_pin()?;
let plan = self.plan();
let deltas = self.keyspace.deltas_after(self.progress.split_ts)?;
for (key, value) in deltas {
let index = route_child(&plan, &key)?;
self.sinks[index].apply_delta(&key, &value)?;
}
Ok(())
}
fn publish_children(&mut self) -> Result<(), SplitError> {
let plan = self.plan();
let command = SplitPublishCommand::from_plan(&plan)?;
mongreldb_fault::inject("tablet.split.before")?;
self.meta.publish_split(&command)?;
mongreldb_fault::inject("tablet.split.after")?;
for (descriptor, child) in command.children.iter().zip(plan.children.iter()) {
child.layout.store_metadata(descriptor)?;
}
self.source_layout.store_metadata(&command.source)?;
self.retention = Some(SourceRetentionGuard::new(
plan.source.tablet_id,
command.publish_generation(),
));
self.snapshot_pin = None;
Ok(())
}
fn remove_source(&mut self) -> Result<(), SplitError> {
let source_id = self.progress.source.tablet_id;
if let Some(guard) = &self.retention {
if !guard.ready_for_removal() {
return Err(SplitError::SourceRetained {
tablet: source_id,
pins: guard.old_generation_pins(),
});
}
}
if let Some(current) = self.meta.tablet(source_id) {
let retired = if current.state == TabletState::Retired {
current
} else {
let retired = current.published_transition(TabletState::Retired)?;
self.meta.set_tablet(&retired)?;
retired
};
self.meta.remove_tablet(source_id, retired.generation)?;
}
self.source_layout.teardown()?;
Ok(())
}
fn persist_progress(&self) -> Result<(), SplitError> {
let file = SplitProgressFile::envelope(&self.progress)?;
let bytes = crate::node::encode_json(SPLIT_PROGRESS_FILENAME, &file).map_err(meta_io)?;
crate::node::write_meta_atomic(
&self.source_layout.tablet_dir(),
SPLIT_PROGRESS_FILENAME,
&bytes,
)
.map_err(ClusterError::Io)
.map_err(meta_io)?;
Ok(())
}
}
fn route_child(plan: &SplitPlan, key: &Key) -> Result<usize, SplitError> {
if plan.children[0].bounds.contains(key) {
return Ok(0);
}
if plan.children[1].bounds.contains(key) {
return Ok(1);
}
Err(SplitError::KeyOutsideSource(key.clone()))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SplitAbortReport {
pub source: TabletId,
pub phase: Option<SplitPhase>,
pub children_removed: Vec<TabletId>,
pub source_after: Option<TabletDescriptor>,
}
pub fn abort_split<M: TabletMetaPlane>(
source_layout: &TabletLayout,
meta: &mut M,
) -> Result<SplitAbortReport, SplitError> {
let Some(progress) = load_progress(source_layout)? else {
return Ok(SplitAbortReport {
source: source_layout.tablet_id(),
phase: None,
children_removed: Vec::new(),
source_after: None,
});
};
if progress.phase >= SplitPhase::Published {
return Err(SplitError::CannotAbort {
tablet: progress.source.tablet_id,
phase: progress.phase,
});
}
let mut children_removed = Vec::new();
for child in &progress.children {
if let Some(current) = meta.tablet(child.tablet_id) {
meta.remove_tablet(child.tablet_id, current.generation)?;
children_removed.push(child.tablet_id);
}
}
let current = meta.tablet(progress.source.tablet_id).ok_or_else(|| {
SplitError::InvalidPlan(format!(
"source tablet {} is missing from the meta plane mid-abort",
progress.source.tablet_id
))
})?;
let restored = if current.state == TabletState::Active {
current
} else {
let restored = current.published_transition(TabletState::Active)?;
meta.set_tablet(&restored)?;
restored
};
source_layout.store_metadata(&restored)?;
for child in &progress.children {
child.plan().layout.teardown()?;
}
let record = source_layout.tablet_dir().join(SPLIT_PROGRESS_FILENAME);
match std::fs::remove_file(&record) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(meta_io(ClusterError::Io(error)));
}
}
Ok(SplitAbortReport {
source: progress.source.tablet_id,
phase: Some(progress.phase),
children_removed,
source_after: Some(restored),
})
}
fn load_progress(source_layout: &TabletLayout) -> Result<Option<SplitProgress>, SplitError> {
let path = source_layout.tablet_dir().join(SPLIT_PROGRESS_FILENAME);
let Some(bytes) = crate::node::read_meta_file(&path).map_err(meta_io)? else {
return Ok(None);
};
let file: SplitProgressFile =
crate::node::decode_json(SPLIT_PROGRESS_FILENAME, &bytes).map_err(meta_io)?;
if file.format_version < MIN_SUPPORTED_SPLIT_PROGRESS_FORMAT_VERSION
|| file.format_version > SPLIT_PROGRESS_FORMAT_VERSION
{
return Err(meta_io(ClusterError::UnsupportedFormatVersion {
file: SPLIT_PROGRESS_FILENAME,
found: file.format_version,
min: MIN_SUPPORTED_SPLIT_PROGRESS_FORMAT_VERSION,
max: SPLIT_PROGRESS_FORMAT_VERSION,
}));
}
if file.checksum != progress_checksum(&file.progress).map_err(meta_io)? {
return Err(meta_io(ClusterError::CorruptMetadata {
file: SPLIT_PROGRESS_FILENAME,
detail: "checksum mismatch".to_owned(),
}));
}
let progress = file.progress;
if progress.source.tablet_id != source_layout.tablet_id()
|| progress.source.raft_group_id != source_layout.raft_group_id()
{
return Err(TabletError::TabletMismatch {
path: source_layout.tablet_dir(),
expected: source_layout.tablet_id(),
found: progress.source.tablet_id,
expected_group: source_layout.raft_group_id(),
found_group: progress.source.raft_group_id,
}
.into());
}
Ok(Some(progress))
}
pub fn split_progress(source_layout: &TabletLayout) -> Result<Option<SplitProgress>, SplitError> {
load_progress(source_layout)
}
#[cfg(test)]
pub(crate) static EXECUTOR_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
mod tests {
use std::time::Duration;
use mongreldb_types::ids::NodeId;
use super::*;
use crate::routing::{
GroupKey, OperationDescriptor, RetryAction, RetryPolicy, RetryState, RoutingCache,
};
use crate::tablet::{
check_generation, find_tablet_for_key, tablets_overlapping, KeyValue, RowKeyEncoder,
};
fn node(byte: u8) -> NodeId {
NodeId::from_bytes([byte; 16])
}
fn tablet_id(byte: u8) -> TabletId {
TabletId::from_bytes([byte; 16])
}
fn group_id(byte: u8) -> RaftGroupId {
RaftGroupId::from_bytes([byte; 16])
}
fn key(bytes: &[u8]) -> Key {
Key::from_bytes(bytes.to_vec())
}
fn text_key(text: &str) -> Key {
RowKeyEncoder::encode_key(&[KeyValue::Text(text.to_owned())])
}
fn ts(micros: u64) -> HlcTimestamp {
HlcTimestamp {
physical_micros: micros,
logical: 0,
node_tiebreaker: 0,
}
}
fn source_descriptor() -> TabletDescriptor {
TabletDescriptor {
tablet_id: tablet_id(1),
table_id: TableId::new(3),
database_id: mongreldb_types::ids::DatabaseId::ZERO,
raft_group_id: group_id(1),
partition: PartitionBounds::new(
Bound::Included(text_key("a")),
Bound::Excluded(text_key("z")),
)
.unwrap(),
replicas: vec![
ReplicaDescriptor {
node_id: node(1),
role: ReplicaRole::Voter,
raft_node_id: 11,
},
ReplicaDescriptor {
node_id: node(2),
role: ReplicaRole::Voter,
raft_node_id: 12,
},
],
leader_hint: Some(node(1)),
generation: 5,
state: TabletState::Active,
}
}
fn allocation(tablet: u8, group: u8, raft_base: u64) -> ChildAllocation {
ChildAllocation {
tablet_id: tablet_id(tablet),
raft_group_id: group_id(group),
replicas: vec![
ReplicaDescriptor {
node_id: node(3),
role: ReplicaRole::Voter,
raft_node_id: raft_base,
},
ReplicaDescriptor {
node_id: node(4),
role: ReplicaRole::Voter,
raft_node_id: raft_base + 1,
},
],
}
}
const LOWER_KEYS: [&str; 6] = ["b", "d", "f", "h", "j", "l"];
const UPPER_KEYS: [&str; 7] = ["m", "o", "q", "s", "u", "w", "y"];
fn seed_keyspace(keyspace: &MapKeyspace) {
for name in LOWER_KEYS.into_iter().chain(UPPER_KEYS) {
keyspace.insert(
text_key(name),
ts(100),
format!("v-{name}@100").into_bytes(),
);
}
keyspace.insert(text_key("b"), ts(200), b"v-b@200".to_vec());
keyspace.insert(text_key("y"), ts(200), b"v-y@200".to_vec());
keyspace.insert(text_key("n"), ts(200), b"v-n@200".to_vec());
}
struct SplitFixture {
_dir: tempfile::TempDir,
source: TabletDescriptor,
source_layout: TabletLayout,
meta: InMemoryMetaPlane,
keyspace: MapKeyspace,
sinks: [MapChildSink; 2],
plan: SplitPlan,
}
fn split_fixture() -> SplitFixture {
let dir = tempfile::tempdir().unwrap();
let source = source_descriptor();
let source_layout = TabletLayout::new(dir.path(), source.tablet_id, source.raft_group_id);
source_layout.create(&source).unwrap();
let mut meta = InMemoryMetaPlane::new();
meta.set_tablet(&source).unwrap();
let keyspace = MapKeyspace::new();
seed_keyspace(&keyspace);
let planner = TabletSplitPlanner::new(dir.path());
let plan = planner
.plan(
&source,
SplitKeySelection::Explicit(text_key("m")),
ts(150),
[allocation(2, 2, 21), allocation(3, 3, 31)],
)
.unwrap();
SplitFixture {
_dir: dir,
source,
source_layout,
meta,
keyspace,
sinks: [MapChildSink::new(), MapChildSink::new()],
plan,
}
}
type TestExecutor = SplitExecutor<InMemoryMetaPlane, MapKeyspace, MapChildSink>;
fn begin_executor(fixture: &SplitFixture) -> TestExecutor {
SplitExecutor::begin(
fixture.plan.clone(),
fixture.source_layout.clone(),
fixture.meta.clone(),
fixture.keyspace.clone(),
fixture.sinks.clone(),
)
.unwrap()
}
fn assert_split_completed(fixture: &SplitFixture) {
assert!(fixture.meta.tablet(fixture.source.tablet_id).is_none());
for (index, id) in [tablet_id(2), tablet_id(3)].into_iter().enumerate() {
let child = fixture.meta.tablet(id).unwrap();
assert_eq!(child.state, TabletState::Active);
assert_eq!(child.generation, 7);
assert!(child
.replicas
.iter()
.all(|replica| replica.role == ReplicaRole::Voter));
assert_eq!(child.partition, fixture.plan.children[index].bounds);
assert_eq!(
fixture.plan.children[index].layout.load_metadata().unwrap(),
child
);
}
assert!(!fixture.source_layout.tablet_dir().exists());
assert!(!fixture.source_layout.group_dir().exists());
assert!(!fixture
.source_layout
.tablet_dir()
.join(SPLIT_PROGRESS_FILENAME)
.exists());
let lower_rows = fixture.sinks[0].rows();
let upper_rows = fixture.sinks[1].rows();
assert!(lower_rows.keys().all(|key| *key < text_key("m")));
assert!(upper_rows.keys().all(|key| *key >= text_key("m")));
let mut union = lower_rows.clone();
for (key, value) in &upper_rows {
assert!(
union.insert(key.clone(), value.clone()).is_none(),
"duplicate key {key} across the split boundary"
);
}
assert_eq!(union, fixture.keyspace.rows_at(ts(u64::MAX)));
}
#[test]
fn midpoint_key_is_deterministic_and_strictly_between() {
let cases: &[(&[u8], &[u8])] = &[
(b"a", b"z"),
(b"aa", b"ab"),
(b"a", b"a\x01"),
(b"m", b"n"),
(b"\x00", b"\xff"),
(b"abc", b"abd"),
(b"a", b"aa"),
(b"", b"\x01"),
];
for (low, high) in cases {
let mid = midpoint_key(&key(low), &key(high)).unwrap();
assert!(key(low) < mid, "midpoint {mid} not above {low:?}");
assert!(mid < key(high), "midpoint {mid} not below {high:?}");
assert_eq!(mid, midpoint_key(&key(low), &key(high)).unwrap());
}
assert_eq!(midpoint_key(&key(b"a"), &key(b"a\x00")), None);
assert_eq!(midpoint_key(&key(b"a"), &key(b"z")).unwrap(), key(b"m"));
assert_eq!(
midpoint_key(&key(b"\x10"), &key(b"\x20")).unwrap(),
key(b"\x18")
);
}
#[test]
fn planner_chooses_and_validates_split_keys() {
let dir = tempfile::tempdir().unwrap();
let source = source_descriptor();
let planner = TabletSplitPlanner::new(dir.path());
let plan = planner
.plan(
&source,
SplitKeySelection::Midpoint,
ts(150),
[allocation(2, 2, 21), allocation(3, 3, 31)],
)
.unwrap();
let (expected_lower, expected_upper) = source.partition.split_at(&plan.split_key).unwrap();
assert_eq!(plan.children[0].bounds, expected_lower);
assert_eq!(plan.children[1].bounds, expected_upper);
assert!(plan.children[0]
.bounds
.meets_start_of(&plan.children[1].bounds));
let mut unbounded = source.clone();
unbounded.partition = PartitionBounds::unbounded();
assert!(matches!(
planner.plan(
&unbounded,
SplitKeySelection::Midpoint,
ts(150),
[allocation(2, 2, 21), allocation(3, 3, 31)],
),
Err(SplitError::UnboundedMidpoint)
));
for bad in ["a", "z", "0"] {
assert!(matches!(
planner.plan(
&source,
SplitKeySelection::Explicit(text_key(bad)),
ts(150),
[allocation(2, 2, 21), allocation(3, 3, 31)],
),
Err(SplitError::InvalidSplitKey { .. })
));
}
let mut splitting = source.clone();
splitting.state = TabletState::Splitting;
assert!(matches!(
planner.plan(
&splitting,
SplitKeySelection::Explicit(text_key("m")),
ts(150),
[allocation(2, 2, 21), allocation(3, 3, 31)],
),
Err(SplitError::SourceNotActive {
state: TabletState::Splitting,
..
})
));
let mut colliding = source.clone();
let error = planner
.plan(
&colliding,
SplitKeySelection::Explicit(text_key("m")),
ts(150),
[allocation(2, 2, 21), allocation(2, 3, 31)],
)
.unwrap_err();
assert!(matches!(error, SplitError::InvalidPlan(_)));
colliding = source.clone();
let error = planner
.plan(
&colliding,
SplitKeySelection::Explicit(text_key("m")),
ts(150),
[allocation(1, 2, 21), allocation(3, 3, 31)],
)
.unwrap_err();
assert!(matches!(error, SplitError::InvalidPlan(_)));
}
#[test]
fn child_descriptors_are_creating_learners_at_the_inception_generation() {
let dir = tempfile::tempdir().unwrap();
let planner = TabletSplitPlanner::new(dir.path());
let plan = planner
.plan(
&source_descriptor(),
SplitKeySelection::Explicit(text_key("m")),
ts(150),
[allocation(2, 2, 21), allocation(3, 3, 31)],
)
.unwrap();
let children = plan.child_descriptors();
assert_eq!(children[0].state, TabletState::Creating);
assert_eq!(children[0].generation, 6); assert!(children
.iter()
.flat_map(|child| child.replicas.iter())
.all(|replica| replica.role == ReplicaRole::Learner));
for child in &children {
child.validate().unwrap();
}
assert!(!children.iter().any(|child| child.state.is_routable()));
}
#[test]
fn publish_command_flips_three_descriptors_at_one_generation() {
let dir = tempfile::tempdir().unwrap();
let planner = TabletSplitPlanner::new(dir.path());
let plan = planner
.plan(
&source_descriptor(),
SplitKeySelection::Explicit(text_key("m")),
ts(150),
[allocation(2, 2, 21), allocation(3, 3, 31)],
)
.unwrap();
let command = SplitPublishCommand::from_plan(&plan).unwrap();
assert_eq!(command.publish_generation(), 7); assert_eq!(command.source.state, TabletState::Retiring);
assert_eq!(command.source.generation, 7);
for child in &command.children {
assert_eq!(child.state, TabletState::Active);
assert_eq!(child.generation, 7);
assert!(child
.replicas
.iter()
.all(|replica| replica.role == ReplicaRole::Voter));
}
let bytes = serde_json::to_vec(&command).unwrap();
let back: SplitPublishCommand = serde_json::from_slice(&bytes).unwrap();
assert_eq!(back, command);
let mut wrong_state = command.clone();
wrong_state.children[0].state = TabletState::Creating;
assert!(wrong_state.validate().is_err());
let mut skewed = command.clone();
skewed.children[0].generation = 8;
assert!(skewed.validate().is_err());
let mut wrong_bounds = command.clone();
wrong_bounds.children[0].partition = PartitionBounds::unbounded();
assert!(wrong_bounds.validate().is_err());
let mut duplicate = command.clone();
duplicate.children[1].tablet_id = duplicate.children[0].tablet_id;
assert!(duplicate.validate().is_err());
}
#[test]
fn full_split_partitions_the_keyspace_and_flips_routing_atomically() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let fixture = split_fixture();
let table = fixture.source.table_id;
let mut executor = begin_executor(&fixture);
assert_eq!(executor.phase(), SplitPhase::Started);
assert_eq!(executor.step().unwrap(), SplitPhase::MarkedSplitting);
let marked = fixture.meta.tablet(fixture.source.tablet_id).unwrap();
assert_eq!(marked.state, TabletState::Splitting);
assert_eq!(marked.generation, 6);
let error = check_generation(&marked, 5).unwrap_err();
assert!(matches!(error, RoutingError::TabletSplit { .. }));
assert!(matches!(
retry_guidance(&error),
RetryGuidance::AwaitSplitPublish { tablet_id } if tablet_id == fixture.source.tablet_id
));
let tablets = fixture.meta.descriptors();
for name in ["b", "l", "m", "y"] {
assert_eq!(
find_tablet_for_key(&tablets, table, &text_key(name))
.unwrap()
.tablet_id,
fixture.source.tablet_id,
"key {name} left the source during the split"
);
}
assert_eq!(executor.step().unwrap(), SplitPhase::ChildrenCreated);
for id in [tablet_id(2), tablet_id(3)] {
let child = fixture.meta.tablet(id).unwrap();
assert_eq!(child.state, TabletState::Creating);
assert_eq!(child.generation, 6);
assert!(child
.replicas
.iter()
.all(|replica| replica.role == ReplicaRole::Learner));
}
let tablets = fixture.meta.descriptors();
for name in ["b", "y"] {
assert_eq!(
find_tablet_for_key(&tablets, table, &text_key(name))
.unwrap()
.tablet_id,
fixture.source.tablet_id,
"Creating child exposed key {name} before catch-up"
);
}
for child in &fixture.plan.children {
assert_eq!(
child.layout.load_metadata().unwrap().state,
TabletState::Creating
);
}
assert_eq!(executor.step().unwrap(), SplitPhase::SnapshotPinned);
assert_eq!(fixture.keyspace.pin_count(), 1);
assert_eq!(executor.phase(), SplitPhase::SnapshotPinned);
assert_eq!(executor.step().unwrap(), SplitPhase::ChildrenBuilt);
assert_eq!(fixture.sinks[0].rows().len(), LOWER_KEYS.len());
assert_eq!(fixture.sinks[1].rows().len(), UPPER_KEYS.len());
assert_eq!(
fixture.sinks[1].rows().get(&text_key("y")),
Some(&b"v-y@100".to_vec()),
"post-split write leaked into the pinned snapshot"
);
assert_eq!(executor.step().unwrap(), SplitPhase::CaughtUp);
assert_eq!(
fixture.sinks[0].rows().get(&text_key("b")),
Some(&b"v-b@200".to_vec())
);
assert_eq!(
fixture.sinks[1].rows().get(&text_key("y")),
Some(&b"v-y@200".to_vec())
);
assert_eq!(
fixture.sinks[1].rows().get(&text_key("n")),
Some(&b"v-n@200".to_vec())
);
assert_eq!(executor.step().unwrap(), SplitPhase::Published);
assert_eq!(fixture.keyspace.pin_count(), 0);
let retiring = fixture.meta.tablet(fixture.source.tablet_id).unwrap();
assert_eq!(retiring.state, TabletState::Retiring);
assert_eq!(retiring.generation, 7);
let tablets = fixture.meta.descriptors();
assert_eq!(
find_tablet_for_key(&tablets, table, &text_key("b"))
.unwrap()
.tablet_id,
tablet_id(2)
);
assert_eq!(
find_tablet_for_key(&tablets, table, &text_key("y"))
.unwrap()
.tablet_id,
tablet_id(3)
);
assert_eq!(
find_tablet_for_key(&tablets, table, &text_key("m"))
.unwrap()
.tablet_id,
tablet_id(3)
);
let overlapping = tablets_overlapping(&tablets, table, &PartitionBounds::unbounded());
assert_eq!(
overlapping
.iter()
.map(|tablet| tablet.tablet_id)
.collect::<Vec<_>>(),
vec![tablet_id(2), tablet_id(3)]
);
let error = check_generation(&retiring, 5).unwrap_err();
assert!(matches!(error, RoutingError::TabletMoved { .. }));
assert!(matches!(
retry_guidance(&error),
RetryGuidance::RefreshAndReroute { .. }
));
for id in [tablet_id(2), tablet_id(3)] {
assert!(check_generation(&fixture.meta.tablet(id).unwrap(), 7).is_ok());
}
let pin = executor.retention().unwrap().pin(5);
assert!(matches!(
executor.step(),
Err(SplitError::SourceRetained { pins: 1, .. })
));
assert_eq!(executor.phase(), SplitPhase::Published);
let fresh = executor.retention().unwrap().pin(7);
assert!(executor.retention().unwrap().unpin(pin));
assert_eq!(executor.step().unwrap(), SplitPhase::SourceRetired);
assert!(executor.retention().unwrap().unpin(fresh));
assert_split_completed(&fixture);
executor.run().unwrap();
assert!(SplitExecutor::resume(
fixture.source_layout.clone(),
fixture.meta.clone(),
fixture.keyspace.clone(),
fixture.sinks.clone(),
)
.unwrap()
.is_none());
}
#[test]
fn split_resumes_after_a_crash_at_every_durable_boundary() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let hooks = [
"tablet.split.phase.1",
"tablet.split.phase.2",
"tablet.split.phase.3",
"tablet.split.phase.4",
"tablet.split.phase.5",
"tablet.split.phase.6",
"tablet.split.phase.7",
"tablet.split.before",
"tablet.split.after",
];
for hook in hooks {
let fixture = split_fixture();
let mut executor = begin_executor(&fixture);
{
let _guard =
mongreldb_fault::ScopedGuard::limited(hook, mongreldb_fault::Action::Fail, 1);
assert!(
matches!(executor.run(), Err(SplitError::Fault(_))),
"hook {hook} did not fire"
);
}
drop(executor);
let resumed = SplitExecutor::resume(
fixture.source_layout.clone(),
fixture.meta.clone(),
fixture.keyspace.clone(),
fixture.sinks.clone(),
)
.unwrap();
if hook == "tablet.split.phase.7" {
assert!(resumed.is_none(), "hook {hook}");
} else {
resumed
.unwrap()
.run()
.unwrap_or_else(|error| panic!("resume after {hook} failed: {error}"));
}
assert_split_completed(&fixture);
}
}
#[test]
fn resume_replays_an_interrupted_step_idempotently() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let fixture = split_fixture();
let mut executor = begin_executor(&fixture);
executor.run_until(SplitPhase::ChildrenCreated).unwrap();
drop(executor);
let mut resumed = SplitExecutor::resume(
fixture.source_layout.clone(),
fixture.meta.clone(),
fixture.keyspace.clone(),
fixture.sinks.clone(),
)
.unwrap()
.unwrap();
assert_eq!(resumed.phase(), SplitPhase::ChildrenCreated);
resumed.step().unwrap();
assert_eq!(resumed.phase(), SplitPhase::SnapshotPinned);
assert_eq!(fixture.keyspace.pin_count(), 1);
resumed.run().unwrap();
assert_split_completed(&fixture);
}
#[test]
fn retention_guard_tracks_old_generation_pins() {
let guard = SourceRetentionGuard::new(tablet_id(1), 7);
assert!(guard.ready_for_removal());
assert_eq!(guard.source(), tablet_id(1));
assert_eq!(guard.retired_generation(), 7);
let stale = guard.pin(5);
let boundary = guard.pin(7); assert_eq!(guard.pin_count(), 2);
assert_eq!(guard.old_generation_pins(), 1);
assert!(!guard.ready_for_removal());
assert!(guard.unpin(stale));
assert!(guard.ready_for_removal());
assert!(guard.unpin(boundary));
assert!(!guard.unpin(boundary), "double release must not succeed");
assert_eq!(guard.pin_count(), 0);
}
#[test]
fn stale_requests_classify_and_guidance_feeds_the_retry_policy() {
let mut descriptor = source_descriptor();
assert!(check_generation(&descriptor, 5).is_ok());
descriptor.state = TabletState::Splitting;
descriptor.generation = 6;
let error = check_generation(&descriptor, 5).unwrap_err();
assert_eq!(
error,
RoutingError::TabletSplit {
tablet_id: tablet_id(1),
used_generation: 5,
current_generation: 6,
}
);
let guidance = retry_guidance(&error);
assert_eq!(guidance.category(), ErrorCategory::TabletSplitting);
descriptor.state = TabletState::Retiring;
descriptor.generation = 7;
let error = check_generation(&descriptor, 5).unwrap_err();
assert!(matches!(error, RoutingError::TabletMoved { .. }));
let guidance = retry_guidance(&error);
assert_eq!(guidance.category(), ErrorCategory::TabletMoved);
descriptor.state = TabletState::Active;
let error = check_generation(&descriptor, 5).unwrap_err();
assert!(matches!(error, RoutingError::StaleMetadata { .. }));
let guidance = retry_guidance(&error);
assert_eq!(guidance.category(), ErrorCategory::StaleMetadata);
let policy = RetryPolicy::default();
let cache = RoutingCache::new();
let operation = OperationDescriptor {
idempotent: true,
idempotency_key: None,
read_only: true,
deadline: Duration::from_secs(30),
max_attempts: 3,
};
for error in [
RoutingError::TabletSplit {
tablet_id: tablet_id(1),
used_generation: 5,
current_generation: 6,
},
RoutingError::TabletMoved {
tablet_id: tablet_id(1),
used_generation: 5,
current_generation: 7,
},
RoutingError::StaleMetadata {
tablet_id: tablet_id(1),
used_generation: 5,
current_generation: 7,
},
] {
let mut state = RetryState::default();
let action = policy.decide(
GroupKey::Tablet(tablet_id(1)),
&operation,
&mut state,
&retry_guidance(&error).failure(),
&cache,
Duration::ZERO,
);
assert!(
matches!(action, RetryAction::RefreshMetadata { .. }),
"{error} did not map onto a metadata refresh"
);
}
}
#[test]
fn progress_record_round_trips_and_fails_closed() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let fixture = split_fixture();
let executor = begin_executor(&fixture);
let path = fixture
.source_layout
.tablet_dir()
.join(SPLIT_PROGRESS_FILENAME);
assert!(path.is_file());
drop(executor);
std::fs::write(&path, b"{ not json").unwrap();
assert!(matches!(
SplitExecutor::resume(
fixture.source_layout.clone(),
fixture.meta.clone(),
fixture.keyspace.clone(),
fixture.sinks.clone(),
),
Err(SplitError::Tablet(TabletError::Metadata(
ClusterError::CorruptMetadata { .. }
)))
));
let executor = begin_executor(&fixture);
drop(executor);
let mut value: serde_json::Value =
serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
value["format_version"] = serde_json::json!(99);
std::fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap();
assert!(matches!(
SplitExecutor::resume(
fixture.source_layout.clone(),
fixture.meta.clone(),
fixture.keyspace.clone(),
fixture.sinks.clone(),
),
Err(SplitError::Tablet(TabletError::Metadata(
ClusterError::UnsupportedFormatVersion { found: 99, .. }
)))
));
let executor = begin_executor(&fixture);
drop(executor);
let mut value: serde_json::Value =
serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
value["progress"]["split_ts"]["physical_micros"] = serde_json::json!(999);
std::fs::write(&path, serde_json::to_vec(&value).unwrap()).unwrap();
assert!(matches!(
SplitExecutor::resume(
fixture.source_layout.clone(),
fixture.meta.clone(),
fixture.keyspace.clone(),
fixture.sinks.clone(),
),
Err(SplitError::Tablet(TabletError::Metadata(
ClusterError::CorruptMetadata { .. }
)))
));
}
#[test]
fn begin_rejects_a_mismatched_source_layout_and_missing_replica() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let fixture = split_fixture();
let foreign = TabletLayout::new(
fixture._dir.path(),
tablet_id(9),
fixture.source.raft_group_id,
);
assert!(matches!(
SplitExecutor::begin(
fixture.plan.clone(),
foreign,
fixture.meta.clone(),
fixture.keyspace.clone(),
fixture.sinks.clone(),
),
Err(SplitError::Tablet(TabletError::TabletMismatch { .. }))
));
let other_dir = tempfile::tempdir().unwrap();
let missing = TabletLayout::new(
other_dir.path(),
fixture.source.tablet_id,
fixture.source.raft_group_id,
);
assert!(matches!(
SplitExecutor::begin(
fixture.plan.clone(),
missing,
fixture.meta.clone(),
fixture.keyspace.clone(),
fixture.sinks.clone(),
),
Err(SplitError::Tablet(TabletError::MissingMetadata(_)))
));
}
#[test]
fn abort_before_publish_restores_the_source_and_removes_the_children() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let fixture = split_fixture();
let mut executor = begin_executor(&fixture);
executor.run_until(SplitPhase::SnapshotPinned).unwrap();
drop(executor);
let marked = fixture.meta.tablet(fixture.source.tablet_id).unwrap();
assert_eq!(marked.state, TabletState::Splitting);
assert_eq!(marked.generation, 6);
for id in [tablet_id(2), tablet_id(3)] {
assert!(fixture.meta.tablet(id).is_some());
}
assert!(fixture
.source_layout
.tablet_dir()
.join(SPLIT_PROGRESS_FILENAME)
.is_file());
let mut meta = fixture.meta.clone();
let report = abort_split(&fixture.source_layout, &mut meta).unwrap();
assert_eq!(report.source, fixture.source.tablet_id);
assert_eq!(report.phase, Some(SplitPhase::SnapshotPinned));
assert_eq!(report.children_removed, vec![tablet_id(2), tablet_id(3)]);
let restored = report.source_after.unwrap();
assert_eq!(restored.state, TabletState::Active);
assert_eq!(restored.generation, 7);
assert_eq!(
meta.tablet(fixture.source.tablet_id),
Some(restored.clone())
);
assert_eq!(fixture.source_layout.load_metadata().unwrap(), restored);
for (index, id) in [tablet_id(2), tablet_id(3)].into_iter().enumerate() {
assert!(meta.tablet(id).is_none());
assert!(!fixture.plan.children[index].layout.tablet_dir().exists());
}
assert!(!fixture
.source_layout
.tablet_dir()
.join(SPLIT_PROGRESS_FILENAME)
.exists());
assert_eq!(fixture.keyspace.rows_at(ts(u64::MAX)).len(), 14);
}
#[test]
fn abort_is_idempotent_across_a_mid_abort_crash() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let fixture = split_fixture();
let mut executor = begin_executor(&fixture);
executor.run_until(SplitPhase::ChildrenCreated).unwrap();
drop(executor);
let mut meta = fixture.meta.clone();
let first = abort_split(&fixture.source_layout, &mut meta).unwrap();
assert_eq!(first.phase, Some(SplitPhase::ChildrenCreated));
let second = abort_split(&fixture.source_layout, &mut meta).unwrap();
assert_eq!(second.phase, None);
assert!(second.children_removed.is_empty());
assert!(second.source_after.is_none());
let restored = meta.tablet(fixture.source.tablet_id).unwrap();
assert_eq!(restored.state, TabletState::Active);
assert_eq!(restored.generation, 7);
}
#[test]
fn abort_at_started_unwinds_before_any_meta_write() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let fixture = split_fixture();
let executor = begin_executor(&fixture);
drop(executor);
let mut meta = fixture.meta.clone();
let report = abort_split(&fixture.source_layout, &mut meta).unwrap();
assert_eq!(report.phase, Some(SplitPhase::Started));
assert!(report.children_removed.is_empty());
let restored = report.source_after.unwrap();
assert_eq!(restored, fixture.source);
assert!(!fixture
.source_layout
.tablet_dir()
.join(SPLIT_PROGRESS_FILENAME)
.exists());
}
#[test]
fn abort_after_publish_fails_closed() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let fixture = split_fixture();
let mut executor = begin_executor(&fixture);
executor.run_until(SplitPhase::Published).unwrap();
drop(executor);
let mut meta = fixture.meta.clone();
let error = abort_split(&fixture.source_layout, &mut meta).unwrap_err();
assert!(matches!(
error,
SplitError::CannotAbort { tablet, phase }
if tablet == fixture.source.tablet_id && phase == SplitPhase::Published
));
assert_eq!(
meta.tablet(fixture.source.tablet_id).unwrap().state,
TabletState::Retiring
);
for id in [tablet_id(2), tablet_id(3)] {
assert_eq!(meta.tablet(id).unwrap().state, TabletState::Active);
}
}
#[test]
fn the_source_can_split_again_after_an_abort() {
let _lock = EXECUTOR_TEST_LOCK.lock().unwrap();
let fixture = split_fixture();
let mut executor = begin_executor(&fixture);
executor.run_until(SplitPhase::ChildrenCreated).unwrap();
drop(executor);
let mut meta = fixture.meta.clone();
abort_split(&fixture.source_layout, &mut meta).unwrap();
let restored = meta.tablet(fixture.source.tablet_id).unwrap();
let planner = TabletSplitPlanner::new(fixture._dir.path());
let plan = planner
.plan(
&restored,
SplitKeySelection::Explicit(text_key("n")),
ts(300),
[allocation(4, 4, 41), allocation(5, 5, 51)],
)
.unwrap();
let sinks = [MapChildSink::new(), MapChildSink::new()];
let mut executor = SplitExecutor::begin(
plan,
fixture.source_layout.clone(),
meta.clone(),
fixture.keyspace.clone(),
sinks.clone(),
)
.unwrap();
executor.run().unwrap();
assert!(meta.tablet(fixture.source.tablet_id).is_none());
let mut union = sinks[0].rows();
union.extend(sinks[1].rows());
assert_eq!(union, fixture.keyspace.rows_at(ts(u64::MAX)));
}
}