发布

  • [OPIK-6505] [BE] Split stats query and gate legacy feedback_scores UNION (#6713)

    frostbyte_neo 发布于 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_scores flag (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 legacy feedback_scores ClickHouse 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.

    下载附件