Describe your environment
opentelemetry-sdk: 1.40.0
Python: 3.9 (reproduces reliably); does not reproduce on 3.12 due to GIL switch-interval differences
OS: Linux
What happened?
With the default trace_based exemplar filter, recording a metric inside a sampled span while a PeriodicExportingMetricReader runs its collection cycle intermittently crashes the recording thread:
File ".../sdk/metrics/_internal/exemplar/exemplar_reservoir.py", line 279, in _find_bucket_index
index = randrange(0, self._measurements_seen)
ValueError: empty range for randrange() (0, 0, 0)
Root cause
SimpleFixedSizeExemplarReservoir._find_bucket_index is unsynchronized:
self._measurements_seen += 1
index = randrange(0, self._measurements_seen)
The exporter thread calls collect() → _reset(), setting _measurements_seen = 0 between those two lines. offer()/collect() on the reservoir share _measurements_seen with no lock, so the recording thread executes randrange(0, 0). In an application this exception propagates out of counter.add() and can kill the calling thread.
Steps to Reproduce
import threading
from opentelemetry.context import Context
from opentelemetry.sdk.metrics._internal.exemplar import (
SimpleFixedSizeExemplarReservoir,
)
res = SimpleFixedSizeExemplarReservoir()
stop = threading.Event()
def offerer():
ctx = Context()
while not stop.is_set():
res.offer(0, 0, {}, ctx)
def collector():
while not stop.is_set():
res.collect({})
t = threading.Thread(target=offerer)
c = threading.Thread(target=collector, daemon=True)
t.start()
c.start()
t.join() # ValueError: empty range for randrange() (0, 0, 0)
Expected Result
Concurrent offer() and collect() on an exemplar reservoir should be safe. Metric recording (counter.add(), histogram.record()) must never raise as a result of a race with the SDK's own export thread.
Actual Result
The recording thread raises ValueError: empty range for randrange() (0, 0, 0) from inside _find_bucket_index. Because it originates in the metrics recording path, the exception propagates out of counter.add() into application code and crashes the calling thread.
Additional context
Guard the reservoir's offer / _find_bucket_index / collect / _reset with a lock, or make _find_bucket_index tolerant of _measurements_seen == 0.
Would you like to implement a fix?
No response
Tip
No response
Describe your environment
opentelemetry-sdk: 1.40.0
Python: 3.9 (reproduces reliably); does not reproduce on 3.12 due to GIL switch-interval differences
OS: Linux
What happened?
With the default
trace_basedexemplar filter, recording a metric inside a sampled span while aPeriodicExportingMetricReaderruns its collection cycle intermittently crashes the recording thread:Root cause
SimpleFixedSizeExemplarReservoir._find_bucket_indexis unsynchronized:The exporter thread calls
collect()→_reset(), setting_measurements_seen = 0between those two lines.offer()/collect()on the reservoir share_measurements_seenwith no lock, so the recording thread executesrandrange(0, 0). In an application this exception propagates out ofcounter.add()and can kill the calling thread.Steps to Reproduce
Expected Result
Concurrent
offer()andcollect()on an exemplar reservoir should be safe. Metric recording (counter.add(),histogram.record()) must never raise as a result of a race with the SDK's own export thread.Actual Result
The recording thread raises
ValueError: empty range for randrange() (0, 0, 0)from inside_find_bucket_index. Because it originates in the metrics recording path, the exception propagates out ofcounter.add()into application code and crashes the calling thread.Additional context
Guard the reservoir's
offer/_find_bucket_index/collect/_resetwith a lock, or make_find_bucket_indextolerant of_measurements_seen == 0.Would you like to implement a fix?
No response
Tip
No response