Skip to content

Commit 584cf9d

Browse files
NodeMemoryFootprint#breakdown should be an Option<T>
because the metrics won't be available in some cases, e.g. early in node boot.
1 parent 0959086 commit 584cf9d

File tree

5 files changed

+266
-5
lines changed

5 files changed

+266
-5
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
## v0.59.0 (in development)
44

5-
No changes yet.
5+
### Enhancements
6+
7+
* `NodeMemoryFootprint#breakdown` is now an `Option<NodeMemoryBreakdown>` to handle cases
8+
where the node memory breakdown stats are not yet available.
9+
10+
### Bug Fixes
11+
12+
* `NodeMemoryTotals#max` now correctly compares all three memory totals (RSS, allocated, and used by runtime) instead of comparing RSS twice.
613

714
## v0.58.0 (Sep 23, 2025)
815

src/responses/cluster.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,33 @@ use crate::commons::SupportedProtocol;
1919
use crate::formatting::*;
2020
use crate::responses::{PluginList, parameters::GlobalRuntimeParameterValue, permissions::TagMap};
2121
use crate::utils::{percentage, percentage_as_text};
22-
use serde::Deserialize;
22+
use serde::{Deserialize, Deserializer};
2323
use serde_aux::prelude::*;
2424
use serde_json::Map;
2525

2626
#[cfg(feature = "tabled")]
2727
use tabled::Tabled;
2828

29+
fn deserialize_memory_breakdown<'de, D>(
30+
deserializer: D,
31+
) -> Result<Option<NodeMemoryBreakdown>, D::Error>
32+
where
33+
D: Deserializer<'de>,
34+
{
35+
use serde::de::Error;
36+
use serde_json::Value;
37+
38+
let value = Value::deserialize(deserializer)?;
39+
40+
match value {
41+
Value::String(s) if s == "not_available" => Ok(None),
42+
_ => {
43+
let breakdown = NodeMemoryBreakdown::deserialize(value).map_err(D::Error::custom)?;
44+
Ok(Some(breakdown))
45+
}
46+
}
47+
}
48+
2949
#[derive(Debug, Deserialize, Clone, PartialEq, Eq, Hash)]
3050
pub struct NodeList(Vec<String>);
3151

