Skip to content

fix(trace): replace overlapping cache fetch results atomically - #913

Open
Gareth.Y (linjiaye929) wants to merge 2 commits into
braintrustdata:mainfrom
linjiaye929:codex/atomic-trace-cache
Open

Gareth.Y (linjiaye929) wants to merge 2 commits into
braintrustdata:mainfrom
linjiaye929:codex/atomic-trace-cache

Conversation

@linjiaye929

Copy link
Copy Markdown

Concurrent trace.get_spans() calls can append overlapping results into the same per-type cache. One caller receives duplicate spans, and later cached reads keep returning them. This can affect scorers sharing a LocalTrace when reads fall back to the backend. It is the concurrent counterpart of the sequential issue fixed in #546.

Build each response's type groups locally, then replace the full cache or update the returned groups in one synchronous step. Failed or cancelled requests leave successful cached data intact. An overlapping empty full response remains retriable. Rows repeated within a single backend response are preserved.

Tests cover full/full, typed/typed, partially overlapping types, full/typed in both completion orders, failure/cancellation, empty full responses, and stale-type replacement. Existing filtering and empty-result retry tests still pass. Concurrent calls may still issue overlapping backend reads.

Validation on Python 3.12.14:

  • Regression tests on unchanged main: 11 failed, 1 passed. With the fix: 12 passed.
  • Complete test_trace.py: 53 passed, including existing cassette-backed backend parity.
  • Repository test_core nox session: 990 passed, 73 skipped, 12 xfailed. Reused the existing core environment with --no-install; verified imports came from this worktree and dependency compatibility checks passed.
  • All pre-commit hooks, targeted pylint --errors-only, and git diff --check passed.

make fixup requires unavailable mise, so its pre-commit run --all-files command was run directly. Other Python versions, full provider suites, whole-SDK pylint, and live backend calls were not tested.

AI assistance was used to implement and test this change.

@linjiaye929
Gareth.Y (linjiaye929) marked this pull request as ready for review October 12, 2026 10:25
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T10:31:40.406366Z d11e988 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d11e988111

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

# append the same spans twice. A failed or cancelled fetch leaves the
# previous cache intact, and a full fetch replaces every prior type.
if span_type is None:
self._span_cache = fetched

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject stale full-fetch responses before replacing the cache

When two concurrent full reads observe different snapshots—for example, request A is issued first and its nonempty response is delayed while request B sees an additional newly visible span—B can cache the newer response first, after which this unconditional assignment lets A replace it with the older subset. A's caller then sets _all_fetched to true, so subsequent get_spans calls never refetch and omit the newer span for the lifetime of the fetcher; coalescing full reads or tracking request generations would prevent an older in-flight response from superseding a newer one.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6eed80c.

Confirmed with differently ordered full responses, and extended coverage to full/typed and typed/typed overlaps. The cache now records request generations for committed full responses and type groups, so an older response cannot overwrite a group from a later-started successful request. Full reads still replace stale groups, including types absent from their response, while preserving later-started committed typed groups.

Failures and cancellation leave committed generations unchanged. Empty full responses stay retryable, including when retaining a newer typed group; empty typed responses and advanced-filter/scorer bypass behavior are preserved. This orders overlapping requests, not server revisions, so it does not promise snapshot freshness.

The new regression set gives 13 failures and 35 passing controls on the prior PR head; the final trace suite passes all 101 tests. Targeted Ruff, Pylint, codespell and diff checks pass. The final full core rerun remains unverified due to an environment restriction on possible outbound test traffic.

AI-assisted follow-up.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant