-
[OPIK-6576] [SDK] feat: add load-test suite for traces, spans, and attachments (#6829)
发布于
2026-05-25 13:46:38 +00:00 - [OPIK-6576] [SDK] feat: add load-test suite for traces, spans, and attachments
Adds a pytest-driven load-test suite under tests_load/suite/python_sdk/
covering the four ingestion shapes from OPIK-6576: high trace/span
counts, heavy payloads, explicit and implicit attachments, and
burst/spread/concurrent patterns. Tests exercise the SDK via the public
surface only — @opik.track decorators and start_as_current_trace /
start_as_current_span context managers — so they mirror real user code.Each scenario captures the submitted trace ids, calls opik.flush_tracker,
then polls search_traces (with attachment-search workarounds for
OPIK-6651) and the attachments list endpoint until every submitted id
lands with required fields set. A regression-style assertion fails fast
if any submitted id is missing post-flush, catching dropped messages
(same shape as the OPIK-6444 unit regression, one level up).Per-phase timings and counts are written to tests_load/.last_run/
<test_name>.json. The new .github/workflows/load_tests.yml runs the
suite weekly via cron and on workflow_dispatch, with -n auto
--dist=worksteal so independent scenarios run in parallel. A summary
step aggregates the per-test JSON into a Markdown table appended to
$GITHUB_STEP_SUMMARY so the metrics render directly on the workflow
run page.Implements OPIK-6576: Load testing for spans & traces against
open-source installation.- fix(sdk): release context project name owner on span context manager exit
start_as_current_span calls _try_acquire_project_name via
add_start_candidates on enter, which sets the context_storage project
name with the span/trace id as owner. The finally block popped span/
trace data but never called release_context_project_name_if_owner, so
the owner leaked across context boundaries. After one
start_as_current_span call, any later @opik.track invocation in the
same thread silently inherited the leaked project name regardless of
its own project_name argument.The decorator path in base_track_decorator.pop_end_candidates already
releases by span/trace id on exit; this change makes the context-
manager path symmetric. Two regression tests in
test_span_context_manager cover the single-call and looped patterns.Discovered while building the OPIK-6576 load-test suite (test ordering
across scenarios was non-deterministic because of this leak).- test(load): add race-stress concurrent scenario + pytest-timeout hang-guard
The existing test_concurrent_writers_share_one_client at scale 1.0 would
catch a missing-lock regression in BatchManager.flush_ready (OPIK-6444
shape) only flakily, because the production CREATE_*_FLUSH_INTERVAL_SECONDS
is 2.0 s and our 30-thread submission window is also ~2 s — only ~1 flush
cycle fits inside the race window per run.Add test_concurrent_writers_race_stress, tuned specifically to surface
lock regressions:- A race_stress_flush_interval context manager monkey-patches
CREATE_TRACES/CREATE_SPANS_MESSAGE_BATCHER_FLUSH_INTERVAL_SECONDS down
from 2 s to 5 ms for the duration of the test, then restores them and
resets the global Opik client so a fresh batcher picks up the patch. - 100 worker threads (vs 30 in the realistic test) and no think-time keep
the submission window dense. - Volume: 100 * 500 = 50k traces at scale 1.0.
Empirical catch rate against the OPIK-6444 lock removal at scale 0.05:
~100% — every run either fails with the clean "X trace(s) missing
required fields (name, end_time)" assertion when partial loss leaks
through, or hangs and is killed by the new 300 s test-level timeout.Hang-guard infrastructure:
- New pytest-timeout dependency in tests_load/suite/python_sdk/requirements.txt
- Global default timeout = 1200 s in pytest.ini (2x the longest legitimate
scenario, test_spread_over_time at ~600 s) - Tighter @pytest.mark.timeout(300) on test_concurrent_writers_race_stress
- timeout_method = thread so the hang is killed mid-test rather than
blocking until process exit
- test(load): add Dataset.insert() many-versions scenario
Covers the failure mode documented in the Notion writeup "Dataset
migration replay: silent data loss on the version chain":
Dataset.insert() creates a new version on the backend, and the
COPY_VERSION_ITEMS step (a ClickHouse INSERT...SELECT that snapshots
the previous version's items into the new one) non-deterministically
returns short on multi-replica deployments, truncating the new
version. Subsequent versions cascade off the truncated baseline.The bug is purely server-side — single-thread sequential REST calls
already trigger it on prod. Localhost docker-compose (single-replica
ClickHouse) doesn't reproduce, so this test passes there as a green
baseline; it'll surface the truncation when run against a multi-
replica environment.Uses the high-level Dataset.insert() / Dataset.get_items() API (not
the lower-level apply_dataset_item_changes the migrate CLI uses), per
the requirement to exercise the public SDK surface.Volume at load_scale=1.0: 50 versions × 50 items × ~4 KB payload =
2500 items, matching the production repro parameters from the
Notion doc.- fix(load): use standard b64encode for attachment list path, matching SDK contract
The attachments-list endpoint expects a base64-encoded base URL in the
pathquery param. Ourverify_attachmentshelper used
urlsafe_b64encode, while the SDK's ownattachment/client.pyand
the e2e verifiers intests/e2e/verifiers.pyuse standard
b64encode. The url-safe variant happened to round-trip correctly
for the URLs we test against (no+//characters in them), but
matching the established contract is the right thing to do.Caught by baz-reviewer on PR #6829.
- chore(ci): unblock lint, eval-metrics, and ADK transfer-to-agent failures
Three independent fixes carried in this PR because all three CI jobs
were red on every recent SDK PR including this one. None of them are
caused by the load-test suite this PR introduces.-
Lint (
pre-commit run --all-files): ruff format applied to
sdks/python/tests/unit/api_objects/dataset/test_stream_dataset_items.py
which landed in main without passing the formatter. Pure whitespace
reformat (22 line diff, no semantic change). -
Evaluation metrics (
ModuleNotFoundError: No module named 'langchain_community.chat_models.vertexai'): ragas 0.4.x imports
from a langchain_community submodule that was removed in
langchain-community 0.3.30+ (migrated to the standalone
langchain-google-vertexai package). Pin ragas to the 0.3.x line for
Python 3.10+ to match the older constraint already in place for
Python < 3.10. Follow-up: rework the test or upgrade ragas once it
catches up with the langchain migration. -
ADK (
test_adk__transfer_to_agent__tracked_and_span_created
failing intermittently): the test's root agent has
instruction="Always transfer the user's message to the Translator sub-agent. Do not translate yourself.", but gemini-2.5-flash
occasionally answers directly without invoking the sub-agent, which
collapses the expectedTranslatorspan out of the trace. This is
LLM non-determinism, not an SDK regression. Add
pytest-rerunfailuresto test_requirements.txt and mark the test
@pytest.mark.flaky(reruns=2, reruns_delay=5)so it only fails when
the SDK is genuinely broken. Inline comment explains the rationale. -
revert(ci): drop ADK rerunfailures (ineffective); switch eval-metrics pin to root cause
ADK: previous commit added pytest-rerunfailures + @pytest.mark.flaky on
test_adk__transfer_to_agent__tracked_and_span_created on the theory that
the failure was LLM non-determinism. CI proved otherwise — the test
failed all 3 attempts (initial + 2 reruns) with the same deep-assert
diff (no Translator span emitted), so the failure is deterministic and
retries don't help. Revert the marker and the new dependency so we
don't carry dead-weight infrastructure for no benefit. This test
remains pre-existing failure on main and is left to be tackled in a
focused follow-up.Eval metrics: the prior commit pinned ragas<0.4, but ragas 0.3.9 still
importsfrom langchain_community.chat_models.vertexai import ChatVertexAI. That submodule was removed in langchain-community 0.4
(replaced by the standalone langchain-google-vertexai package). Pinning
ragas didn't help because the latest langchain-community gets installed
either way. The root cause is langchain-community's removal, so pin
langchain-community<0.4 directly and drop the ragas pin.下载附件