Skip to content

[OPIK-7773] [BE] feat: fail readiness when the traces wrap flag disagrees with the DB topology - #7948

Merged
thiagohora merged 11 commits into
mainfrom
thiagoh/OPIK-7773-fail-fast-traces-topology-assertion
Aug 26, 2026
Merged

[OPIK-7773] [BE] feat: fail readiness when the traces wrap flag disagrees with the DB topology#7948
thiagohora merged 11 commits into
mainfrom
thiagoh/OPIK-7773-fail-fast-traces-topology-assertion

Conversation

@thiagohora

@thiagohora thiagohora commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Details

Post-cutover traces is a Distributed table that cannot take mutations, so TraceDAO routes trace deletes at traces_local only while databaseAnalyticsDataModel.tracesDistributedWrapEnabled is on (OPIK_7455). When an install's flag disagrees with its database every trace delete breaks — code 36/48 one way, code 60 the other — and nothing says so until the first delete runs. This adds a readiness check that asserts the flag against the live topology at startup, so a misconfigured install is pulled from rotation instead of serving traffic it cannot mutate. It depends on each environment's Helm config plus its live database, which is why it can't be a CI guard.

  • ClickHouseTracesTopologyHealthCheck (clickhouse-traces-topology, critical: true / type: ready) — one system.tables lookup covers both tables and both directions: flag on requires traces to be Distributed and traces_local to exist; flag off requires traces to be a (Replicated)MergeTree. An absent traces is unhealthy either way. On a mismatch the message names the flag, the engine actually observed, and the fix.
  • Deliberately not toggle-gated, unlike the sibling clickhouse-cluster / clickhouse-cold-storage-disk probes: flag off over a MergeTree traces is the default on every install as shipped (OSS Docker, self-hosted, pre-cutover SaaS), so the assertion holds universally and only a genuine misconfiguration trips it.
  • No routing logic changed. The flag stays the source of truth — the probe reports, it never re-routes.
  • Package move: the probe extends the package-private AbstractClickHouseHealthCheck, so it can only live beside it. infrastructure/db/healthchecks now holds the family — both abstracts, the four ClickHouse probes, MysqlHealthyCheck — plus their tests, instead of scattering them through infrastructure/db.
  • Reviewer decision — the cutover window is now a readiness window. The flag and the wrap cannot land simultaneously, so the unavoidable mismatch window now takes pods out of rotation in either order rather than only failing deletes. That is what the ticket asks for, it is self-clearing (the probe re-evaluates continuously), and the wrap already requires --confirm-maintenance, so the window belongs inside that maintenance window — which the cutover runbook now says explicitly. If the team would rather the cutover degrade than go dark, the lever is critical: false in config.yml: still reported on /health-check, no longer gating rotation. Worth an explicit call here.
  • On the table name, since it invites confusion: the check targets traces_local, not traces_local_v2. The latter is the backfill shadow table — created empty in every install by migrations 000101/000114, EXCHANGEd and then renamed to traces_pre_cutover_backup before the wrap (exchange_and_wrap.sh hard-errors if it still exists at wrap time). traces_local is what the wrap's RENAME produces and what TraceDAO.java:1934 / :5129 actually mutate under the flag, which is precisely what this check exists to protect.

Change checklist

  • User facing
  • Documentation update

Issues

  • OPIK-7773

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 5
  • Scope: Health check implementation, the three test suites, and the documentation/runbook edits — drafted with AI from the ticket plus the existing AbstractClickHouseExistenceHealthCheck / TracesDistributedWrapMutationTest patterns in this repo.
  • Human verification: author-directed throughout, with three author corrections folded in (package layout, idempotent wrap in the readiness suite, dropping the premature changelog entry). All touched suites run locally and green (see Testing); the already-wrapped branch of the readiness suite was verified by re-running it with the test order reversed. Author review of the full diff before merge.

Testing

