1use std::collections::BTreeMap;
64use std::path::{Path, PathBuf};
65use std::sync::{Arc, Mutex};
66use std::time::Duration;
67
68use mongreldb_consensus::engine_sink::{
69 open_engine_sink, EngineApplySink, EngineGroupConfig, EngineSinkError, EngineTabletPin,
70 TabletDataCommand, TabletDataCommandRecord, TabletDataMutation, COMMAND_TYPE_TABLET_DATA,
71};
72use mongreldb_consensus::error::ConsensusError;
73use mongreldb_consensus::group::{ConsensusGroup, GroupCommitReceipt, GroupConfig, GroupMetrics};
74use mongreldb_consensus::identity::{raft_node_id, CommandKind, RaftNodeId};
75use mongreldb_consensus::raft_log::RaftCommitLog;
76use mongreldb_consensus::state_machine::ApplySink;
77use mongreldb_log::commit_log::{CommitLog, ExecutionControl, LogPosition};
78use mongreldb_log::envelope::CommandEnvelope;
79use mongreldb_types::hlc::HlcTimestamp;
80use mongreldb_types::ids::{DatabaseId, MetadataVersion, NodeId, RaftGroupId, TabletId};
81use openraft::BasicNode;
82#[cfg(test)]
83use serde::{Deserialize, Serialize};
84
85use crate::merge::{
86 merge_progress, MergeExecutor, MergeInputs, MergeMetaPlane, MergePhase, MergePlan,
87 MergePlanner, MergePublishCommand,
88};
89use crate::meta::{MetaCommand, MetaError, MetaGroup, MetaGroupConfig, MetaRejectionReason};
90use crate::network::{
91 InternalRpcHandler, PeerEndpoint, TcpTransport, TransportConfig, TransportError,
92 TransportSecurity, TransportServer,
93};
94use crate::node::{ClusterError, NodeIdentity};
95use crate::split::{
96 abort_split, split_progress, ChildAllocation, ChildStateSink, SnapshotPin, SplitAbortReport,
97 SplitError, SplitExecutor, SplitKeySelection, SplitPhase, SplitPlan, SplitPublishCommand,
98 TabletDataError, TabletKeyspace, TabletMetaPlane, TabletMutation, TabletSplitPlanner,
99};
100use crate::tablet::{
101 Key, ReplicaDescriptor, TablePartitioningRecord, TabletDescriptor, TabletError, TabletLayout,
102 TabletOwnershipGuard, TabletOwnershipRegistry, TabletState, TABLETS_DIR, TABLET_META_FILENAME,
103};
104
105#[derive(Debug, thiserror::Error)]
107pub enum RuntimeError {
108 #[error(transparent)]
110 Cluster(#[from] ClusterError),
111 #[error(transparent)]
113 Meta(#[from] MetaError),
114 #[error(transparent)]
116 Consensus(#[from] ConsensusError),
117 #[error(transparent)]
119 Transport(#[from] TransportError),
120 #[error(transparent)]
122 Tablet(#[from] TabletError),
123 #[error(transparent)]
125 EngineSink(#[from] EngineSinkError),
126 #[error(transparent)]
128 Split(#[from] SplitError),
129 #[error(transparent)]
131 Merge(#[from] crate::merge::MergeError),
132 #[error("runtime I/O error: {0}")]
134 Io(#[from] std::io::Error),
135 #[error("invalid runtime request: {0}")]
138 InvalidRequest(String),
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub struct GroupTiming {
146 pub heartbeat_interval: Duration,
148 pub election_timeout_min: Duration,
150 pub election_timeout_max: Duration,
152 pub install_snapshot_timeout: Duration,
154}
155
156impl GroupTiming {
157 fn apply(&self, config: &mut GroupConfig) {
158 config.heartbeat_interval = self.heartbeat_interval;
159 config.election_timeout_min = self.election_timeout_min;
160 config.election_timeout_max = self.election_timeout_max;
161 config.install_snapshot_timeout = self.install_snapshot_timeout;
162 }
163}
164
165#[derive(Clone, Debug)]
169pub struct MetaMembership {
170 pub meta_group_id: RaftGroupId,
173 pub bootstrap_voters: Option<Vec<(NodeId, String)>>,
178}
179
180#[derive(Clone, Debug)]
182pub struct NodeRuntimeConfig {
183 pub node_data: PathBuf,
185 pub security: TransportSecurity,
187 pub transport: TransportConfig,
189 pub listen_address: String,
192 pub rpc_address: Option<String>,
195 pub peers: Vec<(NodeId, String)>,
199 pub meta: Option<MetaMembership>,
201 pub timing: Option<GroupTiming>,
203}
204
205impl NodeRuntimeConfig {
206 pub fn new(node_data: PathBuf, listen_address: String) -> Self {
209 Self {
210 node_data,
211 security: TransportSecurity::PlaintextForTesting,
212 transport: TransportConfig::default(),
213 listen_address,
214 rpc_address: None,
215 peers: Vec::new(),
216 meta: None,
217 timing: None,
218 }
219 }
220}
221
222#[derive(Clone, Debug)]
224pub struct MetaGroupStatus {
225 pub meta_group_id: RaftGroupId,
227 pub metadata_version: MetadataVersion,
229 pub metrics: GroupMetrics,
231}
232
233#[derive(Clone, Debug)]
235pub struct TabletGroupStatus {
236 pub tablet_id: TabletId,
238 pub raft_group_id: RaftGroupId,
240 pub state: TabletState,
242 pub replicas: Vec<ReplicaDescriptor>,
244 pub applied: LogPosition,
246 pub metrics: GroupMetrics,
248}
249
250#[derive(Clone, Debug)]
253pub struct RuntimeStatus {
254 pub identity: NodeIdentity,
256 pub rpc_address: String,
258 pub meta: Option<MetaGroupStatus>,
260 pub tablets: Vec<TabletGroupStatus>,
262}
263
264fn resolve_tablet_database_id(descriptor: &crate::tablet::TabletDescriptor) -> DatabaseId {
272 if descriptor.database_id != DatabaseId::ZERO {
273 return descriptor.database_id;
274 }
275 DatabaseId::from_bytes(*descriptor.raft_group_id.as_bytes())
276}
277
278fn tablet_group_name(tablet_id: TabletId) -> String {
281 format!("tablet-{}", tablet_id.to_hex())
282}
283
284pub const TABLET_LEDGER_FILENAME: &str = "tablet-ledger.json";
286#[cfg(test)]
288pub const TABLET_LEDGER_FORMAT_VERSION: u32 = 1;
289#[cfg(test)]
291pub const MIN_SUPPORTED_TABLET_LEDGER_FORMAT_VERSION: u32 = 1;
292
293#[cfg(test)]
296const MAX_TIMESTAMP: HlcTimestamp = HlcTimestamp {
297 physical_micros: u64::MAX,
298 logical: u32::MAX,
299 node_tiebreaker: u32::MAX,
300};
301
302#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
304#[cfg(test)]
305struct TabletLedgerCheckpoint {
306 format_version: u32,
308 position: LogPosition,
310 rows: BTreeMap<Key, Vec<(HlcTimestamp, Vec<u8>)>>,
312}
313
314#[derive(Debug)]
317#[cfg(test)]
318pub struct TabletLedger {
319 rows: BTreeMap<Key, Vec<(HlcTimestamp, Vec<u8>)>>,
320 pins: BTreeMap<HlcTimestamp, usize>,
322 position: LogPosition,
323 state_dir: PathBuf,
324}
325
326#[cfg(test)]
327impl TabletLedger {
328 pub fn open(group_dir: &Path) -> Result<Self, RuntimeError> {
330 let state_dir = group_dir.join("raft").join("state");
331 std::fs::create_dir_all(&state_dir).map_err(RuntimeError::Io)?;
332 let path = state_dir.join(TABLET_LEDGER_FILENAME);
333 let Some(bytes) = crate::node::read_meta_file(&path)? else {
334 return Ok(Self {
335 rows: BTreeMap::new(),
336 pins: BTreeMap::new(),
337 position: LogPosition::ZERO,
338 state_dir,
339 });
340 };
341 let checkpoint: TabletLedgerCheckpoint =
342 crate::node::decode_json(TABLET_LEDGER_FILENAME, &bytes)?;
343 if checkpoint.format_version < MIN_SUPPORTED_TABLET_LEDGER_FORMAT_VERSION
344 || checkpoint.format_version > TABLET_LEDGER_FORMAT_VERSION
345 {
346 return Err(ClusterError::UnsupportedFormatVersion {
347 file: TABLET_LEDGER_FILENAME,
348 found: checkpoint.format_version,
349 min: MIN_SUPPORTED_TABLET_LEDGER_FORMAT_VERSION,
350 max: TABLET_LEDGER_FORMAT_VERSION,
351 }
352 .into());
353 }
354 Ok(Self {
355 rows: checkpoint.rows,
356 pins: BTreeMap::new(),
357 position: checkpoint.position,
358 state_dir,
359 })
360 }
361
362 fn apply(
366 &mut self,
367 command: &TabletDataCommand,
368 commit_ts: HlcTimestamp,
369 position: LogPosition,
370 ) -> Result<(), RuntimeError> {
371 if position.index <= self.position.index {
372 return Ok(());
373 }
374 match command {
375 TabletDataCommand::Upsert { entries } => {
376 for (key, value) in entries {
377 self.insert_version(Key::from_bytes(key.clone()), commit_ts, value.clone());
378 }
379 }
380 TabletDataCommand::Delete { keys } => {
381 if !self.pins.is_empty() {
382 return Err(RuntimeError::InvalidRequest(
383 "legacy tablet ledger Delete with live snapshot pins".to_owned(),
384 ));
385 }
386 for key in keys {
387 self.rows.remove(&Key::from_bytes(key.clone()));
388 }
389 }
390 TabletDataCommand::Replace { rows } => {
391 if !self.pins.is_empty() {
396 return Err(RuntimeError::InvalidRequest(
397 "tablet ledger Replace with live snapshot pins".to_owned(),
398 ));
399 }
400 self.rows.clear();
401 for (key, value) in rows {
402 self.insert_version(Key::from_bytes(key.clone()), commit_ts, value.clone());
403 }
404 }
405 }
406 self.position = position;
407 self.persist()
408 }
409
410 fn insert_version(&mut self, key: Key, ts: HlcTimestamp, value: Vec<u8>) {
414 let chain = self.rows.entry(key).or_default();
415 chain.push((ts, value));
416 chain.sort_by_key(|(version, _)| *version);
417 match self.pins.keys().next() {
418 None => {
419 let newest = chain.pop().expect("just inserted");
420 chain.clear();
421 chain.push(newest);
422 }
423 Some(oldest_pin) => {
424 let baseline = chain.partition_point(|(version, _)| *version <= *oldest_pin);
425 if baseline > 1 {
426 chain.drain(..baseline - 1);
427 }
428 }
429 }
430 }
431
432 fn persist(&self) -> Result<(), RuntimeError> {
434 let checkpoint = TabletLedgerCheckpoint {
435 format_version: TABLET_LEDGER_FORMAT_VERSION,
436 position: self.position,
437 rows: self.rows.clone(),
438 };
439 let bytes = crate::node::encode_json(TABLET_LEDGER_FILENAME, &checkpoint)?;
440 crate::node::write_meta_atomic(&self.state_dir, TABLET_LEDGER_FILENAME, &bytes)
441 .map_err(ClusterError::Io)?;
442 Ok(())
443 }
444
445 pub fn applied_position(&self) -> LogPosition {
447 self.position
448 }
449
450 fn pin(&mut self, ts: HlcTimestamp) {
451 *self.pins.entry(ts).or_insert(0) += 1;
452 }
453
454 fn unpin(&mut self, ts: HlcTimestamp) {
455 if let Some(count) = self.pins.get_mut(&ts) {
456 *count -= 1;
457 if *count == 0 {
458 self.pins.remove(&ts);
459 }
460 }
461 }
462
463 pub fn pin_count(&self) -> usize {
465 self.pins.values().sum()
466 }
467
468 pub fn rows_at(&self, ts: HlcTimestamp) -> BTreeMap<Key, Vec<u8>> {
470 self.rows
471 .iter()
472 .filter_map(|(key, chain)| {
473 let visible = chain.iter().rfind(|(version, _)| *version <= ts)?;
474 Some((key.clone(), visible.1.clone()))
475 })
476 .collect()
477 }
478
479 pub fn current_rows(&self) -> BTreeMap<Key, Vec<u8>> {
481 self.rows_at(MAX_TIMESTAMP)
482 }
483
484 pub fn deltas_after(&self, ts: HlcTimestamp) -> Vec<(Key, Vec<u8>)> {
487 let mut deltas: Vec<(HlcTimestamp, Key, Vec<u8>)> = Vec::new();
488 for (key, chain) in &self.rows {
489 for (version, value) in chain {
490 if *version > ts {
491 deltas.push((*version, key.clone(), value.clone()));
492 }
493 }
494 }
495 deltas.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
496 deltas
497 .into_iter()
498 .map(|(_, key, value)| (key, value))
499 .collect()
500 }
501
502 pub fn size_bytes(&self) -> u64 {
505 self.rows
506 .iter()
507 .map(|(key, chain)| {
508 let newest = chain.last().expect("non-empty chain");
509 (key.as_bytes().len() + newest.1.len()) as u64
510 })
511 .sum()
512 }
513
514 fn snapshot_bytes(
515 &self,
516 ) -> Result<Vec<u8>, mongreldb_consensus::state_machine::StateMachineError> {
517 serde_json::to_vec(&TabletLedgerCheckpoint {
518 format_version: TABLET_LEDGER_FORMAT_VERSION,
519 position: self.position,
520 rows: self.rows.clone(),
521 })
522 .map_err(|error| {
523 mongreldb_consensus::state_machine::StateMachineError::Sink(format!(
524 "tablet ledger snapshot: {error}"
525 ))
526 })
527 }
528
529 fn install_bytes(
530 &mut self,
531 bytes: &[u8],
532 ) -> Result<(), mongreldb_consensus::state_machine::StateMachineError> {
533 use mongreldb_consensus::state_machine::StateMachineError;
534
535 let checkpoint: TabletLedgerCheckpoint =
536 serde_json::from_slice(bytes).map_err(|error| {
537 StateMachineError::Corrupt(format!("tablet ledger snapshot: {error}"))
538 })?;
539 if checkpoint.format_version < MIN_SUPPORTED_TABLET_LEDGER_FORMAT_VERSION
540 || checkpoint.format_version > TABLET_LEDGER_FORMAT_VERSION
541 {
542 return Err(StateMachineError::Corrupt(format!(
543 "tablet ledger snapshot format version {} is outside \
544 {MIN_SUPPORTED_TABLET_LEDGER_FORMAT_VERSION}..={TABLET_LEDGER_FORMAT_VERSION}",
545 checkpoint.format_version
546 )));
547 }
548 self.rows = checkpoint.rows;
549 self.position = checkpoint.position;
550 self.persist()
551 .map_err(|error| StateMachineError::Sink(error.to_string()))
552 }
553}
554
555#[cfg(test)]
556fn encode_group_snapshot(engine: &[u8], ledger: &[u8]) -> Vec<u8> {
557 let mut out = Vec::with_capacity(12 + engine.len() + ledger.len());
558 out.extend_from_slice(&1_u32.to_le_bytes());
559 out.extend_from_slice(&(engine.len() as u64).to_le_bytes());
560 out.extend_from_slice(engine);
561 out.extend_from_slice(ledger);
562 out
563}
564
565#[cfg(test)]
566fn decode_group_snapshot(
567 bytes: &[u8],
568) -> Result<(Vec<u8>, Vec<u8>), mongreldb_consensus::state_machine::StateMachineError> {
569 use mongreldb_consensus::state_machine::StateMachineError;
570
571 if bytes.len() < 12 {
572 return Err(StateMachineError::Corrupt(
573 "tablet group snapshot: truncated frame".to_owned(),
574 ));
575 }
576 let version = u32::from_le_bytes(bytes[..4].try_into().expect("4 bytes"));
577 if version != 1 {
578 return Err(StateMachineError::Corrupt(format!(
579 "tablet group snapshot format version {version} is not 1"
580 )));
581 }
582 let engine_len = u64::from_le_bytes(bytes[4..12].try_into().expect("8 bytes")) as usize;
583 if bytes.len() < 12 + engine_len {
584 return Err(StateMachineError::Corrupt(
585 "tablet group snapshot: truncated engine payload".to_owned(),
586 ));
587 }
588 Ok((
589 bytes[12..12 + engine_len].to_vec(),
590 bytes[12 + engine_len..].to_vec(),
591 ))
592}
593
594struct TabletGroup {
597 group: Arc<ConsensusGroup<TcpTransport>>,
598 descriptor: TabletDescriptor,
599 sink: Arc<Mutex<EngineApplySink>>,
600 _ownership: TabletOwnershipGuard<'static>,
601}
602
603#[derive(serde::Deserialize)]
607struct TabletFileProbe {
608 tablet: TabletProbe,
609}
610
611#[derive(serde::Deserialize)]
612struct TabletProbe {
613 raft_group_id: RaftGroupId,
614}
615
616fn scan_tablet_layouts(node_data: &Path) -> Result<Vec<(TabletId, RaftGroupId)>, RuntimeError> {
621 let root = node_data.join(TABLETS_DIR);
622 if !root.is_dir() {
623 return Ok(Vec::new());
624 }
625 let mut found = Vec::new();
626 for entry in std::fs::read_dir(&root)? {
627 let entry = entry?;
628 if !entry.file_type()?.is_dir() {
629 continue;
630 }
631 let name = entry.file_name();
632 let Some(name) = name.to_str() else {
633 return Err(RuntimeError::InvalidRequest(format!(
634 "tablet directory name {name:?} is not UTF-8"
635 )));
636 };
637 let tablet_id: TabletId = name.parse().map_err(|_| {
638 RuntimeError::InvalidRequest(format!("tablet directory `{name}` is not a tablet id"))
639 })?;
640 let probe_path = entry.path().join(TABLET_META_FILENAME);
641 let Some(bytes) = crate::node::read_meta_file(&probe_path)? else {
642 return Err(TabletError::MissingMetadata(probe_path).into());
643 };
644 let probe: TabletFileProbe = serde_json::from_slice(&bytes).map_err(|error| {
645 RuntimeError::InvalidRequest(format!(
646 "tablet metadata probe at {} failed: {error}",
647 probe_path.display()
648 ))
649 })?;
650 found.push((tablet_id, probe.tablet.raft_group_id));
651 }
652 found.sort();
653 Ok(found)
654}
655
656fn active_snapshot_timestamp(
657 node_data: &Path,
658 tablet_id: TabletId,
659) -> Result<Option<HlcTimestamp>, RuntimeError> {
660 let mut found = None;
661 for (source_id, raft_group_id) in scan_tablet_layouts(node_data)? {
662 let layout = TabletLayout::new(node_data.to_path_buf(), source_id, raft_group_id);
663 let timestamp = if let Some(progress) = split_progress(&layout)?.filter(|progress| {
664 progress.source.tablet_id == tablet_id && progress.phase < SplitPhase::Published
665 }) {
666 Some(progress.split_ts)
667 } else {
668 merge_progress(&layout)?
669 .filter(|progress| {
670 progress.phase < MergePhase::Published
671 && progress
672 .sources
673 .iter()
674 .any(|source| source.tablet_id == tablet_id)
675 })
676 .map(|progress| progress.merge_ts)
677 };
678 if let Some(timestamp) = timestamp {
679 if found
680 .replace(timestamp)
681 .is_some_and(|prior| prior != timestamp)
682 {
683 return Err(RuntimeError::InvalidRequest(format!(
684 "tablet {tablet_id} appears in conflicting split/merge progress"
685 )));
686 }
687 }
688 }
689 Ok(found)
690}
691
692fn peer_endpoint(security: &TransportSecurity, node_id: NodeId, address: &str) -> PeerEndpoint {
694 match security {
695 TransportSecurity::Mtls(_) => PeerEndpoint::mtls(address, node_id),
696 TransportSecurity::PlaintextForTesting => PeerEndpoint::plaintext(address),
697 }
698}
699
700struct TabletOpenContext<'a> {
703 node_data: &'a Path,
704 security: &'a TransportSecurity,
705 timing: Option<GroupTiming>,
706 identity: &'a NodeIdentity,
707 peers: &'a BTreeMap<NodeId, String>,
708}
709
710pub struct NodeRuntime {
713 identity: NodeIdentity,
714 node_data: PathBuf,
715 rpc_address: String,
716 security: TransportSecurity,
717 peers: BTreeMap<NodeId, String>,
718 timing: Option<GroupTiming>,
719 transport: Arc<TcpTransport>,
720 server: Option<TransportServer>,
721 meta: Option<Arc<MetaGroup<TcpTransport>>>,
722 tablets: BTreeMap<TabletId, TabletGroup>,
723}
724
725#[derive(Clone)]
727pub struct NodeInternalRpcClient {
728 transport: Arc<TcpTransport>,
729}
730
731impl NodeInternalRpcClient {
732 pub async fn call(
734 &self,
735 target: NodeId,
736 service_id: u32,
737 body: Vec<u8>,
738 ) -> Result<Vec<u8>, RuntimeError> {
739 self.transport
740 .internal_rpc(raft_node_id(&target), service_id, body)
741 .await
742 .map_err(Into::into)
743 }
744}
745
746impl NodeRuntime {
747 pub async fn start(config: NodeRuntimeConfig) -> Result<Self, RuntimeError> {
749 let identity =
750 NodeIdentity::load(&config.node_data)?.ok_or(ClusterError::NotInitialized)?;
751 let transport = Arc::new(TcpTransport::new(
752 config.transport.clone(),
753 config.security.clone(),
754 ));
755 let mut peers = BTreeMap::new();
756 for (node_id, address) in &config.peers {
757 transport.upsert_peer(
758 raft_node_id(node_id),
759 peer_endpoint(&config.security, *node_id, address),
760 );
761 peers.insert(*node_id, address.clone());
762 }
763 let server = TransportServer::bind_shared(
764 &config.listen_address,
765 transport.security_handle(),
766 transport.registry(),
767 config.transport.clone(),
768 )
769 .await?;
770 let rpc_address = config
771 .rpc_address
772 .clone()
773 .unwrap_or_else(|| server.local_addr().to_string());
774
775 let meta = match &config.meta {
776 Some(membership) => {
777 let group =
778 Self::start_meta_group(&config, &identity, membership, transport.clone())
779 .await?;
780 Some(Arc::new(group))
781 }
782 None => None,
783 };
784 let tablets =
785 Self::open_tablet_groups(&config, &identity, &peers, transport.clone()).await?;
786 Ok(Self {
787 identity,
788 node_data: config.node_data.clone(),
789 rpc_address,
790 security: config.security.clone(),
791 peers,
792 timing: config.timing,
793 transport,
794 server: Some(server),
795 meta,
796 tablets,
797 })
798 }
799
800 async fn start_meta_group(
803 config: &NodeRuntimeConfig,
804 identity: &NodeIdentity,
805 membership: &MetaMembership,
806 transport: Arc<TcpTransport>,
807 ) -> Result<MetaGroup<TcpTransport>, RuntimeError> {
808 let meta_config = MetaGroupConfig::new(
809 config.node_data.clone(),
810 membership.meta_group_id,
811 identity.node_id,
812 );
813 let mut group_config = meta_config.group_config();
814 if let Some(timing) = &config.timing {
815 timing.apply(&mut group_config);
816 }
817 let group = MetaGroup::create(meta_config, group_config, transport).await?;
818 if let Some(voters) = &membership.bootstrap_voters {
819 if !voters
820 .iter()
821 .any(|(node_id, _)| *node_id == identity.node_id)
822 {
823 return Err(RuntimeError::InvalidRequest(
824 "meta bootstrap voter set does not include this node".to_owned(),
825 ));
826 }
827 if !group.is_initialized().await? {
828 group.bootstrap(voters).await?;
829 }
830 }
831 Ok(group)
832 }
833
834 async fn open_tablet_groups(
836 config: &NodeRuntimeConfig,
837 identity: &NodeIdentity,
838 peers: &BTreeMap<NodeId, String>,
839 transport: Arc<TcpTransport>,
840 ) -> Result<BTreeMap<TabletId, TabletGroup>, RuntimeError> {
841 let mut groups = BTreeMap::new();
842 let context = TabletOpenContext {
843 node_data: &config.node_data,
844 security: &config.security,
845 timing: config.timing,
846 identity,
847 peers,
848 };
849 for (tablet_id, raft_group_id) in scan_tablet_layouts(&config.node_data)? {
850 let layout = TabletLayout::new(config.node_data.clone(), tablet_id, raft_group_id);
851 let descriptor = layout.validate()?;
852 let group =
853 Self::open_tablet_group(&context, transport.clone(), &layout, &descriptor).await?;
854 groups.insert(tablet_id, group);
855 }
856 Ok(groups)
857 }
858
859 async fn open_tablet_group(
864 context: &TabletOpenContext<'_>,
865 transport: Arc<TcpTransport>,
866 layout: &TabletLayout,
867 descriptor: &TabletDescriptor,
868 ) -> Result<TabletGroup, RuntimeError> {
869 let replica = *descriptor
870 .replica_on(context.identity.node_id)
871 .ok_or_else(|| {
872 RuntimeError::InvalidRequest(format!(
873 "tablet {} descriptor does not list this node ({}) as a replica",
874 descriptor.tablet_id, context.identity.node_id
875 ))
876 })?;
877 if transport.registry().get(replica.raft_node_id).is_some() {
878 return Err(RuntimeError::InvalidRequest(format!(
879 "raft id {} is already attached to this node's transport registry",
880 replica.raft_node_id
881 )));
882 }
883 for other in &descriptor.replicas {
886 let Some(address) = context.peers.get(&other.node_id) else {
887 return Err(RuntimeError::InvalidRequest(format!(
888 "no membership-directory address for replica node {} of tablet {}",
889 other.node_id, descriptor.tablet_id
890 )));
891 };
892 transport.upsert_peer(
893 other.raft_node_id,
894 peer_endpoint(context.security, other.node_id, address),
895 );
896 }
897 let ownership = TabletOwnershipRegistry::global().try_reserve(layout)?;
898 let database_id = resolve_tablet_database_id(descriptor);
899 let engine_config = EngineGroupConfig::new(
900 context.node_data.to_path_buf(),
901 descriptor.raft_group_id,
902 context.identity.cluster_id,
903 context.identity.node_id,
904 database_id,
905 );
906 let sink = open_engine_sink(&engine_config)?;
907 {
908 let mut engine = sink
909 .lock()
910 .map_err(|_| RuntimeError::InvalidRequest("engine sink lock poisoned".into()))?;
911 engine.initialize_tablet_keyspace()?;
912 let legacy_path = engine_config
913 .group_dir()
914 .join("raft")
915 .join("state")
916 .join(TABLET_LEDGER_FILENAME);
917 if legacy_path.is_file() {
918 let bytes = std::fs::read(&legacy_path)?;
919 engine.migrate_legacy_tablet_ledger(
920 &bytes,
921 active_snapshot_timestamp(context.node_data, descriptor.tablet_id)?,
922 )?;
923 std::fs::remove_file(&legacy_path)?;
924 if let Some(parent) = legacy_path.parent() {
925 std::fs::File::open(parent)?.sync_all()?;
926 }
927 }
928 engine.finish_legacy_tablet_migration()?;
929 }
930 let mut group_config = GroupConfig::new(
931 tablet_group_name(descriptor.tablet_id),
932 replica.raft_node_id,
933 engine_config.group_dir(),
934 );
935 group_config.storage = engine_config.storage.clone();
936 group_config.idempotency_retention = engine_config.idempotency_retention;
937 if let Some(timing) = &context.timing {
938 timing.apply(&mut group_config);
939 }
940 let dyn_sink: Arc<Mutex<dyn ApplySink>> = sink.clone();
941 let group = Arc::new(ConsensusGroup::create(group_config, transport, dyn_sink).await?);
942 let commit_log: Arc<dyn CommitLog> = Arc::new(RaftCommitLog::new(group.clone()));
943 sink.lock()
944 .map_err(|_| RuntimeError::InvalidRequest("engine sink lock poisoned".into()))?
945 .bind_commit_log(commit_log)?;
946 Ok(TabletGroup {
947 group,
948 descriptor: descriptor.clone(),
949 sink,
950 _ownership: ownership,
951 })
952 }
953
954 pub fn identity(&self) -> &NodeIdentity {
956 &self.identity
957 }
958
959 pub fn reload_trust(
963 &mut self,
964 trust: crate::bootstrap::TrustConfig,
965 ) -> Result<(), RuntimeError> {
966 let tls = crate::network::TlsConfig::from_trust(&trust)
967 .map_err(|e| RuntimeError::InvalidRequest(format!("reload trust: {e}")))?;
968 let security = crate::network::TransportSecurity::Mtls(tls);
969 self.transport.reload_security(security.clone());
970 self.security = security;
971 Ok(())
972 }
973
974 pub fn rpc_address(&self) -> &str {
976 &self.rpc_address
977 }
978
979 pub fn attach_internal_rpc_handler(
984 &self,
985 service_id: u32,
986 handler: Arc<dyn InternalRpcHandler>,
987 ) {
988 self.transport.registry().attach_internal(
989 raft_node_id(&self.identity.node_id),
990 service_id,
991 handler,
992 );
993 }
994
995 pub async fn internal_rpc(
998 &self,
999 target: NodeId,
1000 service_id: u32,
1001 body: Vec<u8>,
1002 ) -> Result<Vec<u8>, RuntimeError> {
1003 self.transport
1004 .internal_rpc(raft_node_id(&target), service_id, body)
1005 .await
1006 .map_err(Into::into)
1007 }
1008
1009 pub fn internal_rpc_client(&self) -> NodeInternalRpcClient {
1011 NodeInternalRpcClient {
1012 transport: Arc::clone(&self.transport),
1013 }
1014 }
1015
1016 pub fn meta_group(&self) -> Option<&MetaGroup<TcpTransport>> {
1018 self.meta.as_deref()
1019 }
1020
1021 pub fn tablet_group(&self, tablet_id: TabletId) -> Option<&ConsensusGroup<TcpTransport>> {
1023 self.tablets.get(&tablet_id).map(|tablet| &*tablet.group)
1024 }
1025
1026 fn tablet_sink(&self, tablet_id: TabletId) -> Option<Arc<Mutex<EngineApplySink>>> {
1028 self.tablets
1029 .get(&tablet_id)
1030 .map(|tablet| tablet.sink.clone())
1031 }
1032
1033 pub fn tablet_descriptor(&self, tablet_id: TabletId) -> Option<&TabletDescriptor> {
1035 self.tablets
1036 .get(&tablet_id)
1037 .map(|tablet| &tablet.descriptor)
1038 }
1039
1040 pub fn tablet_ids(&self) -> Vec<TabletId> {
1042 self.tablets.keys().copied().collect()
1043 }
1044
1045 fn hosted_tablet(&self, tablet_id: TabletId) -> Result<&TabletGroup, RuntimeError> {
1048 self.tablets.get(&tablet_id).ok_or_else(|| {
1049 RuntimeError::InvalidRequest(format!("this node hosts no tablet {tablet_id}"))
1050 })
1051 }
1052
1053 pub async fn create_tablet(
1070 &mut self,
1071 descriptor: &TabletDescriptor,
1072 partitioning: &TablePartitioningRecord,
1073 bootstrap_voters: Option<&[(NodeId, String)]>,
1074 publish_to_meta: bool,
1075 control: &ExecutionControl,
1076 ) -> Result<(), RuntimeError> {
1077 descriptor.validate()?;
1078 partitioning.validate()?;
1079 if partitioning.table_id != descriptor.table_id {
1080 return Err(RuntimeError::InvalidRequest(format!(
1081 "partitioning record names table {} but the descriptor partitions table {}",
1082 partitioning.table_id, descriptor.table_id
1083 )));
1084 }
1085 self.create_hosted_replica(descriptor, bootstrap_voters)
1086 .await?;
1087 if publish_to_meta {
1088 self.publish_tablet_descriptor(descriptor, control).await?;
1089 }
1090 Ok(())
1091 }
1092
1093 async fn create_hosted_replica(
1101 &mut self,
1102 descriptor: &TabletDescriptor,
1103 bootstrap_voters: Option<&[(NodeId, String)]>,
1104 ) -> Result<(), RuntimeError> {
1105 descriptor.validate()?;
1106 if let Some(existing) = self.tablets.get(&descriptor.tablet_id) {
1107 if existing.descriptor == *descriptor {
1108 return Ok(());
1109 }
1110 return Err(RuntimeError::InvalidRequest(format!(
1111 "tablet {} is already open with a different descriptor",
1112 descriptor.tablet_id
1113 )));
1114 }
1115 if descriptor.replica_on(self.identity.node_id).is_none() {
1118 return Err(RuntimeError::InvalidRequest(format!(
1119 "tablet {} descriptor does not list this node ({}) as a replica",
1120 descriptor.tablet_id, self.identity.node_id
1121 )));
1122 }
1123 let layout = TabletLayout::new(
1124 self.node_data.clone(),
1125 descriptor.tablet_id,
1126 descriptor.raft_group_id,
1127 );
1128 layout.create(descriptor)?;
1129 let context = TabletOpenContext {
1130 node_data: &self.node_data,
1131 security: &self.security,
1132 timing: self.timing,
1133 identity: &self.identity,
1134 peers: &self.peers,
1135 };
1136 let group =
1137 Self::open_tablet_group(&context, self.transport.clone(), &layout, descriptor).await?;
1138 if let Some(voters) = bootstrap_voters {
1139 let mut members = BTreeMap::new();
1140 for (node_id, address) in voters {
1141 let Some(replica) = descriptor.replica_on(*node_id) else {
1142 return Err(RuntimeError::InvalidRequest(format!(
1143 "bootstrap voter {node_id} is not a replica of tablet {}",
1144 descriptor.tablet_id
1145 )));
1146 };
1147 members.insert(replica.raft_node_id, BasicNode::new(address.clone()));
1148 }
1149 if !members.contains_key(&group.group.node_id()) {
1150 return Err(RuntimeError::InvalidRequest(
1151 "tablet bootstrap voter set does not include this node".to_owned(),
1152 ));
1153 }
1154 if !group.group.is_initialized().await? {
1155 group.group.bootstrap(members).await?;
1156 }
1157 }
1158 self.tablets.insert(descriptor.tablet_id, group);
1159 Ok(())
1160 }
1161
1162 pub async fn add_tablet_replica(
1172 &self,
1173 tablet_id: TabletId,
1174 replica: ReplicaDescriptor,
1175 address: &str,
1176 ) -> Result<(), RuntimeError> {
1177 let tablet = self.hosted_tablet(tablet_id)?;
1178 if tablet.descriptor.replicas.iter().any(|existing| {
1179 existing.node_id == replica.node_id || existing.raft_node_id == replica.raft_node_id
1180 }) {
1181 return Err(RuntimeError::InvalidRequest(format!(
1182 "replica node {} / raft id {} already belongs to tablet {}",
1183 replica.node_id, replica.raft_node_id, tablet_id
1184 )));
1185 }
1186 self.transport.upsert_peer(
1187 replica.raft_node_id,
1188 peer_endpoint(&self.security, replica.node_id, address),
1189 );
1190 for _ in 0..10 {
1197 tablet
1198 .group
1199 .wait_uniform_membership(Duration::from_secs(30))
1200 .await?;
1201 let (voters, learners) = tablet.group.members();
1202 if voters.contains(&replica.raft_node_id) {
1203 return Ok(());
1204 }
1205 if !learners.contains(&replica.raft_node_id) {
1206 match tablet
1210 .group
1211 .add_learner(replica.raft_node_id, BasicNode::new(address.to_owned()))
1212 .await
1213 {
1214 Ok(()) => {}
1215 Err(ConsensusError::MembershipInProgress) => continue,
1216 Err(error) => return Err(error.into()),
1217 }
1218 }
1219 match tablet.group.promote(replica.raft_node_id).await {
1220 Ok(()) => return Ok(()),
1221 Err(ConsensusError::MembershipInProgress) => continue,
1222 Err(error) => return Err(error.into()),
1223 }
1224 }
1225 Err(RuntimeError::InvalidRequest(format!(
1226 "membership change for tablet {tablet_id} did not settle"
1227 )))
1228 }
1229
1230 pub async fn publish_tablet_descriptor(
1235 &self,
1236 descriptor: &TabletDescriptor,
1237 control: &ExecutionControl,
1238 ) -> Result<MetadataVersion, RuntimeError> {
1239 let meta = self.meta.as_ref().ok_or_else(|| {
1240 RuntimeError::InvalidRequest("this node hosts no meta group".to_owned())
1241 })?;
1242 let receipt = meta
1243 .propose(
1244 crate::meta::new_command_id()?,
1245 MetaCommand::SetTabletDescriptor {
1246 descriptor: descriptor.clone(),
1247 },
1248 control,
1249 )
1250 .await?;
1251 Ok(receipt.metadata_version)
1252 }
1253
1254 fn meta_group_or_err(&self) -> Result<Arc<MetaGroup<TcpTransport>>, RuntimeError> {
1262 self.meta
1263 .clone()
1264 .ok_or_else(|| RuntimeError::InvalidRequest("this node hosts no meta group".to_owned()))
1265 }
1266
1267 async fn plan_split(
1272 &self,
1273 tablet_id: TabletId,
1274 split_key: Option<Key>,
1275 control: &ExecutionControl,
1276 ) -> Result<SplitPlan, RuntimeError> {
1277 let meta = self.meta_group_or_err()?;
1278 let source = meta.state().tablet(tablet_id).cloned().ok_or_else(|| {
1279 RuntimeError::InvalidRequest(format!("tablet {tablet_id} is not in the meta state"))
1280 })?;
1281 if source.state != TabletState::Active {
1282 return Err(RuntimeError::InvalidRequest(format!(
1283 "tablet {tablet_id} is in state {}, expected Active",
1284 source.state
1285 )));
1286 }
1287 if source.replica_on(self.identity.node_id).is_none() {
1288 return Err(RuntimeError::InvalidRequest(format!(
1289 "this node hosts no replica of tablet {tablet_id}"
1290 )));
1291 }
1292 let replica_count = source.replicas.len();
1293 let raft_ids = meta
1294 .allocate_raft_node_ids(
1295 2 * u32::try_from(replica_count).unwrap_or(u32::MAX),
1296 control,
1297 )
1298 .await?;
1299 let allocation = |ids: &[RaftNodeId]| ChildAllocation {
1300 tablet_id: TabletId::new_random(),
1301 raft_group_id: RaftGroupId::new_random(),
1302 replicas: source
1303 .replicas
1304 .iter()
1305 .zip(ids)
1306 .map(|(replica, raft_node_id)| ReplicaDescriptor {
1307 node_id: replica.node_id,
1308 role: replica.role,
1309 raft_node_id: *raft_node_id,
1310 })
1311 .collect(),
1312 };
1313 let selection = match split_key {
1314 Some(key) => SplitKeySelection::Explicit(key),
1315 None => SplitKeySelection::Midpoint,
1316 };
1317 let plan = TabletSplitPlanner::new(self.node_data.clone()).plan(
1318 &source,
1319 selection,
1320 now_timestamp(),
1321 [
1322 allocation(&raft_ids[..replica_count]),
1323 allocation(&raft_ids[replica_count..]),
1324 ],
1325 )?;
1326 Ok(plan)
1327 }
1328
1329 async fn child_sinks(
1335 &mut self,
1336 children: &[TabletDescriptor; 2],
1337 control: &ExecutionControl,
1338 ) -> Result<[RuntimeChildSink; 2], RuntimeError> {
1339 for child in children {
1340 if child.replica_on(self.identity.node_id).is_some()
1343 && !self.tablets.contains_key(&child.tablet_id)
1344 {
1345 let voters: Vec<(NodeId, String)> = child
1346 .replicas
1347 .iter()
1348 .filter_map(|replica| {
1349 self.peers
1350 .get(&replica.node_id)
1351 .map(|address| (replica.node_id, address.clone()))
1352 })
1353 .collect();
1354 self.create_hosted_replica(child, Some(voters.as_slice()))
1355 .await?;
1356 }
1357 }
1358 let mut sinks = Vec::with_capacity(2);
1359 for child in children {
1360 let group = self
1361 .tablets
1362 .get(&child.tablet_id)
1363 .ok_or_else(|| {
1364 RuntimeError::InvalidRequest(format!(
1365 "child tablet {} is not hosted on this node",
1366 child.tablet_id
1367 ))
1368 })?
1369 .group
1370 .clone();
1371 sinks.push(RuntimeChildSink {
1372 group,
1373 handle: tokio::runtime::Handle::current(),
1374 control: control.clone(),
1375 staged: None,
1376 });
1377 }
1378 let [lower, upper]: [RuntimeChildSink; 2] = sinks.try_into().map_err(|_| {
1379 RuntimeError::InvalidRequest("expected exactly two child sinks".to_owned())
1380 })?;
1381 Ok([lower, upper])
1382 }
1383
1384 async fn drop_hosted_tablet(&mut self, tablet_id: TabletId) -> Result<(), RuntimeError> {
1387 if let Some(tablet) = self.tablets.remove(&tablet_id) {
1388 tablet.group.shutdown().await?;
1389 }
1390 Ok(())
1391 }
1392
1393 fn refresh_local_descriptor(&mut self, layout: &TabletLayout) -> Result<(), RuntimeError> {
1397 let descriptor = layout.load_metadata()?;
1398 if let Some(tablet) = self.tablets.get_mut(&layout.tablet_id()) {
1399 if descriptor.generation > tablet.descriptor.generation {
1400 tablet.descriptor = descriptor;
1401 }
1402 }
1403 Ok(())
1404 }
1405
1406 pub async fn split_step(
1419 &mut self,
1420 tablet_id: TabletId,
1421 split_key: Option<Key>,
1422 control: &ExecutionControl,
1423 ) -> Result<(SplitPhase, Option<SplitPublishCommand>), RuntimeError> {
1424 let meta = self.meta_group_or_err()?;
1425 let raft_group_id = match self.tablets.get(&tablet_id) {
1426 Some(tablet) => tablet.descriptor.raft_group_id,
1427 None => meta
1431 .state()
1432 .tablet(tablet_id)
1433 .map(|descriptor| descriptor.raft_group_id)
1434 .ok_or_else(|| {
1435 RuntimeError::InvalidRequest(format!("this node hosts no tablet {tablet_id}"))
1436 })?,
1437 };
1438 let source_layout = TabletLayout::new(self.node_data.clone(), tablet_id, raft_group_id);
1439 let progress = split_progress(&source_layout)?;
1440 let hosted = self.tablets.contains_key(&tablet_id);
1441 if !hosted
1442 && progress
1443 .as_ref()
1444 .is_none_or(|record| record.phase < SplitPhase::Published)
1445 {
1446 return Err(RuntimeError::InvalidRequest(format!(
1447 "this node hosts no tablet {tablet_id}"
1448 )));
1449 }
1450 let keyspace = RuntimeKeyspace {
1451 sink: if progress
1452 .as_ref()
1453 .is_some_and(|record| record.phase >= SplitPhase::Published)
1454 {
1455 None
1456 } else {
1457 Some(self.tablet_sink(tablet_id).ok_or_else(|| {
1458 RuntimeError::InvalidRequest(format!("this node hosts no tablet {tablet_id}"))
1459 })?)
1460 },
1461 pinned: None,
1462 };
1463 let plane = RuntimeMetaPlane {
1464 meta: meta.clone(),
1465 handle: tokio::runtime::Handle::current(),
1466 control: control.clone(),
1467 };
1468 let mut executor = match progress {
1469 Some(progress) => {
1470 let children = progress.plan().child_descriptors();
1471 let sinks = self.child_sinks(&children, control).await?;
1472 SplitExecutor::resume(source_layout.clone(), plane, keyspace, sinks)?.ok_or_else(
1473 || {
1474 RuntimeError::InvalidRequest(format!(
1475 "split progress of tablet {tablet_id} vanished mid-resume"
1476 ))
1477 },
1478 )?
1479 }
1480 None => {
1481 let plan = self.plan_split(tablet_id, split_key, control).await?;
1482 let sinks = self.child_sinks(&plan.child_descriptors(), control).await?;
1483 SplitExecutor::begin(plan, source_layout.clone(), plane, keyspace, sinks)?
1484 }
1485 };
1486 if executor.phase() == SplitPhase::Published {
1489 if let Some(sink) = self.tablet_sink(tablet_id) {
1490 sink.lock()
1491 .map_err(|_| RuntimeError::InvalidRequest("engine sink lock poisoned".into()))?
1492 .release_tablet_snapshot()?;
1493 }
1494 self.drop_hosted_tablet(tablet_id).await?;
1495 }
1496 let phase = tokio::task::spawn_blocking(move || executor.step())
1497 .await
1498 .map_err(|error| {
1499 RuntimeError::InvalidRequest(format!("split driver task failed: {error}"))
1500 })??;
1501 if phase == SplitPhase::Published {
1502 if let Some(sink) = self.tablet_sink(tablet_id) {
1503 sink.lock()
1504 .map_err(|_| RuntimeError::InvalidRequest("engine sink lock poisoned".into()))?
1505 .release_tablet_snapshot()?;
1506 }
1507 }
1508 if matches!(phase, SplitPhase::MarkedSplitting | SplitPhase::Published) {
1509 self.refresh_local_descriptor(&source_layout)?;
1510 }
1511 let published = if phase >= SplitPhase::Published {
1512 match split_progress(&source_layout)? {
1517 Some(record) => Some(SplitPublishCommand::from_plan(&record.plan())?),
1518 None => None,
1519 }
1520 } else {
1521 None
1522 };
1523 Ok((phase, published))
1524 }
1525
1526 pub async fn split_tablet(
1532 &mut self,
1533 tablet_id: TabletId,
1534 split_key: Option<Key>,
1535 control: &ExecutionControl,
1536 ) -> Result<SplitPublishCommand, RuntimeError> {
1537 let mut published = None;
1538 loop {
1539 let (phase, command) = self
1540 .split_step(tablet_id, split_key.clone(), control)
1541 .await?;
1542 if command.is_some() {
1543 published = command;
1544 }
1545 if phase == SplitPhase::SourceRetired {
1546 break;
1547 }
1548 }
1549 published.ok_or_else(|| {
1550 RuntimeError::InvalidRequest(format!(
1551 "split of tablet {tablet_id} completed without a publication"
1552 ))
1553 })
1554 }
1555
1556 pub async fn abort_split(
1562 &mut self,
1563 tablet_id: TabletId,
1564 control: &ExecutionControl,
1565 ) -> Result<SplitAbortReport, RuntimeError> {
1566 let meta = self.meta_group_or_err()?;
1567 let source_layout = {
1568 let source = self.hosted_tablet(tablet_id)?;
1569 TabletLayout::new(
1570 self.node_data.clone(),
1571 tablet_id,
1572 source.descriptor.raft_group_id,
1573 )
1574 };
1575 if let Some(progress) = split_progress(&source_layout)? {
1578 if progress.phase >= SplitPhase::Published {
1579 return Err(SplitError::CannotAbort {
1580 tablet: tablet_id,
1581 phase: progress.phase,
1582 }
1583 .into());
1584 }
1585 for child in &progress.children {
1586 self.drop_hosted_tablet(child.tablet_id).await?;
1587 }
1588 }
1589 let mut plane = RuntimeMetaPlane {
1590 meta,
1591 handle: tokio::runtime::Handle::current(),
1592 control: control.clone(),
1593 };
1594 let layout = source_layout.clone();
1595 let report = tokio::task::spawn_blocking(move || abort_split(&layout, &mut plane))
1596 .await
1597 .map_err(|error| {
1598 RuntimeError::InvalidRequest(format!("split abort task failed: {error}"))
1599 })??;
1600 if let Some(sink) = self.tablet_sink(tablet_id) {
1601 sink.lock()
1602 .map_err(|_| RuntimeError::InvalidRequest("engine sink lock poisoned".into()))?
1603 .release_tablet_snapshot()?;
1604 }
1605 self.refresh_local_descriptor(&source_layout)?;
1608 Ok(report)
1609 }
1610
1611 pub async fn merge_step(
1619 &mut self,
1620 first: TabletId,
1621 second: TabletId,
1622 control: &ExecutionControl,
1623 ) -> Result<(MergePhase, Option<MergePublishCommand>), RuntimeError> {
1624 let meta = self.meta_group_or_err()?;
1625 let layout_of = |tablet_id: TabletId| -> Result<TabletLayout, RuntimeError> {
1628 let raft_group_id = match self.tablets.get(&tablet_id) {
1629 Some(tablet) => tablet.descriptor.raft_group_id,
1630 None => meta
1631 .state()
1632 .tablet(tablet_id)
1633 .map(|descriptor| descriptor.raft_group_id)
1634 .ok_or_else(|| {
1635 RuntimeError::InvalidRequest(format!(
1636 "this node hosts no tablet {tablet_id}"
1637 ))
1638 })?,
1639 };
1640 Ok(TabletLayout::new(
1641 self.node_data.clone(),
1642 tablet_id,
1643 raft_group_id,
1644 ))
1645 };
1646 let first_layout = layout_of(first)?;
1647 let second_layout = layout_of(second)?;
1648 let phase_in_flight = match (
1653 merge_progress(&first_layout)?,
1654 merge_progress(&second_layout)?,
1655 ) {
1656 (Some(progress), None) => Some(progress.phase),
1657 (None, None) => None,
1658 _ => {
1659 return Err(RuntimeError::InvalidRequest(
1660 "merge progress records on both sources: corrupt state".to_owned(),
1661 ));
1662 }
1663 };
1664 let retired_only = matches!(
1665 phase_in_flight,
1666 Some(MergePhase::Published | MergePhase::SourcesRetired)
1667 );
1668 if !retired_only
1669 && (!self.tablets.contains_key(&first) || !self.tablets.contains_key(&second))
1670 {
1671 return Err(RuntimeError::InvalidRequest(format!(
1672 "this node must host both merge sources ({first}, {second})"
1673 )));
1674 }
1675 let keyspace_of = |layout: &TabletLayout| -> Result<RuntimeKeyspace, RuntimeError> {
1676 Ok(RuntimeKeyspace {
1677 sink: if retired_only {
1678 None
1679 } else {
1680 Some(self.tablet_sink(layout.tablet_id()).ok_or_else(|| {
1681 RuntimeError::InvalidRequest(format!(
1682 "this node hosts no tablet {}",
1683 layout.tablet_id()
1684 ))
1685 })?)
1686 },
1687 pinned: None,
1688 })
1689 };
1690 let (first_keyspace, second_keyspace) =
1691 (keyspace_of(&first_layout)?, keyspace_of(&second_layout)?);
1692 let ordered_layouts = |lower_first: bool| {
1694 if lower_first {
1695 [first_layout.clone(), second_layout.clone()]
1696 } else {
1697 [second_layout.clone(), first_layout.clone()]
1698 }
1699 };
1700 let plane = RuntimeMetaPlane {
1701 meta: meta.clone(),
1702 handle: tokio::runtime::Handle::current(),
1703 control: control.clone(),
1704 };
1705 let mut executor = match phase_in_flight {
1706 Some(_) => {
1707 let progress = merge_progress(&first_layout)?
1708 .or(merge_progress(&second_layout)?)
1709 .expect("phase_in_flight is Some");
1710 let lower_first = progress.sources[0].tablet_id == first;
1711 let sink = self.replacement_sink(&progress.plan(), control).await?;
1712 let keyspaces = if lower_first {
1713 [first_keyspace, second_keyspace]
1714 } else {
1715 [second_keyspace, first_keyspace]
1716 };
1717 MergeExecutor::resume(ordered_layouts(lower_first), plane, keyspaces, sink)?
1718 .ok_or_else(|| {
1719 RuntimeError::InvalidRequest(
1720 "merge progress vanished mid-resume".to_owned(),
1721 )
1722 })?
1723 }
1724 None => {
1725 let plan = self.plan_merge(first, second, control).await?;
1726 let lower_first = plan.sources[0].tablet_id == first;
1727 let sink = self.replacement_sink(&plan, control).await?;
1728 let keyspaces = if lower_first {
1729 [first_keyspace, second_keyspace]
1730 } else {
1731 [second_keyspace, first_keyspace]
1732 };
1733 MergeExecutor::begin(plan, ordered_layouts(lower_first), plane, keyspaces, sink)?
1734 }
1735 };
1736 if executor.phase() == MergePhase::Published {
1739 for tablet_id in [first, second] {
1740 if let Some(sink) = self.tablet_sink(tablet_id) {
1741 sink.lock()
1742 .map_err(|_| {
1743 RuntimeError::InvalidRequest("engine sink lock poisoned".into())
1744 })?
1745 .release_tablet_snapshot()?;
1746 }
1747 }
1748 self.drop_hosted_tablet(first).await?;
1749 self.drop_hosted_tablet(second).await?;
1750 }
1751 let phase = tokio::task::spawn_blocking(move || executor.step())
1752 .await
1753 .map_err(|error| {
1754 RuntimeError::InvalidRequest(format!("merge driver task failed: {error}"))
1755 })??;
1756 if phase == MergePhase::Published {
1757 for tablet_id in [first, second] {
1758 if let Some(sink) = self.tablet_sink(tablet_id) {
1759 sink.lock()
1760 .map_err(|_| {
1761 RuntimeError::InvalidRequest("engine sink lock poisoned".into())
1762 })?
1763 .release_tablet_snapshot()?;
1764 }
1765 }
1766 }
1767 if matches!(phase, MergePhase::MarkedMerging | MergePhase::Published) {
1768 self.refresh_local_descriptor(&first_layout)?;
1769 self.refresh_local_descriptor(&second_layout)?;
1770 }
1771 let published = if phase >= MergePhase::Published {
1772 let progress = merge_progress(&first_layout)?.or(merge_progress(&second_layout)?);
1773 match progress {
1774 Some(record) => Some(MergePublishCommand::from_plan(&record.plan())?),
1775 None => None,
1776 }
1777 } else {
1778 None
1779 };
1780 Ok((phase, published))
1781 }
1782
1783 pub async fn merge_tablets(
1787 &mut self,
1788 first: TabletId,
1789 second: TabletId,
1790 control: &ExecutionControl,
1791 ) -> Result<MergePublishCommand, RuntimeError> {
1792 let mut published = None;
1793 loop {
1794 let (phase, command) = self.merge_step(first, second, control).await?;
1795 if command.is_some() {
1796 published = command;
1797 }
1798 if phase == MergePhase::SourcesRetired {
1799 break;
1800 }
1801 }
1802 published.ok_or_else(|| {
1803 RuntimeError::InvalidRequest("merge completed without a publication".to_owned())
1804 })
1805 }
1806
1807 async fn plan_merge(
1813 &self,
1814 first: TabletId,
1815 second: TabletId,
1816 control: &ExecutionControl,
1817 ) -> Result<MergePlan, RuntimeError> {
1818 let meta = self.meta_group_or_err()?;
1819 let state = meta.state();
1820 let descriptor_of = |tablet_id: TabletId| -> Result<TabletDescriptor, RuntimeError> {
1821 state.tablet(tablet_id).cloned().ok_or_else(|| {
1822 RuntimeError::InvalidRequest(format!("tablet {tablet_id} is not in the meta state"))
1823 })
1824 };
1825 let (first_desc, second_desc) = (descriptor_of(first)?, descriptor_of(second)?);
1826 let schema = state.table(first_desc.table_id).ok_or_else(|| {
1827 RuntimeError::InvalidRequest(format!(
1828 "table {} of tablet {first} is not in the meta state",
1829 first_desc.table_id
1830 ))
1831 })?;
1832 let active_schema_job = state
1833 .schema_jobs
1834 .values()
1835 .find(|job| job.table_id == first_desc.table_id && !job.state.is_terminal())
1836 .map(|job| job.job_id);
1837 let size_of = |tablet_id: TabletId| -> Result<u64, RuntimeError> {
1838 self.tablet_sink(tablet_id)
1839 .ok_or_else(|| {
1840 RuntimeError::InvalidRequest(format!("this node hosts no tablet {tablet_id}"))
1841 })?
1842 .lock()
1843 .map_err(|_| RuntimeError::InvalidRequest("engine sink lock poisoned".into()))?
1844 .tablet_size_bytes()
1845 .map_err(RuntimeError::from)
1846 };
1847 let replica_count = first_desc.replicas.len();
1848 let raft_ids = meta
1849 .allocate_raft_node_ids(u32::try_from(replica_count).unwrap_or(u32::MAX), control)
1850 .await?;
1851 let allocation = ChildAllocation {
1852 tablet_id: TabletId::new_random(),
1853 raft_group_id: RaftGroupId::new_random(),
1854 replicas: first_desc
1855 .replicas
1856 .iter()
1857 .zip(&raft_ids)
1858 .map(|(replica, raft_node_id)| ReplicaDescriptor {
1859 node_id: replica.node_id,
1860 role: replica.role,
1861 raft_node_id: *raft_node_id,
1862 })
1863 .collect(),
1864 };
1865 let plan = MergePlanner::new(self.node_data.clone()).plan(
1866 MergeInputs {
1867 first: first_desc,
1868 second: second_desc,
1869 first_schema: schema.schema_version,
1870 second_schema: schema.schema_version,
1871 active_schema_job,
1872 first_size_bytes: size_of(first)?,
1873 second_size_bytes: size_of(second)?,
1874 max_merged_size_bytes: DEFAULT_MAX_MERGED_SIZE_BYTES,
1875 },
1876 now_timestamp(),
1877 allocation,
1878 )?;
1879 Ok(plan)
1880 }
1881
1882 async fn replacement_sink(
1886 &mut self,
1887 plan: &MergePlan,
1888 control: &ExecutionControl,
1889 ) -> Result<RuntimeChildSink, RuntimeError> {
1890 let descriptor = plan.replacement_descriptor();
1891 if descriptor.replica_on(self.identity.node_id).is_some()
1892 && !self.tablets.contains_key(&descriptor.tablet_id)
1893 {
1894 let voters: Vec<(NodeId, String)> = descriptor
1895 .replicas
1896 .iter()
1897 .filter_map(|replica| {
1898 self.peers
1899 .get(&replica.node_id)
1900 .map(|address| (replica.node_id, address.clone()))
1901 })
1902 .collect();
1903 self.create_hosted_replica(&descriptor, Some(voters.as_slice()))
1904 .await?;
1905 }
1906 let group = self
1907 .tablets
1908 .get(&descriptor.tablet_id)
1909 .ok_or_else(|| {
1910 RuntimeError::InvalidRequest(format!(
1911 "replacement tablet {} is not hosted on this node",
1912 descriptor.tablet_id
1913 ))
1914 })?
1915 .group
1916 .clone();
1917 Ok(RuntimeChildSink {
1918 group,
1919 handle: tokio::runtime::Handle::current(),
1920 control: control.clone(),
1921 staged: None,
1922 })
1923 }
1924
1925 pub async fn sync_hosted_tablets(
1935 &mut self,
1936 _control: &ExecutionControl,
1937 ) -> Result<HostedSyncReport, RuntimeError> {
1938 let meta = self.meta_group_or_err()?;
1939 let state = meta.state();
1940 let node_id = self.identity.node_id;
1941 let mut report = HostedSyncReport::default();
1942 for record in state.tablets.values() {
1944 let descriptor = &record.descriptor;
1945 if self.tablets.contains_key(&descriptor.tablet_id)
1946 || descriptor.replica_on(node_id).is_none()
1947 {
1948 continue;
1949 }
1950 self.create_hosted_replica(descriptor, None).await?;
1951 report.created.push(descriptor.tablet_id);
1952 }
1953 for record in state.tablets.values() {
1956 let published = &record.descriptor;
1957 let Some(tablet) = self.tablets.get(&published.tablet_id) else {
1958 continue;
1959 };
1960 if published.generation <= tablet.descriptor.generation {
1961 continue;
1962 }
1963 if published.raft_group_id != tablet.descriptor.raft_group_id {
1964 return Err(RuntimeError::InvalidRequest(format!(
1965 "meta descriptor for tablet {} names a different raft group than the \
1966 hosted replica",
1967 published.tablet_id
1968 )));
1969 }
1970 if published.replica_on(node_id).is_none() {
1971 continue; }
1973 let layout = TabletLayout::new(
1974 self.node_data.clone(),
1975 published.tablet_id,
1976 published.raft_group_id,
1977 );
1978 layout.store_metadata(published)?;
1979 self.tablets
1980 .get_mut(&published.tablet_id)
1981 .expect("checked above")
1982 .descriptor = published.clone();
1983 report.refreshed.push(published.tablet_id);
1984 }
1985 let outgoing: Vec<TabletId> = self
1988 .tablets
1989 .keys()
1990 .filter(|tablet_id| {
1991 state
1992 .tablets
1993 .get(tablet_id)
1994 .is_none_or(|record| record.descriptor.replica_on(node_id).is_none())
1995 })
1996 .copied()
1997 .collect();
1998 for tablet_id in outgoing {
1999 let Some(tablet) = self.tablets.remove(&tablet_id) else {
2000 continue;
2001 };
2002 let layout = TabletLayout::new(
2003 self.node_data.clone(),
2004 tablet_id,
2005 tablet.descriptor.raft_group_id,
2006 );
2007 tablet.group.shutdown().await?;
2008 drop(tablet);
2009 layout.teardown()?;
2010 report.torn_down.push(tablet_id);
2011 }
2012 Ok(report)
2013 }
2014
2015 pub async fn write_tablet_rows(
2021 &self,
2022 tablet_id: TabletId,
2023 entries: &[(Key, Vec<u8>)],
2024 control: &ExecutionControl,
2025 ) -> Result<GroupCommitReceipt, RuntimeError> {
2026 let tablet = self.hosted_tablet(tablet_id)?;
2027 let envelope = CommandEnvelope::new(
2028 COMMAND_TYPE_TABLET_DATA,
2029 new_data_command_id()?,
2030 TabletDataCommandRecord::new(TabletDataCommand::Upsert {
2031 entries: entries
2032 .iter()
2033 .map(|(key, value)| (key.as_bytes().to_vec(), value.clone()))
2034 .collect(),
2035 })
2036 .encode(),
2037 );
2038 let receipt = tablet
2039 .group
2040 .propose(CommandKind::Catalog, envelope, control)
2041 .await?;
2042 Ok(receipt)
2043 }
2044
2045 pub fn tablet_rows(&self, tablet_id: TabletId) -> Result<BTreeMap<Key, Vec<u8>>, RuntimeError> {
2048 let sink = self.tablet_sink(tablet_id).ok_or_else(|| {
2049 RuntimeError::InvalidRequest(format!("this node hosts no tablet {tablet_id}"))
2050 })?;
2051 let rows = sink
2052 .lock()
2053 .map_err(|_| RuntimeError::InvalidRequest("engine sink lock poisoned".into()))?
2054 .tablet_rows()?;
2055 Ok(rows
2056 .into_iter()
2057 .map(|(key, value)| (Key::from_bytes(key), value))
2058 .collect())
2059 }
2060
2061 pub fn status(&self) -> RuntimeStatus {
2064 RuntimeStatus {
2065 identity: self.identity.clone(),
2066 rpc_address: self.rpc_address.clone(),
2067 meta: self.meta.as_ref().map(|meta| MetaGroupStatus {
2068 meta_group_id: meta.meta_group_id(),
2069 metadata_version: meta.metadata_version(),
2070 metrics: meta.group().metrics(),
2071 }),
2072 tablets: self
2073 .tablets
2074 .values()
2075 .map(|tablet| TabletGroupStatus {
2076 tablet_id: tablet.descriptor.tablet_id,
2077 raft_group_id: tablet.descriptor.raft_group_id,
2078 state: tablet.descriptor.state,
2079 replicas: tablet.descriptor.replicas.clone(),
2080 applied: tablet.group.applied_position(),
2081 metrics: tablet.group.metrics(),
2082 })
2083 .collect(),
2084 }
2085 }
2086
2087 pub async fn shutdown(mut self) -> Result<(), RuntimeError> {
2091 if let Some(server) = self.server.take() {
2094 server.shutdown().await;
2095 }
2096 let mut first_error: Option<RuntimeError> = None;
2097 for (_, tablet) in std::mem::take(&mut self.tablets) {
2100 if let Err(error) = tablet.group.shutdown().await {
2101 if first_error.is_none() {
2102 first_error = Some(error.into());
2103 }
2104 }
2105 let close_result = tablet
2106 .sink
2107 .lock()
2108 .map_err(|_| RuntimeError::InvalidRequest("engine sink lock poisoned".to_owned()))
2109 .and_then(|mut sink| sink.close().map_err(RuntimeError::from));
2110 if let Err(error) = close_result {
2111 if first_error.is_none() {
2112 first_error = Some(error);
2113 }
2114 }
2115 }
2116 if let Some(meta) = self.meta.take() {
2117 if let Err(error) = meta.shutdown().await {
2118 if first_error.is_none() {
2119 first_error = Some(error.into());
2120 }
2121 }
2122 }
2123 match first_error {
2125 Some(error) => Err(error),
2126 None => Ok(()),
2127 }
2128 }
2129
2130 pub async fn crash(mut self) {
2136 drop(self.server.take());
2138 for (_, tablet) in std::mem::take(&mut self.tablets) {
2139 let TabletGroup { group, sink, .. } = tablet;
2140 match Arc::try_unwrap(group) {
2141 Ok(group) => group.crash().await,
2142 Err(group) => {
2143 let _ = group.shutdown().await;
2144 }
2145 }
2146 if let Ok(mut sink) = sink.lock() {
2147 let _ = sink.crash();
2148 };
2149 }
2150 if let Some(meta) = self.meta.take() {
2151 match Arc::try_unwrap(meta) {
2152 Ok(meta) => meta.crash().await,
2153 Err(meta) => {
2154 let _ = meta.shutdown().await;
2155 }
2156 }
2157 }
2158 }
2159}
2160
2161pub const DEFAULT_MAX_MERGED_SIZE_BYTES: u64 = 64 * 1024 * 1024;
2169
2170fn now_timestamp() -> HlcTimestamp {
2174 let micros = std::time::SystemTime::now()
2175 .duration_since(std::time::UNIX_EPOCH)
2176 .map(|duration| duration.as_micros() as u64)
2177 .unwrap_or(0);
2178 HlcTimestamp {
2179 physical_micros: micros,
2180 logical: 0,
2181 node_tiebreaker: 0,
2182 }
2183}
2184
2185fn new_data_command_id() -> Result<[u8; 16], RuntimeError> {
2187 let mut id = [0u8; 16];
2188 getrandom::getrandom(&mut id)
2189 .map_err(|error| RuntimeError::InvalidRequest(format!("CSPRNG failed: {error}")))?;
2190 Ok(id)
2191}
2192
2193struct RuntimeMetaPlane {
2201 meta: Arc<MetaGroup<TcpTransport>>,
2202 handle: tokio::runtime::Handle,
2203 control: ExecutionControl,
2204}
2205
2206impl RuntimeMetaPlane {
2207 fn propose(&self, command: MetaCommand) -> Result<(), MetaRejectionReason> {
2208 let meta = self.meta.clone();
2209 let control = self.control.clone();
2210 self.handle
2211 .block_on(async move {
2212 meta.propose(crate::meta::new_command_id()?, command, &control)
2213 .await
2214 })
2215 .map(|_| ())
2216 .map_err(|error| match error {
2217 MetaError::Rejected(reason) => reason,
2218 MetaError::Consensus(ConsensusError::NotLeader { leader }) => {
2219 MetaRejectionReason::NotLeader { leader }
2220 }
2221 other => MetaRejectionReason::ProposalFailed {
2222 reason: other.to_string(),
2223 },
2224 })
2225 }
2226}
2227
2228impl TabletMetaPlane for RuntimeMetaPlane {
2229 fn set_tablet(&mut self, descriptor: &TabletDescriptor) -> Result<(), MetaRejectionReason> {
2230 self.propose(MetaCommand::SetTabletDescriptor {
2231 descriptor: descriptor.clone(),
2232 })
2233 }
2234
2235 fn tablet(&self, tablet_id: TabletId) -> Option<TabletDescriptor> {
2236 self.meta.state().tablet(tablet_id).cloned()
2237 }
2238
2239 fn remove_tablet(
2240 &mut self,
2241 tablet_id: TabletId,
2242 generation: u64,
2243 ) -> Result<(), MetaRejectionReason> {
2244 self.propose(MetaCommand::RemoveTabletDescriptor {
2245 tablet_id,
2246 generation,
2247 })
2248 }
2249
2250 fn publish_split(&mut self, command: &SplitPublishCommand) -> Result<(), MetaRejectionReason> {
2251 self.propose(MetaCommand::PublishSplit {
2252 command: command.clone(),
2253 })
2254 }
2255}
2256
2257impl MergeMetaPlane for RuntimeMetaPlane {
2258 fn publish_merge(&mut self, command: &MergePublishCommand) -> Result<(), MetaRejectionReason> {
2259 self.propose(MetaCommand::PublishMerge {
2260 command: command.clone(),
2261 })
2262 }
2263}
2264
2265struct RuntimeKeyspace {
2267 sink: Option<Arc<Mutex<EngineApplySink>>>,
2268 pinned: Option<(HlcTimestamp, u64)>,
2269}
2270
2271struct RuntimeSnapshotPin {
2272 ts: HlcTimestamp,
2273 _engine: EngineTabletPin,
2274}
2275
2276impl SnapshotPin for RuntimeSnapshotPin {
2277 fn pinned_at(&self) -> HlcTimestamp {
2278 self.ts
2279 }
2280}
2281
2282impl TabletKeyspace for RuntimeKeyspace {
2283 fn pin_snapshot(&mut self, ts: HlcTimestamp) -> Result<Box<dyn SnapshotPin>, TabletDataError> {
2284 let pin = self
2285 .sink
2286 .as_ref()
2287 .ok_or_else(|| TabletDataError::Keyspace("tablet engine is not open".to_owned()))?
2288 .lock()
2289 .map_err(|_| TabletDataError::Keyspace("engine sink lock poisoned".to_owned()))?
2290 .pin_tablet_snapshot(ts)
2291 .map_err(|error| TabletDataError::Keyspace(error.to_string()))?;
2292 self.pinned = Some((ts, pin.epoch()));
2293 Ok(Box::new(RuntimeSnapshotPin { ts, _engine: pin }))
2294 }
2295
2296 fn snapshot_at(
2297 &self,
2298 ts: HlcTimestamp,
2299 ) -> Result<crate::split::RecordStream<'_>, TabletDataError> {
2300 let (_, epoch) = self
2301 .pinned
2302 .filter(|(pinned, _)| *pinned == ts)
2303 .ok_or_else(|| {
2304 TabletDataError::Keyspace(format!("tablet snapshot {ts:?} is not pinned"))
2305 })?;
2306 let rows = self
2307 .sink
2308 .as_ref()
2309 .ok_or_else(|| TabletDataError::Keyspace("tablet engine is not open".to_owned()))?
2310 .lock()
2311 .map_err(|_| TabletDataError::Keyspace("engine sink lock poisoned".to_owned()))?
2312 .tablet_rows_at_epoch(epoch)
2313 .map_err(|error| TabletDataError::Keyspace(error.to_string()))?;
2314 Ok(Box::new(
2315 rows.into_iter()
2316 .map(|(key, value)| (Key::from_bytes(key), value)),
2317 ))
2318 }
2319
2320 fn deltas_after(
2321 &self,
2322 ts: HlcTimestamp,
2323 ) -> Result<crate::split::MutationStream<'_>, TabletDataError> {
2324 let (_, epoch) = self
2325 .pinned
2326 .filter(|(pinned, _)| *pinned == ts)
2327 .ok_or_else(|| {
2328 TabletDataError::Keyspace(format!("tablet snapshot {ts:?} is not pinned"))
2329 })?;
2330 let deltas = self
2331 .sink
2332 .as_ref()
2333 .ok_or_else(|| TabletDataError::Keyspace("tablet engine is not open".to_owned()))?
2334 .lock()
2335 .map_err(|_| TabletDataError::Keyspace("engine sink lock poisoned".to_owned()))?
2336 .tablet_deltas_after_epoch(epoch)
2337 .map_err(|error| TabletDataError::Keyspace(error.to_string()))?;
2338 Ok(Box::new(deltas.into_iter().map(
2339 |mutation| match mutation {
2340 TabletDataMutation::Upsert(key, value) => {
2341 TabletMutation::Upsert(Key::from_bytes(key), value)
2342 }
2343 TabletDataMutation::Delete(key) => TabletMutation::Delete(Key::from_bytes(key)),
2344 },
2345 )))
2346 }
2347}
2348
2349struct RuntimeChildSink {
2352 group: Arc<ConsensusGroup<TcpTransport>>,
2353 handle: tokio::runtime::Handle,
2354 control: ExecutionControl,
2355 staged: Option<BTreeMap<Key, Vec<u8>>>,
2356}
2357
2358impl RuntimeChildSink {
2359 fn propose_data(&self, command: TabletDataCommand) -> Result<(), TabletDataError> {
2360 let envelope = CommandEnvelope::new(
2361 COMMAND_TYPE_TABLET_DATA,
2362 new_data_command_id().map_err(|error| TabletDataError::Sink(error.to_string()))?,
2363 TabletDataCommandRecord::new(command).encode(),
2364 );
2365 self.handle
2366 .block_on(
2367 self.group
2368 .propose(CommandKind::Catalog, envelope, &self.control),
2369 )
2370 .map(|_| ())
2371 .map_err(|error| TabletDataError::Sink(error.to_string()))
2372 }
2373}
2374
2375impl ChildStateSink for RuntimeChildSink {
2376 fn begin_build(&mut self) -> Result<(), TabletDataError> {
2377 self.staged = Some(BTreeMap::new());
2378 Ok(())
2379 }
2380
2381 fn stage(&mut self, key: &Key, value: &[u8]) -> Result<(), TabletDataError> {
2382 let staged = self.staged.as_mut().ok_or(TabletDataError::NoStagedBuild)?;
2383 staged.insert(key.clone(), value.to_vec());
2384 Ok(())
2385 }
2386
2387 fn install_staged(&mut self) -> Result<(), TabletDataError> {
2388 let rows = self.staged.take().ok_or(TabletDataError::NoStagedBuild)?;
2389 self.propose_data(TabletDataCommand::Replace {
2390 rows: rows
2391 .into_iter()
2392 .map(|(key, value)| (key.into_bytes(), value))
2393 .collect(),
2394 })
2395 }
2396
2397 fn apply_delta(&mut self, mutation: &TabletMutation) -> Result<(), TabletDataError> {
2398 match mutation {
2399 TabletMutation::Upsert(key, value) => self.propose_data(TabletDataCommand::Upsert {
2400 entries: vec![(key.as_bytes().to_vec(), value.clone())],
2401 }),
2402 TabletMutation::Delete(key) => self.propose_data(TabletDataCommand::Delete {
2403 keys: vec![key.as_bytes().to_vec()],
2404 }),
2405 }
2406 }
2407}
2408
2409#[derive(Clone, Debug, Default, PartialEq, Eq)]
2411pub struct HostedSyncReport {
2412 pub created: Vec<TabletId>,
2414 pub refreshed: Vec<TabletId>,
2417 pub torn_down: Vec<TabletId>,
2420}
2421
2422#[cfg(test)]
2423mod tests {
2424 use super::*;
2425
2426 #[test]
2427 fn layout_scan_tolerates_a_node_without_tablets() {
2428 let tmp = tempfile::tempdir().unwrap();
2429 assert_eq!(scan_tablet_layouts(tmp.path()).unwrap(), Vec::new());
2430 std::fs::create_dir_all(tmp.path().join(TABLETS_DIR)).unwrap();
2432 assert_eq!(scan_tablet_layouts(tmp.path()).unwrap(), Vec::new());
2433 }
2434
2435 #[test]
2436 fn layout_scan_fails_closed_on_garbage_directories() {
2437 let tmp = tempfile::tempdir().unwrap();
2438 let garbage = tmp.path().join(TABLETS_DIR).join("not-a-tablet");
2440 std::fs::create_dir_all(&garbage).unwrap();
2441 assert!(matches!(
2442 scan_tablet_layouts(tmp.path()),
2443 Err(RuntimeError::InvalidRequest(_))
2444 ));
2445 std::fs::remove_dir_all(&garbage).unwrap();
2446 let tablet_id = TabletId::new_random();
2449 std::fs::create_dir_all(tmp.path().join(TABLETS_DIR).join(tablet_id.to_hex())).unwrap();
2450 assert!(matches!(
2451 scan_tablet_layouts(tmp.path()),
2452 Err(RuntimeError::Tablet(TabletError::MissingMetadata(_)))
2453 ));
2454 }
2455
2456 fn ts(micros: u64) -> HlcTimestamp {
2459 HlcTimestamp {
2460 physical_micros: micros,
2461 logical: 0,
2462 node_tiebreaker: 0,
2463 }
2464 }
2465
2466 fn pos(index: u64) -> LogPosition {
2467 LogPosition { term: 1, index }
2468 }
2469
2470 fn upsert(key: &[u8], value: &[u8]) -> TabletDataCommand {
2471 TabletDataCommand::Upsert {
2472 entries: vec![(key.to_vec(), value.to_vec())],
2473 }
2474 }
2475
2476 #[test]
2477 fn tablet_ledger_applies_versions_and_partitions_the_timeline() {
2478 let tmp = tempfile::tempdir().unwrap();
2479 let mut ledger = TabletLedger::open(tmp.path()).unwrap();
2480 ledger
2481 .apply(&upsert(b"a", b"a@1"), ts(100), pos(1))
2482 .unwrap();
2483 ledger
2484 .apply(&upsert(b"b", b"b@1"), ts(100), pos(2))
2485 .unwrap();
2486 ledger.pin(ts(100));
2489 ledger
2490 .apply(&upsert(b"a", b"a@2"), ts(200), pos(3))
2491 .unwrap();
2492 assert_eq!(
2494 ledger.rows_at(ts(100)),
2495 BTreeMap::from([
2496 (Key::from_bytes(b"a".to_vec()), b"a@1".to_vec()),
2497 (Key::from_bytes(b"b".to_vec()), b"b@1".to_vec()),
2498 ])
2499 );
2500 assert_eq!(
2501 ledger.current_rows().get(&Key::from_bytes(b"a".to_vec())),
2502 Some(&b"a@2".to_vec())
2503 );
2504 assert_eq!(
2506 ledger.deltas_after(ts(100)),
2507 vec![(Key::from_bytes(b"a".to_vec()), b"a@2".to_vec())]
2508 );
2509 assert!(ledger.deltas_after(ts(200)).is_empty());
2510 ledger.unpin(ts(100));
2511 ledger
2513 .apply(&upsert(b"z", b"z@1"), ts(300), pos(3))
2514 .unwrap();
2515 assert!(!ledger
2516 .current_rows()
2517 .contains_key(&Key::from_bytes(b"z".to_vec())));
2518 let position = ledger.applied_position();
2520 drop(ledger);
2521 let reopened = TabletLedger::open(tmp.path()).unwrap();
2522 assert_eq!(reopened.applied_position(), position);
2523 assert_eq!(
2524 reopened.current_rows().get(&Key::from_bytes(b"a".to_vec())),
2525 Some(&b"a@2".to_vec())
2526 );
2527 std::fs::write(
2529 tmp.path()
2530 .join("raft")
2531 .join("state")
2532 .join(TABLET_LEDGER_FILENAME),
2533 b"junk",
2534 )
2535 .unwrap();
2536 assert!(TabletLedger::open(tmp.path()).is_err());
2537 }
2538
2539 #[test]
2540 fn tablet_ledger_compacts_against_the_oldest_pin() {
2541 let tmp = tempfile::tempdir().unwrap();
2542 let mut ledger = TabletLedger::open(tmp.path()).unwrap();
2543 ledger
2544 .apply(&upsert(b"a", b"a@1"), ts(100), pos(1))
2545 .unwrap();
2546 ledger.pin(ts(150));
2549 ledger
2550 .apply(&upsert(b"a", b"a@2"), ts(200), pos(2))
2551 .unwrap();
2552 ledger
2553 .apply(&upsert(b"a", b"a@3"), ts(300), pos(3))
2554 .unwrap();
2555 assert_eq!(
2556 ledger.rows_at(ts(150)).get(&Key::from_bytes(b"a".to_vec())),
2557 Some(&b"a@1".to_vec())
2558 );
2559 assert_eq!(
2560 ledger.deltas_after(ts(150)),
2561 vec![
2562 (Key::from_bytes(b"a".to_vec()), b"a@2".to_vec()),
2563 (Key::from_bytes(b"a".to_vec()), b"a@3".to_vec()),
2564 ]
2565 );
2566 ledger.unpin(ts(150));
2568 ledger
2569 .apply(&upsert(b"a", b"a@4"), ts(400), pos(4))
2570 .unwrap();
2571 assert_eq!(
2572 ledger.rows_at(ts(150)).get(&Key::from_bytes(b"a".to_vec())),
2573 None
2574 );
2575 assert_eq!(
2576 ledger.current_rows().get(&Key::from_bytes(b"a".to_vec())),
2577 Some(&b"a@4".to_vec())
2578 );
2579 assert_eq!(ledger.pin_count(), 0);
2580 }
2581
2582 #[test]
2583 fn tablet_ledger_replace_is_atomic_and_refused_under_a_live_pin() {
2584 let tmp = tempfile::tempdir().unwrap();
2585 let mut ledger = TabletLedger::open(tmp.path()).unwrap();
2586 ledger
2587 .apply(&upsert(b"a", b"a@1"), ts(100), pos(1))
2588 .unwrap();
2589 ledger.pin(ts(150));
2590 let replace = TabletDataCommand::Replace {
2591 rows: vec![(b"b".to_vec(), b"b@2".to_vec())],
2592 };
2593 assert!(ledger.apply(&replace, ts(200), pos(2)).is_err());
2594 ledger.unpin(ts(150));
2595 ledger.apply(&replace, ts(200), pos(2)).unwrap();
2596 assert_eq!(
2597 ledger.current_rows(),
2598 BTreeMap::from([(Key::from_bytes(b"b".to_vec()), b"b@2".to_vec())])
2599 );
2600 let bytes = ledger.snapshot_bytes().unwrap();
2602 let follower_dir = tempfile::tempdir().unwrap();
2603 let mut follower = TabletLedger::open(follower_dir.path()).unwrap();
2604 follower.install_bytes(&bytes).unwrap();
2605 assert_eq!(follower.current_rows(), ledger.current_rows());
2606 assert!(follower.install_bytes(b"junk").is_err());
2607 }
2608
2609 #[test]
2610 fn group_snapshot_frame_round_trips_and_fails_closed() {
2611 let engine = b"engine-half".to_vec();
2612 let ledger = b"ledger-half".to_vec();
2613 let frame = encode_group_snapshot(&engine, &ledger);
2614 let (engine_back, ledger_back) = decode_group_snapshot(&frame).unwrap();
2615 assert_eq!(engine_back, engine);
2616 assert_eq!(ledger_back, ledger);
2617 let (empty_engine, empty_ledger) =
2619 decode_group_snapshot(&encode_group_snapshot(&[], &[])).unwrap();
2620 assert!(empty_engine.is_empty() && empty_ledger.is_empty());
2621 assert!(decode_group_snapshot(&frame[..6]).is_err());
2625 assert!(decode_group_snapshot(&frame[..12 + engine.len() - 1]).is_err());
2626 let mut future = frame.clone();
2627 future[..4].copy_from_slice(&99_u32.to_le_bytes());
2628 assert!(decode_group_snapshot(&future).is_err());
2629 }
2630
2631 #[test]
2632 fn tablet_data_command_record_round_trips_and_fails_closed() {
2633 let record = TabletDataCommandRecord::new(upsert(b"k", b"v"));
2634 let decoded = TabletDataCommandRecord::decode(&record.encode()).unwrap();
2635 assert_eq!(decoded, record);
2636 assert!(TabletDataCommandRecord::decode(b"not json").is_err());
2637 let mut value: serde_json::Value = serde_json::from_slice(&record.encode()).unwrap();
2638 value["format_version"] = serde_json::json!(99);
2639 assert!(TabletDataCommandRecord::decode(&serde_json::to_vec(&value).unwrap()).is_err());
2640 }
2641}