Skip to content

feat(experiments): opt in to concurrent synchronous tasks - #1981

Closed
po-et wants to merge 11 commits into
langfuse:prepare-v5-releasefrom
po-et:fix/sync-experiment-task-concurrency
Closed

po-et wants to merge 11 commits into
langfuse:prepare-v5-releasefrom
po-et:fix/sync-experiment-task-concurrency

Conversation

@po-et

@po-et po-et commented Oct 12, 2026 •

Copy link
Copy Markdown

What does this PR do?

Blocking synchronous experiment tasks execute serially on the event loop despite max_concurrency. Add run_sync_tasks_in_threads=True to opt into concurrent synchronous tasks, while preserving the default execution context for existing tasks and caller-owned SQLite connections.

result = langfuse.run_experiment(
    name="blocking-io-evaluation",
    data=items,
    task=my_thread_safe_task,
    max_concurrency=50,
    run_sync_tasks_in_threads=True,
)
  • Each experiment uses a dedicated pool with up to max_concurrency workers; a smaller asyncio default executor does not cap its capacity.
  • Copy each item's context into its worker so traces keep the correct parent and experiment attributes. Async functions, decorated async wrappers, partials and async callable objects stay on the experiment loop and invoke the original callable.
  • Expose the option through Langfuse, DatasetClient and RunnerContext. Document resource ownership, item-error handling and cancellation in the regular API reference. Remove the earlier breaking-change README and standalone migration note.
  • Close native coroutine results that arrive after cancellation prevents awaiting the worker result. Shutdown cancels queued calls without blocking the event loop. Running synchronous calls cannot be stopped and can continue after cancellation or delay Python process exit; tasks should use finite I/O timeouts.

This targets prepare-v5-release and integrates its abbc6cb24d2af237ffec029fa44b017d295af4ff head with a normal merge. Existing branch history is preserved. A retarget to main after v5 publication remains subject to the maintainer's release plan.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor
  • Documentation update
  • Tooling, CI, or repo maintenance

Verification

Python 3.12.12 on macOS arm64, dependencies installed with uv sync --locked --python 3.12:

# Use the workflow's dummy test configuration for the unit suite.
LANGFUSE_BASE_URL=http://localhost:3000 LANGFUSE_PUBLIC_KEY=pk-lf-test \
LANGFUSE_SECRET_KEY=sk-lf-test OPENAI_API_KEY=test-openai-key \
uv run --frozen --no-sync --python 3.12 pytest -n 4 --dist worksteal -q tests/unit
uv run --frozen --no-sync --python 3.12 ruff check .
uv run --frozen --no-sync --python 3.12 mypy langfuse --no-error-summary
uv run --frozen --no-sync --python 3.12 ruff format --check \
  langfuse/experiment.py langfuse/_client/client.py \
  langfuse/_client/datasets.py tests/unit/test_experiment.py
  • Complete unit suite on the updated v5 base: 958 passed, 2 skipped, 17 warnings, exit 0. Includes real SQLite compatibility through all three public entry points, opt-in task overlap and trace parenting, capacity with a one-worker default executor, and cancellation while a synchronous call remains running.
  • Independent automated review found a late-coroutine cancellation cleanup issue; it was reproduced, fixed, and independently rechecked. The smoke confirms the late coroutine is closed without executing it, queued calls remain cancelled, and the dedicated worker exits after release.
  • Ruff lint, mypy and formatting of the four changed Python files pass. Generated API code and unrelated files match the v5 target.
  • No Langfuse-server E2E, live-provider calls, other Python versions or other platforms were exercised. Local verification does not imply upstream CI success or maintainer approval.

Checklist

  • I self-reviewed the diff using code_review.md.
  • I added or updated tests for behavior changes.
  • I updated the API documentation for the opt-in execution context.
  • Relative to the target, generated API files are unchanged.
  • I did not commit secrets or credentials.

AI assistance was used for implementation, tests, description and automated review.

niklassemmler and others added 6 commits October 7, 2026 17:47
…ngfuse#1946)

