|
14 | 14 |
|
15 | 15 | # pylint: disable=invalid-name,no-self-use |
16 | 16 |
|
| 17 | +from threading import Event, Thread |
17 | 18 | from time import sleep |
18 | 19 | from unittest import TestCase |
19 | 20 | from unittest.mock import MagicMock, Mock, patch |
@@ -193,3 +194,89 @@ def sleep_1(*args, **kwargs): |
193 | 194 | callback_options_time_call, |
194 | 195 | 10000, |
195 | 196 | ) |
| 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) |
0 commit comments