@@ -84,8 +104,9 @@ impl IntoIterator for NodeList {
84104
#[cfg_attr(feature = "tabled", derive(Tabled))]
85105
#[allow(dead_code)]
86106
pub struct NodeMemoryFootprint {
87-
#[serde(rename = "memory")]
88-
pub breakdown: NodeMemoryBreakdown,
107+
#[serde(rename = "memory", deserialize_with = "deserialize_memory_breakdown")]
108+
#[cfg_attr(feature = "tabled", tabled(display = "display_option"))]
109+
pub breakdown: Option<NodeMemoryBreakdown>,
89110
}
90111

91112
#[derive(Debug, Deserialize, Clone)]
@@ -102,7 +123,10 @@ impl NodeMemoryTotals {
102123
/// Returns the greatest value between the totals computed
103124
/// using different mechanisms (RSS, runtime allocator metrics)
104125
pub fn max(&self) -> u64 {
105-
std::cmp::max(std::cmp::max(self.used_by_runtime, self.rss), self.rss)
126+
std::cmp::max(
127+
std::cmp::max(self.used_by_runtime, self.rss),
128+
self.allocated,
129+
)
106130
}
107131
}
108132

tests/async_node_tests.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,28 @@ async fn test_async_get_node_info() {
3939
assert!(node.uptime >= 1);
4040
assert!(node.total_erlang_processes >= 1);
4141
}
42+
43+
#[tokio::test]
44+
async fn test_async_get_node_memory_footprint() {
45+
let endpoint = endpoint();
46+
let rc = Client::new(&endpoint, USERNAME, PASSWORD);
47+
let nodes = rc.list_nodes().await.unwrap();
48+
let name = nodes.first().unwrap().name.clone();
49+
let result = rc.get_node_memory_footprint(&name).await;
50+
51+
assert!(result.is_ok());
52+
let footprint = result.unwrap();
53+
54+
// In some cases (e.g. early in node boot), these metrics won't yet be available.
55+
match footprint.breakdown {
56+
Some(breakdown) => {
57+
assert!(breakdown.total.rss > 0);
58+
assert!(breakdown.total.allocated > 0);
59+
assert!(breakdown.total.used_by_runtime > 0);
60+
assert!(!breakdown.calculation_strategy.is_empty());
61+
}
62+
None => {
63+
// OK
64+
}
65+
}
66+
}

tests/blocking_node_tests.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,28 @@ fn test_blocking_get_node_info() {
3939
assert!(node.uptime >= 1);
4040
assert!(node.total_erlang_processes >= 1);
4141
}
42+
43+
#[test]
44+
fn test_blocking_get_node_memory_footprint() {
45+
let endpoint = endpoint();
46+
let rc = Client::new(&endpoint, USERNAME, PASSWORD);
47+
let nodes = rc.list_nodes().unwrap();
48+
let name = nodes.first().unwrap().name.clone();
49+
let result = rc.get_node_memory_footprint(&name);
50+
51+
assert!(result.is_ok());
52+
let footprint = result.unwrap();
53+
54+
// In some cases (e.g. early in node boot), these metrics won't yet be available.
55+
match footprint.breakdown {
56+
Some(breakdown) => {
57+
assert!(breakdown.total.rss > 0);
58+
assert!(breakdown.total.allocated > 0);
59+
assert!(breakdown.total.used_by_runtime > 0);
60+
assert!(!breakdown.calculation_strategy.is_empty());
61+
}
62+
None => {
63+
// OK
64+
}
65+
}
66+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (C) 2023-2025 RabbitMQ Core Team ([email protected])
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+
15+
mod test_helpers;
16+
17+
use rabbitmq_http_client::responses::cluster::{
18+
NodeMemoryBreakdown, NodeMemoryFootprint, NodeMemoryTotals,
19+
};
20+
use serde_json::json;
21+
22+
#[test]
23+
fn test_unit_node_memory_footprint_with_breakdown() {
24+
let json = json!({
25+
"memory": {
26+
"connection_readers": 1000,
27+
"connection_writers": 2000,
28+
"connection_channels": 3000,
29+
"connection_other": 4000,
30+
"queue_procs": 5000,
31+
"quorum_queue_procs": 6000,
32+
"stream_queue_procs": 7000,
33+
"stream_queue_replica_reader_procs": 8000,
34+
"stream_queue_coordinator_procs": 9000,
35+
"plugins": 10000,
36+
"metadata_store": 11000,
37+
"other_proc": 12000,
38+
"metrics": 13000,
39+
"mgmt_db": 14000,
40+
"mnesia": 15000,
41+
"quorum_ets": 16000,
42+
"metadata_store_ets": 17000,
43+
"other_ets": 18000,
44+
"binary": 19000,
45+
"msg_index": 20000,
46+
"code": 21000,
47+
"atom": 22000,
48+
"other_system": 23000,
49+
"allocated_unused": 24000,
50+
"reserved_unallocated": 25000,
51+
"strategy": "allocated",
52+
"total": {
53+
"rss": 100000,
54+
"allocated": 200000,
55+
"erlang": 150000
56+
}
57+
}
58+
});
59+
60+
let footprint: NodeMemoryFootprint = serde_json::from_value(json).unwrap();
61+
62+
assert!(footprint.breakdown.is_some());
63+
let breakdown = footprint.breakdown.unwrap();
64+
65+
assert_eq!(breakdown.connection_readers, 1000);
66+
assert_eq!(breakdown.connection_writers, 2000);
67+
assert_eq!(breakdown.connection_channels, 3000);
68+
assert_eq!(breakdown.connection_other, 4000);
69+
assert_eq!(breakdown.classic_queue_procs, 5000);
70+
assert_eq!(breakdown.quorum_queue_procs, 6000);
71+
assert_eq!(breakdown.stream_queue_procs, 7000);
72+
assert_eq!(breakdown.stream_queue_replica_reader_procs, 8000);
73+
assert_eq!(breakdown.stream_queue_coordinator_procs, 9000);
74+
assert_eq!(breakdown.plugins, 10000);
75+
assert_eq!(breakdown.metadata_store, 11000);
76+
assert_eq!(breakdown.other_procs, 12000);
77+
assert_eq!(breakdown.metrics, 13000);
78+
assert_eq!(breakdown.management_db, 14000);
79+
assert_eq!(breakdown.mnesia, 15000);
80+
assert_eq!(breakdown.quorum_queue_ets_tables, 16000);
81+
assert_eq!(breakdown.metadata_store_ets_tables, 17000);
82+
assert_eq!(breakdown.other_ets_tables, 18000);
83+
assert_eq!(breakdown.binary_heap, 19000);
84+
assert_eq!(breakdown.message_indices, 20000);
85+
assert_eq!(breakdown.code, 21000);
86+
assert_eq!(breakdown.atom_table, 22000);
87+
assert_eq!(breakdown.other_system, 23000);
88+
assert_eq!(breakdown.allocated_but_unused, 24000);
89+
assert_eq!(breakdown.reserved_but_unallocated, 25000);
90+
assert_eq!(breakdown.calculation_strategy, "allocated");
91+
92+
assert_eq!(breakdown.total.rss, 100000);
93+
assert_eq!(breakdown.total.allocated, 200000);
94+
assert_eq!(breakdown.total.used_by_runtime, 150000);
95+
}
96+
97+
#[test]
98+
fn test_unit_node_memory_footprint_not_available() {
99+
let json = json!({
100+
"memory": "not_available"
101+
});
102+
103+
let footprint: NodeMemoryFootprint = serde_json::from_value(json).unwrap();
104+
105+
assert!(footprint.breakdown.is_none());
106+
}
107+
108+
#[test]
109+
fn test_unit_node_memory_footprint_invalid_string() {
110+
let json = json!({
111+
"memory": "some_other_string"
112+
});
113+
114+
let result: Result<NodeMemoryFootprint, _> = serde_json::from_value(json);
115+
116+
// Should fail to deserialize when the string is not "not_available"
117+
assert!(result.is_err());
118+
}
119+
120+
#[test]
121+
fn test_unit_node_memory_breakdown_grand_total() {
122+
let total = NodeMemoryTotals {
123+
rss: 100000,
124+
allocated: 200000,
125+
used_by_runtime: 150000,
126+
};
127+
128+
let breakdown = NodeMemoryBreakdown {
129+
connection_readers: 1000,
130+
connection_writers: 2000,
131+
connection_channels: 3000,
132+
connection_other: 4000,
133+
classic_queue_procs: 5000,
134+
quorum_queue_procs: 6000,
135+
stream_queue_procs: 7000,
136+
stream_queue_replica_reader_procs: 8000,
137+
stream_queue_coordinator_procs: 9000,
138+
plugins: 10000,
139+
metadata_store: 11000,
140+
other_procs: 12000,
141+
metrics: 13000,
142+
management_db: 14000,
143+
mnesia: 15000,
144+
quorum_queue_ets_tables: 16000,
145+
metadata_store_ets_tables: 17000,
146+
other_ets_tables: 18000,
147+
binary_heap: 19000,
148+
message_indices: 20000,
149+
code: 21000,
150+
atom_table: 22000,
151+
other_system: 23000,
152+
allocated_but_unused: 24000,
153+
reserved_but_unallocated: 25000,
154+
calculation_strategy: "allocated".to_string(),
155+
total,
156+
};
157+
158+
// grand_total should return the maximum of all totals
159+
assert_eq!(breakdown.grand_total(), 200000);
160+
}
161+
162+
#[test]
163+
fn test_unit_node_memory_totals_max() {
164+
let totals = NodeMemoryTotals {
165+
rss: 100000,
166+
allocated: 200000,
167+
used_by_runtime: 150000,
168+
};
169+
170+
// max should return the maximum value
171+
assert_eq!(totals.max(), 200000);
172+
173+
let totals2 = NodeMemoryTotals {
174+
rss: 300000,
175+
allocated: 200000,
176+
used_by_runtime: 150000,
177+
};
178+
179+
assert_eq!(totals2.max(), 300000);
180+
}

0 commit comments

Comments
 (0)