Skip to content

Commit e2894fb

Browse files
committed
Fix IntStat comparison operators and metrics cleanup on shutdown
- Add comparison operators (__eq__, __ne__, __lt__, __le__, __gt__, __ge__) to IntStat class for direct comparison with integers - Add __hash__ method to IntStat for use in sets/dicts - Add Metrics.shutdown() method to remove stats from global registry - Call metrics.shutdown() from Cluster.shutdown() to prevent stale weakref errors when accessing getStats() after cluster shutdown - Fix test using 'is' instead of '==' for IntStat comparison - Add unit tests for new comparison operators and shutdown functionality
1 parent ea73376 commit e2894fb

4 files changed

Lines changed: 130 additions & 2 deletions

File tree

cassandra/cluster.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,6 +1795,9 @@ def shutdown(self):
17951795

17961796
self.executor.shutdown()
17971797

1798+
if self.metrics_enabled and self.metrics:
1799+
self.metrics.shutdown()
1800+
17981801
_discard_cluster_shutdown(self)
17991802

18001803
def __enter__(self):

cassandra/metrics.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,37 @@ def __iadd__(self, other):
6161
def __int__(self):
6262
return self._value
6363

64+
def __eq__(self, other):
65+
if isinstance(other, IntStat):
66+
return self._value == other._value
67+
return self._value == other
68+
69+
def __ne__(self, other):
70+
return not self.__eq__(other)
71+
72+
def __lt__(self, other):
73+
if isinstance(other, IntStat):
74+
return self._value < other._value
75+
return self._value < other
76+
77+
def __le__(self, other):
78+
if isinstance(other, IntStat):
79+
return self._value <= other._value
80+
return self._value <= other
81+
82+
def __gt__(self, other):
83+
if isinstance(other, IntStat):
84+
return self._value > other._value
85+
return self._value > other
86+
87+
def __ge__(self, other):
88+
if isinstance(other, IntStat):
89+
return self._value >= other._value
90+
return self._value >= other
91+
92+
def __hash__(self):
93+
return hash(self._value)
94+
6495
def __repr__(self):
6596
return f"IntStat({self.name}={self._value})"
6697

@@ -477,3 +508,15 @@ def set_stats_name(self, stats_name):
477508
del _stats_registry[self.stats_name]
478509
self.stats_name = stats_name
479510
_stats_registry[self.stats_name] = stats
511+
512+
def shutdown(self):
513+
"""
514+
Remove this metrics instance from the global registry.
515+
Called when the cluster is shutdown to prevent stale references.
516+
"""
517+
with _registry_lock:
518+
if self.stats_name in _stats_registry:
519+
del _stats_registry[self.stats_name]
520+
# Also clean up the legacy 'cassandra' entry if it points to our stats
521+
if _stats_registry.get('cassandra') is self.stats:
522+
del _stats_registry['cassandra']

tests/integration/standard/test_metrics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,10 +355,10 @@ def setUpClass(cls):
355355
def wait_for_count(self, ra, expected_count, error=False):
356356
for _ in range(10):
357357
if not error:
358-
if ra.successful is expected_count:
358+
if ra.successful == expected_count:
359359
return True
360360
else:
361-
if ra.errors is expected_count:
361+
if ra.errors == expected_count:
362362
return True
363363
time.sleep(.01)
364364
return False

tests/unit/test_metrics.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,35 @@ def test_repr(self):
6363
stat += 42
6464
self.assertEqual(repr(stat), "IntStat(my_counter=42)")
6565

66+
def test_equality(self):
67+
stat = IntStat('test')
68+
stat += 5
69+
self.assertEqual(stat, 5)
70+
self.assertEqual(5, stat)
71+
self.assertNotEqual(stat, 3)
72+
self.assertNotEqual(stat, 10)
73+
74+
def test_comparison_operators(self):
75+
stat = IntStat('test')
76+
stat += 5
77+
self.assertTrue(stat > 0)
78+
self.assertTrue(stat >= 5)
79+
self.assertTrue(stat < 10)
80+
self.assertTrue(stat <= 5)
81+
self.assertFalse(stat > 5)
82+
self.assertFalse(stat < 5)
83+
84+
def test_comparison_with_intstat(self):
85+
stat1 = IntStat('test1')
86+
stat2 = IntStat('test2')
87+
stat1 += 5
88+
stat2 += 5
89+
self.assertEqual(stat1, stat2)
90+
stat2 += 1
91+
self.assertNotEqual(stat1, stat2)
92+
self.assertTrue(stat1 < stat2)
93+
self.assertTrue(stat2 > stat1)
94+
6695

6796
class StatTest(unittest.TestCase):
6897
"""Tests for Stat (gauge) class."""
@@ -300,5 +329,58 @@ def __init__(self):
300329
self.assertNotIn('/test_path', _stats_registry)
301330

302331

332+
class MetricsShutdownTest(unittest.TestCase):
333+
"""Tests for Metrics shutdown functionality."""
334+
335+
def setUp(self):
336+
# Clean up registry before each test
337+
with _registry_lock:
338+
keys_to_remove = [k for k in _stats_registry.keys()
339+
if k.startswith('cassandra')]
340+
for k in keys_to_remove:
341+
del _stats_registry[k]
342+
343+
def test_shutdown_removes_from_registry(self):
344+
import weakref
345+
from cassandra.metrics import Metrics
346+
347+
class MockCluster:
348+
def __init__(self):
349+
self.metadata = type('obj', (object,), {'all_hosts': lambda: []})()
350+
self.sessions = []
351+
352+
cluster = MockCluster()
353+
proxy = weakref.proxy(cluster)
354+
355+
metrics = Metrics(proxy)
356+
stats_name = metrics.stats_name
357+
358+
with _registry_lock:
359+
self.assertIn(stats_name, _stats_registry)
360+
361+
metrics.shutdown()
362+
363+
with _registry_lock:
364+
self.assertNotIn(stats_name, _stats_registry)
365+
366+
def test_shutdown_is_idempotent(self):
367+
import weakref
368+
from cassandra.metrics import Metrics
369+
370+
class MockCluster:
371+
def __init__(self):
372+
self.metadata = type('obj', (object,), {'all_hosts': lambda: []})()
373+
self.sessions = []
374+
375+
cluster = MockCluster()
376+
proxy = weakref.proxy(cluster)
377+
378+
metrics = Metrics(proxy)
379+
380+
# Should not raise even if called multiple times
381+
metrics.shutdown()
382+
metrics.shutdown()
383+
384+
303385
if __name__ == '__main__':
304386
unittest.main()

0 commit comments

Comments
 (0)