1use std::path::{Path, PathBuf};
21use std::sync::Arc;
22use std::time::Duration;
23
24use mongreldb_cluster::bootstrap::{self, ClusterStatus, TrustConfig};
25use mongreldb_cluster::network::{TlsConfig, TransportConfig, TransportSecurity};
26use mongreldb_cluster::node::{ClusterError, NodeIdentity};
27use mongreldb_cluster::runtime::{
28 GroupTiming, MetaMembership, NodeInternalRpcClient, NodeRuntime, NodeRuntimeConfig,
29 RuntimeError, RuntimeStatus,
30};
31use mongreldb_cluster::tablet::Key;
32use mongreldb_log::commit_log::ExecutionControl;
33use mongreldb_types::ids::{NodeId, TabletId};
34use serde::{Deserialize, Serialize};
35use serde_json::{json, Value};
36use tokio::sync::Mutex;
37
38#[derive(Clone, Debug)]
40pub struct ClusterRuntimeOptions {
41 pub node_data: PathBuf,
43 pub rpc_listen: String,
45 pub plaintext_test: bool,
47 pub fast_timing: bool,
49}
50
51impl ClusterRuntimeOptions {
52 pub fn resolve(node_data: PathBuf, rpc_listen: Option<String>) -> Self {
58 let rpc_listen = rpc_listen
59 .or_else(|| std::env::var("MONGRELDB_CLUSTER_RPC_LISTEN").ok())
60 .filter(|s| !s.trim().is_empty())
61 .unwrap_or_else(|| "127.0.0.1:17443".to_owned());
62 let plaintext_test = env_flag_true("MONGRELDB_CLUSTER_PLAINTEXT_TEST");
63 Self {
64 node_data,
65 rpc_listen,
66 plaintext_test,
67 fast_timing: plaintext_test,
70 }
71 }
72}
73
74#[derive(Clone)]
81pub struct ClusterRuntimeHandle {
82 inner: Arc<Mutex<Option<NodeRuntime>>>,
83 node_data: PathBuf,
84}
85
86#[derive(Debug)]
88pub enum ClusterRuntimeError {
89 Cluster(ClusterError),
91 Runtime(RuntimeError),
93 Config(String),
95}
96
97impl std::fmt::Display for ClusterRuntimeError {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 match self {
100 Self::Cluster(error) => write!(f, "{error}"),
101 Self::Runtime(error) => write!(f, "{error}"),
102 Self::Config(message) => write!(f, "{message}"),
103 }
104 }
105}
106
107impl std::error::Error for ClusterRuntimeError {
108 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
109 match self {
110 Self::Cluster(error) => Some(error),
111 Self::Runtime(error) => Some(error),
112 Self::Config(_) => None,
113 }
114 }
115}
116
117impl From<ClusterError> for ClusterRuntimeError {
118 fn from(error: ClusterError) -> Self {
119 Self::Cluster(error)
120 }
121}
122
123impl From<RuntimeError> for ClusterRuntimeError {
124 fn from(error: RuntimeError) -> Self {
125 Self::Runtime(error)
126 }
127}
128
129impl From<std::io::Error> for ClusterRuntimeError {
130 fn from(error: std::io::Error) -> Self {
131 Self::Config(format!("cluster runtime I/O: {error}"))
132 }
133}
134
135impl ClusterRuntimeHandle {
136 pub async fn start(options: ClusterRuntimeOptions) -> Result<Self, ClusterRuntimeError> {
140 let _identity =
141 NodeIdentity::load(&options.node_data)?.ok_or(ClusterError::NotInitialized)?;
142 let status = bootstrap::cluster_status(&options.node_data)?;
143 let listen = resolve_listen_address(&options.rpc_listen)?;
144 let security = resolve_security(&options.node_data, options.plaintext_test)?;
145 let peers = peers_from_status(&status, &listen);
146 let meta = meta_membership_from_status(&status, &listen);
147
148 let config = NodeRuntimeConfig {
149 node_data: options.node_data.clone(),
150 security,
151 transport: transport_config(options.plaintext_test),
152 listen_address: listen.clone(),
153 rpc_address: Some(listen),
154 peers,
155 meta,
156 timing: options.fast_timing.then(fast_timing),
157 };
158 let runtime = NodeRuntime::start(config).await?;
159 Ok(Self {
160 inner: Arc::new(Mutex::new(Some(runtime))),
161 node_data: options.node_data,
162 })
163 }
164
165 pub fn node_data(&self) -> &Path {
167 &self.node_data
168 }
169
170 pub async fn is_live(&self) -> bool {
172 self.inner.lock().await.is_some()
173 }
174
175 pub async fn runtime_status_json(&self) -> Result<Value, ClusterRuntimeError> {
177 let guard = self.inner.lock().await;
178 let runtime = guard
179 .as_ref()
180 .ok_or_else(|| ClusterRuntimeError::Config("cluster runtime not running".into()))?;
181 Ok(runtime_status_to_json(&runtime.status()))
182 }
183
184 pub async fn transfer_leader(
186 &self,
187 tablet_id: TabletId,
188 to: NodeId,
189 ) -> Result<Value, ClusterRuntimeError> {
190 let guard = self.inner.lock().await;
191 let runtime = guard
192 .as_ref()
193 .ok_or_else(|| ClusterRuntimeError::Config("cluster runtime not running".into()))?;
194 let descriptor = runtime.tablet_descriptor(tablet_id).ok_or_else(|| {
195 ClusterRuntimeError::Config(format!(
196 "this node hosts no live tablet group for tablet {tablet_id}"
197 ))
198 })?;
199 let target = descriptor.replica_on(to).ok_or_else(|| {
200 ClusterRuntimeError::Config(format!("node {to} is not a replica of tablet {tablet_id}"))
201 })?;
202 let group = runtime.tablet_group(tablet_id).ok_or_else(|| {
203 ClusterRuntimeError::Config(format!(
204 "this node hosts no live tablet group for tablet {tablet_id}"
205 ))
206 })?;
207 group
208 .transfer_leader(target.raft_node_id, LEADER_TIMEOUT)
209 .await
210 .map_err(|error| ClusterRuntimeError::Config(error.to_string()))?;
211 Ok(json!({
212 "command": "TRANSFER LEADER",
213 "tablet_id": tablet_id.to_string(),
214 "to": to.to_string(),
215 "status": "ok",
216 "target_raft_node_id": target.raft_node_id,
217 }))
218 }
219
220 pub async fn split_tablet(
222 &self,
223 tablet_id: TabletId,
224 at_key_hex: Option<String>,
225 ) -> Result<Value, ClusterRuntimeError> {
226 let split_key = match at_key_hex.as_deref() {
227 Some(hex) => Some(parse_key_hex(hex)?),
228 None => None,
229 };
230 let mut guard = self.inner.lock().await;
231 let runtime = guard
232 .as_mut()
233 .ok_or_else(|| ClusterRuntimeError::Config("cluster runtime not running".into()))?;
234 if runtime.tablet_group(tablet_id).is_none() {
235 return Err(ClusterRuntimeError::Config(format!(
236 "this node hosts no live tablet group for tablet {tablet_id}"
237 )));
238 }
239 let control = ExecutionControl::default();
240 let published = runtime.split_tablet(tablet_id, split_key, &control).await?;
241 Ok(json!({
242 "command": "SPLIT TABLET",
243 "tablet_id": tablet_id.to_string(),
244 "at_key_hex": at_key_hex,
245 "status": "ok",
246 "published": true,
247 "left_tablet_id": published.children[0].tablet_id.to_string(),
248 "right_tablet_id": published.children[1].tablet_id.to_string(),
249 }))
250 }
251
252 pub async fn merge_tablets(
254 &self,
255 left: TabletId,
256 right: TabletId,
257 ) -> Result<Value, ClusterRuntimeError> {
258 let mut guard = self.inner.lock().await;
259 let runtime = guard
260 .as_mut()
261 .ok_or_else(|| ClusterRuntimeError::Config("cluster runtime not running".into()))?;
262 for tablet_id in [left, right] {
263 if runtime.tablet_group(tablet_id).is_none() {
264 return Err(ClusterRuntimeError::Config(format!(
265 "this node hosts no live tablet group for tablet {tablet_id}"
266 )));
267 }
268 }
269 let control = ExecutionControl::default();
270 let published = runtime.merge_tablets(left, right, &control).await?;
271 Ok(json!({
272 "command": "MERGE TABLETS",
273 "left": left.to_string(),
274 "right": right.to_string(),
275 "status": "ok",
276 "published": true,
277 "replacement_tablet_id": published.replacement.tablet_id.to_string(),
278 }))
279 }
280
281 pub fn runtime_mutex(&self) -> Arc<Mutex<Option<NodeRuntime>>> {
283 Arc::clone(&self.inner)
284 }
285
286 pub async fn attach_internal_rpc_handler(
288 &self,
289 service_id: u32,
290 handler: Arc<dyn mongreldb_cluster::network::InternalRpcHandler>,
291 ) -> Result<(), ClusterRuntimeError> {
292 let guard = self.inner.lock().await;
293 let runtime = guard
294 .as_ref()
295 .ok_or_else(|| ClusterRuntimeError::Config("cluster runtime not running".into()))?;
296 runtime.attach_internal_rpc_handler(service_id, handler);
297 Ok(())
298 }
299
300 pub async fn internal_rpc_client(&self) -> Result<NodeInternalRpcClient, ClusterRuntimeError> {
302 let guard = self.inner.lock().await;
303 let runtime = guard
304 .as_ref()
305 .ok_or_else(|| ClusterRuntimeError::Config("cluster runtime not running".into()))?;
306 Ok(runtime.internal_rpc_client())
307 }
308
309 pub async fn shutdown(&self) -> Result<(), ClusterRuntimeError> {
311 let runtime = {
312 let mut guard = self.inner.lock().await;
313 guard.take()
314 };
315 if let Some(runtime) = runtime {
316 runtime.shutdown().await?;
317 }
318 Ok(())
319 }
320}
321
322const LEADER_TIMEOUT: Duration = Duration::from_secs(15);
323
324fn env_flag_true(name: &str) -> bool {
325 match std::env::var(name) {
326 Ok(value) => {
327 let value = value.trim();
328 value == "1" || value.eq_ignore_ascii_case("true") || value.eq_ignore_ascii_case("yes")
329 }
330 Err(_) => false,
331 }
332}
333
334fn resolve_listen_address(listen: &str) -> Result<String, ClusterRuntimeError> {
337 let trimmed = listen.trim();
338 if trimmed.is_empty() {
339 return Err(ClusterRuntimeError::Config(
340 "cluster RPC listen address is empty".into(),
341 ));
342 }
343 if trimmed.ends_with(":0") {
345 let listener = std::net::TcpListener::bind(trimmed)?;
346 return Ok(listener.local_addr()?.to_string());
347 }
348 Ok(trimmed.to_owned())
349}
350
351fn resolve_security(
352 node_data: &Path,
353 plaintext_test: bool,
354) -> Result<TransportSecurity, ClusterRuntimeError> {
355 if plaintext_test {
356 return Ok(TransportSecurity::PlaintextForTesting);
357 }
358 let trust = load_persisted_trust(node_data)?.ok_or_else(|| {
359 ClusterRuntimeError::Config(
360 "cluster trust material missing under cluster-meta/trust.json; \
361 run `mongreldb-server cluster init` first, or set \
362 MONGRELDB_CLUSTER_PLAINTEXT_TEST=1 for non-production tests"
363 .into(),
364 )
365 })?;
366 let tls = TlsConfig::from_trust(&trust).map_err(|error| {
367 ClusterRuntimeError::Config(format!(
368 "cluster trust PEMs are not usable for mTLS ({error}); \
369 supply real certificates or set MONGRELDB_CLUSTER_PLAINTEXT_TEST=1 \
370 for non-production tests"
371 ))
372 })?;
373 Ok(TransportSecurity::Mtls(tls))
374}
375
376#[derive(Deserialize, Serialize)]
378#[serde(deny_unknown_fields)]
379struct TrustEnvelope {
380 format_version: u32,
381 trust: TrustConfig,
382}
383
384fn load_persisted_trust(node_data: &Path) -> Result<Option<TrustConfig>, ClusterRuntimeError> {
385 let path = node_data.join("cluster-meta").join("trust.json");
386 if !path.exists() {
387 return Ok(None);
388 }
389 let bytes = std::fs::read(&path)?;
390 let envelope: TrustEnvelope = serde_json::from_slice(&bytes).map_err(|error| {
391 ClusterRuntimeError::Config(format!("cluster-meta/trust.json is corrupt: {error}"))
392 })?;
393 if envelope.format_version != 1 {
394 return Err(ClusterRuntimeError::Config(format!(
395 "cluster-meta/trust.json format version {} is unsupported",
396 envelope.format_version
397 )));
398 }
399 envelope
400 .trust
401 .validate()
402 .map_err(ClusterRuntimeError::from)?;
403 Ok(Some(envelope.trust))
404}
405
406fn peers_from_status(status: &ClusterStatus, self_listen: &str) -> Vec<(NodeId, String)> {
407 if !status.membership.is_empty() {
408 return status
409 .membership
410 .iter()
411 .map(|member| {
412 let address = if member.node_id == status.identity.node_id {
413 self_listen.to_owned()
414 } else {
415 member.rpc_address.clone()
416 };
417 (member.node_id, address)
418 })
419 .collect();
420 }
421 vec![(status.identity.node_id, self_listen.to_owned())]
423}
424
425fn meta_membership_from_status(
429 status: &ClusterStatus,
430 self_listen: &str,
431) -> Option<MetaMembership> {
432 let group = status.database_group.as_ref()?;
433 if !group.voter_ids.contains(&status.identity.node_id) {
434 return None;
435 }
436 let bootstrap_voters = if group.voter_ids.len() == 1 {
437 Some(vec![(status.identity.node_id, self_listen.to_owned())])
438 } else {
439 None
440 };
441 Some(MetaMembership {
442 meta_group_id: group.raft_group_id,
443 bootstrap_voters,
444 })
445}
446
447fn transport_config(plaintext_test: bool) -> TransportConfig {
448 if plaintext_test {
449 TransportConfig {
450 connect_timeout: Duration::from_millis(500),
451 rpc_timeout: Duration::from_secs(2),
452 snapshot_timeout: Duration::from_secs(5),
453 connect_attempts: 5,
454 reconnect_backoff: Duration::from_millis(10),
455 max_frame_bytes: 16 * 1024 * 1024,
456 max_connections: 64,
457 handshake_timeout: Duration::from_secs(2),
458 shutdown_grace: Duration::from_secs(2),
459 }
460 } else {
461 TransportConfig::default()
462 }
463}
464
465fn fast_timing() -> GroupTiming {
466 GroupTiming {
467 heartbeat_interval: Duration::from_millis(100),
468 election_timeout_min: Duration::from_millis(300),
469 election_timeout_max: Duration::from_millis(600),
470 install_snapshot_timeout: Duration::from_millis(2_000),
471 }
472}
473
474fn parse_key_hex(text: &str) -> Result<Key, ClusterRuntimeError> {
475 let trimmed = text.trim();
476 if trimmed.is_empty() {
477 return Err(ClusterRuntimeError::Config("split key hex is empty".into()));
478 }
479 if !trimmed.len().is_multiple_of(2) {
480 return Err(ClusterRuntimeError::Config(
481 "split key hex must have an even number of digits".into(),
482 ));
483 }
484 let mut bytes = Vec::with_capacity(trimmed.len() / 2);
485 let chars: Vec<char> = trimmed.chars().collect();
486 for chunk in chars.chunks(2) {
487 let hi = chunk[0].to_digit(16).ok_or_else(|| {
488 ClusterRuntimeError::Config(format!("invalid split key hex `{text}`"))
489 })?;
490 let lo = chunk[1].to_digit(16).ok_or_else(|| {
491 ClusterRuntimeError::Config(format!("invalid split key hex `{text}`"))
492 })?;
493 bytes.push(((hi << 4) | lo) as u8);
494 }
495 Ok(Key::from_bytes(bytes))
496}
497
498fn runtime_status_to_json(status: &RuntimeStatus) -> Value {
499 json!({
500 "live": true,
501 "node_id": status.identity.node_id.to_string(),
502 "cluster_id": status.identity.cluster_id.to_string(),
503 "rpc_address": status.rpc_address,
504 "meta_present": status.meta.is_some(),
505 "meta": status.meta.as_ref().map(|meta| json!({
506 "meta_group_id": meta.meta_group_id.to_string(),
507 "metadata_version": meta.metadata_version.get(),
508 "current_leader": meta.metrics.current_leader,
509 "local_raft_node_id": meta.metrics.node_id,
510 })),
511 "tablet_count": status.tablets.len(),
512 "tablets": status.tablets.iter().map(|tablet| json!({
513 "tablet_id": tablet.tablet_id.to_string(),
514 "raft_group_id": tablet.raft_group_id.to_string(),
515 "state": tablet.state.to_string(),
516 "replica_count": tablet.replicas.len(),
517 "applied_index": tablet.applied.index,
518 "current_leader": tablet.metrics.current_leader,
519 "local_raft_node_id": tablet.metrics.node_id,
520 })).collect::<Vec<_>>(),
521 })
522}
523
524pub fn cluster_node_data_from_env(cli_value: Option<String>) -> Option<PathBuf> {
526 cli_value
527 .or_else(|| std::env::var("MONGRELDB_CLUSTER_NODE_DATA").ok())
528 .map(|s| s.trim().to_owned())
529 .filter(|s| !s.is_empty())
530 .map(PathBuf::from)
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536 use mongreldb_cluster::bootstrap::{cluster_init, InitRequest};
537 use mongreldb_cluster::node::{Locality, NodeCapacity};
538 use tempfile::tempdir;
539
540 const CA_PEM: &str = "-----BEGIN CERTIFICATE-----\nY2E=\n-----END CERTIFICATE-----\n";
541 const CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\nbm9kZQ==\n-----END CERTIFICATE-----\n";
542 const KEY_PEM: &str = "-----BEGIN PRIVATE KEY-----\nc2VjcmV0\n-----END PRIVATE KEY-----\n";
543
544 fn bootstrap(data: &Path) -> NodeIdentity {
545 let mut counter = 0u64;
546 let mut csprng = |buf: &mut [u8]| {
547 for chunk in buf.chunks_mut(8) {
548 counter += 1;
549 let bytes = counter.to_le_bytes();
550 chunk.copy_from_slice(&bytes[..chunk.len()]);
551 }
552 Ok(())
553 };
554 let identity = NodeIdentity::load_or_create(data, &mut csprng).unwrap();
555 let request = InitRequest {
556 rpc_address: "127.0.0.1:0".to_owned(),
557 locality: Locality::default(),
558 capacity: NodeCapacity::default(),
559 trust: TrustConfig::from_pems(
560 CA_PEM.to_owned(),
561 CERT_PEM.to_owned(),
562 KEY_PEM.to_owned(),
563 vec![identity.node_id],
564 )
565 .unwrap(),
566 };
567 cluster_init(data, &request, &mut csprng).unwrap().identity
568 }
569
570 #[tokio::test]
571 async fn plaintext_start_status_and_missing_tablet_errors() {
572 let dir = tempdir().unwrap();
573 let identity = bootstrap(dir.path());
574 let listen = {
575 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
576 listener.local_addr().unwrap().to_string()
577 };
578 let handle = ClusterRuntimeHandle::start(ClusterRuntimeOptions {
579 node_data: dir.path().to_path_buf(),
580 rpc_listen: listen.clone(),
581 plaintext_test: true,
582 fast_timing: true,
583 })
584 .await
585 .expect("runtime starts after cluster init");
586
587 assert!(handle.is_live().await);
588 let status = handle.runtime_status_json().await.unwrap();
589 assert_eq!(status["live"], true);
590 assert_eq!(status["node_id"], identity.node_id.to_string());
591 assert_eq!(status["rpc_address"], listen);
592 assert_eq!(status["meta_present"], true);
593 assert_eq!(status["tablet_count"], 0);
594
595 let missing = TabletId::from_bytes([0xAB; 16]);
596 let err = handle
597 .transfer_leader(missing, identity.node_id)
598 .await
599 .unwrap_err();
600 assert!(
601 err.to_string().contains("hosts no live tablet"),
602 "transfer without tablet must fail closed: {err}"
603 );
604 let err = handle.split_tablet(missing, None).await.unwrap_err();
605 assert!(
606 err.to_string().contains("hosts no live tablet"),
607 "split without tablet must fail closed: {err}"
608 );
609
610 handle.shutdown().await.unwrap();
611 assert!(!handle.is_live().await);
612 }
613
614 #[tokio::test]
615 async fn start_fails_closed_without_cluster_init() {
616 let dir = tempdir().unwrap();
617 match ClusterRuntimeHandle::start(ClusterRuntimeOptions {
618 node_data: dir.path().to_path_buf(),
619 rpc_listen: "127.0.0.1:0".into(),
620 plaintext_test: true,
621 fast_timing: true,
622 })
623 .await
624 {
625 Ok(_) => panic!("expected NotInitialized without cluster init"),
626 Err(err) => assert!(
627 matches!(
628 err,
629 ClusterRuntimeError::Cluster(ClusterError::NotInitialized)
630 ) || err
631 .to_string()
632 .to_ascii_lowercase()
633 .contains("not initialized")
634 || err.to_string().contains("NotInitialized"),
635 "expected NotInitialized, got {err}"
636 ),
637 }
638 }
639
640 #[test]
641 fn parse_key_hex_round_trip() {
642 let key = parse_key_hex("0a1b").unwrap();
643 assert_eq!(key.as_bytes(), &[0x0a, 0x1b]);
644 assert!(parse_key_hex("zz").is_err());
645 assert!(parse_key_hex("abc").is_err());
646 }
647}