]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/telemetry: fix 500 error when generating telemetry report 70814/head
authorAnurag Raut <anuragtraut08@ibm.com>
Tue, 4 Aug 2026 05:50:51 +0000 (11:20 +0530)
committerAnurag Raut <anuragtraut08@ibm.com>
Tue, 4 Aug 2026 09:13:29 +0000 (14:43 +0530)
Fixes: https://tracker.ceph.com/issues/79003
Signed-off-by: Anurag Raut <anuragtraut08@ibm.com>
src/pybind/mgr/telemetry/module.py
src/pybind/mgr/telemetry/tests/test_telemetry.py

index ce834bb470c5a820f157095242a774d43b1c866d..fc48b92fd7796451b37b65c95747c7819766ed09 100644 (file)
@@ -27,6 +27,31 @@ from mgr_module import MgrModule, Option, OptionValue, ServiceInfoT
 
 ALL_CHANNELS = ['basic', 'ident', 'crash', 'device', 'perf']
 
+
+def _defaultdict_list() -> defaultdict:
+    return defaultdict(list)
+
+
+def _defaultdict_dict() -> defaultdict:
+    return defaultdict(dict)
+
+
+def _defaultdict_int() -> defaultdict:
+    return defaultdict(int)
+
+
+def _defaultdict_defaultdict_int() -> defaultdict:
+    return defaultdict(_defaultdict_int)
+
+
+def _defaultdict_defaultdict_defaultdict_int() -> defaultdict:
+    return defaultdict(_defaultdict_defaultdict_int)
+
+
+def _defaultdict_histogram() -> defaultdict:
+    """Six-level nested defaultdict(int) used by get_osd_histograms."""
+    return defaultdict(_defaultdict_defaultdict_defaultdict_int)
+
 LICENSE = 'sharing-1-0'
 LICENSE_NAME = 'Community Data License Agreement - Sharing - Version 1.0'
 LICENSE_URL = 'https://cdla.io/sharing-1-0/'
@@ -502,7 +527,7 @@ class Module(MgrModule):
         return  etype + '.' + m.hexdigest()
 
     def get_heap_stats(self) -> Dict[str, dict]:
-        result: Dict[str, dict] = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
+        result: Dict[str, dict] = defaultdict(_defaultdict_defaultdict_int)
         anonymized_daemons = {}
         osd_map = self.get('osd_map')
 
@@ -577,7 +602,7 @@ class Module(MgrModule):
         return parsed_output
 
     def get_mempool(self, mode: str = 'separated') -> Dict[str, dict]:
-        result: Dict[str, dict] = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
+        result: Dict[str, dict] = defaultdict(_defaultdict_defaultdict_int)
         anonymized_daemons = {}
         osd_map = self.get('osd_map')
 
@@ -633,11 +658,7 @@ class Module(MgrModule):
 
     def get_osd_histograms(self, mode: str = 'separated') -> List[Dict[str, dict]]:
         # Initialize result dict
-        result: Dict[str, dict] = defaultdict(lambda: defaultdict(
-                                              lambda: defaultdict(
-                                              lambda: defaultdict(
-                                              lambda: defaultdict(
-                                              lambda: defaultdict(int))))))
+        result: Dict[str, dict] = defaultdict(_defaultdict_histogram)
 
         # Get list of osd ids from the metadata
         osd_metadata = self.get('osd_metadata')
@@ -839,7 +860,7 @@ class Module(MgrModule):
         perf_counters = self.get_perf_counters()
 
         # Initialize 'result' dict
-        result: Dict[str, dict] = defaultdict(lambda: defaultdict(list))
+        result: Dict[str, dict] = defaultdict(_defaultdict_list)
 
         # 'separated' mode
         anonymized_daemon_dict = {}
@@ -878,7 +899,7 @@ class Module(MgrModule):
                 result[daemon][collection] = []
 
                 for sub_collection in sub_collection_list:
-                    sub_collection_result: Dict[str, dict] = defaultdict(lambda: defaultdict(dict))
+                    sub_collection_result: Dict[str, dict] = defaultdict(_defaultdict_dict)
                     sub_collection_result['labels'] = sub_collection['labels']
                     for sub_collection_counter_name, sub_collection_counter_info in sub_collection['counters'].items():
                         if mode == 'separated':
index 188d0efa8285199eb3c423e4e9c68866cf67aa0a..b77c674c8aa7b4fb9f807d5c122288131508406d 100644 (file)
@@ -1,6 +1,8 @@
 import json
+import pickle
 import pytest
 import unittest
+from collections import defaultdict
 from unittest import mock
 
 import telemetry
@@ -119,3 +121,22 @@ class TestTelemetry:
         assert m.is_opted_in() == expected['is_opted_in']
         assert m.is_enabled_collection(Collection.basic_base) == expected['is_enabled_collection']['basic_base']
         assert m.is_enabled_collection(Collection.basic_mds_metadata) == expected['is_enabled_collection']['basic_mds_metadata']
+
+    def test_defaultdict_helpers_are_picklable(self) -> None:
+        """Regression test for pickle serialization of defaultdict factories.
+
+        The telemetry report returned through mgr.remote() must be pickle-serializable.
+        Verify that the module-level defaultdict factories can be pickled.
+        """
+        factories = [
+            telemetry.module._defaultdict_list,
+            telemetry.module._defaultdict_dict,
+            telemetry.module._defaultdict_defaultdict_int,
+            telemetry.module._defaultdict_histogram,
+        ]
+
+        for factory in factories:
+            d = defaultdict(factory)
+            restored = pickle.loads(pickle.dumps(d))
+            assert restored is not None, \
+                f"defaultdict({factory.__name__}) failed pickle round-trip"