-
Notifications
You must be signed in to change notification settings - Fork 1.8k
[OPIK-7186] [SDK] feat: surface batch-flush data loss via FlushResult #7513
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
0683c27
d8ae4a0
5de292f
d9f4a25
0c0337c
86e3b46
da9fabe
c1dc3de
0e61e84
1156750
ef23478
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 |
|---|---|---|
|
|
@@ -70,6 +70,7 @@ | |
| url_helpers, | ||
| ) | ||
| from ..message_processing import ( | ||
| data_loss, | ||
| messages, | ||
| ) | ||
| from ..message_processing.batching import sequence_splitter | ||
|
|
@@ -216,6 +217,8 @@ def _bind_resources(self) -> None: | |
| self._rest_client = self._resources.rest_client | ||
| self.__internal_api__message_processor__ = self._resources.message_processor | ||
| self._streamer = self._resources.streamer | ||
| self._flush_reporter = self._resources.flush_reporter | ||
| self._last_flush_result: Optional[data_loss.FlushResult] = None | ||
|
|
||
| def _display_trace_url(self, trace_id: str, project_name: str) -> None: | ||
| project_url = url_helpers.get_project_url_by_trace_id( | ||
|
|
@@ -1878,9 +1881,13 @@ def get_experiment_by_id(self, id: str) -> experiment.Experiment: | |
| project_name=experiment_public.project_name, | ||
| ) | ||
|
|
||
| def end(self, timeout: Optional[int] = None, *, flush: bool = True) -> None: | ||
| def end( | ||
| self, timeout: Optional[int] = None, *, flush: bool = True | ||
| ) -> Optional[data_loss.FlushResult]: | ||
| """ | ||
| End the Opik session and submit all pending messages. | ||
| End the Opik session, releasing this client's connection reference. When | ||
| ``flush`` is True (the default), all pending messages are submitted | ||
| first; when ``flush`` is False, anything still queued is dropped. | ||
|
|
||
|
Comment on lines
1936
to
1940
Contributor
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. end() doc hides drop mode
Want Baz to fix this for you? Activate Fixer Other fix methodsPrompt for AI Agents
Contributor
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. Commit d8ae4a0 addressed this comment by updating
Collaborator
Author
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. Fixed in d8ae4a0 — 🤖 Reply posted via /address-github-pr-comments |
||
| Connection resources are shared and ref-counted across clients with a | ||
| matching configuration: this releases the current client's reference. | ||
|
|
@@ -1908,28 +1915,84 @@ def end(self, timeout: Optional[int] = None, *, flush: bool = True) -> None: | |
| is shared — it may still succeed by riding another live client's | ||
| resources. Do not rely on either outcome; create a new client instead. | ||
|
|
||
| The outcome is also available afterwards via :attr:`last_flush_result`. | ||
|
|
||
| Returns: | ||
| None | ||
| The flush outcome (including any data-loss detail) when ``flush`` is | ||
| True; ``None`` when ``flush`` is False (nothing was flushed). | ||
| """ | ||
| timeout = timeout if timeout is not None else self._flush_timeout | ||
| marker = self._flush_reporter.marker() | ||
| # Explicit teardown on a user thread, so close on the last reference | ||
| # (close_on_zero=True). Releasing is idempotent, so the detached GC | ||
| # finalizer cannot double-decrement. | ||
| self._lease.release(timeout, flush=flush, close_on_zero=True) | ||
| # finalizer cannot double-decrement. release() returns the authoritative | ||
| # flush outcome computed inside the drain (streamer.flush) — the same | ||
| # source flush() uses — rather than the weaker queue_size()==0 proxy, | ||
| # which can read empty on the pop-vs-processed race and while file | ||
| # uploads are still in flight. | ||
| flushed = self._lease.release(timeout, flush=flush, close_on_zero=True) | ||
| self._finalizer.detach() | ||
| if not flush: | ||
| return None | ||
| if flushed is None: | ||
| # No drain ran on this call — e.g. a repeated end() after the client | ||
| # was already released. Keep the outcome from the release that did | ||
| # the work rather than overwriting it with a spurious not-flushed | ||
| # result, so end() is idempotent. | ||
| return self._last_flush_result | ||
| self._last_flush_result = self._flush_reporter.build_result( | ||
| marker, flushed=flushed | ||
| ) | ||
| return self._last_flush_result | ||
|
Comment on lines
+1982
to
+1995
Contributor
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. Idempotent end overwrites success
Want Baz to fix this for you? Activate Fixer Other fix methodsPrompt for AI Agents
Contributor
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. Commit d8ae4a0 addressed this comment by short-circuiting
Collaborator
Author
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. Fixed in d8ae4a0 — 🤖 Reply posted via /address-github-pr-comments |
||
|
|
||
| def flush(self, timeout: Optional[int] = None) -> bool: | ||
| """ | ||
| Flush the streamer to ensure all messages are sent. | ||
|
|
||
| Covers delivery of trace/span/feedback messages; attachment/file uploads | ||
|
Contributor
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. 🧹 nit | Docs
This is only true for the data-loss detail ( Consider tightening to something like: "upload failures are not counted in the dropped-message detail, though an incomplete upload still makes the flush report as not fully flushed." 🤖 Review posted via /review-github-pr
Contributor
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. Commit 86e3b46 addressed this comment by documenting that flush covers attachment/file uploads while distinguishing the flush outcome from its data-loss detail.
Collaborator
Author
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. Fixed — you're right, |
||
| are not reflected in the outcome. Never raises and never blocks beyond | ||
| ``timeout``: an observability SDK must not disrupt the app it instruments. | ||
| Detailed outcome — including any data that was dropped — is available via | ||
| :attr:`last_flush_result`. | ||
|
|
||
| Args: | ||
| timeout (Optional[int]): The timeout for flushing the streamer. Once the timeout is reached, the flush method will return regardless of whether all messages have been sent. | ||
|
|
||
| Returns: | ||
| True if all messages have been sent within specified timeout, False otherwise. | ||
| True if all messages were delivered within the timeout with no data | ||
| loss; False if the timeout was hit or any message was dropped. | ||
|
Comment on lines
2012
to
+2014
Contributor
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. FlushResult hides upload failures
Want Baz to fix this for you? Activate Fixer Other fix methodsPrompt for AI Agents
Contributor
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. Commit d8ae4a0 addressed this comment by narrowing
Collaborator
Author
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. Fixed in d8ae4a0 — 🤖 Reply posted via /address-github-pr-comments |
||
| """ | ||
| timeout = timeout if timeout is not None else self._flush_timeout | ||
| return self._streamer.flush(timeout) | ||
| try: | ||
| marker = self._flush_reporter.marker() | ||
| flushed = self._streamer.flush(timeout) | ||
| self._last_flush_result = self._flush_reporter.build_result( | ||
| marker, flushed=flushed | ||
| ) | ||
| return self._last_flush_result.success | ||
| except Exception: | ||
| # An observability SDK must not disrupt the app it instruments: a | ||
| # failure inside flush is reported as "not flushed", never raised. | ||
| # Record a failed outcome so last_flush_result reflects this attempt | ||
| # rather than keeping a stale prior success. Built directly (not via | ||
| # build_result, which may itself be what raised) so it cannot re-raise. | ||
| LOGGER.error("Opik flush failed unexpectedly", exc_info=True) | ||
| self._last_flush_result = data_loss.FlushResult( | ||
| flushed=False, | ||
| remaining_queue_size=0, | ||
| dropped_messages=0, | ||
| dropped_items=0, | ||
| failures=[], | ||
| ) | ||
| return False | ||
|
alexkuzmik marked this conversation as resolved.
|
||
|
|
||
| @property | ||
| def last_flush_result(self) -> Optional[data_loss.FlushResult]: | ||
| """Outcome of the most recent ``flush()``/``end()`` on this client. | ||
|
|
||
| ``None`` until the first flush. | ||
| """ | ||
| return self._last_flush_result | ||
|
|
||
| def __internal_api__drain_to_processors__( | ||
| self, timeout: Optional[float] = None | ||
|
|
||
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.
Concurrent flushes race on shared bundle
Two racing
release()calls can both pass theentry.refcount > 1check and then callshared_bundle.flush(timeout)concurrently, so the same shared resource gets drained twice andFlushResultcan become inconsistent — should we mark the bundle as flush-in-progress underself._lock?Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
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.
Skipping — this shared-bundle pre-flush block predates this PR (the change here only captures its return value). A concurrent double
streamer.flush()is benign: the drain is idempotent (the second flush finds an already-drained queue), and each client builds its ownFlushResultfrom its own marker, so there's no cross-client inconsistency. Adding lock-held flush-in-progress state is the extra concurrency machinery this MVP-scoped PR deliberately avoids.🤖 Reply posted via /address-github-pr-comments
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.
Thanks, that makes sense — I’ll remember that this pre-existing shared-bundle flush path is intentionally tolerated here and save it to memory once the PR is merged.