Skip to content

[superlog] Retry transient ClickHouse single-query drops#511

Merged
izadoesdev merged 1 commit into
stagingfrom
codex/superlog-retry-transient-clickhouse-queries
Jun 30, 2026
Merged

[superlog] Retry transient ClickHouse single-query drops#511
izadoesdev merged 1 commit into
stagingfrom
codex/superlog-retry-transient-clickhouse-queries

Conversation

@izadoesdev

@izadoesdev izadoesdev commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

  • retry single-query analytics executions once when ClickHouse fails with a transient connection drop
  • inspect error code, message, and nested cause fields while explicitly skipping AbortError
  • add regression coverage for retry success, retry final failure, non-transient no-retry, and AbortError no-retry

Supersedes the useful part of #505 from a clean staging branch. The original #505 is conflicted and polluted with stale already-landed history, so this PR carries only the intended AI query change.

Verification

  • cd packages/ai && bun test src/query/batch-executor.test.ts
  • cd packages/ai && bun run check-types
  • bun run lint
  • bun run check-types
  • bun run test

Summary by cubic

Retries single-query analytics once on transient ClickHouse connection drops to reduce flaky failures. AbortError and non-transient errors are not retried; if the retry also fails, the final error is returned.

  • Bug Fixes
    • Detect transient failures via error code, message, and nested cause (e.g., ECONNRESET, ECONNREFUSED, EPIPE, ETIMEDOUT, undici socket errors); AbortError is excluded.
    • Applies to the single-query path only; query metrics are still recorded.
    • Added tests mocking @databuddy/db/clickhouse to cover retry success, final failure after retry, non-transient no-retry, and AbortError no-retry.

Written for commit 4dee431. Summary will update on new commits.

Review in cubic

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
databuddy-status Ready Ready Preview, Comment Jun 30, 2026 3:52pm
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
dashboard Skipped Skipped Jun 30, 2026 3:52pm
documentation Skipped Skipped Jun 30, 2026 3:52pm

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1e580da9-37b4-4eb6-b541-0687784f9ed7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/superlog-retry-transient-clickhouse-queries

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@izadoesdev izadoesdev left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewed the clean replacement for #505. This carries only the intended packages/ai/src/query/batch-executor.ts retry behavior plus focused tests, with no stale branch noise.

Local verification passed:

  • cd packages/ai && bun test src/query/batch-executor.test.ts
  • cd packages/ai && bun run check-types
  • bun run lint
  • bun run check-types
  • bun run test

I will merge this to staging after GitHub CI is green.

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a single-retry mechanism to runSingle for transient ClickHouse connection drops, while correctly excluding AbortError from retry eligibility. Four regression tests cover the key paths (retry success, double-transient failure, non-transient no-retry, and AbortError no-retry).

  • isTransientClickHouseError walks the error cause chain up to 5 levels deep, matching against a set of well-known error codes (ECONNRESET, EPIPE, etc.) and lower-cased message substrings, and hasAbortError short-circuits the check to prevent retrying intentional cancellations.
  • The retry loop in runSingle runs at most two attempts; union-query failures that fall back to individual runSingle calls transitively benefit from the same retry logic.
  • No telemetry is emitted when a retry fires, making it difficult to observe retry frequency in production tracing.

Confidence Score: 4/5

Safe to merge — the retry is strictly bounded to one extra attempt, AbortError is correctly excluded, and the new test suite validates all four key scenarios.

The implementation is correct and well-tested. The only gaps are a missing telemetry event when a retry fires (making production retry frequency invisible) and a logically unreachable fallback return after the loop — neither affects correctness or user-facing behavior.

Both changed files are straightforward. The unreachable return and absent retry telemetry are both in batch-executor.ts.

Important Files Changed

