Skip to content

fix(executions): retry transient ClickHouse failures - #13843

Open
claudear wants to merge 1 commit into
mainfrom
fix/execution-store-clickhouse-retry
Open

claudear wants to merge 1 commit into
mainfrom
fix/execution-store-clickhouse-retry

Conversation

@claudear

Copy link
Copy Markdown
Contributor

What

The executions worker fails ~946 times with RuntimeException: ClickHouse execution insert failed with HTTP 503: no available server (CLOUD-3R3R).

A ClickHouse cluster behind a load balancer answers 503 no available server while it has no healthy backend, and refuses connections outright while a node restarts. Both clear within milliseconds during a failover or a rolling restart, but Store::insertRows() and Store::query() each made a single attempt, so every blip failed the job (and any API read that landed on it).

Every ClickHouse request now goes through a bounded retry in Store::send(): 3 attempts, 100ms linear backoff, on connection failures and the availability status codes (429, 502, 503, 504). ClickHouse answers its own query errors with 500, so a rejected statement still fails on the first attempt instead of being replayed twice for nothing.

Replaying an insert is safe: every row carries its own version, and ReplacingMergeTree collapses a duplicate snapshot (reads resolve with argMax anyway). The request body is rewound before each attempt, the same way the Swoole client adapter rewinds on redirect.

Tests

TDD — the retry tests were written first and failed against the old single-attempt code. tests/unit/Execution/StoreTest.php:

  • retries an insert through a 503 no available server and re-sends the same body
  • retries an insert through a connection failure
  • stops after 3 attempts and keeps the original error message
  • does not retry an insert ClickHouse rejected with 500
  • retries a read query through a 503

The new RecordingClient reads each request body at send time, the way a real HTTP client does, so a retry that forgets to rewind the stream shows up as an empty second body.

vendor/bin/phpunit tests/unit/Execution/StoreTest.php → 24 tests, 90 assertions, green. composer lint, composer analyze and Rector clean on both files. The rest of tests/unit/ is unchanged (the failures on this machine are missing extensions — imagick/yaml/maxminddb — and docker/DNS, all pre-existing).

🤖 Generated with Claude Code

A ClickHouse cluster behind a load balancer answers 503 "no available
server" while it has no healthy backend, and refuses connections while a
node restarts. Both clear within milliseconds during a failover or a
rolling restart, but every blip failed the executions worker job and the
API reads that hit it.

Send every ClickHouse request through a bounded retry: three attempts,
100ms linear backoff, on connection failures and the availability status
codes (429, 502, 503, 504). ClickHouse answers its own query errors with
500, so those still fail on the first attempt. Replaying an insert is
safe -- every row carries its own version and ReplacingMergeTree
collapses a duplicate snapshot -- and the request body is rewound before
each attempt.

Co-Authored-By: Claude Opus 5 <[email protected]>
@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The runtime change has no identified blocking correctness failure, but the implementation-coupled retry-count tests violate an explicit repository requirement and must be revised before merging.

Fix All in Claude CodeFindings

  1. P2 Retries Permanent Client Failures ▶
  2. P2 Tests Mirror Retry Configuration ▶
Fix with agent prompt
### Issue 1
src/Appwrite/Execution/Store.php:477
This catches every `Throwable`, so deterministic PSR-18 request failures are retried along with temporary network failures. A malformed request or adapter precondition failure is sent three times and delayed by 300 ms before being reported. Limit connection retries to network exceptions and propagate permanent client failures immediately.

### Issue 2
tests/unit/Execution/StoreTest.php:414-418
This fixture supplies exactly three responses and later asserts exactly three sends, duplicating the production retry limit in the test. The connection-failure and rejected-request tests use the same implementation-coupled pattern. This violates the repository directive to test observable behavior instead of mirroring source configuration. Replace exact send-count assertions with outcomes such as eventual success, preserved payload, and final failure classification. This explicit repository requirement must be satisfied before merging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR centralizes ClickHouse execution-store requests behind a bounded retry helper, retries transient transport and availability failures, rewinds request bodies before replay, and adds insert/query regression coverage.

  • Retries connection failures and HTTP 429/502/503/504 responses up to three attempts.
  • Keeps ClickHouse statement failures such as HTTP 500 non-retryable.
  • Preserves the existing execution-store exception context for inserts and queries.
  • Adds tests for successful recovery, exhaustion, non-retryable rejection, and request-body replay.

Reviews (1) · Last reviewed commit: "fix(executions): retry transient ClickHo..."


try {
$response = $client->sendRequest($request);
} catch (Throwable $th) {

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.

P2 Retries permanent client failures

This catches every Throwable, so deterministic PSR-18 request failures are retried along with temporary network failures. A malformed request or adapter precondition failure is sent three times and delayed by 300 ms before being reported. Limit connection retries to network exceptions and propagate permanent client failures immediately.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Appwrite/Execution/Store.php
Line: 477

Comment:
**Retries permanent client failures**

This catches every `Throwable`, so deterministic PSR-18 request failures are retried along with temporary network failures. A malformed request or adapter precondition failure is sent three times and delayed by 300 ms before being reported. Limit connection retries to network exceptions and propagate permanent client failures immediately.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Comment on lines +414 to +418
$client = new RecordingClient([
new Response(503, body: new Stream('no available server')),
new Response(503, body: new Stream('no available server')),
new Response(503, body: new Stream('no available server')),
]);

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.

P2 Tests mirror retry configuration

This fixture supplies exactly three responses and later asserts exactly three sends, duplicating the production retry limit in the test. The connection-failure and rejected-request tests use the same implementation-coupled pattern. This violates the repository directive to test observable behavior instead of mirroring source configuration. Replace exact send-count assertions with outcomes such as eventual success, preserved payload, and final failure classification. This explicit repository requirement must be satisfied before merging.

Context Used: Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/Execution/StoreTest.php
Line: 414-418

Comment:
**Tests mirror retry configuration**

This fixture supplies exactly three responses and later asserts exactly three sends, duplicating the production retry limit in the test. The connection-failure and rejected-request tests use the same implementation-coupled pattern. This violates the repository directive to test observable behavior instead of mirroring source configuration. Replace exact send-count assertions with outcomes such as eventual success, preserved payload, and final failure classification. This explicit repository requirement must be satisfied before merging.

**Context Used:** Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

@github-actions

Copy link
Copy Markdown

✨ Benchmark results

Comparing main (before) → fix/execution-store-clickhouse-retry (after).

Metric Before After Change
🚀 Requests/sec 199.52 203.77 ⚪ +2.1%
⏱️ Latency P50 86.92 ms 85.31 ms ⚪ -1.8%
⏱️ Latency P95 202.98 ms 200.33 ms ⚪ -1.3%
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 85.31 200.33 12,540 203.77 -2.65
Account 165.46 318.97 660 11.39 -29.57
TablesDB 81.05 146.92 6,820 113.65 -14.01
Storage 82.84 175.2 3,300 56.57 +9.51
Functions 122.28 255.6 1,760 30.74 +19.4

Top API waits (after)

API request Max wait (ms)
functions.variables.create 472.4
account.name.update 433.2
account.prefs.update 422.29
functions.variables.delete 414.12
functions.create 384.76

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.

1 participant