Skip to content

Add mobile_verifier_compare crate: PG ↔ Trino reward-query parity CLI#1199

Open
macpie wants to merge 4 commits into
mainfrom
macpie/awesome-goodall-68d09c
Open

Add mobile_verifier_compare crate: PG ↔ Trino reward-query parity CLI#1199
macpie wants to merge 4 commits into
mainfrom
macpie/awesome-goodall-68d09c

Conversation

@macpie

@macpie macpie commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

New standalone binary crate mobile-verifier-compare that diffs every Postgres reward-input query mobile_verifier runs against an equivalent query over the matching Iceberg tables in Trino (poc.heartbeats, poc.speedtests, rewards.data_transfer), then prints a side-by-side table with per-row MATCH / MISMATCH / PG_ONLY / TRINO_ONLY status.

Lives in its own workspace member — mobile_verifier itself is untouched. The prod/dev boundary is enforced by the dep graph rather than a Cargo feature flag, so release builds of mobile_verifier contain no Trino client code by construction.

Why

We want to verify that the Iceberg-side data the new backfillers and writers produce is byte-equivalent to what the Postgres-side reward pipeline reads, before we trust downstream consumers to use Iceberg instead of PG. This is the validation harness for that work.

What's in the crate

Diff subcommands

Each subcommand calls the production PG function so the Postgres SQL cannot drift from what the reward pipeline actually executes:

Subcommand PG function (reused via path dep) Trino side
heartbeats HeartbeatReward::validated (valid_radios.sql) mirrored CTE over poc.heartbeats with count(*) >= 12 filter
speedtests aggregate_epoch_speedtests window-function translation over poc.speedtests
latest-speedtests get_latest_speedtests_for_pubkey per-hotspot lookup
data-sessions aggregate_hotspot_data_sessions_to_dc aggregate rewards.data_transfer over the window
payer-burns sum_data_sessions_to_dc_by_payer rolled-up rewards.data_transfer (header notes Iceberg has no payer column)
pending-burns mobile_packet_verifier::pending_burns::initialize SQL n/a — pending is by definition not in Iceberg
all runs every comparison sequentially

The diff table shows mismatches by default (--show-all to dump matches too) and prints totals + max delta.

seed subcommand (zero-config bootstrap)

Brings the local docker-compose stack from nothing to a runnable diff in one command:

  1. Drops + creates mobile_verifier and mobile_packet_verifier Postgres databases on the docker-compose PG.
  2. Runs sqlx migrations against both (paths resolved relative to CARGO_MANIFEST_DIR).
  3. CREATE SCHEMA + CREATE TABLE for iceberg.poc.heartbeats, iceberg.poc.speedtests, iceberg.rewards.data_transfer via Trino DDL.
  4. Populates deterministic fixtures across four buckets (60% match / 20% mismatch / 10% pg-only / 10% trino-only) so every diff status appears on the very first run.
  5. Auto-writes mobile_verifier_compare/pkg/compare-trino-settings.local.toml (gitignored) with docker-compose defaults, and prints the exact next cargo run … all command to copy.

Takes every endpoint as a CLI flag with docker-compose-friendly defaults — works with no -c config file.

Reviewer notes

  • The data-sessions / payer-burns comparisons diff PG input sessions against rewards.data_transfer output rewards — the printed table headers call out that this is an input-vs-output sanity check, not strict row equality. A future raw-sessions Iceberg writer would let us swap that SQL.
  • payer-burns rolls Iceberg to a single __all__ bucket because rewards.data_transfer has no payer column. Per-payer PG rows are shown for visibility but always tag as PG_ONLY — by design today.
  • pending-burns is one-sided (no Iceberg counterpart for unprocessed sessions); reported as PG_ONLY with a header note.

macpie added 2 commits May 22, 2026 13:40
New standalone binary crate that diffs every Postgres reward-input query
mobile_verifier runs against an equivalent query over the matching Iceberg
tables in Trino (poc.heartbeats, poc.speedtests, rewards.data_transfer),
then prints a side-by-side diff with per-row MATCH / MISMATCH / PG_ONLY /
TRINO_ONLY status.

The Postgres side reuses the production functions
(HeartbeatReward::validated, aggregate_epoch_speedtests,
aggregate_hotspot_data_sessions_to_dc, sum_data_sessions_to_dc_by_payer)
via a path-dep on mobile_verifier so the SQL cannot drift from what the
reward pipeline actually executes.

Ships a `seed` subcommand that bootstraps the local docker-compose stack
end-to-end: creates the mobile_verifier and mobile_packet_verifier
Postgres databases, runs sqlx migrations against both, creates the
Iceberg schemas + tables via Trino DDL, and populates deterministic
fixtures across four buckets so every diff status appears on a first
run with zero prior config.

Lives in its own workspace member so mobile_verifier release builds
contain no Trino client code — the prod/dev boundary is enforced by the
dep graph rather than a Cargo feature flag.
@macpie macpie requested a review from bbalser May 26, 2026 16:15
@macpie macpie marked this pull request as ready for review June 5, 2026 20:21
Comment thread mobile_verifier_compare/src/speedtests.rs Outdated
…coding

mobile_verifier_compare hardcoded SPEEDTEST_LAPSE_HOURS to 24, but the
production constant is 48 — speedtest comparisons were diffing against
the wrong sliding window. SPEEDTEST_LAPSE was already pub, so the fix is
a direct import.

Audited the rest of the parity-critical constants and found two more
that were copied by value (and could drift the same way):
- MINIMUM_HEARTBEAT_COUNT (12) was hardcoded in both the Trino HAVING
  clause and the seed fixture counts.
- SPEEDTEST_AVG_MAX_DATA_POINTS (6) was hardcoded in the Trino window
  function bind, the Trino LIMIT clause, and the seed row count.

Bumped both to pub in mobile_verifier and imported them. The Trino
HAVING uses :min_count bind; the LIMIT uses format! since Trino doesn't
allow LIMIT placeholders. Seed fixtures derive their counts from the
imported constants so future production-side tuning automatically
propagates.
Comment thread mobile_verifier_compare/src/heartbeats.rs
Comment thread mobile_verifier_compare/src/data_sessions.rs Outdated
Two bugs spotted by @bbalser on #1199 that would have produced spurious
MISMATCH rows for cases where production and Iceberg actually agree:

heartbeats: the Trino CTE averaged location_trust_score_multiplier across
every heartbeat in an hour, but production picks one value per hour —
the multiplier of the first heartbeat to land in that hour. PG enforces
this via wifi_heartbeats's (hotspot_key, truncated_timestamp) PK plus an
ON CONFLICT DO UPDATE clause that only refreshes first_timestamp +
coverage_object, leaving the trust multiplier sticky. Switched the CTE
to min_by(location_trust_score_multiplier, heartbeat_timestamp) so the
Trino side mirrors that semantics exactly.

data_sessions: the comparison diffed PG's `num_dcs` against Trino's
`dc_transfer_reward`. Those are different units — num_dcs is the count
of DCs the session consumes (input to the burn step) while
dc_transfer_reward is the paid reward in bones (output of burn × price).
Dropped that column from the diff entirely; only rewardable_bytes is
apples-to-apples across the two tables. Rewrote the module doc to
explain what is and isn't being validated.
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