Visitar URL original
fix(executor): reuse connections to the executor across calls by HarshMN2345 · Pull Request #14214 · appwrite/appwrite · GitHub
Skip to content

fix(executor): reuse connections to the executor across calls - #14214

Draft
HarshMN2345 wants to merge 1 commit into
mainfrom
cursor/executor-connection-reuse-9adc
Draft

HarshMN2345 wants to merge 1 commit into
mainfrom
cursor/executor-connection-reuse-9adc

Conversation

@HarshMN2345

Copy link
Copy Markdown
Member

What does this PR do?

Fixes the Appwrite side of #14094. Executor::call() built a new curl client for every request and forbade connection reuse, so each execution opened a TCP connection to the executor and left it in TIME_WAIT. Under sustained load that fills the API container's source-port range and throughput falls to what TIME_WAIT expiry allows.

Each call still gets its own easy handle, so concurrent coroutines never share a busy transfer. A process-wide curl share (CURL_LOCK_DATA_CONNECT and CURL_LOCK_DATA_DNS) keeps idle connections, and withConnectionReuse() stops the adapter from closing the socket when the handle is discarded.

The executor image still dials a runtime per execution (open-runtimes/executor #259). Until that ships, the executor service sets net.ipv4.tcp_tw_reuse=1. That only affects outgoing connects. The kernel default (2) reuses TIME_WAIT ports for loopback only, which is why the executor's port search pegs a core after the first burst.

Test Plan

  • tests/unit/Executor/ConnectionReuseTest.php
    • Five sequential deleteRuntime calls against a keep-alive server accept one TCP connection.
    • When that server closes the first connection, the later calls succeed and only one new connection is opened.
    • The same test against the previous client accepts one connection per call.
  • vendor/bin/pint --test on the changed PHP files.
  • PHPStan (level 4) on src/Executor/Executor.php and the new test.

Related PRs and Issues

Checklist

  • Have you read the Contributing Guidelines on issues?
  • If the PR includes a change to an API's metadata (desc, label, params, etc.), does it also include updated API specs and example docs?
Open in Web Open in Cursor 

Executor::call() opened a new curl handle for every request, so each
execution left a TIME_WAIT socket in the API container. A persistent
share keeps idle connections in the worker. The executor service also
reuses outgoing TIME_WAIT ports until its image pools connections to
runtimes.
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

Security rules

No new WARNING or ERROR findings from security rules.

32 existing findings tracked in .semgrep/baseline.json
  • php.appwrite.guest-write-without-abuse-limit (17)
  • php.appwrite.permissive-write-permission (8)
  • php.appwrite.secret-compare-timing (5)
  • php.appwrite.weak-secret-env-default (2)

Posted by Checks / Rules. Re-runs update this comment in place. Rule details and baseline: .semgrep/README.md.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

✨ Benchmark results

Comparing main (before) → cursor/executor-connection-reuse-9adc (after).

Metric Before After Change
🚀 Requests/sec 177.95 184.37 ⚪ +3.6%
⏱️ Latency P50 96.75 ms 93.84 ms ⚪ -3%
⏱️ Latency P95 229.02 ms 227.58 ms ⚪ -0.6%
Per-scenario breakdown & investigation details

Metrics below reflect the current branch (after). Δ P95 compares against the base.

Scenario P50 (ms) P95 (ms) Requests RPS Δ P95 (ms)
API total 93.84 227.58 11,400 184.37 -1.44
Account 182.54 388.26 600 10.45 +42.59
TablesDB 91.27 182.18 6,200 103.58 +1.65
Storage 85.24 195.42 3,000 51.74 -2.34
Functions 127.71 264.31 1,600 28.34 -26.99

Top API waits (after)

API request Max wait (ms)
account.prefs.update 535.1
storage.files.create 482.98
account.name.update 481.11
account.get 477.93
functions.create 444.6

Copy link
Copy Markdown
Member Author

@hansi-codes review

@hansi-codes

hansi-codes Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

🟢 Tier S · Ready to merge

No concrete defects requiring changes were found in the client integration, Compose configuration, or regression tests.

The executor client now shares DNS and idle TCP connections across calls while keeping a separate easy handle for each request. The Compose executor service enables outgoing TIME_WAIT port reuse, and new tests exercise sequential connection reuse and reconnection after a server closes a socket.

Verdict New comments Fixed Still open
✅ Approved 0 0 0
📂 Walkthrough · 3
File Change
docker-compose.yml Enable net.ipv4.tcp_tw_reuse for the executor container.
src/Executor/Executor.php Attach a persistent cURL DNS/connection share and enable connection reuse.
tests/unit/Executor/ConnectionReuseTest.php Verify reuse across sequential calls and recovery from a closed connection.

Reviewed c913d13 · Details · Comment @hansi-codes review to re-run, or mention @hansi-codes with a question.

@hansi-codes hansi-codes Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Tier S · Looks good to merge. Summary

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.

🐛 Bug Report: Executor opens a new connection per execution, throughput drops under load

1 participant