Skip to content

ValueError: empty range for randrange() — data race in SimpleFixedSizeExemplarReservoir between offer() and collect() #5431

Description

@deepanshi-s

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions