Skip to content

Commit 40c82e3

Browse files
committed
perf: use time.time_ns() in MonotonicTimestampGenerator for precision and integer arithmetic
Replace int(time.time() * 1e6) with time.time_ns() // 1000 to avoid float precision loss for timestamps far from epoch. Remove unnecessary lock acquisition in __init__ (no other thread can see the object yet). Use integer literal 1_000_000 instead of float 1e6 in _maybe_warn threshold/interval comparisons. Update tests to mock time_ns and use nanosecond input values.
1 parent 8071a81 commit 40c82e3

2 files changed

Lines changed: 47 additions & 46 deletions

File tree

cassandra/timestamps.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@
2323

2424
log = logging.getLogger(__name__)
2525

26+
2627
class MonotonicTimestampGenerator(object):
2728
"""
28-
An object that, when called, returns ``int(time.time() * 1e6)`` when
29-
possible, but, if the value returned by ``time.time`` doesn't increase,
29+
An object that, when called, returns ``time.time_ns() // 1000`` when
30+
possible, but, if the value returned by ``time.time_ns`` doesn't increase,
3031
drifts into the future and logs warnings.
3132
Exposed configuration attributes can be configured with arguments to
3233
``__init__`` or by changing attributes on an initialized object.
@@ -55,9 +56,8 @@ class MonotonicTimestampGenerator(object):
5556

5657
def __init__(self, warn_on_drift=True, warning_threshold=1, warning_interval=1):
5758
self.lock = Lock()
58-
with self.lock:
59-
self.last = 0
60-
self._last_warn = 0
59+
self.last = 0
60+
self._last_warn = 0
6161
self.warn_on_drift = warn_on_drift
6262
self.warning_threshold = warning_threshold
6363
self.warning_interval = warning_interval
@@ -88,22 +88,25 @@ def __call__(self):
8888
internally to _next_timestamp.
8989
"""
9090
with self.lock:
91-
return self._next_timestamp(now=int(time.time() * 1e6),
92-
last=self.last)
91+
return self._next_timestamp(now=time.time_ns() // 1000, last=self.last)
9392

9493
def _maybe_warn(self, now):
9594
# should be called from inside the self.lock.
9695
diff = self.last - now
9796
since_last_warn = now - self._last_warn
9897

99-
warn = (self.warn_on_drift and
100-
(diff >= self.warning_threshold * 1e6) and
101-
(since_last_warn >= self.warning_interval * 1e6))
98+
warn = (
99+
self.warn_on_drift
100+
and (diff >= self.warning_threshold * 1_000_000)
101+
and (since_last_warn >= self.warning_interval * 1_000_000)
102+
)
102103
if warn:
103104
log.warning(
104105
"Clock skew detected: current tick ({now}) was {diff} "
105106
"microseconds behind the last generated timestamp "
106107
"({last}), returned timestamps will be artificially "
107108
"incremented to guarantee monotonicity.".format(
108-
now=now, diff=diff, last=self.last))
109+
now=now, diff=diff, last=self.last
110+
)
111+
)
109112
self._last_warn = now

tests/unit/test_timestamps.py

Lines changed: 33 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,39 @@
2222

2323

2424
class _TimestampTestMixin(object):
25-
26-
@mock.patch('cassandra.timestamps.time')
27-
def _call_and_check_results(self,
28-
patched_time_module,
29-
system_time_expected_stamp_pairs,
30-
timestamp_generator=None):
25+
@mock.patch("cassandra.timestamps.time")
26+
def _call_and_check_results(
27+
self,
28+
patched_time_module,
29+
system_time_expected_stamp_pairs,
30+
timestamp_generator=None,
31+
):
3132
"""
32-
For each element in an iterable of (system_time, expected_timestamp)
33+
For each element in an iterable of (system_time_ns, expected_timestamp)
3334
pairs, call a :class:`cassandra.timestamps.MonotonicTimestampGenerator`
34-
with system_times as the underlying time.time() result, then assert
35+
with system_times as the underlying time.time_ns() result, then assert
3536
that the result is expected_timestamp. Skips the check if
3637
expected_timestamp is None.
3738
"""
38-
patched_time_module.time = mock.Mock()
39+
patched_time_module.time_ns = mock.Mock()
3940
system_times, expected_timestamps = zip(*system_time_expected_stamp_pairs)
4041

41-
patched_time_module.time.side_effect = system_times
42+
patched_time_module.time_ns.side_effect = system_times
4243
tsg = timestamp_generator or timestamps.MonotonicTimestampGenerator()
4344

4445
for expected in expected_timestamps:
4546
actual = tsg()
4647
if expected is not None:
4748
assert actual == expected
4849

49-
# assert we patched timestamps.time.time correctly
50+
# assert we patched timestamps.time.time_ns correctly
5051
with pytest.raises(StopIteration):
5152
tsg()
5253

5354

5455
class TestTimestampGeneratorOutput(unittest.TestCase, _TimestampTestMixin):
5556
"""
56-
Mock time.time and test the output of MonotonicTimestampGenerator.__call__
57+
Mock time.time_ns and test the output of MonotonicTimestampGenerator.__call__
5758
given different patterns of changing results.
5859
"""
5960

