Skip to content

feat(dify-agent): add an opt-in Rust local runtime canary - #41070

Open
pineapple880066 wants to merge 7 commits into
langgenius:mainfrom
pineapple880066:agent/rust-local-runtime-canary
Open

feat(dify-agent): add an opt-in Rust local runtime canary#41070
pineapple880066 wants to merge 7 commits into
langgenius:mainfrom
pineapple880066:agent/rust-local-runtime-canary

Conversation

@pineapple880066

@pineapple880066 pineapple880066 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Important

  1. Make sure you have read our contribution guidelines
  2. Ensure there is an associated issue and you have been assigned to it
  3. Use the correct syntax to link this PR: Fixes #<issue number>.

Note

This supersedes #40477. That PR was closed after its head diverged from the latest main; GitHub would not reopen it after the branch was rebased and force-pushed. This PR contains the same reviewed direction, rebased onto a9b8c84e9be41376c04901e81ce690f35c1ffe86, plus the latest shellctl compatibility and test updates.

Summary

Fixes #39976.

This adds an opt-in Rust implementation of the local dify-agent runtime as a guarded canary. Go remains the default implementation and the compatibility fallback. The Rust service is isolated behind a separate Compose overlay and deterministic percentage rollout, so the change can be evaluated incrementally without replacing existing Go-owned sessions.

The implementation targets runtime overhead only: local shell/file execution, process supervision, state handling, and data movement. It does not claim to improve LLM/provider, database, vector-store, or frontend latency.

What changed

  • Added protocol-compatible Rust shellctl, runner, runner-exit, and sanitizer components.
  • Preserved Go as the default runtime and owner of legacy unprefixed references.
  • Added deterministic BLAKE2b canary selection and opaque rust+ references for sticky routing across the complete Binding/Workspace/Home Snapshot lifecycle.
  • Added a bounded Rust preflight before the first mutating operation. If Rust is unavailable, new eligible bindings fall back to Go; after mutation, a session is never replayed across runtimes.
  • Added a separate Compose overlay and independent Rust state, health checks, Docker build targets, and CI entries.
  • Added mode=pty compatibility plus mode=stdio, including separate stdout/stderr capture and explicit rejection of unsupported stdin.
  • Added SQLite schema-v1 migration support, treating legacy jobs as PTY jobs.
  • Forced TMPDIR, TMP, and TEMP into the workspace and created /workspace in the Rust image.
  • Kept shared Go process-management improvements used by both the baseline and comparison harnesses.
  • Added dual-runtime acceptance, rollout, recovery, security, benchmark, and integration coverage.

Rollout and rollback

  1. The base Compose configuration remains Go-only.
  2. Enabling the overlay starts Rust with independent state.
  3. DIFY_AGENT_RUNTIME_RUST_CANARY_PERCENT can be raised gradually.
  4. Setting the percentage back to 0 immediately stops assigning new sessions to Rust.
  5. Existing rust+ sessions remain sticky, so the Rust service should stay up until those references drain.
  6. After draining, the overlay can be removed without migrating or rewriting Go state.

Current-tip paired Linux container A/B

The final-source Go and Rust images were tested on Linux arm64 for five alternating paired rounds, with 50 jobs per round, 2 CPUs, and a 1 GiB memory limit. The benchmark was run from a clean clone at 07914d47673493da07bef6cd2b62e79fd17d31e8; the subsequent tip changes only ShellCheck-safe quoting in an integration runner and leaves the benchmarked runtime and harness content unchanged. Ratios and reductions below are medians of the paired per-round ratios, not ratios calculated from the displayed marginal medians.

Measurement Go median Rust median Paired Go/Rust Rust reduction
Startup to ready 164.534 ms 161.288 ms 1.03x 2.8%
Idle cgroup memory 9.113 MiB 2.686 MiB 3.40x 70.6%
Idle Docker stats memory 8.598 MiB 2.117 MiB 4.05x 75.3%
Idle long-lived process RSS 19.426 MiB 8.688 MiB 2.24x 55.3%
Post-job cgroup memory 21.326 MiB 10.463 MiB 2.04x 50.9%
Fresh health preflight 0.956 ms 0.973 ms 0.98x -1.8%
Sequential small job 122.297 ms 21.519 ms 5.68x 82.4%
32 KiB output job 132.944 ms 29.622 ms 4.58x 78.2%
Concurrent job, 8 workers 114.133 ms 20.509 ms 5.67x 82.4%
Cold first job 120.534 ms 26.407 ms 4.56x 78.1%

Startup and health-preflight confidence intervals cross zero and should be treated as parity; the measurable gains are lower steady-state memory and lower local job overhead.

Full Agent v2 shadow A/B

A non-disruptive 20-pair Agent v2 shadow test on a deployed Linux host also exercised binding creation, run, 32 KiB read, and destroy through the real lifecycle. It measured a full-session median of 1999.212 ms for Go and 670.795 ms for Rust (2.980x, 66.4% lower; paired 95% bootstrap CI for the reduction: 65.8%–67.4%). This deployment used source 8e74d..., older than the current tip, so it is supporting end-to-end evidence rather than the current-tip microbenchmark.

Validation on the rebased tip

  • Rust: cargo fmt --check, cargo clippy --locked --all-targets -- -D warnings, and cargo test --locked in the official Linux Rust container; 23 unit tests passed.
  • Go: go test -race -count=1 ./... in the official Linux Go container and golangci-lint 2.12.2; all packages passed with 0 lint issues.
  • Dual-runtime integration: make integration from a clean source archive; all 40 Go/Rust acceptance groups passed, covering PTY/stdio, isolation/no-isolation, restart recovery, concurrency, Landlock, validation, and lifecycle behavior.
  • Real rollout integration: 3 tests passed and the E2B-only case was deselected, including Rust sticky routing/state isolation and preflight fallback to the real Go runtime.
  • Python: full Ruff passed; BasedPyright reported 0 errors/0 warnings for every PR-touched dify-agent source and test. The clean-clone suite, run with importlib mode to avoid upstream same-name test-module collection collisions, completed with 809 passed and 34 skipped; its sole network-isolated dependency-install case passed separately with network enabled. The only full-tree type error also reproduces on main in an unchanged integration test.
  • Benchmark harness: compilation plus all 13 harness unit tests passed.
  • Compose: base configuration plus Rust overlay passed docker compose config --quiet.
  • Shell: the integration runner passed ShellCheck 0.11.0 and sh -n.
  • Documentation: docs: document Rust local runtime rollout dify-docs#942 passed internal-link, termbase, Mintlify, and changed-file validation.

No Dify API or frontend code is changed by this PR.

Screenshots

Not applicable; this is a backend runtime and rollout change.

Checklist

  • This change requires a documentation update, included: langgenius/dify-docs#942
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran the applicable backend lint and type-check equivalents. API and frontend are unchanged; the relevant Go, Rust, dify-agent, Compose, benchmark, and integration checks are listed above.

From Codex

@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor/Chore] Benchmark Rust runtime optimization opportunities

1 participant