Filename Overview
packages/ai/src/query/batch-executor.ts Adds transient-error detection helpers and a two-attempt retry loop inside runSingle. The logic is sound — AbortError is correctly excluded, both error code and error message heuristics are walked up the cause chain, and the retry is limited to exactly one extra attempt. Two minor concerns: retry events emit no telemetry, and the post-loop fallback return is logically unreachable.
packages/ai/src/query/batch-executor.test.ts Adds four focused test cases covering retry success, double-transient failure, non-transient no-retry, and AbortError no-retry. The module-level vi.mock and beforeEach reset pattern are correct. The hand-rolled MockChQuery type is unconventional but functional for the usage patterns here.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[runSingle called] --> B{config exists?}
    B -- No --> C[return unknown type error]
    B -- Yes --> D[attempt = 0]

    D --> E[build SimpleQueryBuilder\nexecute query]
    E -- success --> F[mergeWideEvent success\nreturn data]
    E -- throws --> G{attempt == 0 AND\nisTransientClickHouseError?}

    G -- Yes --> H[attempt = 1\nretry]
    H --> E

    G -- No --> I[mergeWideEvent error\nreturn error result]

    E2[attempt 1 throws] --> I
    H --> E2
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[runSingle called] --> B{config exists?}
    B -- No --> C[return unknown type error]
    B -- Yes --> D[attempt = 0]

    D --> E[build SimpleQueryBuilder\nexecute query]
    E -- success --> F[mergeWideEvent success\nreturn data]
    E -- throws --> G{attempt == 0 AND\nisTransientClickHouseError?}

    G -- Yes --> H[attempt = 1\nretry]
    H --> E

    G -- No --> I[mergeWideEvent error\nreturn error result]

    E2[attempt 1 throws] --> I
    H --> E2
Loading

Reviews (1): Last reviewed commit: "fix(ai): retry transient clickhouse quer..." | Re-trigger Greptile

Comment on lines +342 to +344
if (attempt === 0 && isTransientClickHouseError(e)) {
continue;
}

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 No retry observability

When a transient error triggers the retry path (continue), there is no mergeWideEvent call to record the event. In production you will have no signal for how often retries occur, making it impossible to distinguish "working as intended" from "constantly hammering ClickHouse on every request." A counter like mergeWideEvent({ query_retry: 1, query_retry_code: … }) before the continue would surface this in your wide traces.

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!

Comment on lines +352 to 355
return { type: req.type, data: [], error: "Query failed" };
}