# unit + integration, all suites this PR touches or moves
mvn -o test -Dtest='ClickHouseTracesTopologyHealthCheckTest,ClickHouseTracesTopologyReadinessTest,\
HealthCheckIntegrationTest*,AbstractClickHouseHealthCheckTest,ClickHouseExistenceHealthCheckTest,\
IsAliveE2ETest,IsAliveResourceTest,TracesDistributedWrapMutationTest' -DfailIfNoTests=false
# -> Tests run: 54, Failures: 0, Errors: 0, Skipped: 0

mvn -o test-compile   # whole test tree compiles after the package move
mvn -o spotless:apply # no reformatting produced

Scenarios validated:

Flag traces topology Expected Covered by
false (Replicated)MergeTree ready HealthCheckIntegrationTest.DefaultConfig (real system.tables query)
true MergeTree (never wrapped) not ready HealthCheckIntegrationTest.TracesDistributedWrapEnabledWithoutTheWrap
false Distributed (cut over) not ready ClickHouseTracesTopologyReadinessTest
true Distributed over traces_local ready ClickHouseTracesTopologyReadinessTest
  • Both mismatch directions assert /health-check?name=all&type=ready returns 503 — the chart's actual component.backend.readinessProbe path — so the tests prove the pod really leaves rotation, not just that a JSON row flipped.
  • ClickHouseTracesTopologyReadinessTest uses dedicated, non-reused ClickHouse + ZooKeeper containers (the wrap destructively renames the live traces) and applies the wrap block verbatim from 000003_exchange_and_wrap.sql, walking the real transition: ready → wrap → not ready.
  • Edge cases in ClickHouseTracesTopologyHealthCheckTest (13 cases, asserting exact messages since the message is all an operator sees): traces absent under either flag; Distributed traces with traces_local missing; MergeTree/Replicated/Shared all accepted when the flag is off; a non-MergeTree engine rejected; query failure and interrupt paths cancel the in-flight query and restore the interrupt flag.
  • Regression: TracesDistributedWrapMutationTest (OPIK_7455's suite, which boots with the flag on and the wrap applied) still passes with the new critical readiness check in place.
  • Environment: local, macOS, Testcontainers (ClickHouse 26.3.16.16-alpine + ZooKeeper 3.9.4, plus the shared MySQL/Redis containers).

Not run, with reason:

  • helm unittest — plugin not installed locally. helm template also unavailable (chart dependencies not vendored). The values.yaml change is comment-only, so nothing renders differently; tests/configmap_env_test.yaml is untouched and its assertions are unaffected.
  • No video: the change has no UI surface.

Documentation

  • self-host/troubleshooting.mdx — new "Backend Not Ready: clickhouse-traces-topology" section, mirrored into both docs/ and docs-v2/ (these pages are kept in sync): the failure is intentional, here is the flag/engine table and the system.tables query to resolve it, and do not remove the check instead of fixing the mismatch.
  • data-migrations/traces-local-v2-cutover/README.md — the readiness-window consequence described above; its claim that no readiness endpoint exposes the flag's value is replaced by the endpoint that now does.
  • apps/opik-backend/config.yml, DatabaseAnalyticsDataModelConfig javadoc, deployment/helm_chart/opik/values.yaml — the flag is now asserted at readiness. The chart already wires the flag through values → configmap → env (OPIK_7455), so values.yaml gains only that note; these keys don't use the # -- prefix helm-docs reads, so the chart README.md needs no regeneration.
  • No self-host changelog entry yet — deliberately held back until the shipping release is decided, so the page doesn't state a version we can't stand behind.

🤖 Generated with Claude Code

@thiagohora
thiagohora requested review from a team as code owners August 21, 2026 12:22
@github-actions github-actions Bot added documentation Improvements or additions to documentation java Pull requests that update Java code Backend Infrastructure tests Including test files, or tests related like configuration. 🔴 size/XL labels Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
⚓ helm-docs Regenerate Helm chart README 7.08s
☕ spotless — java backend Format Java code 5.11s
🛡️ semgrep — java backend sql Block SQL injection-prone string formatting 2.20s
🐍 mypy — python sdk Static type check 1.23s
🐍 fix end of files — python sdk Ensure files end in a newline 0.03s
🐍 trim trailing whitespace — python sdk Strip trailing whitespace 0.02s
🐍 ruff-format — python sdk Format Python code (ruff) 0.01s
🐍 ruff — python sdk Lint + autofix Python (ruff) 0.01s
Total (8 ran) 15.69s
⏭️ 36 skipped (no matching files changed)
Hook Description Result
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️
🌈 zizmor — github workflows security Security-scan GitHub Actions workflows ⏭️

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🌿 Preview your docs: https://opik-preview-01a03dd3-ae0e-770f-86e1-b79fff1b3d60.docs.buildwithfern.com/docs/opik

No broken links found

Unverified links (timeout / rate-limited / server error — not failing the check)

https://aistudio.google.com/apikey (401)
↳ on page: /docs/opik/development/optimization-runs/optimization/configure_models
https://console.cloud.google.com/iam-admin/iam (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.cloud.google.com/iam-admin/roles (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.cloud.google.com/iam-admin/serviceaccounts (401)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://console.mistral.ai/api-keys/ (timeout)
↳ on page: /docs/opik/integrations/mistral
https://console.x.ai/ (403)
↳ on page: /docs/opik/integrations/xai-grok
https://docs.predibase.com/integrations/comet (403)
↳ on page: /docs/opik/integrations/predibase
https://portal.azure.com/ (403)
↳ on page: /docs/opik/administration/workspace-settings/ai_providers
https://x.ai/ (403)
↳ on page: /docs/opik/integrations/xai-grok


📌 Results for commit 99130a2

@CometActions

CometActions commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Already covered by a test in this PR.

The probe ships with its own cover, at the endpoints that matter. HealthCheckIntegrationTest.TracesDistributedWrapEnabledWithoutTheWrap boots the app with tracesDistributedWrapEnabled=true over an unwrapped traces and asserts both the per-check row (healthy=false, critical, ready) and GET /health-check?name=all&type=ready returning 503 — the exact URL the chart's readinessProbe hits. ClickHouseTracesTopologyReadinessTest drives the real transition on its own containers: ready before the wrap, not ready after it with the flag still off, then healthy again through a probe built with the flag on over the wrapped topology. That is the change, both mismatch directions plus the two matching states, so a Playwright spec would only repeat it — and a default OSS install (flag off over a ReplicatedMergeTree traces) is byte-identical to today, which HealthCheckIntegrationTest's all case pins. The remaining files are a package move into infrastructure/db/healthchecks and docs. Note: existing_tests.json reported no tests only because it scans tests_end_to_end/.

Not testable yet. The /is-alive/ping half is asserted nowhere. Because the check is critical, IsAliveResource — which filters on isCritical alone and ignores type — makes a mismatched install answer /is-alive/ping with 500 'Not Healthy' to every SDK and to the FE. config.yml calls that out as intended, but neither new test touches that endpoint; they stop at /health-check. Cheapest fix is one more assertion inside TracesDistributedWrapEnabledWithoutTheWrap, where the mismatched app is already running. From the e2e side it stays deferred: the estate deploys a stock install and has no way to stand up a deliberately mismatched one.

also touches Backend (Java API / internal), Deployment / Helm

Run

Advisory, from the QA test radar. Nothing here blocks this PR, and anything it proposes is a draft for review.

Re-checked after a push on 26 Aug 11:31 UTC.

Comment thread apps/opik-backend/config.yml Outdated
Comment thread apps/opik-documentation/documentation/fern/docs-v2/self-host/changelog.mdx Outdated
Comment thread apps/opik-backend/data-migrations/traces-local-v2-cutover/README.md Outdated
@comet-ml comet-ml deleted a comment from github-actions Bot Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 14

333 tests   331 ✅  5m 51s ⏱️
 29 suites    2 💤
 29 files      0 ❌

Results for commit 5643882.

♻️ This comment has been updated with latest results.

andrescrz
andrescrz previously approved these changes Aug 24, 2026

@andrescrz andrescrz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

Comment thread apps/opik-backend/config.yml
Comment thread apps/opik-backend/config.yml
Comment thread apps/opik-documentation/documentation/fern/docs/self-host/troubleshooting.mdx Outdated
@thiagohora
thiagohora force-pushed the thiagoh/OPIK-7773-fail-fast-traces-topology-assertion branch from 4dc98c5 to cc90574 Compare August 25, 2026 10:21
andrescrz
andrescrz previously approved these changes Aug 26, 2026

@andrescrz andrescrz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just something to double check, but no blockers.

Comment thread apps/opik-documentation/documentation/fern/docs-v2/self-host/troubleshooting.mdx Outdated
Comment thread apps/opik-backend/config.yml
Comment thread apps/opik-documentation/documentation/fern/docs-v2/self-host/troubleshooting.mdx Outdated
@thiagohora
thiagohora force-pushed the thiagoh/OPIK-7773-fail-fast-traces-topology-assertion branch from 33fabf6 to f0cbf44 Compare August 26, 2026 10:31
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Backend Tests - Unit Tests

3 481 tests   3 479 ✅  1m 51s ⏱️
  427 suites      2 💤
  427 files        0 ❌

Results for commit 7a9bcd4.

♻️ This comment has been updated with latest results.

andrescrz
andrescrz previously approved these changes Aug 26, 2026
thiagohora and others added 11 commits August 26, 2026 13:19
…rees with the DB

Post-cutover `traces` is a Distributed table that cannot take mutations, so TraceDAO routes
trace deletes at `traces_local` only while
`databaseAnalyticsDataModel.tracesDistributedWrapEnabled` is on (OPIK-7455). If an install's
flag does not match its database, every trace delete breaks — stale-`false` over a wrapped
`traces` with code 36/48, stale-`true` over an unwrapped one with code 60 — and nothing says
so until the first delete runs. The answer depends on each environment's Helm config plus its
live database, so this cannot be a CI guard; it belongs at readiness.

Adds ClickHouseTracesTopologyHealthCheck (`clickhouse-traces-topology`, critical/ready): one
`system.tables` lookup covering both tables and both directions — flag on asserts `traces` is
Distributed and `traces_local` exists, flag off asserts `traces` is a (Replicated)MergeTree —
reporting the flag, the observed engine and the fix on a mismatch. An absent `traces` is
unhealthy either way, since trace reads and writes cannot work at all. Unlike the sibling
`clickhouse-cluster` / `clickhouse-cold-storage-disk` probes it is deliberately not
toggle-gated: flag off over a MergeTree `traces` is the default on every install as shipped
(OSS Docker, self-hosted, pre-cutover SaaS), so the assertion holds universally and only a
genuine misconfiguration trips it. The flag stays the source of truth — the probe reports, it
never re-routes, and no routing logic changed.

Collects the db health checks into `infrastructure/db/healthchecks`: the new probe extends the
package-private AbstractClickHouseHealthCheck, so it can only live alongside it, and the
package now holds the family (both abstracts, the four ClickHouse probes, MysqlHealthyCheck)
plus their tests instead of scattering them through `infrastructure/db`.

Test coverage is three-layered. ClickHouseTracesTopologyHealthCheckTest asserts the exact
message of every case, not merely that the probe went red — the message is all an operator
sees. HealthCheckIntegrationTest covers the matching flag-off install (healthy, folded into
the existing per-check and aggregate expectations) and the flag-on-without-the-wrap mismatch
on the shared containers, since the probe only reads system.tables.
ClickHouseTracesTopologyReadinessTest takes dedicated, non-reused ClickHouse and ZooKeeper
containers — the wrap destructively renames the live `traces` — and walks the real transition:
ready, apply the wrap block verbatim from 000003_exchange_and_wrap.sql, then not ready. Both
mismatch directions assert `/health-check?name=all&type=ready` returns 503, the chart's actual
`component.backend.readinessProbe` path, so the tests prove the pod really does leave rotation
rather than merely that a row flipped.

Documents the check where operators meet it: a self-host changelog entry and a troubleshooting
section (mirrored into both docs trees) stating that the failure is intentional, giving the
engine/flag table and the `system.tables` query to resolve it, and warning against removing
the check instead of fixing the mismatch. The chart already wires the flag through
values -> configmap -> env (OPIK-7455), so values.yaml gains only the note that the flag is
asserted at readiness.

The cutover runbook gains the operational consequence this introduces: the flag and the wrap
cannot land simultaneously, so the unavoidable mismatch window is now a readiness window in
either order — pods leave rotation rather than just failing deletes. It must therefore sit
inside the maintenance window `--confirm-maintenance` already asserts for the wrap. The
window is self-clearing, since the probe re-evaluates continuously and rotation returns once
the two sides are back in step. The runbook's claim that no readiness endpoint exposes the
flag's value is replaced by the endpoint that now does.

Implements OPIK-7773.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…op the changelog entry

The readiness suite assumed it starts on an unwrapped `traces`, which holds today (the wrap
lives in data-migrations/, outside the migrations/ directory the analytics changelog includes)
but breaks the day it lands as a regular migration: the container would arrive already
Distributed, `CREATE TABLE traces_dist` would fail on the second pass, and asserting "healthy
first" would fail on a correctly behaving probe.

applyDistributedWrap() now returns early when `traces` is already Distributed, and the
pre-wrap state is read rather than assumed: the healthy half of the transition runs only when
there is something to transition from, while the mismatch half — the point of the test — always
runs. probeWithTheFlagOnIsHealthyOverTheWrappedTopology wraps idempotently instead of asserting
that a previous test left the topology in place, so it no longer depends on @order to pass;
the ordering stays only so the transition test still gets the pristine pre-wrap state.
Verified both ways by running the suite with the order reversed.

Drops the self-host changelog entry: which release ships this is not decided yet, so the entry
would state a version we cannot stand behind. The troubleshooting section carries the operator
guidance in the meantime and does not link to the changelog, so nothing dangles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…traces_local to a mutable engine

Two review findings, both in scope.

SQL is never assembled with Java string operations (.agents/skills/opik-backend/SKILL.md:146
— "don't copy them and don't add new ones"), and both new queries did. TOPOLOGY_QUERY is now a
literal text block instead of `.formatted(TRACES_TABLE, TRACES_LOCAL_TABLE)`; the constants stay
as the single source for the map lookups and the messages, and the unit test stubs the exact
query text, so the two cannot drift apart unnoticed. The readiness suite's wrap DDL likewise
spells the database out rather than interpolating DATABASE_NAME — ClickHouse cannot bind an
identifier inside a Distributed() engine argument, so the literal is held honest by an assertion
in beforeAll instead. The `.formatted(...)` calls that remain build health check messages, which
the rule explicitly still allows.

The flag-on branch accepted `traces_local` on presence alone, so a same-named View, Log or
nested Distributed table would report healthy while TraceDAO's DELETEs failed against it —
the same outcome as an absent table, which the probe already rejects. It now holds that table
to the MergeTree family, sharing the existing family-suffix predicate with the flag-off branch.
This needs no extra query: the engine was already in the row the probe reads, so the assertion
is free. Validating the Distributed engine_full's cluster/database/sharding-key arguments and
the shard's full schema was left out — that is a topology audit, well past the ticket's "one
cheap system.tables check", and it belongs with the CI DDL guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…he readiness DTO builder

Two more review findings, both in scope.

The runbook's self-clearing note had the restart on the wrong path: it read "no extra restart
needed on the wrap-first path, and only the already-planned restart on the toggle-first path",
which is backwards. Both orderings spend exactly the one planned rolling restart the toggle
already requires, and neither needs another; what differs is its position, and with it what
closes the mismatch window — the wrap DDL on the toggle-first path (the restart is already
done by then), the restart completing on the wrap-first path. Reworded to say that.

HealthCheckResponse used a plain @builder, copied from the same record in
HealthCheckIntegrationTest, where the convention predates the rule. Records and DTOs take
@builder(toBuilder = true) (.agents/skills/opik-backend/SKILL.md:52, which lists bare @builder
as the anti-pattern), so the new copy uses it; the older one is left alone as unrelated to
this ticket.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the accepted engines

Review feedback, and the first one was demonstrated by my own test fixture.

**The not-wrapped message promised the wrong failure.** It ended "otherwise trace
deletes fail with UNKNOWN_TABLE (60)", but that branch fires whenever the flag is
on and `traces` is not Distributed — and `traces_local` may well still exist in
that state. A rollback leaves exactly that shape: the wrapper dropped, the
original `traces` promoted back, the old shard lingering. The routed delete then
*succeeds against the stale shard* and the live rows are never touched, which is
quieter than the error and worse. The check's own unit test seeds `traces_local`
as present while asserting the UNKNOWN_TABLE wording, so the fixture disproved the
message.

Now states both outcomes and which one applies when, with the reasoning recorded
on the constant so it does not get "simplified" back.

**SharedMergeTree was accepted but undocumented.** The probe matches on the
`MergeTree` suffix, so ClickHouse Cloud's SharedMergeTree family satisfies it, but
config.yml, the Javadoc and the troubleshooting table all said
"(Replicated)MergeTree" — leaving a valid Cloud install looking misconfigured.
All three now describe the accepted family.

**Documented that `critical: true` also gates /is-alive/ping.** IsAliveResource
filters on isCritical alone and ignores `type`, so a readiness mismatch reports
the server as down to SDKs and makes them buffer. That is the existing behaviour
of every `ready` check here (`clickhouse`, `db`, `redis`, `mysql`), and it is
intended for this one too — but it was an unstated consequence, so it is now
stated where the flag is set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g node

Review follow-up: the probe reads the node-local system.tables, and nothing said
so or said why. It stays node-local — clusterAllReplicas needs REMOTE + CLUSTER
grants the app user is not guaranteed to hold (so a non-toggle-gated critical
check would fail on installs as shipped), one unreachable replica would pull the
whole fleet from rotation over a condition that breaks no delete, and on a shared
catalog (ClickHouse Cloud / SharedMergeTree) node-local already is cluster-wide.

The limit that follows is now stated instead of implied: a divergence confined to
some replicas degrades into sporadic unhealthy reports rather than a fleet-wide
outage, and the cluster-wide "did this ON CLUSTER DDL reach every replica" gate
stays in exchange_and_wrap.sh / finalize.sh, where it is fail-loud and operator-run.
The troubleshooting page gains the clusterAllReplicas form for an operator chasing
an intermittent failure, with the fix being the lagging replica, not the flag.

Comment and documentation only; no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…m env precedence

Two review catches on the troubleshooting page.

The cluster-wide diagnostic said "the engine must be identical on all of them",
which reads as if `traces` and `traces_local` must match each other — the exact
opposite of the healthy post-wrap shape. Compare down each table instead: one
table's engine must be the same on every host, and the two tables differing from
each other is the wrap working.

The resolution told operators to set the chart value, but the configmap derives
ANALYTICS_DB_DATA_MODEL_TRACES_DISTRIBUTED_WRAP_ENABLED only when it is absent
from component.backend.env, so an explicit entry there silently wins and the
check keeps failing after the change. Say so, and give the configmap read that
shows what the backend actually receives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the flag

Review feedback, and a good catch for a troubleshooting page specifically.

configmap-backend.yaml emits the derived
ANALYTICS_DB_DATA_MODEL_TRACES_DISTRIBUTED_WRAP_ENABLED key only when
`component.backend.env` does not already define it (`if not (hasKey $env $k)`).
So an operator following this page, changing
databaseAnalyticsDataModel.tracesDistributedWrapEnabled while an explicit env
entry exists, sees no effect at all: the check keeps failing with the same
message and nothing they change appears to help. That is precisely the state a
troubleshooting page exists to get someone out of.

Adds a callout with the precedence, the `printenv` command to see what the pod
actually received, and the fix — remove the explicit key so the setting is the
single source, or update it there instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page already documented this, further down the same Resolution section. I
added a second callout saying the same thing before checking, which is worse than
having missed it. Reverted to the existing wording.

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

The node-local query hardcoded `database = 'opik'` while the cluster-wide one
right below it used `<database_name>`, so the page contradicted itself — and on
any install with a custom ANALYTICS_DB_DATABASE_NAME the hardcoded form returns
no rows, which reads as "the tables are missing" rather than "wrong database".
Both queries now take the placeholder, with the substitution stated once, up
front, where it covers both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…econd reader

OPIK-7772's routing guard (#7953) landed on main after this branch last rebased
and allows exactly one reader of tracesDistributedWrapEnabled — TraceDAOImpl's
accessor. This probe is a second one, so the rebase turned it into a build
failure. The guard is right to notice; the exemption is what was missing.

Widened to an allowlist of two, still member-scoped so it is not a category
hole: TraceDAOImpl#tracesDistributedWrapEnabled and the probe's constructor.
What the rule protects is the routing decision — a second place that branches on
the flag is a second place that can name the wrong table — and the probe names no
table and issues no mutation. It reads the flag to assert it against the live
`system.tables` engine and report a mismatch at readiness.

byCodeUnitsThat rather than byMethodsThat because the flag is read at injection
and a constructor is not a JavaMethod, so the method-only form could report the
call but never admit it. Renamed the rule off "exactly_one_place", which two
readers would have made a lie.

Verified the teeth both ways: a third reader added as a method on a sibling
health check in the same package, and again as a constructor on another, each
fails the rule. Suites run: TraceMutationRoutingArchTest, TraceMutationSqlRoutingTest,
Trace/SpanDeletionEventArchTest, the four healthcheck suites (55 tests, green).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thiagohora
thiagohora force-pushed the thiagoh/OPIK-7773-fail-fast-traces-topology-assertion branch from 268a83f to 7a9bcd4 Compare August 26, 2026 11:27
Comment on lines +70 to +72
private static DescribedPredicate<JavaCodeUnit> only(Class<?> owner, String memberName) {
return DescribedPredicate.describe("%s.%s".formatted(owner.getSimpleName(), memberName),
codeUnit -> codeUnit.getOwner().isEquivalentTo(owner) && codeUnit.getName().equals(memberName));

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.

Unauthorized overloads bypass routing guard

only matches code units by owner and getName() alone, so same-named constructor or method overloads can satisfy the exemptions and let an unauthorized flag reader or routing-table resolver pass the architecture guard — should we match parameter types for ClickHouseTracesTopologyHealthCheck, TraceDAOImpl#tracesDistributedWrapEnabled, and #tracesMutationTable, with wrong-signature regression coverage?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`apps/opik-backend/src/test/java/com/comet/opik/domain/TraceMutationRoutingArchTest.java`
around lines 70-72, update the `only` predicate so it matches the owner, member name,
and exact parameter types rather than accepting every same-named constructor or method.
Use the intended `ClickHouseTracesTopologyHealthCheck` three-argument constructor and
the exact `TraceDAOImpl` accessor/resolver signatures, then add ArchUnit regression
fixtures or tests proving wrong-signature overloads are rejected.

@thiagohora
thiagohora merged commit 766a2d7 into main Aug 26, 2026
85 checks passed
@thiagohora
thiagohora deleted the thiagoh/OPIK-7773-fail-fast-traces-topology-assertion branch August 26, 2026 11:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend documentation Improvements or additions to documentation Infrastructure java Pull requests that update Java code 🔴 size/XL tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants