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::fmt::{Display, Formatter};
16use std::hash::{DefaultHasher, Hash, Hasher};
17use std::str::FromStr;
18
19use api::v1::meta::{DatanodeWorkloads, FlownodeWorkloads, HeartbeatRequest};
20use common_error::ext::ErrorExt;
21use lazy_static::lazy_static;
22use regex::Regex;
23use serde::{Deserialize, Serialize};
24use snafu::{OptionExt, ResultExt, ensure};
25
26use crate::datanode::RegionStat;
27use crate::error::{
28    DecodeJsonSnafu, EncodeJsonSnafu, Error, FromUtf8Snafu, InvalidNodeInfoKeySnafu,
29    InvalidRoleSnafu, ParseNumSnafu, Result,
30};
31use crate::key::flow::flow_state::FlowStat;
32use crate::peer::Peer;
33
34const CLUSTER_NODE_INFO_PREFIX: &str = "__meta_cluster_node_info";
35
36lazy_static! {
37    static ref CLUSTER_NODE_INFO_PREFIX_PATTERN: Regex = Regex::new(&format!(
38        "^{CLUSTER_NODE_INFO_PREFIX}-([0-9]+)-([0-9]+)-([0-9]+)$"
39    ))
40    .unwrap();
41}
42
43/// [ClusterInfo] provides information about the cluster.
44#[async_trait::async_trait]
45pub trait ClusterInfo {
46    type Error: ErrorExt;
47
48    /// List all nodes by role in the cluster. If `role` is `None`, list all nodes.
49    async fn list_nodes(
50        &self,
51        role: Option<Role>,
52    ) -> std::result::Result<Vec<NodeInfo>, Self::Error>;
53
54    /// List all region stats in the cluster.
55    async fn list_region_stats(&self) -> std::result::Result<Vec<RegionStat>, Self::Error>;
56
57    /// List all flow stats in the cluster.
58    async fn list_flow_stats(&self) -> std::result::Result<Option<FlowStat>, Self::Error>;
59
60    // TODO(jeremy): Other info, like region status, etc.
61}
62
63/// The key of [NodeInfo] in the storage. The format is `__meta_cluster_node_info-0-{role}-{node_id}`.
64#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize, PartialOrd, Ord)]
65pub struct NodeInfoKey {
66    /// The role of the node. It can be `[Role::Datanode]` or `[Role::Frontend]`.
67    pub role: Role,
68    /// The node id.
69    pub node_id: u64,
70}
71
72impl NodeInfoKey {
73    /// Try to create a `NodeInfoKey` from a "good" heartbeat request. "good" as in every needed
74    /// piece of information is provided and valid.  
75    pub fn new(request: &HeartbeatRequest) -> Option<Self> {
76        let HeartbeatRequest { header, peer, .. } = request;
77        let header = header.as_ref()?;
78        let peer = peer.as_ref()?;
79
80        let role = header.role.try_into().ok()?;
81        let node_id = match role {
82            // Because the Frontend is stateless, it's too easy to neglect choosing a unique id
83            // for it when setting up a cluster. So we calculate its id from its address.
84            Role::Frontend => calculate_node_id(&peer.addr),
85            _ => peer.id,
86        };
87
88        Some(NodeInfoKey { role, node_id })
89    }
90
91    pub fn key_prefix() -> String {
92        format!("{}-0-", CLUSTER_NODE_INFO_PREFIX)
93    }
94
95    pub fn key_prefix_with_role(role: Role) -> String {
96        format!("{}-0-{}-", CLUSTER_NODE_INFO_PREFIX, i32::from(role))
97    }
98}
99
100/// Calculate (by using the DefaultHasher) the node's id from its address.
101fn calculate_node_id(addr: &str) -> u64 {
102    let mut hasher = DefaultHasher::new();
103    addr.hash(&mut hasher);
104    hasher.finish()
105}
106
107/// The information of a node in the cluster.
108#[derive(Debug, Serialize, Deserialize)]
109pub struct NodeInfo {
110    /// The peer information. [node_id, address]
111    pub peer: Peer,
112    /// Last activity time in milliseconds.
113    pub last_activity_ts: i64,
114    /// The status of the node. Different roles have different node status.
115    pub status: NodeStatus,
116    // The node build version
117    pub version: String,
118    // The node build git commit hash
119    pub git_commit: String,
120    // The node star timestamp
121    pub start_time_ms: u64,
122    // The node build cpus
123    #[serde(default)]
124    pub total_cpu_millicores: i64,
125    // The node build memory bytes
126    #[serde(default)]
127    pub total_memory_bytes: i64,
128    // The node build cpu usage millicores
129    #[serde(default)]
130    pub cpu_usage_millicores: i64,
131    // The node build memory usage bytes
132    #[serde(default)]
133    pub memory_usage_bytes: i64,
134    // The node build hostname
135    #[serde(default)]
136    pub hostname: String,
137}
138
139#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize, PartialOrd, Ord)]
140pub enum Role {
141    Datanode,
142    Frontend,
143    Flownode,
144    Metasrv,
145}
146
147#[derive(Debug, Serialize, Deserialize)]
148pub enum NodeStatus {
149    Datanode(DatanodeStatus),
150    Frontend(FrontendStatus),
151    Flownode(FlownodeStatus),
152    Metasrv(MetasrvStatus),
153    Standalone,
154}
155
156impl NodeStatus {
157    // Get the role name of the node status
158    pub fn role_name(&self) -> &str {
159        match self {
160            NodeStatus::Datanode(_) => "DATANODE",
161            NodeStatus::Frontend(_) => "FRONTEND",
162            NodeStatus::Flownode(_) => "FLOWNODE",
163            NodeStatus::Metasrv(_) => "METASRV",
164            NodeStatus::Standalone => "STANDALONE",
165        }
166    }
167}
168
169/// The status of a datanode.
170#[derive(Debug, Serialize, Deserialize)]
171pub struct DatanodeStatus {
172    /// The read capacity units during this period.
173    pub rcus: i64,
174    /// The write capacity units during this period.
175    pub wcus: i64,
176    /// How many leader regions on this node.
177    pub leader_regions: usize,
178    /// How many follower regions on this node.
179    pub follower_regions: usize,
180    /// The workloads of the datanode.
181    pub workloads: DatanodeWorkloads,
182}
183
184/// The status of a frontend.
185#[derive(Debug, Serialize, Deserialize)]
186pub struct FrontendStatus {}
187
188/// The status of a flownode.
189#[derive(Debug, Serialize, Deserialize)]
190pub struct FlownodeStatus {
191    /// The workloads of the flownode.
192    #[serde(default)]
193    pub workloads: FlownodeWorkloads,
194}
195
196/// The status of a metasrv.
197#[derive(Debug, Serialize, Deserialize)]
198pub struct MetasrvStatus {
199    pub is_leader: bool,
200}
201
202impl FromStr for NodeInfoKey {
203    type Err = Error;
204
205    fn from_str(key: &str) -> Result<Self> {
206        let caps = CLUSTER_NODE_INFO_PREFIX_PATTERN
207            .captures(key)
208            .context(InvalidNodeInfoKeySnafu { key })?;
209        ensure!(caps.len() == 4, InvalidNodeInfoKeySnafu { key });
210
211        let role = caps[2].to_string();
212        let node_id = caps[3].to_string();
213        let role: i32 = role.parse().context(ParseNumSnafu {
214            err_msg: format!("invalid role {role}"),
215        })?;
216        let role = Role::try_from(role)?;
217        let node_id: u64 = node_id.parse().context(ParseNumSnafu {
218            err_msg: format!("invalid node_id: {node_id}"),
219        })?;
220
221        Ok(Self { role, node_id })
222    }
223}
224
225impl TryFrom<Vec<u8>> for NodeInfoKey {
226    type Error = Error;
227
228    fn try_from(bytes: Vec<u8>) -> Result<Self> {
229        String::from_utf8(bytes)
230            .context(FromUtf8Snafu {
231                name: "NodeInfoKey",
232            })
233            .map(|x| x.parse())?
234    }
235}
236
237impl From<&NodeInfoKey> for Vec<u8> {
238    fn from(key: &NodeInfoKey) -> Self {
239        format!(
240            "{}-0-{}-{}",
241            CLUSTER_NODE_INFO_PREFIX,
242            i32::from(key.role),
243            key.node_id
244        )
245        .into_bytes()
246    }
247}
248
249impl Display for NodeInfoKey {
250    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
251        write!(f, "{:?}-{}", self.role, self.node_id)
252    }
253}
254
255impl FromStr for NodeInfo {
256    type Err = Error;
257
258    fn from_str(value: &str) -> Result<Self> {
259        serde_json::from_str(value).context(DecodeJsonSnafu)
260    }
261}
262
263impl TryFrom<Vec<u8>> for NodeInfo {
264    type Error = Error;
265
266    fn try_from(bytes: Vec<u8>) -> Result<Self> {
267        String::from_utf8(bytes)
268            .context(FromUtf8Snafu { name: "NodeInfo" })
269            .map(|x| x.parse())?
270    }
271}
272
273impl TryFrom<NodeInfo> for Vec<u8> {
274    type Error = Error;
275
276    fn try_from(info: NodeInfo) -> Result<Self> {
277        Ok(serde_json::to_string(&info)
278            .context(EncodeJsonSnafu)?
279            .into_bytes())
280    }
281}
282
283impl From<Role> for i32 {
284    fn from(role: Role) -> Self {
285        match role {
286            Role::Datanode => 0,
287            Role::Frontend => 1,
288            Role::Flownode => 2,
289            Role::Metasrv => 99,
290        }
291    }
292}
293
294impl TryFrom<i32> for Role {
295    type Error = Error;
296
297    fn try_from(role: i32) -> Result<Self> {
298        match role {
299            0 => Ok(Self::Datanode),
300            1 => Ok(Self::Frontend),
301            2 => Ok(Self::Flownode),
302            99 => Ok(Self::Metasrv),
303            _ => InvalidRoleSnafu { role }.fail(),
304        }
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use std::assert_matches;
311
312    use common_workload::DatanodeWorkloadType;
313
314    use super::*;
315    use crate::cluster::Role::{Datanode, Frontend};
316    use crate::cluster::{DatanodeStatus, FlownodeStatus, NodeInfo, NodeInfoKey, NodeStatus};
317    use crate::peer::Peer;
318
319    #[test]
320    fn test_node_info_key_round_trip() {
321        let key = NodeInfoKey {
322            role: Datanode,
323            node_id: 2,
324        };
325
326        let key_bytes: Vec<u8> = (&key).into();
327        let new_key: NodeInfoKey = key_bytes.try_into().unwrap();
328
329        assert_eq!(Datanode, new_key.role);
330        assert_eq!(2, new_key.node_id);
331    }
332
333    #[test]
334    fn test_node_info_round_trip() {
335        let node_info = NodeInfo {
336            peer: Peer {
337                id: 1,
338                addr: "127.0.0.1".to_string(),
339            },
340            last_activity_ts: 123,
341            status: NodeStatus::Datanode(DatanodeStatus {
342                rcus: 1,
343                wcus: 2,
344                leader_regions: 3,
345                follower_regions: 4,
346                workloads: DatanodeWorkloads {
347                    types: vec![DatanodeWorkloadType::Hybrid.to_i32()],
348                },
349            }),
350            version: "".to_string(),
351            git_commit: "".to_string(),
352            start_time_ms: 1,
353            total_cpu_millicores: 0,
354            total_memory_bytes: 0,
355            cpu_usage_millicores: 0,
356            memory_usage_bytes: 0,
357            hostname: "test_hostname".to_string(),
358        };
359
360        let node_info_bytes: Vec<u8> = node_info.try_into().unwrap();
361        let new_node_info: NodeInfo = node_info_bytes.try_into().unwrap();
362
363        assert_matches!(
364            new_node_info,
365            NodeInfo {
366                peer: Peer { id: 1, .. },
367                last_activity_ts: 123,
368                status: NodeStatus::Datanode(DatanodeStatus {
369                    rcus: 1,
370                    wcus: 2,
371                    leader_regions: 3,
372                    follower_regions: 4,
373                    ..
374                }),
375                start_time_ms: 1,
376                ..
377            }
378        );
379    }
380
381    #[test]
382    fn test_node_info_key_prefix() {
383        let prefix = NodeInfoKey::key_prefix();
384        assert_eq!(prefix, "__meta_cluster_node_info-0-");
385
386        let prefix = NodeInfoKey::key_prefix_with_role(Frontend);
387        assert_eq!(prefix, "__meta_cluster_node_info-0-1-");
388    }
389
390    #[test]
391    fn test_calculate_node_id_from_addr() {
392        // Test empty string
393        assert_eq!(calculate_node_id(""), calculate_node_id(""));
394
395        // Test same addresses return same ids
396        let addr1 = "127.0.0.1:8080";
397        let id1 = calculate_node_id(addr1);
398        let id2 = calculate_node_id(addr1);
399        assert_eq!(id1, id2);
400
401        // Test different addresses return different ids
402        let addr2 = "127.0.0.1:8081";
403        let id3 = calculate_node_id(addr2);
404        assert_ne!(id1, id3);
405
406        // Test long address
407        let long_addr = "very.long.domain.name.example.com:9999";
408        let id4 = calculate_node_id(long_addr);
409        assert!(id4 > 0);
410    }
411
412    #[test]
413    fn test_flownode_status_backward_compatible_without_workloads() {
414        let raw = r#"{
415            "peer":{"id":1,"addr":"127.0.0.1"},
416            "last_activity_ts":123,
417            "status":{"Flownode":{}},
418            "version":"",
419            "git_commit":"",
420            "start_time_ms":1,
421            "total_cpu_millicores":0,
422            "total_memory_bytes":0,
423            "cpu_usage_millicores":0,
424            "memory_usage_bytes":0,
425            "hostname":""
426        }"#;
427
428        let node_info: NodeInfo = raw.parse().unwrap();
429        assert_matches!(
430            node_info.status,
431            NodeStatus::Flownode(FlownodeStatus { workloads }) if workloads.types.is_empty()
432        );
433    }
434
435    #[test]
436    fn test_flownode_status_round_trip_with_workloads() {
437        let node_info = NodeInfo {
438            peer: Peer {
439                id: 1,
440                addr: "127.0.0.1".to_string(),
441            },
442            last_activity_ts: 123,
443            status: NodeStatus::Flownode(FlownodeStatus {
444                workloads: FlownodeWorkloads { types: vec![7] },
445            }),
446            version: "".to_string(),
447            git_commit: "".to_string(),
448            start_time_ms: 1,
449            total_cpu_millicores: 0,
450            total_memory_bytes: 0,
451            cpu_usage_millicores: 0,
452            memory_usage_bytes: 0,
453            hostname: "test_hostname".to_string(),
454        };
455
456        let node_info_bytes: Vec<u8> = node_info.try_into().unwrap();
457        let new_node_info: NodeInfo = node_info_bytes.try_into().unwrap();
458
459        assert_matches!(
460            new_node_info,
461            NodeInfo {
462                status: NodeStatus::Flownode(FlownodeStatus { workloads }),
463                ..
464            } if workloads.types == vec![7]
465        );
466    }
467}