Skip to content

Commit 04a652a

Browse files
committed
Addressed PR comments
1 parent 9b99ec9 commit 04a652a

5 files changed

Lines changed: 114 additions & 26 deletions

File tree

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,7 @@ def add_metric_reader(
599599
f"MetricReader {metric_reader} has been registered already!"
600600
)
601601
self._measurement_consumer.add_metric_reader(metric_reader)
602+
# pylint: disable-next=protected-access
602603
metric_reader._set_collect_callback(
603604
self._measurement_consumer.collect
604605
)
@@ -614,6 +615,7 @@ def remove_metric_reader(
614615
f"MetricReader {metric_reader} has not been registered!"
615616
)
616617
self._measurement_consumer.remove_metric_reader(metric_reader)
618+
# pylint: disable-next=protected-access
617619
metric_reader._set_collect_callback(None)
618620
metric_reader.shutdown()
619621
self._all_metric_readers.remove(metric_reader)

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/measurement_consumer.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,15 @@
1414

1515
# pylint: disable=unused-import
1616

17+
import weakref
1718
from abc import ABC, abstractmethod
1819
from threading import Lock
1920
from time import time_ns
20-
from typing import List, Mapping, Optional
21-
from weakref import WeakSet
21+
from typing import Iterable, List, Mapping, Optional
2222

