-
Notifications
You must be signed in to change notification settings - Fork 828
Improve BatchExportSpanProcessor #1062
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 2 commits
5bacc13
8f71459
6ce570e
6795892
fc15079
cd60321
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -20,8 +20,7 @@ | |||||
| import typing | ||||||
| from enum import Enum | ||||||
|
|
||||||
| from opentelemetry.context import attach, detach, get_current, set_value | ||||||
| from opentelemetry.trace import DefaultSpan | ||||||
| from opentelemetry.context import attach, detach, set_value | ||||||
| from opentelemetry.util import time_ns | ||||||
|
|
||||||
| from .. import Span, SpanProcessor | ||||||
|
|
@@ -91,15 +90,23 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: | |||||
| return True | ||||||
|
|
||||||
|
|
||||||
| class _FlushRequest: | ||||||
| """Represents a request for the BatchExportSpanProcessor to flush spans.""" | ||||||
|
|
||||||
| __slots__ = ["event", "num_spans"] | ||||||
|
|
||||||
| def __init__(self): | ||||||
| self.event = threading.Event() | ||||||
| self.num_spans = 0 | ||||||
|
|
||||||
|
|
||||||
| class BatchExportSpanProcessor(SpanProcessor): | ||||||
| """Batch span processor implementation. | ||||||
|
|
||||||
| BatchExportSpanProcessor is an implementation of `SpanProcessor` that | ||||||
| batches ended spans and pushes them to the configured `SpanExporter`. | ||||||
| """ | ||||||
|
|
||||||
| _FLUSH_TOKEN_SPAN = DefaultSpan(context=None) | ||||||
|
|
||||||
| def __init__( | ||||||
| self, | ||||||
| span_exporter: SpanExporter, | ||||||
|
|
@@ -129,9 +136,7 @@ def __init__( | |||||
| ) # type: typing.Deque[Span] | ||||||
| self.worker_thread = threading.Thread(target=self.worker, daemon=True) | ||||||
| self.condition = threading.Condition(threading.Lock()) | ||||||
| self.flush_condition = threading.Condition(threading.Lock()) | ||||||
| # flag to indicate that there is a flush operation on progress | ||||||
| self._flushing = False | ||||||
| self._flush_request = None # type: typing.Optional[_FlushRequest] | ||||||
| self.schedule_delay_millis = schedule_delay_millis | ||||||
| self.max_export_batch_size = max_export_batch_size | ||||||
| self.max_queue_size = max_queue_size | ||||||
|
|
@@ -164,60 +169,128 @@ def on_end(self, span: Span) -> None: | |||||
|
|
||||||
| def worker(self): | ||||||
| timeout = self.schedule_delay_millis / 1e3 | ||||||
| flush_request = None # type: typing.Optional[_FlushRequest] | ||||||
| while not self.done: | ||||||
| if ( | ||||||
| len(self.queue) < self.max_export_batch_size | ||||||
| and not self._flushing | ||||||
| ): | ||||||
| with self.condition: | ||||||
| with self.condition: | ||||||
lzchen marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| if self.done: | ||||||
| # done flag may have changed, avoid waiting | ||||||
| break | ||||||
| flush_request = self._get_and_unset_flush_request() | ||||||
| if ( | ||||||
| len(self.queue) < self.max_export_batch_size | ||||||
| and flush_request is None | ||||||
| ): | ||||||
|
|
||||||
| self.condition.wait(timeout) | ||||||
| flush_request = self._get_and_unset_flush_request() | ||||||
| if not self.queue: | ||||||
| # spurious notification, let's wait again | ||||||
| self._notify_flush_request_finished(flush_request) | ||||||
| flush_request = None | ||||||
| continue | ||||||
| if self.done: | ||||||
| # missing spans will be sent when calling flush | ||||||
| break | ||||||
|
|
||||||
| # substract the duration of this export call to the next timeout | ||||||
| # subtract the duration of this export call to the next timeout | ||||||
| start = time_ns() | ||||||
| self.export() | ||||||
| self.export(flush_request) | ||||||
| end = time_ns() | ||||||
| duration = (end - start) / 1e9 | ||||||
| timeout = self.schedule_delay_millis / 1e3 - duration | ||||||
|
|
||||||
| self._notify_flush_request_finished(flush_request) | ||||||
| flush_request = None | ||||||
|
|
||||||
| # there might have been a new flush request while export was running | ||||||
| # and before the done flag switched to true | ||||||
| with self.condition: | ||||||
| shutdown_flush_request = self._get_and_unset_flush_request() | ||||||
|
|
||||||
| # be sure that all spans are sent | ||||||
| self._drain_queue() | ||||||
| self._notify_flush_request_finished(flush_request) | ||||||
| self._notify_flush_request_finished(shutdown_flush_request) | ||||||
|
|
||||||
| def _get_and_unset_flush_request(self,) -> typing.Optional[_FlushRequest]: | ||||||
| """Returns the current flush request and makes it invisible to the | ||||||
| worker thread for subsequent calls. | ||||||
| """ | ||||||
| flush_request = self._flush_request | ||||||
| self._flush_request = None | ||||||
| if flush_request is not None: | ||||||
| flush_request.num_spans = len(self.queue) | ||||||
| return flush_request | ||||||
|
|
||||||
| @staticmethod | ||||||
| def _notify_flush_request_finished( | ||||||
| flush_request: typing.Optional[_FlushRequest], | ||||||
| ): | ||||||
| """Notifies the flush initiator(s) waiting on the given request/event | ||||||
| that the flush operation was finished. | ||||||
| """ | ||||||
| if flush_request is not None: | ||||||
| flush_request.event.set() | ||||||
|
|
||||||
| def _get_or_create_flush_request(self) -> _FlushRequest: | ||||||
| """Either returns the current active flush event or creates a new one. | ||||||
|
|
||||||
| def export(self) -> None: | ||||||
| """Exports at most max_export_batch_size spans.""" | ||||||
| The flush event will be visible and read by the worker thread before an | ||||||
| export operation starts. Callers of a flush operation may wait on the | ||||||
| returned event to be notified when the flush/export operation was | ||||||
| finished. | ||||||
|
|
||||||
| This method is not thread-safe, i.e. callers need to take care about | ||||||
| synchronization/locking. | ||||||
| """ | ||||||
| if self._flush_request is None: | ||||||
| self._flush_request = _FlushRequest() | ||||||
| return self._flush_request | ||||||
|
|
||||||
| def export(self, flush_request: typing.Optional[_FlushRequest]): | ||||||
| """Exports spans considering the given flush_request. | ||||||
|
|
||||||
| In case of a given flush_requests spans are exported in batches until | ||||||
| the number of exported spans reached or exceeded the number of spans in | ||||||
| the flush request. | ||||||
| In no flush_request was given at most max_export_batch_size spans are | ||||||
| exported. | ||||||
| """ | ||||||
| if not flush_request: | ||||||
| self.export_batch() | ||||||
| return | ||||||
|
|
||||||
| num_spans = flush_request.num_spans | ||||||
| while self.queue: | ||||||
| num_exported = self.export_batch() | ||||||
| num_spans -= num_exported | ||||||
|
|
||||||
| if num_spans <= 0: | ||||||
| break | ||||||
|
|
||||||
| def export_batch(self) -> int: | ||||||
|
||||||
| def export_batch(self) -> int: | |
| def _export_batch(self) -> int: |
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.
sounds good since it should only be called by the worker thread. same goes for the export method.
Uh oh!
There was an error while loading. Please reload this page.