|
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | 15 | import abc |
| 16 | +import gc |
| 17 | +import multiprocessing |
| 18 | +import os |
16 | 19 | import time |
17 | 20 | import typing |
18 | 21 | import unittest |
| 22 | +import weakref |
19 | 23 | from platform import python_implementation, system |
20 | 24 | from threading import Event |
21 | 25 | from typing import Optional |
|
26 | 30 | from opentelemetry import trace as trace_api |
27 | 31 | from opentelemetry.context import Context |
28 | 32 | from opentelemetry.sdk import trace |
| 33 | +from opentelemetry.sdk.trace.export import SimpleSpanProcessor |
| 34 | +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( |
| 35 | + InMemorySpanExporter, |
| 36 | +) |
29 | 37 |
|
30 | 38 |
|
31 | 39 | def span_event_start_fmt(span_processor_name, span_name): |
@@ -486,3 +494,66 @@ def test_force_flush_late_by_span_processor(self): |
486 | 494 | for mock_processor in mocks: |
487 | 495 | self.assertEqual(1, mock_processor.force_flush.call_count) |
488 | 496 | multi_processor.shutdown() |
| 497 | + |
| 498 | + def test_processor_gc(self): |
| 499 | + multi_processor = trace.ConcurrentMultiSpanProcessor(5) |
| 500 | + weak_ref = weakref.ref(multi_processor) |
| 501 | + multi_processor.shutdown() |
| 502 | + |
| 503 | + # When the processor is garbage collected |
| 504 | + del multi_processor |
| 505 | + gc.collect() |
| 506 | + |
| 507 | + # Then the reference to the processor should no longer exist |
| 508 | + self.assertIsNone( |
| 509 | + weak_ref(), |
| 510 | + "The ConcurrentMultiSpanProcessor object created by this test wasn't garbage collected", |
| 511 | + ) |
| 512 | + |
| 513 | + @unittest.skipUnless(hasattr(os, "fork"), "needs *nix") |
| 514 | + def test_batch_span_processor_fork(self): |
| 515 | + multiprocessing_context = multiprocessing.get_context("fork") |
| 516 | + tracer_provider = trace.TracerProvider() |
| 517 | + tracer = tracer_provider.get_tracer(__name__) |
| 518 | + exporter = InMemorySpanExporter() |
| 519 | + multi_processor = trace.ConcurrentMultiSpanProcessor(2) |
| 520 | + multi_processor.add_span_processor(SimpleSpanProcessor(exporter)) |
| 521 | + tracer_provider.add_span_processor(multi_processor) |
| 522 | + |
| 523 | + # Use the ConcurrentMultiSpanProcessor in the main process. |
| 524 | + # This is necessary in this test to start using the underlying ThreadPoolExecutor and avoid false positive: |
| 525 | + with tracer.start_as_current_span("main process before fork span"): |
| 526 | + pass |
| 527 | + assert ( |
| 528 | + exporter.get_finished_spans()[-1].name |
| 529 | + == "main process before fork span" |
| 530 | + ) |
| 531 | + |
| 532 | + # The forked ConcurrentMultiSpanProcessor is usable in the child process: |
| 533 | + def child(conn): |
| 534 | + with tracer.start_as_current_span("child process span"): |
| 535 | + pass |
| 536 | + conn.send(exporter.get_finished_spans()[-1].name) |
| 537 | + conn.close() |
| 538 | + |
| 539 | + parent_conn, child_conn = multiprocessing_context.Pipe() |
| 540 | + process = multiprocessing_context.Process( |
| 541 | + target=child, args=(child_conn,) |
| 542 | + ) |
| 543 | + process.start() |
| 544 | + has_response = parent_conn.poll(timeout=5) |
| 545 | + if not has_response: |
| 546 | + process.kill() |
| 547 | + self.fail( |
| 548 | + "The child process did not send any message after 5 seconds, it's very probably locked" |
| 549 | + ) |
| 550 | + process.join(timeout=5) |
| 551 | + assert parent_conn.recv() == "child process span" |
| 552 | + |
| 553 | + # The ConcurrentMultiSpanProcessor is still usable in the main process after the child process termination: |
| 554 | + with tracer.start_as_current_span("main process after fork span"): |
| 555 | + pass |
| 556 | + assert ( |
| 557 | + exporter.get_finished_spans()[-1].name |
| 558 | + == "main process after fork span" |
| 559 | + ) |
0 commit comments