function groupBySchema(

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 Unreachable fallback return

Given the loop's exit conditions, this return is logically dead code. Attempt 0 always either succeeds (early return), is transient (continues to attempt 1), or is non-transient (early return). Attempt 1's catch block always returns because attempt === 0 is false for both success and error paths. TypeScript requires a trailing return here for type-narrowing, but a comment would signal the unreachability to future readers.

Suggested change
return { type: req.type, data: [], error: "Query failed" };
}
function groupBySchema(
// This point is logically unreachable: attempt 0 always returns or continues,
// and attempt 1 always returns from within the catch block.
return { type: req.type, data: [], error: "Query failed" };
}
function groupBySchema(

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!

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 2 files

Confidence score: 4/5

  • In packages/ai/src/query/batch-executor.ts, the retry branch can continue after transient errors without a mergeWideEvent, which creates an observability gap where healthy retries and ongoing connection instability look the same in production — add telemetry on the retry path before merging.
  • In packages/ai/src/query/batch-executor.ts, retries may still run after cancellation if the error looks transient but is not AbortError, so canceled work can continue and waste resources or surprise callers — check opts.abortSignal before scheduling/entering retry and short-circuit when aborted.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/ai/src/query/batch-executor.ts">

<violation number="1" location="packages/ai/src/query/batch-executor.ts:342">
P2: Honor opts.abortSignal before retrying. Otherwise a canceled request can issue the retry path whenever the thrown error is transient-shaped but not named AbortError.</violation>

<violation number="2" location="packages/ai/src/query/batch-executor.ts:343">
P2: The retry path emits no telemetry. When a transient error triggers `continue`, there is no `mergeWideEvent` call to record it. In production this makes it impossible to distinguish healthy retries from persistent connection issues. Consider adding something like `mergeWideEvent({ query_retry: 1, query_retry_code: ... })` before `continue`.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant BE as Batch Executor
    participant QB as SimpleQueryBuilder
    participant CH as ClickHouse (chQuery)
    participant Metrics as Wide Event Metrics

    Note over Client,Metrics: Single Query Execution with Retry

    Client->>BE: executeBatch([singleQueryRequest])
    BE->>BE: runSingle(config, req)
    loop attempt=0..1
        BE->>QB: new SimpleQueryBuilder(config, req)
        QB->>CH: builder.execute(abortSignal)
        alt Success
            CH-->>QB: query result data
            QB-->>BE: data array
            BE->>Metrics: mergeWideEvent(query_type, from, to, rows)
            BE-->>Client: {type, data}
        else Error raised
            CH-->>QB: throws Error
            QB-->>BE: error object
            BE->>BE: isTransientClickHouseError(error)
            alt transient AND attempt===0
                BE->>BE: continue (retry)
            else not transient OR attempt===1
                BE->>Metrics: mergeWideEvent(query_error)
                BE-->>Client: {type, data:[], error}
            end
        end
    end

    Note over BE,BE: Transient Error Detection (nested cause traversal)

    BE->>BE: hasAbortError(error)
    alt name==="AbortError" OR code==="ABORT_ERR"
        BE->>BE: return false (no retry)
    else
        BE->>BE: check code in TRANSIENT_CODES (ECONNRESET, EPIPE, ETIMEDOUT, etc.)
        alt code matches
            BE->>BE: return true (transient)
        else
            BE->>BE: check message against TRANSIENT_MESSAGES
            alt message matches
                BE->>BE: return true (transient)
            else
                BE->>BE: recurse into cause (max depth 5)
            end
        end
    end

    Note over BE,BE: Single retry only on first attempt (attempt 0)
Loading

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

return { type: req.type, data: [], error };
return { type: req.type, data };
} catch (e) {
if (attempt === 0 && isTransientClickHouseError(e)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Honor opts.abortSignal before retrying. Otherwise a canceled request can issue the retry path whenever the thrown error is transient-shaped but not named AbortError.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/query/batch-executor.ts, line 342:

<comment>Honor opts.abortSignal before retrying. Otherwise a canceled request can issue the retry path whenever the thrown error is transient-shaped but not named AbortError.</comment>

<file context>
@@ -247,27 +321,35 @@ async function runSingle(
-		return { type: req.type, data: [], error };
+			return { type: req.type, data };
+		} catch (e) {
+			if (attempt === 0 && isTransientClickHouseError(e)) {
+				continue;
+			}
</file context>
Suggested change
if (attempt === 0 && isTransientClickHouseError(e)) {
if (attempt === 0 && !opts?.abortSignal?.aborted && isTransientClickHouseError(e)) {

return { type: req.type, data };
} catch (e) {
if (attempt === 0 && isTransientClickHouseError(e)) {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The retry path emits no telemetry. When a transient error triggers continue, there is no mergeWideEvent call to record it. In production this makes it impossible to distinguish healthy retries from persistent connection issues. Consider adding something like mergeWideEvent({ query_retry: 1, query_retry_code: ... }) before continue.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/query/batch-executor.ts, line 343:

<comment>The retry path emits no telemetry. When a transient error triggers `continue`, there is no `mergeWideEvent` call to record it. In production this makes it impossible to distinguish healthy retries from persistent connection issues. Consider adding something like `mergeWideEvent({ query_retry: 1, query_retry_code: ... })` before `continue`.</comment>

<file context>
@@ -247,27 +321,35 @@ async function runSingle(
+			return { type: req.type, data };
+		} catch (e) {
+			if (attempt === 0 && isTransientClickHouseError(e)) {
+				continue;
+			}
+
</file context>

@izadoesdev
izadoesdev merged commit 699f3d4 into staging Jun 30, 2026
19 checks passed
@izadoesdev
izadoesdev deleted the codex/superlog-retry-transient-clickhouse-queries branch June 30, 2026 16:01
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