-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Fix tracing exporter reuse after shutdown #4711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dc24347
74ce034
4aef966
0aa75d0
0a5d79f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,6 +85,8 @@ def __init__( | |
| self.base_delay = base_delay | ||
| self.max_delay = max_delay | ||
| self._shutdown_event = threading.Event() | ||
| self._shutdown_generation = 0 | ||
| self._shutdown_lock = threading.Lock() | ||
|
|
||
| # Keep a client open for connection pooling across multiple export calls | ||
| self._client = httpx2.Client(timeout=httpx2.Timeout(timeout=60, connect=5.0)) | ||
|
|
@@ -534,8 +536,17 @@ def close(self): | |
| """Close the underlying HTTP client.""" | ||
| self._client.close() | ||
|
|
||
| def _request_shutdown(self) -> None: | ||
| self._shutdown_event.set() | ||
| def _request_shutdown(self) -> int: | ||
| with self._shutdown_lock: | ||
| self._shutdown_generation += 1 | ||
| generation = self._shutdown_generation | ||
| self._shutdown_event.set() | ||
| return generation | ||
|
|
||
| def _reset_shutdown(self, generation: int) -> None: | ||
| with self._shutdown_lock: | ||
| if generation == self._shutdown_generation: | ||
| self._shutdown_event.clear() | ||
|
|
||
|
|
||
| class BatchTraceProcessor(TracingProcessor): | ||
|
|
@@ -580,6 +591,7 @@ def __init__( | |
| self._thread_start_lock = threading.Lock() | ||
| self._export_lock = threading.Lock() | ||
| self._shutdown_deadline: float | None = None | ||
| self._exporter_shutdown_generation: int | None = None | ||
|
|
||
| def _ensure_thread_started(self) -> None: | ||
| # Fast path without holding the lock | ||
|
|
@@ -628,7 +640,7 @@ def shutdown(self, timeout: float | None = None): | |
| if timeout is not None: | ||
| request_exporter_shutdown = getattr(self._exporter, "_request_shutdown", None) | ||
| if callable(request_exporter_shutdown): | ||
| request_exporter_shutdown() | ||
| self._exporter_shutdown_generation = request_exporter_shutdown() | ||
|
|
||
| deadline = None if timeout is None else time.monotonic() + timeout | ||
| self._shutdown_deadline = deadline | ||
|
|
@@ -640,32 +652,50 @@ def shutdown(self, timeout: float | None = None): | |
| logger.warning( | ||
| "[non-fatal] Tracing: shutdown timeout reached; dropping queued traces." | ||
| ) | ||
| else: | ||
|
Comment on lines
652
to
+655
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When AGENTS.md reference: AGENTS.md:L149-L149 Useful? React with 👍 / 👎. |
||
| self._reset_exporter_shutdown() | ||
| else: | ||
| # No background thread: process any remaining items synchronously. | ||
| self._export_batches(deadline=deadline) | ||
| try: | ||
| self._export_batches(deadline=deadline) | ||
| finally: | ||
| self._reset_exporter_shutdown() | ||
|
|
||
| def force_flush(self): | ||
| """ | ||
| Forces an immediate flush of all queued spans. | ||
| """ | ||
| self._export_batches() | ||
|
|
||
| def _reset_exporter_shutdown(self) -> None: | ||
| if self._exporter_shutdown_generation is None: | ||
| return | ||
| reset_exporter_shutdown = getattr(self._exporter, "_reset_shutdown", None) | ||
| if callable(reset_exporter_shutdown): | ||
| reset_exporter_shutdown(self._exporter_shutdown_generation) | ||
|
|
||
| def _run(self): | ||
| while not self._shutdown_event.is_set(): | ||
| current_time = time.monotonic() | ||
| queue_size = self._queue.qsize() | ||
|
|
||
| # If it's time for a scheduled flush or queue is above the trigger threshold | ||
| if current_time >= self._next_export_time or queue_size >= self._export_trigger_size: | ||
| self._export_batches() | ||
| # Reset the next scheduled flush time | ||
| self._next_export_time = time.monotonic() + self._schedule_delay | ||
| else: | ||
| # Sleep a short interval so we don't busy-wait. | ||
| time.sleep(0.2) | ||
| try: | ||
| while not self._shutdown_event.is_set(): | ||
| current_time = time.monotonic() | ||
| queue_size = self._queue.qsize() | ||
|
|
||
| # If it's time for a scheduled flush or queue is above the trigger threshold | ||
| if ( | ||
| current_time >= self._next_export_time | ||
| or queue_size >= self._export_trigger_size | ||
| ): | ||
| self._export_batches() | ||
| # Reset the next scheduled flush time | ||
| self._next_export_time = time.monotonic() + self._schedule_delay | ||
| else: | ||
| # Sleep a short interval so we don't busy-wait. | ||
| time.sleep(0.2) | ||
|
|
||
| # Final drain after shutdown | ||
| self._export_batches(deadline=self._shutdown_deadline) | ||
| # Final drain after shutdown | ||
| self._export_batches(deadline=self._shutdown_deadline) | ||
| finally: | ||
| self._reset_exporter_shutdown() | ||
|
|
||
| def _export_batches(self, deadline: float | None = None): | ||
| """Drains the queue and exports in batches of up to `max_batch_size` until the queue | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a timed-out processor still has an export blocked, shutting down a replacement processor with no worker creates a newer generation and immediately resets it here, clearing the shared event even though the older shutdown remains active. If the old request later returns a retryable response, its deadline-less export resumes retries after shutdown. The fresh evidence in this revision is that the generation equality check prevents stale resets but does not track outstanding older shutdown owners; keep cancellation set until all active owners finish, or scope cancellation per export generation.
AGENTS.md reference: AGENTS.md:L149-L149
Useful? React with 👍 / 👎.