Skip to content

Commit bb028af

Browse files
committed
Add concurrency tests for meter/instrument methods
1 parent f2c6757 commit bb028af

3 files changed

Lines changed: 131 additions & 6 deletions

File tree

opentelemetry-api/tests/metrics/test_instruments.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
# type: ignore
1515

16+
import threading
1617
from inspect import Signature, isabstract, signature
1718
from unittest import TestCase
1819

@@ -32,8 +33,6 @@
3233
_Gauge,
3334
)
3435

35-
# FIXME Test that the instrument methods can be called concurrently safely.
36-
3736

3837
class ChildInstrument(Instrument):
3938
# pylint: disable=useless-parent-delegation
@@ -724,3 +723,44 @@ def test_description_check(self):
724723
],
725724
"",
726725
)
726+
727+
728+
class TestConcurrency(TestCase):
729+
def _run_concurrently(self, fn, num_threads=10, calls_per_thread=100):
730+
"""Helper: run fn concurrently across threads and assert no exceptions."""
731+
errors = []
732+
733+
def worker():
734+
try:
735+
for _ in range(calls_per_thread):
736+
fn()
737+
except Exception as exc: # pylint: disable=broad-except
738+
errors.append(exc)
739+
740+
threads = [threading.Thread(target=worker) for _ in range(num_threads)]
741+
for t in threads:
742+
t.start()
743+
for t in threads:
744+
t.join()
745+
746+
self.assertEqual([], errors)
747+
748+
def test_counter_add_concurrent(self):
749+
"""Test that Counter.add can be called concurrently safely."""
750+
counter = NoOpCounter("name")
751+
self._run_concurrently(lambda: counter.add(1))
752+
753+
def test_up_down_counter_add_concurrent(self):
754+
"""Test that UpDownCounter.add can be called concurrently safely."""
755+
up_down_counter = NoOpUpDownCounter("name")
756+
self._run_concurrently(lambda: up_down_counter.add(1))
757+
758+
def test_histogram_record_concurrent(self):
759+
"""Test that Histogram.record can be called concurrently safely."""
760+
histogram = NoOpHistogram("name")
761+
self._run_concurrently(lambda: histogram.record(1))
762+
763+
def test_gauge_set_concurrent(self):
764+
"""Test that Gauge.set can be called concurrently safely."""
765+
gauge = NoOpMeter("name").create_gauge("name")
766+
self._run_concurrently(lambda: gauge.set(1))

opentelemetry-api/tests/metrics/test_meter.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@
1313
# limitations under the License.
1414
# type: ignore
1515

16+
import threading
1617
from logging import WARNING
1718
from unittest import TestCase
1819
from unittest.mock import Mock, patch
1920

2021
from opentelemetry.metrics import Meter, NoOpMeter
2122

22-
# FIXME Test that the meter methods can be called concurrently safely.
23-
2423