2323
# This kind of import is needed to avoid Sphinx errors.
2424
import opentelemetry.sdk.metrics
2525
import opentelemetry.sdk.metrics._internal.instrument
26-
import opentelemetry.sdk.metrics._internal.sdk_configuration
2726
from opentelemetry.metrics._internal.instrument import CallbackOptions
2827
from opentelemetry.sdk.metrics._internal.exceptions import MetricsTimeoutError
2928
from opentelemetry.sdk.metrics._internal.measurement import Measurement
@@ -88,7 +87,7 @@ def consume_measurement(self, measurement: Measurement) -> None:
8887
)
8988
)
9089
with self._lock:
91-
reader_storages = WeakSet(self._reader_storages.values())
90+
reader_storages = weakref.WeakSet(self._reader_storages.values())
9291
for reader_storage in reader_storages:
9392
reader_storage.consume_measurement(
9493
measurement, should_sample_exemplar
@@ -154,7 +153,9 @@ def add_metric_reader(
154153
with self._lock:
155154
self._reader_storages[metric_reader] = MetricReaderStorage(
156155
self._sdk_config,
156+
# pylint: disable-next=protected-access
157157
metric_reader._instrument_class_temporality,
158+
# pylint: disable-next=protected-access
158159
metric_reader._instrument_class_aggregation,
159160
)
160161

opentelemetry-sdk/tests/metrics/test_measurement_consumer.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
# pylint: disable=invalid-name,no-self-use
1616

17+
from threading import Event, Thread
1718
from time import sleep
1819
from unittest import TestCase
1920
from unittest.mock import MagicMock, Mock, patch
@@ -193,3 +194,89 @@ def sleep_1(*args, **kwargs):
193194
callback_options_time_call,
194195
10000,
195196
)
197+
198+
199+
class TestSynchronousMeasurementConsumerConcurrency(TestCase):
200+
def test_concurrent_changes_to_metric_readers(self):
201+
timeout = 1
202+
failure = None
203+
iteration_started = Event()
204+
mutation_done = Event()
205+
iteration_timeout_error = "Timed out waiting for iteration to start"
206+
mutation_timeout_error = "Timed out waiting for mutation to be done"
207+
208+
consumer = SynchronousMeasurementConsumer(
209+
SdkConfiguration(
210+
exemplar_filter=MagicMock(),
211+
resource=MagicMock(),
212+
views=MagicMock(),
213+
),
214+
metric_readers=[MagicMock()],
215+
)
216+
217+
def _hooked_iter(iterable):
218+
nonlocal failure
219+
220+
iterable = iter(iterable)
221+
iteration_started.set()
222+
if not mutation_done.wait(timeout):
223+
failure = mutation_timeout_error
224+
yield next(iterable, None)
225+
yield from iterable
226+
227+
class HookedDict(dict):
228+
def __iter__(self):
229+
return _hooked_iter(super().__iter__())
230+
231+
def keys(self):
232+
return _hooked_iter(super().keys())
233+
234+
def values(self):
235+
return _hooked_iter(super().values())
236+
237+
def items(self):
238+
return _hooked_iter(super().items())
239+
240+
with patch.object(
241+
# pylint: disable-next=protected-access
242+
consumer, "_reader_storages", HookedDict(consumer._reader_storages)
243+
):
244+
245+
def mutate():
246+
"""Directly mutate _reader_storages after iteration starts"""
247+
nonlocal failure
248+
if not iteration_started.wait(timeout):
249+
failure = iteration_timeout_error
250+
#pylint: disable-next=protected-access
251+
consumer._reader_storages.clear()
252+
253+
# Verify that test setup works (direct mutation with no synchronization fails)
254+
with self.assertRaises(RuntimeError) as cm:
255+
t = Thread(target=mutate)
256+
t.start()
257+
try:
258+
consumer.consume_measurement(MagicMock())
259+
finally:
260+
t.join()
261+
self.assertEqual(
262+
"dictionary changed size during iteration", str(cm.exception)
263+
)
264+
265+
def add_and_remove_readers():
266+
"""Modifies _reader_storages after iteration starts"""
267+
nonlocal failure
268+
if not iteration_started.wait(timeout):
269+
failure = iteration_timeout_error
270+
reader = MagicMock()
271+
consumer.add_metric_reader(reader)
272+
consumer.remove_metric_reader(reader)
273+
274+
# Verify the API calls do not attempt concurrent modification of reader storages
275+
t = Thread(target=add_and_remove_readers)
276+
t.start()
277+
try:
278+
consumer.add_metric_reader(MagicMock())
279+
consumer.consume_measurement(MagicMock())
280+
finally:
281+
t.join()
282+
self.assertEqual(mutation_timeout_error, failure)

opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,6 @@ def test_race_collect_with_new_instruments(self):
280280
SdkConfiguration(
281281
exemplar_filter=Mock(),
282282
resource=Mock(),
283-
metric_readers=(),
284283
views=(View(instrument_name="test"),),
285284
),
286285
MagicMock(

opentelemetry-sdk/tests/metrics/test_metrics.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -429,34 +429,33 @@ def test_consume_measurement_gauge(self, mock_sync_measurement_consumer):
429429
sync_consumer_instance.consume_measurement.assert_called()
430430

431431
def test_addition_of_metric_reader(self):
432+
logger_name = "opentelemetry.sdk.metrics._internal.export"
433+
434+
reader = InMemoryMetricReader()
435+
meter_provider = MeterProvider()
436+
meter = meter_provider.get_meter(__name__)
437+
counter = meter.create_counter("counter")
438+
counter.add(1)
432439
# Suppress warnings for calling collect on an unregistered metric reader
433-
with self.assertLogs(
434-
"opentelemetry.sdk.metrics._internal.export", DEBUG
435-
):
436-
reader = InMemoryMetricReader()
437-
meter_provider = MeterProvider()
438-
meter = meter_provider.get_meter(__name__)
439-
counter = meter.create_counter("counter")
440-
counter.add(1)
440+
with self.assertLogs(logger_name, DEBUG):
441441
self.assertIsNone(reader.get_metrics_data())
442442

443-
meter_provider.add_metric_reader(reader)
444-
counter.add(1)
445-
self.assertIsNotNone(reader.get_metrics_data())
443+
meter_provider.add_metric_reader(reader)
444+
counter.add(1)
445+
self.assertIsNotNone(reader.get_metrics_data())
446446

447-
with self.assertRaises(ValueError) as cm:
448-
meter_provider.add_metric_reader(reader)
449-
self.assertIn(
450-
"has been registered already!", str(cm.exception)
451-
)
447+
with self.assertRaises(ValueError) as cm:
448+
meter_provider.add_metric_reader(reader)
449+
self.assertIn("has been registered already!", str(cm.exception))
452450

453-
meter_provider.remove_metric_reader(reader)
454-
counter.add(1)
451+
meter_provider.remove_metric_reader(reader)
452+
counter.add(1)
453+
with self.assertLogs(logger_name, DEBUG):
455454
self.assertIsNone(reader.get_metrics_data())
456455

457-
with self.assertRaises(ValueError) as cm:
458-
meter_provider.remove_metric_reader(reader)
459-
self.assertIn("has not been registered!", str(cm.exception))
456+
with self.assertRaises(ValueError) as cm:
457+
meter_provider.remove_metric_reader(reader)
458+
self.assertIn("has not been registered!", str(cm.exception))
460459

461460

462461
class TestMeterConcurrency(ConcurrencyTestBase, TestCase):

0 commit comments

Comments
 (0)