fix(warehouse): repair capability probing and the service-map rollup - #342
Merged
Merged
Conversation
Two silent, total failures found while investigating query performance. Capability inspection never succeeded in production. Tinybird's /v0/sql answers 403 for system.columns and system.data_skipping_indices, and the gateway serves system.columns in p50 2.2s — over the 2s inspection budget (inspectCapabilities p99 was exactly 2000ms, clamped at the timeout). One denied probe failed the whole Effect.all, so every query fell back to baselineWarehouseCapabilities(): attributeIndexMode "none" and logBodySearchMode "scan". idx_span_attr_keys, idx_log_attr_keys and idx_lower_body are all deployed and were never used by a single query. The misses also added p50 286ms / p99 4.5s to ~10% of logs/traces queries. Backends running the schema we deploy now answer from the generated snapshot (BackendDialect.managedSchema -> managedWarehouseCapabilities), parsed out of latestSnapshotStatements so adding an index to datasources.ts enables its feature with no code change here. BYO ClickHouse still probes live, now degrading per-probe instead of collapsing, with a 1h cache TTL. service_map_edges_hourly has been frozen since 2026-05-15 — 32 rows total. It declares jsonPaths: false but is written directly via POST /v0/events, so Tinybird rejected every write with "Data Source needs to have JSONPaths defined". An hour seals only once its edge rows land, so nothing sealed, so all six lookback hours re-ran every tick for ~260 orgs forever: ~73k queries and ~175 GB/day. The fan-out was the symptom. Removes jsonPaths: false from both rollup targets (see alertChecks for the exemplar), gates the tick on activeOrgsByTracesQuery (14 orgs had spans in 7h vs ~260 processed), and bounds the resolutions repair pass with a serviceMapResolutionsExistingHoursSQL probe instead of re-running the raw-traces self-join for every sealed hour. Also two-stages spanSearchQuery's raw-traces path like tracesListQuery. traces is sorted (OrgId, ServiceName, SpanName, toDateTime(Timestamp)), so ORDER BY Timestamp DESC cannot read in order and the single-stage query materialized both attribute Maps for every matching row before LIMIT — 9-13 GB per call. Note: the datasource change needs a Tinybird deploy before edge writes resume.
…ource change Editing datasources.ts drives the generated ClickHouse schema and the local CLI store artifacts alongside the Tinybird manifest; only the Tinybird one was regenerated, so clickhouse:schema:check failed. JSONPaths are Tinybird metadata, so the DDL itself is unchanged — the diff is the project revision hash, which now matches the manifest.
The insert-mappings generator also writes the Rust constant consumed by apps/ingest; only the TS/JSON outputs were committed last time.
This comment has been minimized.
This comment has been minimized.
…fixture 1ded298 added userName, userEmail, groupId and groupName to SessionReplayListItem as required Schema.String fields but did not update baseRow in session-replay.schema.test.ts, so five tests fail with 'SchemaError: Missing key at ["userName"]'. main is red with this today — it is not introduced by this branch, and this commit is self-contained so it can be cherry-picked to main or dropped from this PR. Uses "" for all four, the never-identified state the schema documents, which is what this fixture's anonymous userId: null row carries.
🍁 Maple PR previewNote Preview resources were removed when this pull request closed. Final commit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Investigated production query performance via
tinybird.pipe_stats_rtand Maple's own traces. Aggregate p99 is healthy (0.198s over 174k queries/day) — but two things turned out to be completely broken, silently.1. Capability probing never succeeded — not once
inspectCapabilitiesruns foursystem.*probes. In production:system.columnssystem.data_skipping_indicessystem.projectionssystem.settings,version()Tinybird's
/v0/sqldoes not exposesystem.*to workspace tokens; the gateway servessystem.columnsin 2.2s, over the 2s budget. Maple's own spans confirm it —inspectCapabilitiesp99 was exactly 2000ms, clamped at the timeout, with ~370 "fell back to conservative plan" log lines/day.Because
Effect.allfailed the whole set on one denied probe, every query gotbaselineWarehouseCapabilities()→attributeIndexMode: "none",logBodySearchMode: "scan". WithindexMode === "none",buildAttrFilterConditionemits only the exact Map lookup and skips thehas(mapKeys(...))candidate prefilter — soidx_span_attr_keys,idx_log_attr_keysandidx_lower_bodyare all deployed and were never used by a single production query. On top of that, misses (10% of logs/traces queries) added p50 286ms / p99 4.5s of overhead before the real query started.Fix: we deploy the managed schema, so its indices are a compile-time fact.
BackendDialect.managedSchema(true fortinybird+tinybird-gateway) short-circuitsresolveCapabilitiestomanagedWarehouseCapabilities(), derived by parsinglatestSnapshotStatements— not hand-listed, so adding an index todatasources.tsenables the matching feature with no change here. BYO ClickHouse still probes live, now with per-probe degradation (degradeToEmpty) and a 1h cache TTL.2. The service-map edge rollup has been dead since 2026-05-15
Chasing a 73k-queries/day fan-out led to the actual bug:
service_map_edges_hourlyholds 32 rows, newest 2026-05-15.Both rollup targets declare
jsonPaths: falsebut are written directly viaPOST /v0/eventsbyServiceMapRollupService, not by a materialized view. Every write was rejected. Since an hour seals only once its edge rows land (if (rows.length > 0)), nothing ever sealed — so all sixLOOKBACK_HOURScandidates re-ran on every tick for ~260 orgs, forever: ~73,000 queries and ~175 GB/day. The fan-out was the symptom, not the bug.Fixes:
jsonPaths: falsefromserviceMapEdgesHourlyandserviceAddressResolutionsHourly(alertChecksis the exemplar — the other directly-ingested datasource). Manifest regenerated.CH.activeOrgsByTracesQuery(), the same pattern the error-issue and anomaly ticks already use. Only 14 orgs had spans in the last 7h, vs ~260 being processed. Fails open (unlike the anomaly tick) because a skipped rollup hour is a permanent gap once it ages out, whereas a skipped anomaly evaluation is just a late alert.serviceMapResolutionsExistingHoursSQLprobe. It previously re-ran the raw-tracesself-join — the most expensive query in the tick — for every sealed hour on every tick, unconditionally.3.
spanSearchQuerytwo-stagingWithout a
traceIdit reads rawtraces, whose sort key(OrgId, ServiceName, SpanName, toDateTime(Timestamp))cannot serveORDER BY Timestamp DESC. Single-stage, it materialized both attribute Maps for every matching row in range beforeLIMITdiscarded all but N — 9–13 GB per call, occasionally timing out. Now two-staged exactly like its siblingtracesListQuery. Thetrace_detail_spanspath is unchanged (sort-key prefix, one trace, no cutoff needed).Reviewer notes
spanSearchQuery'sORDER BYstill uses thetoString(Timestamp)alias — left alone deliberately to keep results identical; the win is the cutoff, and stage 2's row set is now small.traceSummariesQuery'sTraceId IN (SELECT ...)subquery. Dropped — it already has one; I verified by compiling the query.tracesBaseWhereConditionsadds theTimestampbounds unconditionally.max_execution_time=10(28 of them the internal ingest-volume dashboard).Verification
tsc --noEmitclean inpackages/query-engine,packages/domain,apps/api.spanSearchQuerycutoff, nothing else.managed-capabilities.test.tsasserts the managed schema yieldslogs.attributes.bloom,traces.attributes.bloomandlogs.body.tokenbf. The rewritten gateway test asserts nosystem.*query is issued at all.After deploy, re-check:
system.columnsrequest count inpipe_stats_rtshould go to ~zero (and the 403s with it);maple.query.capabilities.metadata_availableshould betrue; the "conservative plan" log line should stop;service_map_edges_hourlyshould start filling for active orgs;read_byteson attribute-filtered log searches should drop.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.