2524
class ChildMeter(Meter):
2625
# pylint: disable=signature-differs
@@ -195,3 +194,59 @@ def test_create_observable_up_down_counter(self):
195194
self.assertTrue(
196195
Meter.create_observable_up_down_counter.__isabstractmethod__
197196
)
197+
198+
199+
class TestConcurrency(TestCase):
200+
def _run_concurrently(self, fn, num_threads=10, calls_per_thread=100):
201+
"""Helper: run fn concurrently across threads and assert no exceptions."""
202+
errors = []
203+
204+
def worker():
205+
try:
206+
for _ in range(calls_per_thread):
207+
fn()
208+
except Exception as exc: # pylint: disable=broad-except
209+
errors.append(exc)
210+
211+
threads = [threading.Thread(target=worker) for _ in range(num_threads)]
212+
for t in threads:
213+
t.start()
214+
for t in threads:
215+
t.join()
216+
217+
self.assertEqual([], errors)
218+
219+
def test_create_counter_concurrent(self):
220+
"""Test that Meter.create_counter can be called concurrently safely."""
221+
meter = NoOpMeter("name")
222+
self._run_concurrently(lambda: meter.create_counter("counter"))
223+
224+
def test_create_up_down_counter_concurrent(self):
225+
"""Test that Meter.create_up_down_counter can be called concurrently safely."""
226+
meter = NoOpMeter("name")
227+
self._run_concurrently(lambda: meter.create_up_down_counter("up_down_counter"))
228+
229+
def test_create_observable_counter_concurrent(self):
230+
"""Test that Meter.create_observable_counter can be called concurrently safely."""
231+
meter = NoOpMeter("name")
232+
self._run_concurrently(lambda: meter.create_observable_counter("observable_counter", lambda options: []))
233+
234+
def test_create_histogram_concurrent(self):
235+
"""Test that Meter.create_histogram can be called concurrently safely."""
236+
meter = NoOpMeter("name")
237+
self._run_concurrently(lambda: meter.create_histogram("histogram"))
238+
239+
def test_create_gauge_concurrent(self):
240+
"""Test that Meter.create_gauge can be called concurrently safely."""
241+
meter = NoOpMeter("name")
242+
self._run_concurrently(lambda: meter.create_gauge("gauge"))
243+
244+
def test_create_observable_gauge_concurrent(self):
245+
"""Test that Meter.create_observable_gauge can be called concurrently safely."""
246+
meter = NoOpMeter("name")
247+
self._run_concurrently(lambda: meter.create_observable_gauge("observable_gauge", lambda options: []))
248+
249+
def test_create_observable_up_down_counter_concurrent(self):
250+
"""Test that Meter.create_observable_up_down_counter can be called concurrently safely."""
251+
meter = NoOpMeter("name")
252+
self._run_concurrently(lambda: meter.create_observable_up_down_counter("observable_up_down_counter", lambda options: []))

opentelemetry-api/tests/metrics/test_meter_provider.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
# pylint: disable=protected-access
1717

18+
import threading
1819
from unittest import TestCase
1920
from unittest.mock import Mock, patch
2021

@@ -48,8 +49,6 @@
4849
reset_metrics_globals,
4950
)
5051

51-
# FIXME Test that the instrument methods can be called concurrently safely.
52-
5352

5453
@fixture
5554
def reset_meter_provider():
@@ -165,6 +164,37 @@ def test_get_meter_wrapper(self):
165164
self.assertEqual(meter.schema_url, "schema_url")
166165

167166

167+
class TestConcurrency(TestCase):
168+
def _run_concurrently(self, fn, num_threads=10, calls_per_thread=100):
169+
"""Helper: run fn concurrently across threads and assert no exceptions."""
170+
errors = []
171+
172+
def worker():
173+
try:
174+
for _ in range(calls_per_thread):
175+
fn()
176+
except Exception as exc: # pylint: disable=broad-except
177+
errors.append(exc)
178+
179+
threads = [threading.Thread(target=worker) for _ in range(num_threads)]
180+
for t in threads:
181+
t.start()
182+
for t in threads:
183+
t.join()
184+
185+
self.assertEqual([], errors)
186+
187+
def test_no_op_meter_provider_get_meter_concurrent(self):
188+
"""Test that NoOpMeterProvider.get_meter can be called concurrently safely."""
189+
meter_provider = NoOpMeterProvider()
190+
self._run_concurrently(lambda: meter_provider.get_meter("name"))
191+
192+
def test_proxy_meter_provider_get_meter_concurrent(self):
193+
"""Test that _ProxyMeterProvider.get_meter can be called concurrently safely."""
194+
meter_provider = _ProxyMeterProvider()
195+
self._run_concurrently(lambda: meter_provider.get_meter("name"))
196+
197+
168198
class TestProxy(MetricsGlobalsTest, TestCase):
169199
def test_global_proxy_meter_provider(self):
170200
# Global get_meter_provider() should initially be a _ProxyMeterProvider

0 commit comments

Comments
 (0)