-
[OPIK-6505] [BE] Split stats query and gate legacy feedback_scores UNION (#6713)
发布于
2026-05-15 16:13:50 +00:00 - [OPIK-6505] [BE] Split traces/spans stats query and gate legacy feedback_scores UNION
Splits the project-stats endpoints into two parallel queries (traces+spans
aggregation and feedback-score aggregation) and runs them via Mono.zip. The
per-trace/per-span JOIN against the rich groupArray-tuple feedback CTE was
the dominant cost at scale.Adds a
workspaces.has_legacy_scoresflag (defaults TRUE) detected during
workspace version determination; the stats SQL gates the legacy
feedback_scores table UNION on it so workspaces with no legacy data skip the
empty-table scan.Applies the same split + gate to SpanDAO.getStats. On the OPIK-6505 customer
dataset this brings end-to-end stats wall clock down from ~78s to ~4s
unfiltered and ~0.3s with a trace-side filter, with byte-identical API
output (144-key feedback map matched to float64 precision).- [OPIK-6505] [BE] Align StatsMapperTest with split-A / split-B contract
The split moved feedback-score row mapping out of mapProjectStats (split-A
no longer carries those columns) into a new mapProjectScoresStats(Row) used
by SELECT_FEEDBACK_SCORES_STATS / SELECT_SPAN_FEEDBACK_SCORES_STATS. Update
the test to assert the new contract:- mapProjectScoresStats_withSpanFeedbackScores_returnsSpanFeedbackScoresStats
asserts the new method emits the feedback stat items. - mapProjectStats_doesNotEmitFeedbackScoreStats asserts the trace+spans
mapper now intentionally drops feedback-score columns.
- [OPIK-6505] [BE] Address review: NPE guard, consolidate hasLegacyScores, add regression test
- StatsMerger.merge: avoid NPE for projects with no feedback rows by
falling back to ProjectStats.empty() at the call site instead of guarding
inside the single-project overload. - Move the reactive has_legacy_scores lookup onto WorkspacesService
(Mono hasLegacyScores) so TraceDAO and SpanDAO share one
implementation instead of duplicating a Schedulers.boundedElastic wrapper. - Add ProjectsResourceTest coverage that flips the workspace flag and
re-hits /stats to verify the endpoint stays consistent.
- [OPIK-6505] [BE] Fix CI regressions: thread stats feedback + merge order
- StatsMapper.mapProjectStats: re-emit feedback_scores when the row carries
that column. Thread stats inline feedback in the same row and call this
mapper directly with no merge step; the prior linter refactor dropped them
silently. Split-A trace/span rows omit the column, so the metadata-guard
is a no-op there. - StatsMerger.merge: splice the feedback ProjectStats into the base list at
the canonical position (just before guardrails / error-count entries)
instead of appending. The pre-split mapper emitted feedback there, and
StatsUtils-built expected stats assert that order. - StatsMapperTest: replace the "drops feedback" assertion with two cases —
emits when the column is present, omits when it is not — matching the
real contract for split-A vs thread paths.
- [OPIK-6505] [BE] Defend stats Mono.zip against empty traces/spans branch
SELECT_TRACES_SPANS_STATS and SELECT_SPANS_STATS both end with GROUP BY
project_id, so when no rows match the filter the projection emits zero
rows and singleOrEmpty() completes empty. Mono.zip then drops the
feedback branch as well, so the response silently loses feedback-only
stats. Match the safety net already on the feedback side by attaching
.switchIfEmpty(Mono.just(new ProjectStats(List.of()))) to both
traces/spans branches.- [OPIK-6505] [BE] Address review: gate legacy probe, dedupe stats orchestration
WorkspaceVersionService.persistAndEmitBlocking now reuses the existing
findById(workspaceId) lookup (already loaded for lastKnownVersion) to gate
the ClickHouse probe + MySQL upsert behind the stored has_legacy_scores
flag. Once that flag is false the legacy feedback_scores table only ever
shrinks (no new writes land there), so the per-cache-miss probe is wasted
work. When the flag is true we still probe but only upsert when the result
differs, eliminating the redundant rewrites flagged in review.Extracted the split-A / split-B Mono.zip orchestration into
StatsMerger.zipAndMerge so SpanDAO.getStats and TraceDAO.getStats invoke
the same logic. The helper centralises the empty-default safety net that
the prior commit introduced ad-hoc on each side — future tweaks to the
zip semantics now live in one place.- [OPIK-6505] [BE] Address review: short-circuit zipAndMerge, trim comments
zipAndMerge: when the aggregates Mono emits empty, return empty stats
instead of forcing a feedback-only response. This matches the
traces-driven scope the map overload enforces (no resurrecting
projects from feedback alone). Use the existing ProjectStats.empty()
factory instead of allocating new ProjectStats(List.of()).Trim verbose comments on StatsMerger, mapProjectStats, and the
WorkspaceVersionService gated probe down to one-line whys.- [OPIK-6505] [BE] Add regression test with real legacy_scores data
Seeds a project via buildProjectStats then writes one feedback score
directly into the legacyfeedback_scoresClickHouse table via the
DAO's author=null path (the public API always routes to
authored_feedback_scores). Asserts the /stats response — full object
recursive comparison — includes the legacy score when has_legacy_scores
is true and drops it after flipping the workspace flag to false.Closes the gap @ldaugusto flagged on the PR: previously the UNION
branch was only verified by manual production benchmarks.- [OPIK-6505] [BE] Cover all three legacy-UNION endpoints with real-data tests
Each test writes one feedback score directly into the legacy
feedback_scores ClickHouse table via the author=null DAO path (the
public API always routes to authored_feedback_scores), then triggers
the natural workspace-version determination flow via
workspaceResourceClient.getWorkspaceVersion so the probe sees the
legacy rows and persists has_legacy_scores=true the same way it does
in production. Stats assertions use whole-object recursive comparison.- ProjectsResourceTest: multi-project /projects/stats path.
- MultiValueFeedbackScoresE2ETest: single-project /traces/stats and
/spans/stats paths (new SQL templates on the span side).
Drops the previous flag=false branch on the multi-project test — that
state is impossible when legacy data exists, the existing
getProjects__whenHasLegacyScoresFlipped__thenStatsStayConsistent test
already covers the no-legacy-data case.下载附件