Regenerated langfuse/api from the upstream Fern definition: baseline
langfuse/langfuse 619dbe3 (last sync, langfuse#1866) plus langfuse/langfuse#17357 and #18404.
Split out of the bundled API-spec bot PR langfuse#1943.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…se#1949)

Regenerated langfuse/api from the upstream Fern definition: baseline
langfuse/langfuse 619dbe3 (last sync, langfuse#1866) plus langfuse/langfuse#17892 and the datasets.yml/opentelemetry.yml part of #18406.
Split out of the bundled API-spec bot PR langfuse#1943.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…angfuse#1948)

Regenerated langfuse/api from the upstream Fern definition: baseline
langfuse/langfuse 619dbe3 (last sync, langfuse#1866) plus langfuse/langfuse#17888.
Split out of the bundled API-spec bot PR langfuse#1943.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
langfuse#1947)

* docs(api): document fromStartTime as required on experiments endpoints

Regenerated langfuse/api from the upstream Fern definition: baseline
langfuse/langfuse 619dbe3 (last sync, langfuse#1866) plus langfuse/langfuse#17603.
Split out of the bundled API-spec bot PR langfuse#1943.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(api): use language-neutral wording for experiments time window

Regenerated from langfuse/langfuse branch
docs/experiments-from-start-time-neutral-wording. The fromStartTime
docstrings named `toStartTime`, but the Python parameter is
`to_start_time`; they now say "an upper bound" instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(api): type read-side metadata as optional objects

Regenerated langfuse/api from the upstream Fern definition: baseline
langfuse/langfuse 619dbe3 (last sync, langfuse#1866) plus langfuse/langfuse#18122 and the commons.yml part of #18406.
Split out of the bundled API-spec bot PR langfuse#1943.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(api): pin read-side metadata parsing on generated models

Cover omitted, null, empty and nested-JSON metadata plus rejection of
non-object metadata for Trace, Observation, ObservationV2, BaseScore,
BaseScoreV1 and every Score / ScoreV1 / GetScoresResponseData variant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@claude claude 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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@CLAassistant

CLAassistant commented Oct 12, 2026 •

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ po-et
❌ niklassemmler
You have signed the CLA already but the status is still pending? Let us recheck it.

@chrikrah chrikrah 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.

@po-et this fixes the serialization: sync tasks overlap now, and two of your new cases plus the concurrency case fail on main. Your docs note already says thread-affine tasks should move to async. I'd ship it in v5 and not in a patch, because the cost of missing that note is every item.

$ pytest -q tests/unit/test_experiment.py   # uv sync --locked, Python 3.12
36 passed                  # head 47c4ee4
3 failed, 33 passed        # langfuse/experiment.py from bf11ec1, your tests kept
FAILED ...::TestExperimentObservationTree::test_sync_tasks_run_concurrently_with_trace_context

$ python lf_sync_task_sqlite.py   # sync task reads a sqlite3 connection opened by the caller, max_concurrency=2
outputs: ['A', 'B'] items: 2   # bf11ec1
Item 0 failed: SQLite objects created in a thread can only be used in that same thread. ...
outputs: [] items: 0           # head 47c4ee4
# not run: the e2e suites against a Langfuse server

non-blocking: called from a plain script, run_experiment returns zero item results for that task and raises nothing. The failure shows only as an ERROR log line and an error span in the trace. In a notebook, run_async_safely already moves the loop off the caller's thread, so scripts are what changes. With the bug-fix box ticked it would ship with no migration note. I'd retarget this to prepare-v5-release with a ! in the title and tick "Breaking change". #1934 landed experiment.py changes there, so expect a rebase.

@hassiebp does this belong on prepare-v5-release?

BREAKING CHANGE: synchronous experiment tasks run in worker threads. Create thread-affine resources where they are used and audit shared state before upgrading.
@po-et
po-et requested a review from a team as a code owner October 13, 2026 02:16
@po-et po-et changed the title fix(experiments): run synchronous tasks outside the event loop fix(experiments)!: run synchronous tasks in worker threads Oct 13, 2026
@po-et
po-et changed the base branch from main to prepare-v5-release October 13, 2026 02:17
@cursor

cursor Bot commented Oct 13, 2026

Copy link
Copy Markdown

Thanks a lot for this, the implementation looks solid. Context propagation into the worker threads and the async detection both hold up well in review.

We've decided not to include it in v5.0, which ships tomorrow. Changing the default execution context the day before a major release is too risky for us. A few things also need more design work first:

  • Tasks that hold thread-bound resources, such as sqlite connections or some client sessions, would start failing, and those items are only logged.
  • The default executor caps effective concurrency at min(32, cpus + 4). For example, max_concurrency=50 on 8 CPUs ran only 12 tasks at once. A dedicated ThreadPoolExecutor(max_workers=max_concurrency) would avoid that.
  • Ctrl-C can't stop tasks that are already running in threads.

We'd be happy to ship this in a v5.x minor release behind an opt-in flag, so the default doesn't change. Once v5 is out, could you retarget this PR to main and add the flag? The migration notes can then go into the regular docs, instead of the MIGRATION_V5_SYNC_TASKS.md and README additions.

@po-et po-et changed the title fix(experiments)!: run synchronous tasks in worker threads feat(experiments): opt in to concurrent synchronous tasks Oct 13, 2026
@hassiebp
hassiebp deleted the branch langfuse:prepare-v5-release October 14, 2026 10:46
@hassiebp hassiebp closed this Oct 14, 2026
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.

5 participants