[OPIK-7425] [SDK] fix: LangChain OpikTracer no longer leaks per-run state - #7566
Merged
alexkuzmik merged 6 commits intoJul 22, 2026
Merged
Conversation
…tate OpikTracer only ever added to its per-run bookkeeping and never removed anything, so a long-lived tracer reused across invocations - the documented "build once, pass on every invoke" pattern - grew unbounded for the process lifetime, pinning full prompt/completion payloads and slowing the per-run map scans. Extract that bookkeeping into a dedicated RunStateStore (run id -> span/ trace data, skipped-langgraph roots, trace ownership derived from the trace data map). The tracer releases a root run's whole subtree once it fully ends - in the root run's end-span handler, which LangChain calls after _persist_run, so _finalize_trace still sees every child span and the root span's own output is recorded before its state is dropped. Also drop the write-only _langgraph_parent_span_ids map. created_traces() is left accumulating - it is public API. Tested via a dedicated RunStateStore unit suite (public API only) and end-to-end memory checks asserting live SpanData does not grow across repeated invocations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up cleanups, no behavior change: - Drop _save_span_trace_data_to_local_maps and its "local/external maps" docstrings/comments - there are no local maps since the state moved to RunStateStore. Call the store directly instead. - Extract the repeated "emit start trace/span if configured" gate into _emit_start_trace / _emit_start_span (+ _should_log_start_events). - Extract the identical end-of-span cleanup shared by _process_end_span and _process_end_span_with_error into _release_ended_span_state. - Simplify _skip_tracking to a single return. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-langchain-tracer-memory-leak
Contributor
⏱️ pre-commit per-hook timing
⏭️ 36 skipped (no matching files changed)
|
- Restore test_opik_tracer__attach_span_to_parent_span__stream_restart_root (accidentally dropped in the run-state extraction); it now sets up and asserts through RunStateStore's public API instead of raw private maps. - Replace the gc.collect()/weakref memory checks with deterministic assertions on RunStateStore.is_empty() - eviction is a dict pop, so it must not depend on garbage collection timing. - Document in _persist_run why finalization is gated on trace ownership (LangChain calls _persist_run only for root runs; external traces are left for their real owner to finalize). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
alexkuzmik
marked this pull request as ready for review
July 22, 2026 13:54
Address review feedback: drop private tracer._run_state access from the integration tests and assert observable behavior instead. - Rework the repeated-invocation and nested-@track tests to assert full fake_backend.trace_trees contents (structure, per-run inputs, outputs) and created_traces(), so a regression that drops root output or child spans is caught - not just that state was cleared. - Remove the white-box stream-restart test that seeded tracer._run_state and called the private _attach_span_to_parent_span; the trace_id fallback it guarded is covered on the public store API by test_run_state__link_child_run_falls_back_to_trace_id_lookup. The deterministic per-run release guarantee remains covered by the RunStateStore unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
alexkuzmik
deleted the
aliaksandrk/OPIK-7425-langchain-tracer-memory-leak
branch
July 22, 2026 16:38
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Details
opik.integrations.langchain.OpikTraceronly ever added to its per-run bookkeeping and never evicted anything, so a tracer built once and reused on every invocation — the documented pattern — grew unbounded for the process lifetime, pinning full prompt/completion payloads (and slowing per-run map scans as the process aged). This makes a long-lived tracer's memory stay flat across invocations.RunStateStore(run id → span/trace data, skipped-LangGraph roots; trace ownership is derived from the trace-data map instead of a redundant parallel set)._persist_run— so_finalize_tracestill sees every child span and the root span's own output is recorded before its state is dropped._langgraph_parent_span_idsmap (dead accumulation);created_traces()is intentionally left accumulating since it is public API.Change checklist
Issues
AI-WATERMARK
AI-WATERMARK: yes
Testing
RunStateStoreunit suite that exercises the store through its public API only (save/get, ownership, child-trace linking with the stream-restart fallback, subtree release, and aweakrefcheck that a releasedSpanDatais garbage-collected).@track-nested chain) asserting the liveSpanDatacount does not grow across repeated invocations. Confirmed these fail when eviction is disabled, so they genuinely guard the regression.FakeListLLM): retained payload bytes stay flat at 0 across 50 invocations, versus growing linearly before the fix.sdks/python):pytest tests/library_integration/langchain/{test_run_state,test_opik_tracer,test_langchain,test_langgraph,test_langchain_openai}.py→ 117 passed, 2 skipped (skips are unrelated optional provider packages not installed locally).ruff check,ruff format --check, andmypyon the changed files — all clean.Documentation
N/A — no user-facing documentation changes; the fix restores the documented "build once, reuse on every invocation" behavior.