Skip to main content

common_meta/
cluster.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// [ClusterInfo] provides information about the cluster.
45#[async_trait::async_trait]
46pub trait ClusterInfo {
47    type Error: ErrorExt;
48
49    /// List all nodes by role in the cluster. If `role` is `None`, list all nodes.
50    async fn list_nodes(
51        &self,
52        role: Option<Role>,
53    ) -> std::result::Result<Vec<NodeInfo>, Self::Error>;
54
55    /// List all region stats in the cluster.
56    async fn list_region_stats(&self) -> std::result::Result<Vec<RegionStat>, Self::Error>;
57
58    /// List all flow stats in the cluster.
59    async fn list_flow_stats(&self) -> std::result::Result<Option<FlowStat>, Self::Error>;
60
61    // TODO(jeremy): Other info, like region status, etc.
62}
63
64/// The key of [NodeInfo] in the storage. The format is `__meta_cluster_node_info-0-{role}-{node_id}`.
65#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize, PartialOrd, Ord)]
66pub struct NodeInfoKey {
67    /// The role of the node. It can be `[Role::Datanode]` or `[Role::Frontend]`.
68    pub role: Role,
69    /// The node id.
70    pub node_id: u64,
71}
72
73impl NodeInfoKey {
74    /// Try to create a `NodeInfoKey` from a "good" heartbeat request. "good" as in every needed
75    /// piece of information is provided and valid.  
76    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            // Because the Frontend is stateless, it's too easy to neglect choosing a unique id
84            // for it when setting up a cluster. So we calculate its id from its address.
85            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
101/// Calculate (by using the DefaultHasher) the node's id from its address.
102fn calculate_node_id(addr: &str) -> u64 {
103    let mut hasher = DefaultHasher::new();
104    addr.hash(&mut hasher);
105    hasher.finish()
106}
107
108/// The information of a node in the cluster.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct NodeInfo {
111    /// The peer information. [node_id, address]
112    pub peer: Peer,
113    /// Last activity time in milliseconds.
114    pub last_activity_ts: i64,
115    /// The status of the node. Different roles have different node status.
116    pub status: NodeStatus,
117    // The node build version
118    pub version: String,
119    // The node build git commit hash
120    pub git_commit: String,
121    // The node star timestamp
122    pub start_time_ms: u64,
123    // The node build cpus
124    #[serde(default)]
125    pub total_cpu_millicores: i64,
126    // The node build memory bytes
127    #[serde(default)]
128    pub total_memory_bytes: i64,
129    // The node build cpu usage millicores
130    #[serde(default)]
131    pub cpu_usage_millicores: i64,
132    // The node build memory usage bytes
133    #[serde(default)]
134    pub memory_usage_bytes: i64,
135    // The node build hostname
136    #[serde(default)]
137    pub hostname: String,
138    /// Environment variables reported by the node.
139    #[serde(default)]
140    pub env_vars: HashMap<String, String>,
141    /// Whether metasrv may place new regions on this node.
142    ///
143    /// `None` means the flag was not resolved (deserialized heartbeat data, tests,
144    /// standalone); callers treat `None` as schedulable. Filled by the metasrv
145    /// discovery decorator, never by the heartbeat writer.
146    #[serde(skip)]
147    pub schedulable: Option<bool>,
148}
149
150impl NodeInfo {
151    /// Returns `false` only if the node is explicitly marked unschedulable.
152    ///
153    /// `None` (unresolved) counts as schedulable.
154    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    // Get the role name of the node status
178    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/// The status of a datanode.
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct DatanodeStatus {
192    /// The read capacity units during this period.
193    pub rcus: i64,
194    /// The write capacity units during this period.
195    pub wcus: i64,
196    /// How many leader regions on this node.
197    pub leader_regions: usize,
198    /// How many follower regions on this node.
199    pub follower_regions: usize,
200    /// The workloads of the datanode.
201    pub workloads: DatanodeWorkloads,
202}
203
204/// The status of a frontend.
205#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206pub struct FrontendStatus {
207    /// The workloads of the frontend.
208    #[serde(default)]
209    pub workloads: FrontendWorkloads,
210}
211
212/// The status of a flownode.
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct FlownodeStatus {
215    /// The workloads of the flownode.
216    #[serde(default)]
217    pub workloads: FlownodeWorkloads,
218}
219
220/// The status of a metasrv.
221#[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        // Test empty string
478        assert_eq!(calculate_node_id(""), calculate_node_id(""));
479
480        // Test same addresses return same ids
481        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        // Test different addresses return different ids
487        let addr2 = "127.0.0.1:8081";
488        let id3 = calculate_node_id(addr2);
489        assert_ne!(id1, id3);
490
491        // Test long address
492        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        // Simulate a NodeInfo serialized before env_vars was added
558        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}