1use std::collections::HashMap;
16use std::fmt::{Display, Formatter};
17use std::hash::{DefaultHasher, Hash, Hasher};
18use std::str::FromStr;
19
20use api::v1::meta::{DatanodeWorkloads, FlownodeWorkloads, FrontendWorkloads, HeartbeatRequest};
21use common_error::ext::ErrorExt;
22use lazy_static::lazy_static;
23use regex::Regex;
24use serde::{Deserialize, Serialize};
25use snafu::{OptionExt, ResultExt, ensure};
26
27use crate::datanode::RegionStat;
28use crate::error::{
29 DecodeJsonSnafu, EncodeJsonSnafu, Error, FromUtf8Snafu, InvalidNodeInfoKeySnafu,
30 InvalidRoleSnafu, ParseNumSnafu, Result,
31};
32use crate::key::flow::flow_state::FlowStat;
33use crate::peer::Peer;
34
35const CLUSTER_NODE_INFO_PREFIX: &str = "__meta_cluster_node_info";
36
37lazy_static! {
38 static ref CLUSTER_NODE_INFO_PREFIX_PATTERN: Regex = Regex::new(&format!(
39 "^{CLUSTER_NODE_INFO_PREFIX}-([0-9]+)-([0-9]+)-([0-9]+)$"
40 ))
41 .unwrap();
42}
43
44#[async_trait::async_trait]
46pub trait ClusterInfo {
47 type Error: ErrorExt;
48
49 async fn list_nodes(
51 &self,
52 role: Option<Role>,
53 ) -> std::result::Result<Vec<NodeInfo>, Self::Error>;
54
55 async fn list_region_stats(&self) -> std::result::Result<Vec<RegionStat>, Self::Error>;
57
58 async fn list_flow_stats(&self) -> std::result::Result<Option<FlowStat>, Self::Error>;
60
61 }
63
64#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize, PartialOrd, Ord)]
66pub struct NodeInfoKey {
67 pub role: Role,
69 pub node_id: u64,
71}
72
73impl NodeInfoKey {
74 pub fn new(request: &HeartbeatRequest) -> Option<Self> {
77 let HeartbeatRequest { header, peer, .. } = request;
78 let header = header.as_ref()?;
79 let peer = peer.as_ref()?;
80
81 let role = header.role.try_into().ok()?;
82 let node_id = match role {
83 Role::Frontend => calculate_node_id(&peer.addr),
86 _ => peer.id,
87 };
88
89 Some(NodeInfoKey { role, node_id })
90 }
91
92 pub fn key_prefix() -> String {
93 format!("{}-0-", CLUSTER_NODE_INFO_PREFIX)
94 }
95
96 pub fn key_prefix_with_role(role: Role) -> String {
97 format!("{}-0-{}-", CLUSTER_NODE_INFO_PREFIX, i32::from(role))
98 }
99}
100
101fn calculate_node_id(addr: &str) -> u64 {
103 let mut hasher = DefaultHasher::new();
104 addr.hash(&mut hasher);
105 hasher.finish()
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct NodeInfo {
111 pub peer: Peer,
113 pub last_activity_ts: i64,
115 pub status: NodeStatus,
117 pub version: String,
119 pub git_commit: String,
121 pub start_time_ms: u64,
123 #[serde(default)]
125 pub total_cpu_millicores: i64,
126 #[serde(default)]
128 pub total_memory_bytes: i64,
129 #[serde(default)]
131 pub cpu_usage_millicores: i64,
132 #[serde(default)]
134 pub memory_usage_bytes: i64,
135 #[serde(default)]
137 pub hostname: String,
138 #[serde(default)]
140 pub env_vars: HashMap<String, String>,
141 #[serde(skip)]
147 pub schedulable: Option<bool>,
148}
149
150impl NodeInfo {
151 pub fn is_schedulable(&self) -> bool {
155 self.schedulable != Some(false)
156 }
157}
158
159#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize, PartialOrd, Ord)]
160pub enum Role {
161 Datanode,
162 Frontend,
163 Flownode,
164 Metasrv,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub enum NodeStatus {
169 Datanode(DatanodeStatus),
170 Frontend(FrontendStatus),
171 Flownode(FlownodeStatus),
172 Metasrv(MetasrvStatus),
173 Standalone,
174}
175
176impl NodeStatus {
177 pub fn role_name(&self) -> &str {
179 match self {
180 NodeStatus::Datanode(_) => "DATANODE",
181 NodeStatus::Frontend(_) => "FRONTEND",
182 NodeStatus::Flownode(_) => "FLOWNODE",
183 NodeStatus::Metasrv(_) => "METASRV",
184 NodeStatus::Standalone => "STANDALONE",
185 }
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct DatanodeStatus {
192 pub rcus: i64,
194 pub wcus: i64,
196 pub leader_regions: usize,
198 pub follower_regions: usize,
200 pub workloads: DatanodeWorkloads,
202}
203
204#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206pub struct FrontendStatus {
207 #[serde(default)]
209 pub workloads: FrontendWorkloads,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct FlownodeStatus {
215 #[serde(default)]
217 pub workloads: FlownodeWorkloads,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct MetasrvStatus {
223 pub is_leader: bool,
224}
225
226impl FromStr for NodeInfoKey {
227 type Err = Error;
228
229 fn from_str(key: &str) -> Result<Self> {
230 let caps = CLUSTER_NODE_INFO_PREFIX_PATTERN
231 .captures(key)
232 .context(InvalidNodeInfoKeySnafu { key })?;
233 ensure!(caps.len() == 4, InvalidNodeInfoKeySnafu { key });
234
235 let role = caps[2].to_string();
236 let node_id = caps[3].to_string();
237 let role: i32 = role.parse().context(ParseNumSnafu {
238 err_msg: format!("invalid role {role}"),
239 })?;
240 let role = Role::try_from(role)?;
241 let node_id: u64 = node_id.parse().context(ParseNumSnafu {
242 err_msg: format!("invalid node_id: {node_id}"),
243 })?;
244
245 Ok(Self { role, node_id })
246 }
247}
248
249impl TryFrom<Vec<u8>> for NodeInfoKey {
250 type Error = Error;
251
252 fn try_from(bytes: Vec<u8>) -> Result<Self> {
253 String::from_utf8(bytes)
254 .context(FromUtf8Snafu {
255 name: "NodeInfoKey",
256 })
257 .map(|x| x.parse())?
258 }
259}
260
261impl From<&NodeInfoKey> for Vec<u8> {
262 fn from(key: &NodeInfoKey) -> Self {
263 format!(
264 "{}-0-{}-{}",
265 CLUSTER_NODE_INFO_PREFIX,
266 i32::from(key.role),
267 key.node_id
268 )
269 .into_bytes()
270 }
271}
272
273impl Display for NodeInfoKey {
274 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
275 write!(f, "{:?}-{}", self.role, self.node_id)
276 }
277}
278
279impl FromStr for NodeInfo {
280 type Err = Error;
281
282 fn from_str(value: &str) -> Result<Self> {
283 serde_json::from_str(value).context(DecodeJsonSnafu)
284 }
285}
286
287impl TryFrom<Vec<u8>> for NodeInfo {
288 type Error = Error;
289
290 fn try_from(bytes: Vec<u8>) -> Result<Self> {
291 String::from_utf8(bytes)
292 .context(FromUtf8Snafu { name: "NodeInfo" })
293 .map(|x| x.parse())?
294 }
295}
296
297impl TryFrom<NodeInfo> for Vec<u8> {
298 type Error = Error;
299
300 fn try_from(info: NodeInfo) -> Result<Self> {
301 Ok(serde_json::to_string(&info)
302 .context(EncodeJsonSnafu)?
303 .into_bytes())
304 }
305}
306
307impl From<Role> for i32 {
308 fn from(role: Role) -> Self {
309 match role {
310 Role::Datanode => 0,
311 Role::Frontend => 1,
312 Role::Flownode => 2,
313 Role::Metasrv => 99,
314 }
315 }
316}
317
318impl TryFrom<i32> for Role {
319 type Error = Error;
320
321 fn try_from(role: i32) -> Result<Self> {
322 match role {
323 0 => Ok(Self::Datanode),
324 1 => Ok(Self::Frontend),
325 2 => Ok(Self::Flownode),
326 99 => Ok(Self::Metasrv),
327 _ => InvalidRoleSnafu { role }.fail(),
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use std::assert_matches;
335
336 use common_workload::DatanodeWorkloadType;
337
338 use super::*;
339 use crate::cluster::Role::{Datanode, Frontend};
340 use crate::cluster::{DatanodeStatus, FlownodeStatus, NodeInfo, NodeInfoKey, NodeStatus};
341 use crate::peer::Peer;
342
343 fn new_test_node_info() -> NodeInfo {
344 NodeInfo {
345 peer: Peer {
346 id: 1,
347 addr: "127.0.0.1".to_string(),
348 },
349 last_activity_ts: 123,
350 status: NodeStatus::Datanode(DatanodeStatus {
351 rcus: 1,
352 wcus: 2,
353 leader_regions: 3,
354 follower_regions: 4,
355 workloads: DatanodeWorkloads {
356 types: vec![DatanodeWorkloadType::Hybrid.to_i32()],
357 },
358 }),
359 version: "".to_string(),
360 git_commit: "".to_string(),
361 start_time_ms: 1,
362 total_cpu_millicores: 0,
363 total_memory_bytes: 0,
364 cpu_usage_millicores: 0,
365 memory_usage_bytes: 0,
366 hostname: "test_hostname".to_string(),
367 env_vars: Default::default(),
368 schedulable: None,
369 }
370 }
371
372 #[test]
373 fn test_node_info_key_round_trip() {
374 let key = NodeInfoKey {
375 role: Datanode,
376 node_id: 2,
377 };
378
379 let key_bytes: Vec<u8> = (&key).into();
380 let new_key: NodeInfoKey = key_bytes.try_into().unwrap();
381
382 assert_eq!(Datanode, new_key.role);
383 assert_eq!(2, new_key.node_id);
384 }
385
386 #[test]
387 fn test_node_info_round_trip() {
388 let node_info = NodeInfo {
389 peer: Peer {
390 id: 1,
391 addr: "127.0.0.1".to_string(),
392 },
393 last_activity_ts: 123,
394 status: NodeStatus::Datanode(DatanodeStatus {
395 rcus: 1,
396 wcus: 2,
397 leader_regions: 3,
398 follower_regions: 4,
399 workloads: DatanodeWorkloads {
400 types: vec![DatanodeWorkloadType::Hybrid.to_i32()],
401 },
402 }),
403 version: "".to_string(),
404 git_commit: "".to_string(),
405 start_time_ms: 1,
406 total_cpu_millicores: 0,
407 total_memory_bytes: 0,
408 cpu_usage_millicores: 0,
409 memory_usage_bytes: 0,
410 hostname: "test_hostname".to_string(),
411 env_vars: Default::default(),
412 schedulable: None,
413 };
414
415 let node_info_bytes: Vec<u8> = node_info.try_into().unwrap();
416 assert!(
417 !String::from_utf8_lossy(&node_info_bytes).contains("schedulable"),
418 "the runtime scheduling flag must not be persisted"
419 );
420 let new_node_info: NodeInfo = node_info_bytes.try_into().unwrap();
421 assert!(new_node_info.schedulable.is_none());
422
423 assert_matches!(
424 new_node_info,
425 NodeInfo {
426 peer: Peer { id: 1, .. },
427 last_activity_ts: 123,
428 status: NodeStatus::Datanode(DatanodeStatus {
429 rcus: 1,
430 wcus: 2,
431 leader_regions: 3,
432 follower_regions: 4,
433 ..
434 }),
435 start_time_ms: 1,
436 ..
437 }
438 );
439 }
440
441 #[test]
442 fn test_is_schedulable() {
443 assert!(
444 NodeInfo {
445 schedulable: None,
446 ..new_test_node_info()
447 }
448 .is_schedulable()
449 );
450 assert!(
451 NodeInfo {
452 schedulable: Some(true),
453 ..new_test_node_info()
454 }
455 .is_schedulable()
456 );
457 assert!(
458 !NodeInfo {
459 schedulable: Some(false),
460 ..new_test_node_info()
461 }
462 .is_schedulable()
463 );
464 }
465
466 #[test]
467 fn test_node_info_key_prefix() {
468 let prefix = NodeInfoKey::key_prefix();
469 assert_eq!(prefix, "__meta_cluster_node_info-0-");
470
471 let prefix = NodeInfoKey::key_prefix_with_role(Frontend);
472 assert_eq!(prefix, "__meta_cluster_node_info-0-1-");
473 }
474
475 #[test]
476 fn test_calculate_node_id_from_addr() {
477 assert_eq!(calculate_node_id(""), calculate_node_id(""));
479
480 let addr1 = "127.0.0.1:8080";
482 let id1 = calculate_node_id(addr1);
483 let id2 = calculate_node_id(addr1);
484 assert_eq!(id1, id2);
485
486 let addr2 = "127.0.0.1:8081";
488 let id3 = calculate_node_id(addr2);
489 assert_ne!(id1, id3);
490
491 let long_addr = "very.long.domain.name.example.com:9999";
493 let id4 = calculate_node_id(long_addr);
494 assert!(id4 > 0);
495 }
496
497 #[test]
498 fn test_flownode_status_backward_compatible_without_workloads() {
499 let raw = r#"{
500 "peer":{"id":1,"addr":"127.0.0.1"},
501 "last_activity_ts":123,
502 "status":{"Flownode":{}},
503 "version":"",
504 "git_commit":"",
505 "start_time_ms":1,
506 "total_cpu_millicores":0,
507 "total_memory_bytes":0,
508 "cpu_usage_millicores":0,
509 "memory_usage_bytes":0,
510 "hostname":""
511 }"#;
512
513 let node_info: NodeInfo = raw.parse().unwrap();
514 assert_matches!(
515 node_info.status,
516 NodeStatus::Flownode(FlownodeStatus { workloads }) if workloads.types.is_empty()
517 );
518 }
519
520 #[test]
521 fn test_flownode_status_round_trip_with_workloads() {
522 let node_info = NodeInfo {
523 peer: Peer {
524 id: 1,
525 addr: "127.0.0.1".to_string(),
526 },
527 last_activity_ts: 123,
528 status: NodeStatus::Flownode(FlownodeStatus {
529 workloads: FlownodeWorkloads { types: vec![7] },
530 }),
531 version: "".to_string(),
532 git_commit: "".to_string(),
533 start_time_ms: 1,
534 total_cpu_millicores: 0,
535 total_memory_bytes: 0,
536 cpu_usage_millicores: 0,
537 memory_usage_bytes: 0,
538 hostname: "test_hostname".to_string(),
539 env_vars: Default::default(),
540 schedulable: None,
541 };
542
543 let node_info_bytes: Vec<u8> = node_info.try_into().unwrap();
544 let new_node_info: NodeInfo = node_info_bytes.try_into().unwrap();
545
546 assert_matches!(
547 new_node_info,
548 NodeInfo {
549 status: NodeStatus::Flownode(FlownodeStatus { workloads }),
550 ..
551 } if workloads.types == vec![7]
552 );
553 }
554
555 #[test]
556 fn test_node_info_backward_compatible_without_env_vars() {
557 let raw = r#"{
559 "peer":{"id":1,"addr":"127.0.0.1"},
560 "last_activity_ts":123,
561 "status":{"Datanode":{"rcus":0,"wcus":0,"leader_regions":0,"follower_regions":0,"workloads":{"types":[0]}}},
562 "version":"",
563 "git_commit":"",
564 "start_time_ms":1,
565 "total_cpu_millicores":0,
566 "total_memory_bytes":0,
567 "cpu_usage_millicores":0,
568 "memory_usage_bytes":0,
569 "hostname":"test"
570 }"#;
571
572 let node_info: NodeInfo = raw.parse().unwrap();
573 assert!(node_info.env_vars.is_empty());
574 }
575
576 #[test]
577 fn test_node_info_with_env_vars_round_trip() {
578 let mut env_vars = HashMap::new();
579 env_vars.insert("AZ".to_string(), "us-east-1a".to_string());
580
581 let node_info = NodeInfo {
582 peer: Peer {
583 id: 1,
584 addr: "127.0.0.1".to_string(),
585 },
586 last_activity_ts: 123,
587 status: NodeStatus::Datanode(DatanodeStatus {
588 rcus: 0,
589 wcus: 0,
590 leader_regions: 0,
591 follower_regions: 0,
592 workloads: DatanodeWorkloads { types: vec![] },
593 }),
594 version: "".to_string(),
595 git_commit: "".to_string(),
596 start_time_ms: 1,
597 total_cpu_millicores: 0,
598 total_memory_bytes: 0,
599 cpu_usage_millicores: 0,
600 memory_usage_bytes: 0,
601 hostname: "test".to_string(),
602 env_vars,
603 schedulable: None,
604 };
605
606 let node_info_bytes: Vec<u8> = node_info.try_into().unwrap();
607 let new_node_info: NodeInfo = node_info_bytes.try_into().unwrap();
608 assert_eq!(new_node_info.env_vars.get("AZ").unwrap(), "us-east-1a");
609 }
610}