@@ -71,10 +72,11 @@ def test_timestamps_during_and_after_same_system_time(self):
7172
"""
7273
self._call_and_check_results(
7374
system_time_expected_stamp_pairs=(
74-
(15.0, 15 * 1e6),
75-
(15.0, 15 * 1e6 + 1),
76-
(15.0, 15 * 1e6 + 2),
77-
(15.01, 15.01 * 1e6))
75+
(15_000_000_000, 15_000_000),
76+
(15_000_000_000, 15_000_001),
77+
(15_000_000_000, 15_000_002),
78+
(15_010_000_000, 15_010_000),
79+
)
7880
)
7981

8082
def test_timestamps_during_and_after_backwards_system_time(self):
@@ -87,18 +89,18 @@ def test_timestamps_during_and_after_backwards_system_time(self):
8789
"""
8890
self._call_and_check_results(
8991
system_time_expected_stamp_pairs=(
90-
(15.0, 15 * 1e6),
91-
(13.0, 15 * 1e6 + 1),
92-
(14.0, 15 * 1e6 + 2),
93-
(13.5, 15 * 1e6 + 3),
94-
(15.01, 15.01 * 1e6))
92+
(15_000_000_000, 15_000_000),
93+
(13_000_000_000, 15_000_001),
94+
(14_000_000_000, 15_000_002),
95+
(13_500_000_000, 15_000_003),
96+
(15_010_000_000, 15_010_000),
97+
)
9598
)
9699

97100

98101
class TestTimestampGeneratorLogging(unittest.TestCase):
99-
100102
def setUp(self):
101-
self.log_patcher = mock.patch('cassandra.timestamps.log')
103+
self.log_patcher = mock.patch("cassandra.timestamps.log")
102104
self.addCleanup(self.log_patcher.stop)
103105
self.patched_timestamp_log = self.log_patcher.start()
104106

@@ -119,10 +121,9 @@ def test_basic_log_content(self):
119121
@test_category timing
120122
"""
121123
tsg = timestamps.MonotonicTimestampGenerator(
122-
warning_threshold=1e-6,
123-
warning_interval=1e-6
124+
warning_threshold=1e-6, warning_interval=1e-6
124125
)
125-
#The units of _last_warn is seconds
126+
# The units of _last_warn is seconds
126127
tsg._last_warn = 12
127128

128129
tsg._next_timestamp(20, tsg.last)
@@ -132,7 +133,7 @@ def test_basic_log_content(self):
132133
assert len(self.patched_timestamp_log.warning.call_args_list) == 1
133134
self.assertLastCallArgRegex(
134135
self.patched_timestamp_log.warning.call_args,
135-
r'Clock skew detected:.*\b16\b.*\b4\b.*\b20\b'
136+
r"Clock skew detected:.*\b16\b.*\b4\b.*\b20\b",
136137
)
137138

138139
def test_disable_logging(self):
@@ -179,8 +180,7 @@ def test_warning_threshold_respected_logs(self):
179180
@test_category timing
180181
"""
181182
tsg = timestamps.MonotonicTimestampGenerator(
182-
warning_threshold=1e-6,
183-
warning_interval=1e-6
183+
warning_threshold=1e-6, warning_interval=1e-6
184184
)
185185
tsg.last, tsg._last_warn = 100, 97
186186
tsg._next_timestamp(98, tsg.last)
@@ -197,8 +197,7 @@ def test_warning_interval_respected_no_logging(self):
197197
@test_category timing
198198
"""
199199
tsg = timestamps.MonotonicTimestampGenerator(
200-
warning_threshold=1e-6,
201-
warning_interval=2e-6
200+
warning_threshold=1e-6, warning_interval=2e-6
202201
)
203202
tsg.last = 100
204203
tsg._next_timestamp(70, tsg.last)
@@ -231,7 +230,6 @@ def test_warning_interval_respected_logs(self):
231230

232231

233232
class TestTimestampGeneratorMultipleThreads(unittest.TestCase):
234-
235233
def test_should_generate_incrementing_timestamps_for_all_threads(self):
236234
"""
237235
Tests when time is "stopped", values are assigned incrementally
@@ -251,13 +249,13 @@ def request_time():
251249
generated_timestamps.append(timestamp)
252250

253251
tsg = timestamps.MonotonicTimestampGenerator()
254-
fixed_time = 1
252+
fixed_time_ns = 1_000_000_000
255253
num_threads = 5
256254

257255
timestamp_to_generate = 1000
258256
generated_timestamps = []
259257

260-
with mock.patch('time.time', new=mock.Mock(return_value=fixed_time)):
258+
with mock.patch.object(timestamps.time, "time_ns", return_value=fixed_time_ns):
261259
threads = []
262260
for _ in range(num_threads):
263261
threads.append(Thread(target=request_time))
@@ -270,4 +268,4 @@ def request_time():
270268

271269
assert len(generated_timestamps) == num_threads * timestamp_to_generate
272270
for i, timestamp in enumerate(sorted(generated_timestamps)):
273-
assert int(i + 1e6) == timestamp
271+
assert i + 1_000_000 == timestamp

0 commit comments

Comments
 (0)