8000 feat(metrics): add BigQuery schema, row normalization and writer by AntonTyb · Pull Request #1405 · google/xmc · GitHub
[go: up one dir, main page]

Skip to content

feat(metrics): add BigQuery schema, row normalization and writer - #1405

Merged
AntonTyb merged 3 commits into
mainfrom
feat/metrics-ingester-bigquery-writer
Oct 27, 2026
Merged

AntonTyb merged 3 commits into
mainfrom
feat/metrics-ingester-bigquery-writer

Conversation

@AntonTyb
@AntonTyb AntonTyb commented Oct 25, 2026 •
Copy link
Copy Markdown
Collaborator

Stack (1/6): main ← #1405 (this PR) ← #1406 ← #1407 ← #1408 ← #1409 ← #1410 — review and merge bottom-up.

Summary

Adds the canonical BigQuery xmc_metrics table schema (deploy/bigquery/schema.json), MV-equivalent Go row normalization (NormalizeMetrics), and the Tier 1 BigQueryWriter core for cmd/metrics_ingester (wired into the CLI runtime in #1406).

Motivation & Context

XMC deployments need the option to store experiment metrics in Google Cloud BigQuery instead of ClickHouse. In ClickHouse, incoming metrics land in xmc_metrics_pubsub_queue (ENGINE = Null) and pass through xmc_metrics_pubsub_mv, which drops rows with xid = 0, empty metric_name, or execution_id = 0, defaults empty replica_id to '0', and clamps timestamps outside [2020-01-01, 2050-01-01] UTC to ingestion time. Because BigQuery Storage Write API appends directly into xmc_metrics, NormalizeMetrics implements those exact rules in Go so both storage backends store identical rows.

BigQuery Storage Write API also requires proto2 wire encoding: using proto3 implicit field presence would omit zero-valued fields (step = 0, metric_value = 0.0, job_name = "") on the wire and either violate REQUIRED column constraints or insert NULLs that change SQL aggregation semantics (AVG, = ''). MetricRowDescriptor() declares all 10 fields LABEL_REQUIRED and marshalMetricRow encodes every field unconditionally via protowire.

Key Changes

  • deploy/bigquery/{schema.json, schema_test.py, README.md, BUILD.bazel}: Defines the 10-column REQUIRED BigQuery xmc_metrics schema with daily DATE(timestamp) partitioning (require_partition_filter = false) and xid, metric_name, work_unit_id clustering, plus a 1-to-1 column/type/non-nullability parity test against deploy/clickhouse/schema.sql.
  • cmd/metrics_ingester/metricsingester/{normalize.go, normalize_test.go}: Implements NormalizeMetrics matching xmc_metrics_pubsub_mv filtering, replica_id defaulting, and UTC microsecond timestamp clamping.
  • cmd/metrics_ingester/metricsingester/{bigquery_writer.go, bigquery_writer_test.go, writer.go, BUILD.bazel}: Implements BigQueryWriter, RowAppender, MetricRowDescriptor(), and 8 MiB chunked proto2 wire serialization, verified bit-for-bit against deploy/bigquery/schema.json and api/protos/metrics/metrics.proto (pb.Metric).
  • go.mod, go.sum, MODULE.bazel: Adds cloud.google.com/go/bigquery v1.77.0 aligned with existing Google Cloud SDK versions without bumping unrelated modules.

Assumptions

  • Timestamps outside [2020-01-01T00:00:00Z, 2050-01-01T00:00:00Z] are clamped to ingestion time, and rows with xid = 0, empty metric_name, or execution_id = 0 are dropped (returning nil when an entire batch is filtered so malformed messages are acked rather than redelivered indefinitely) — chosen to match ClickHouse's xmc_metrics_pubsub_mv sanitization rules in deploy/clickhouse/schema.sql — if historical replay workloads intentionally emit pre-2020 or post-2050 metric timestamps, those timestamps will be clamped to ingestion time unless the shared bounds are widened.
  • Individual AppendRows requests are chunked at defaultMaxAppendBytes = 8 MiB — chosen to stay safely below the BigQuery Storage Write API 10 MiB gRPC request limit while leaving headroom for protobuf framing — if a single NormalizedMetric row ever exceeded 8 MiB, it would be sent as a single-row chunk and rejected by BigQuery if above 10 MiB.
  • deploy/bigquery/schema.json configures daily DATE(timestamp) partitioning with require_partition_filter = false and clusters by xid, metric_name, work_unit_id — chosen because XMC dashboard queries filter by xid = ? (pruned via BigQuery block clustering) without requiring a mandatory timestamp window — ad-hoc manual queries that omit both xid and timestamp predicates will scan all partitions unless a filter is supplied.

Verification & Testing

  • Commands Executed:
    • bazel test --@io_bazel_rules_go//go/config:race --runs_per_test=10 //cmd/metrics_ingester/... //deploy/bigquery/... //deploy/clickhouse/...
    • bazel coverage --combined_report=lcov //cmd/metrics_ingester/metricsingester:metricsingester_test
    • ./.githooks/pre-commit
  • Coverage & Test Status: All repository unit tests in pre-commit and 30/30 race-enabled package test runs passed; 100.0% line coverage across bigquery_writer.go, normalize.go, and writer.go.

Related Issues / Bugs

N/A

TAG=agy
CONV=0fafd02b-f3b1-4563-a73d-49ec0845afbd

Add the canonical BigQuery `xmc_metrics` table schema, MV-equivalent Go row
normalization, and Tier 1 `BigQueryWriter` core for `cmd/metrics_ingester` as
the first step toward supporting BigQuery alongside ClickHouse for experiment
metrics storage.

Unlike ClickHouse, where `xmc_metrics_pubsub_mv` filters invalid records,
defaults `replica_id`, and clamps out-of-range timestamps inside the database,
BigQuery appends directly into `xmc_metrics`. `NormalizeMetrics` reproduces the
exact `xmc_metrics_pubsub_mv` semantics in Go (`xid != 0`, `metric_name != ""`,
`execution_id != 0`, `replica_id` defaulting to `"0"`, and `[2020-01-01,
2050-01-01]` UTC timestamp clamping) so both storage backends persist identical
rows.

`BigQueryWriter` encodes rows using proto2 `LABEL_REQUIRED` wire format via
`protowire` so zero values (`step = 0`, `metric_value = 0.0`, `job_name = ""`)
are always serialized explicitly rather than omitted as `NULL`, and chunks
serialized payloads under an 8 MiB ceiling below the BigQuery Storage Write
API's 10 MB request limit. Schema parity tests in both Python (`schema_test.py`)
and Go (`bigquery_writer_test.go`) lock `deploy/bigquery/schema.json`,
`deploy/clickhouse/schema.sql`, `MetricRowDescriptor()`, and `marshalMetricRow`
together.

TAG=agy
CONV=0fafd02b-f3b1-4563-a73d-49ec0845afbd

Change-Id: Ied7abfc7a1b75b937caef8c64f05c30c3d905e1d
@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds a new BigQuery writer for the metrics ingester, including the necessary schema definitions, normalization logic, and corresponding tests. The reviewer suggested adding context cancellation checks in the chunking loop, adding a nil check for metrics in the normalization function, and improving the robustness of the schema test's type mapping logic.

Comment thread deploy/bigquery/schema_test.py Resolved
… schema test

Address automated code review feedback on PR #1405:
- Check `ctx.Err()` at the start of each chunk iteration in
  `BigQueryWriter.appendChunked` so canceled contexts abort immediately without
  issuing further `AppendRows` calls, with unit test coverage.
- Add an explicit `m == nil` guard at the start of `normalizeSingle` so nil
  slice entries do not rely implicitly on generated protobuf getter nil-safety.
- Broaden `_map_clickhouse_type` in `deploy/bigquery/schema_test.py` to match
  `f"{prefix} "` so ClickHouse column modifiers beyond `DEFAULT` (such as
  `CODEC`, `COMMENT`, or `TTL`) map cleanly.

Comment link: #1405 (comment)
Comment link: #1405 (comment)
Comment link: #1405 (comment)

TAG=agy
CONV=0fafd02b-f3b1-4563-a73d-49ec0845afbd

Change-Id: I2caa4dd820ce9f76ba2b9dd28eb416f0ff79d1c9
@AntonTyb
AntonTyb marked this pull request as ready for review October 25, 2026 12:29
@github-actions
github-actions Bot requested a review from janzak-dev October 25, 2026 12:29
@VLukiantsev
VLukiantsev added this pull request to stack #1412 October 26, 2026 09:37
…w schema

Add TestMetricProtoMatchesBigQueryRowSchema using protoreflect to assert that
MetricRowDescriptor() matches api/protos/metrics/metrics.proto (pb.Metric) in
field count (+ created_at), field numbers, names, and descriptor types, and that
every dynamically populated pb.Metric field survives NormalizeMetrics and
marshalMetricRow without loss.

Comment link: #1405 (comment)

TAG=agy
CONV=0fafd02b-f3b1-4563-a73d-49ec0845afbd

Change-Id: Ied450259fa838c0de06c77fd64bb866bee71f555
AntonTyb added a commit that referenced this pull request Oct 26, 2026
…/metrics-ingester-bigquery-config

Sync downstream stack branch with PR #1405 review fixes.

TAG=agy
CONV=0fafd02b-f3b1-4563-a73d-49ec0845afbd

Change-Id: I27c9fb9e4325e0f34ff68d219f66ce5cdcc4df5c
AntonTyb added a commit that referenced this pull request Oct 26, 2026
…at/dataengine-query-backend-abstraction

Sync downstream stack branch with PR #1405/#1406 ancestry.

TAG=agy
CONV=0fafd02b-f3b1-4563-a73d-49ec0845afbd

Change-Id: If1841fa3894b857f6480650ecad7292bc3eea1f1
@AntonTyb
AntonTyb requested a review from janzak-dev October 27, 2026 12:46
@AntonTyb
AntonTyb added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit f0a2c1b Oct 27, 2026
14 checks passed
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.

2 participants

0