Skip to content

feat(identity): sync CLI — copy the persons log into ClickHouse (metrics person_id, iteration 1) - #2065

Merged
mozhaev-dev merged 1 commit into
mainfrom
feat/metrics-identity-person-id-resolve
Jul 31, 2026
Merged

feat(identity): sync CLI — copy the persons log into ClickHouse (metrics person_id, iteration 1)#2065
mozhaev-dev merged 1 commit into
mainfrom
feat/metrics-identity-person-id-resolve

Conversation

@mozhaev-dev

@mozhaev-dev mozhaev-dev commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What

A new identity-resolution sync CLI subcommand (+ Helm CronJob + read-only journal GETs) that copies the MariaDB persons observation log into ClickHouse identity.identity_persons — the table the metrics dbt builds will resolve email → person_id against.

Consumer-side foundation for the identity mapping engine epic (#1873): however matching improves there, its output reaches metrics through this pipe; the dbt side (resolve macro + person_id in the observation models + activity-weighted match-rate view) follows in a companion PR.

How

Trigger model — CLI-only from birth, mirroring the persons-seed shape (#1690): identity-resolution sync runs one copy and exits (Helm CronJob scheduled 15 min after the seed's — the seed rewrites the log, the sync publishes it; manual kubectl create job --from=cronjob/...-sync). Exit codes 0 ok / 1 failed / 2 lock busy / 3 guard. HTTP keeps only the admin-gated journal: GET /v1/persons-sync + /{id}.

Copy semantics — full snapshot per run, atomic publish:

  • per-run staging table → RowBinary insert → count-verify (a short write is never swapped in) → EXCHANGE TABLES → drop; readers only ever see a complete snapshot, and a failed run leaves the live one untouched;
  • whole run holds a global MariaDB GET_LOCK (RAII guard, released even on process death); the _synced_at watermark guard before the swap is a backstop for anything bypassing the runner; stale stagings from crashed runs are GC'd after 1h;
  • the log is copied verbatim (source provenance columns, NULL vs '' preserved) — all resolution semantics stay downstream in the dbt macro, and per-source resolution stays possible;
  • empty-log guard: publishing a 0-row read would atomically erase a populated snapshot (usually a misconfigured DB, not "no people") — refused with exit 3, --force overrides deliberately.

Journal — every run (guard refusals included) lands in operations under the resolved tenant with the SYSTEM_AUTHOR nil UUID; the summary carries rows / max_id / max_created_at / synced_at — the resolution watermark for "why did the numbers change between builds".

Testing

  • cargo test -p identity-resolution — 81 unit tests (incl. run_sync orchestration on fakes, guard decisions, wire-row mapping, live-gated lock mutual-exclusion);
  • clippy pedantic clean, fmt clean;
  • e2e (containerized stack, E2E_IDENTITY_IMPLEMENTATION=rust): new identity/test_persons_sync.py — CLI exit 0 + terminal journal row, ClickHouse snapshot assertions against the fixture dataset (fixed person UUIDs, shared-email pair stays unresolved — the copy is a log, not a resolution), replace-not-append semantics, journal type-scoping vs the seed surface, 401/403/404;
  • full identity contract suite: 124 passed, 5 skipped;
  • helm lint + template render (2 CronJobs) clean.

Refs #1873, #1690.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added scheduled synchronization that publishes identity data to ClickHouse.
    • Added a command-line sync operation with concurrency protection and safeguards against empty snapshots.
    • Added admin-only APIs to list and retrieve synchronization history, including status filtering.
    • Added atomic snapshot replacement so ClickHouse reflects the latest successful run.
  • Bug Fixes
    • Improved reliability by preventing overlapping runs and protecting newer snapshots from being overwritten.

@mozhaev-dev
mozhaev-dev requested a review from a team as a code owner July 30, 2026 11:06
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mozhaev-dev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5675152d-a995-4c3e-9401-131be5c93dfe

📥 Commits

Reviewing files that changed from the base of the PR and between 710e0ea5f1a1563526b03a38405cfe94d49f6ac1 and b1ee354.

⛔ Files ignored due to path filters (1)
  • src/backend/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • src/backend/Cargo.toml
  • src/backend/services/identity-resolution/helm/templates/sync-cronjob.yaml
  • src/backend/services/identity-resolution/helm/tests/test_seed_cronjob_contract.py
  • src/backend/services/identity-resolution/helm/values.yaml
  • src/backend/services/identity-resolution/src/api/error.rs
  • src/backend/services/identity-resolution/src/api/mod.rs
  • src/backend/services/identity-resolution/src/api/seed.rs
  • src/backend/services/identity-resolution/src/api/sync.rs
  • src/backend/services/identity-resolution/src/domain/mod.rs
  • src/backend/services/identity-resolution/src/domain/sync_service.rs
  • src/backend/services/identity-resolution/src/gear.rs
  • src/backend/services/identity-resolution/src/infra/db/mod.rs
  • src/backend/services/identity-resolution/src/infra/db/ops_repo.rs
  • src/backend/services/identity-resolution/src/infra/db/persons_log_repo.rs
  • src/backend/services/identity-resolution/src/infra/identity_persons.rs
  • src/backend/services/identity-resolution/src/infra/mod.rs
  • src/backend/services/identity-resolution/src/main.rs
  • src/backend/services/identity-resolution/src/seed_runner.rs
  • src/backend/services/identity-resolution/src/sync_runner.rs
  • src/ingestion/tests/e2e/identity/test_persons_sync.py
  • src/ingestion/tests/e2e/lib/identity.py
📝 Walkthrough

Walkthrough

Adds a Rust persons-sync CLI that copies MariaDB person logs into an atomically replaced ClickHouse snapshot, journals each run, exposes admin-only read endpoints, schedules syncs through Helm, and adds unit, Helm contract, and end-to-end coverage.

Changes

Persons synchronization

Layer / File(s) Summary
Sync contracts and snapshot storage
src/backend/services/identity-resolution/src/domain/*, src/backend/services/identity-resolution/src/infra/*, src/backend/Cargo.toml
Defines sync contracts, reads MariaDB logs, and replaces the ClickHouse snapshot through staging tables, count verification, watermark checks, and atomic exchange.
Runner and CLI execution
src/backend/services/identity-resolution/src/sync_runner.rs, src/backend/services/identity-resolution/src/gear.rs, src/backend/services/identity-resolution/src/main.rs
Adds advisory locking, empty-log guarding, operation journaling, timeouts, and the sync CLI command with exit-code mapping.
Journal API surface
src/backend/services/identity-resolution/src/api/*
Adds typed admin-gated list and detail endpoints for persons-sync operations with status filtering, limits, and canonical errors.
Scheduled sync deployment
src/backend/services/identity-resolution/helm/templates/sync-cronjob.yaml, src/backend/services/identity-resolution/helm/values.yaml
Adds configurable CronJob scheduling, resource settings, secret/config wiring, retries, concurrency control, and pod security settings.
CLI harness and validation
src/backend/services/identity-resolution/helm/tests/*, src/ingestion/tests/e2e/identity/*
Adds Helm contract coverage, Rust capability detection, sync CLI support, and checks for snapshots, replacement behavior, journal APIs, filtering, authentication, and authorization.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: mitasovr, cyberantonz

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant IdentityResolution
  participant MariaDB
  participant ClickHouse
  participant JournalAPI
  Operator->>IdentityResolution: run sync CLI
  IdentityResolution->>MariaDB: acquire lock and read persons log
  IdentityResolution->>ClickHouse: stage and exchange snapshot
  IdentityResolution->>MariaDB: record operation result
  Operator->>JournalAPI: list or fetch persons-sync operation
  JournalAPI->>MariaDB: query tenant-scoped operation
  JournalAPI-->>Operator: return operation response
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/metrics-identity-person-id-resolve

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Regenerate the connectors-ddl snapshot

This PR changes src/ingestion/**. If your change affects any
bronze / silver / gold schema, regenerate the committed DDL snapshot
and include it in this PR.

Prerequisites (details: src/ingestion/scripts/bootstrap-db/README.md):

  • docker + a fresh throwaway ClickHouse 25.7.5 (README "Local ClickHouse for testing")
  • .env from .env.bootstrap.example pointing at it; use the host LAN IP,
    reachable from both the host and connector containers
    (host.docker.internal does not resolve on the macOS host itself)
  • python3.12 or python3.11 on PATH (pinned dbt venv)
  • HubSpot + Salesforce credentials in .env — their discover calls the
    live APIs; without them, apply ../connectors-ddl/{hubspot,salesforce}.sql
    (relative to bootstrap-db/) to seed their bronze, then run the dbt step
cd src/ingestion/scripts/bootstrap-db
set -a; source pins.env; source .env; set +a
./bootstrap-db.sh connectors-config.yaml   # fresh ClickHouse 25.7.5
./dump-ddl.sh                              # writes scripts/connectors-ddl/*.sql

Commit the resulting scripts/connectors-ddl/*.sql diff. If nothing
changed, no snapshot update is needed. (Regeneration is manual for now.)

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/backend/services/identity-resolution/src/main.rs (1)

87-91: 📐 Maintainability & Code Quality | 🔵 Trivial

Naming: EXIT_SEED_* constants are now shared with sync.

The doc comment above already documents these as "one shared scheme," but the constant names still read as seed-only, which is a little confusing at both use-sites (124-141 for Sync). A rename (e.g., EXIT_LOCK_BUSY/EXIT_GUARD/EXIT_FAILED) would make the sharing explicit; purely cosmetic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/services/identity-resolution/src/main.rs` around lines 87 - 91,
Rename the shared exit-code constants from EXIT_SEED_FAILED,
EXIT_SEED_LOCK_BUSY, and EXIT_SEED_GUARD to neutral names such as EXIT_FAILED,
EXIT_LOCK_BUSY, and EXIT_GUARD, then update every reference in both the seed and
sync subcommand paths, including the Sync use-sites.
src/ingestion/tests/e2e/lib/identity.py (1)

289-331: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a shared helper for run_sync_cli/run_seed_cli.

run_sync_cli duplicates run_seed_cli's capability-gate/env-wiring/subprocess.run shape almost line for line, differing only in the subcommand name and the seed-only --mode flag. Extracting a private _run_gear_cli(subcommand, *, tenant, force, mode=None, timeout_s, extra_env) helper would remove the duplication and keep future CLI subcommands from repeating it again.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/ingestion/tests/e2e/lib/identity.py` around lines 289 - 331, Extract the
duplicated capability check, environment setup, command construction, and
subprocess execution from run_sync_cli and run_seed_cli into a private
_run_gear_cli helper. Make the helper accept subcommand, tenant, force, mode,
timeout_s, and extra_env, applying --mode only when provided, then have both
public methods delegate to it while preserving their existing behavior and
signatures.
src/backend/services/identity-resolution/src/infra/db/mod.rs (1)

148-203: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting a shared advisory-lock helper.

SyncLockGuard duplicates SeedLockGuard's GET_LOCK/RELEASE_LOCK session logic almost verbatim (the doc comment itself calls it "the global sibling of SeedLockGuard, with identical lifetime semantics"). A small generic AdvisoryLockGuard (parameterized by lock name) would remove the duplication and keep both lock types from drifting independently.

♻️ Sketch of a shared helper
-pub struct SyncLockGuard {
-    conn: DatabaseConnection,
-}
-
-impl SyncLockGuard {
-    pub async fn try_acquire(database_url: &str) -> anyhow::Result<Option<Self>> {
-        use sea_orm::{ConnectionTrait, DbBackend, Statement};
-        let conn = connect_single(database_url).await?;
-        let acquired: Option<i8> = conn
-            .query_one(Statement::from_sql_and_values(
-                DbBackend::MySql,
-                "SELECT GET_LOCK(?, 0)",
-                [SYNC_LOCK.into()],
-            ))
-            .await?
-            .map(|r| r.try_get_by_index::<Option<i8>>(0))
-            .transpose()?
-            .flatten();
-        if acquired == Some(1) {
-            Ok(Some(Self { conn }))
-        } else {
-            Ok(None)
-        }
-    }
-    pub async fn release(self) { /* ... */ }
-}
+async fn try_acquire_named(database_url: &str, name: &str) -> anyhow::Result<Option<DatabaseConnection>> {
+    use sea_orm::{ConnectionTrait, DbBackend, Statement};
+    let conn = connect_single(database_url).await?;
+    let acquired: Option<i8> = conn
+        .query_one(Statement::from_sql_and_values(
+            DbBackend::MySql,
+            "SELECT GET_LOCK(?, 0)",
+            [name.into()],
+        ))
+        .await?
+        .map(|r| r.try_get_by_index::<Option<i8>>(0))
+        .transpose()?
+        .flatten();
+    Ok((acquired == Some(1)).then_some(conn))
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/services/identity-resolution/src/infra/db/mod.rs` around lines
148 - 203, Extract the duplicated GET_LOCK/RELEASE_LOCK session logic from
SyncLockGuard and SeedLockGuard into a shared AdvisoryLockGuard parameterized by
the advisory lock name. Refactor both guards to reuse this helper while
preserving their existing lock constants, non-blocking acquisition behavior,
release semantics, and public APIs.
src/backend/services/identity-resolution/src/infra/db/persons_log_repo.rs (1)

41-48: 🚀 Performance & Scalability | 🔵 Trivial

Note: full-table materialization per run.

read_all loads the entire persons log into memory as a Vec before any row is streamed to ClickHouse (the writer only streams the write side, in fill_and_swap). This matches the documented "plain entity scan, full snapshot" design, but as the log grows, memory and read latency scale linearly with total row count on every run. If persons-log volume becomes large, consider a cursor-based/streaming read (e.g., SeaORM's stream()) so the read and the ClickHouse insert can be pipelined instead of buffered as one Vec.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/services/identity-resolution/src/infra/db/persons_log_repo.rs`
around lines 41 - 48, Replace the full-table buffering in read_all with a
cursor-based or streaming entity scan, and expose rows incrementally so
ClickHouse writes can be pipelined without materializing all PersonsLogRow
values in a Vec. Preserve the ascending persons::Column::Id ordering and map
each database model through map_row.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/services/identity-resolution/helm/values.yaml`:
- Around line 100-117: Expose configurable CronJob controls by adding
sync.concurrencyPolicy, sync.backoffLimit, and sync.activeDeadlineSeconds to
src/backend/services/identity-resolution/helm/values.yaml with defaults matching
the current behavior, then update the sync CronJob template in
src/backend/services/identity-resolution/helm/templates/sync-cronjob.yaml to
reference those values instead of the hardcoded Forbid, 2, and 600 settings.

In `@src/backend/services/identity-resolution/src/infra/identity_persons.rs`:
- Around line 140-176: Harden identifier validation in drop_stale_stagings
before constructing the DROP TABLE statement: require name to exactly match
STAGING_PREFIX followed by 32 lowercase hexadecimal characters, rather than only
checking starts_with(STAGING_PREFIX). Skip any name that fails this shape
validation, preserving the existing drop and logging behavior for valid
generated staging names.

---

Nitpick comments:
In `@src/backend/services/identity-resolution/src/infra/db/mod.rs`:
- Around line 148-203: Extract the duplicated GET_LOCK/RELEASE_LOCK session
logic from SyncLockGuard and SeedLockGuard into a shared AdvisoryLockGuard
parameterized by the advisory lock name. Refactor both guards to reuse this
helper while preserving their existing lock constants, non-blocking acquisition
behavior, release semantics, and public APIs.

In `@src/backend/services/identity-resolution/src/infra/db/persons_log_repo.rs`:
- Around line 41-48: Replace the full-table buffering in read_all with a
cursor-based or streaming entity scan, and expose rows incrementally so
ClickHouse writes can be pipelined without materializing all PersonsLogRow
values in a Vec. Preserve the ascending persons::Column::Id ordering and map
each database model through map_row.

In `@src/backend/services/identity-resolution/src/main.rs`:
- Around line 87-91: Rename the shared exit-code constants from
EXIT_SEED_FAILED, EXIT_SEED_LOCK_BUSY, and EXIT_SEED_GUARD to neutral names such
as EXIT_FAILED, EXIT_LOCK_BUSY, and EXIT_GUARD, then update every reference in
both the seed and sync subcommand paths, including the Sync use-sites.

In `@src/ingestion/tests/e2e/lib/identity.py`:
- Around line 289-331: Extract the duplicated capability check, environment
setup, command construction, and subprocess execution from run_sync_cli and
run_seed_cli into a private _run_gear_cli helper. Make the helper accept
subcommand, tenant, force, mode, timeout_s, and extra_env, applying --mode only
when provided, then have both public methods delegate to it while preserving
their existing behavior and signatures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a3d3fcd6-5d74-4505-83b7-5f2359dabc85

📥 Commits

Reviewing files that changed from the base of the PR and between 7f31597 and 276224b64957732baaaab58903d18a4ea3782e09.

⛔ Files ignored due to path filters (1)
  • src/backend/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • src/backend/Cargo.toml
  • src/backend/services/identity-resolution/helm/templates/sync-cronjob.yaml
  • src/backend/services/identity-resolution/helm/values.yaml
  • src/backend/services/identity-resolution/src/api/error.rs
  • src/backend/services/identity-resolution/src/api/mod.rs
  • src/backend/services/identity-resolution/src/api/seed.rs
  • src/backend/services/identity-resolution/src/api/sync.rs
  • src/backend/services/identity-resolution/src/domain/mod.rs
  • src/backend/services/identity-resolution/src/domain/sync_service.rs
  • src/backend/services/identity-resolution/src/gear.rs
  • src/backend/services/identity-resolution/src/infra/db/mod.rs
  • src/backend/services/identity-resolution/src/infra/db/ops_repo.rs
  • src/backend/services/identity-resolution/src/infra/db/persons_log_repo.rs
  • src/backend/services/identity-resolution/src/infra/identity_persons.rs
  • src/backend/services/identity-resolution/src/infra/mod.rs
  • src/backend/services/identity-resolution/src/main.rs
  • src/backend/services/identity-resolution/src/seed_runner.rs
  • src/backend/services/identity-resolution/src/sync_runner.rs
  • src/ingestion/tests/e2e/identity/test_persons_sync.py
  • src/ingestion/tests/e2e/lib/identity.py

Comment thread src/backend/services/identity-resolution/helm/values.yaml
@mozhaev-dev
mozhaev-dev force-pushed the feat/metrics-identity-person-id-resolve branch from 276224b to 710e0ea Compare July 30, 2026 11:24
@mozhaev-dev
mozhaev-dev marked this pull request as ready for review July 31, 2026 00:27
@mozhaev-dev

Copy link
Copy Markdown
Contributor Author

Both bot findings addressed (branch also rebased onto current main):

  • CronJob controls via values: sync.concurrencyPolicy / sync.backoffLimit / sync.activeDeadlineSeconds added with defaults matching the previous literals; the template consumes them. (The seed CronJob keeps its literals — aligning it is a separate change outside this PR's scope.)
  • Stale-staging GC identifier validation: tightened to the exact shape this code mints — identity_persons_staging_ + 32 hex chars — before the name reaches the identifier-interpolated DROP, closing the quoted-identifier escape the prefix check alone left open.

Checks re-run locally: clippy pedantic 0, fmt clean, 81 unit tests, helm contract suite 17/17.

@mozhaev-dev
mozhaev-dev force-pushed the feat/metrics-identity-person-id-resolve branch from 710e0ea to c6799d9 Compare July 31, 2026 00:31
Iteration 1 of the metrics person_id rework (#1873 consumer side), reworked
onto the CLI trigger model the persons-seed established in #1690 — the
earlier POST /v1/persons-sync + queue + worker draft never merged; this
lands the surface CLI-only from birth.

`identity-resolution sync` runs one copy of the MariaDB `persons`
observation log into ClickHouse `identity.identity_persons` (next to
identity_inputs) and exits — the table dbt gold builds resolve
email → person_id against. Execution mirrors the seed subcommand: global
MariaDB GET_LOCK advisory lock held for the whole run (RAII guard, dies
with the session), zombie sweep, `operations` journal row under the
resolved tenant with the SYSTEM_AUTHOR nil UUID, exit codes
0 ok / 1 failed / 2 lock busy / 3 guard. The natural pairing is "sync after
seed": the seed rewrites the log, the sync publishes it — the Helm CronJob
is scheduled 15 minutes after the seed's (sync-cronjob.yaml, same wiring).

Copy semantics: full snapshot per run — per-run staging table,
count-verify, atomic EXCHANGE TABLES swap, `_synced_at` watermark guard as
a backstop, >1h stale-staging GC. Readers never see an empty or partial
table; a failed run leaves the live snapshot untouched. The log is copied
verbatim (source provenance columns, NULLs preserved) so future per-source
resolution lives entirely in the dbt-side resolve macro. An empty-log run
is refused by a guard (publishing it would erase a populated snapshot —
the destructive-zero-rows lesson); `--force` overrides deliberately.

HTTP keeps only the read-only journal (GET /v1/persons-sync + /{id}),
admin-gated, same wire conventions as the seed journal.

e2e: rust-only CLI contract module — exit 0 + terminal journal row,
snapshot verification in ClickHouse against the fixture dataset
(fixed person UUIDs, dup pair stays unresolved, watermark stamped),
replace-not-append, journal type-scoping vs the seed surface, status
filter, 401/403/404. Full identity suite green (124 passed, 5 skipped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
@mozhaev-dev
mozhaev-dev force-pushed the feat/metrics-identity-person-id-resolve branch from c6799d9 to b1ee354 Compare July 31, 2026 00:32
# Journal-row tenant (UUID); same override semantics as seed.tenantDefaultId
# (the copy itself is tenant-agnostic — this only scopes the operations
# journal the admin GET routes read).
tenantDefaultId: ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The insight-gitops should be modified for customer cluster or this value should be set from values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already covered on the gitops path: deploy/gitops/scripts/compose-app-secrets.sh (the "First-admin bootstrap inputs" block) writes APP__gears__identity-resolution__config__tenant_default_id into insight-identity-resolution-config whenever TENANT_DEFAULT is configured for the cluster — and the sync CronJob consumes that same Secret via envFrom, so customer clusters need no gitops change. sync.tenantDefaultId here is the standalone-install override, mirroring the seed's pattern from #1690. And the tenant is only journal-row scoping for the sync — a cluster without it still runs via the sole-tenant inference fallback.

@mitasovr mitasovr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the #1873 epic. Architecturally sound iteration 1 of the consumer path: the MariaDB persons log is the right publication point per ADR-0002 (any future matching improvements flow through the same pipe), the verbatim copy keeps resolution semantics downstream, and the full-snapshot + EXCHANGE swap with empty-log guard / count-verify / watermark backstop is proportionate to the blast radius of a bad publish. Known trade-offs (time-based seed→sync coupling, lockstep schema copies, PII now in a second store for #719 to account for) are acknowledged in-code and fine as follow-ups. Approving.

/// (writes) and the `GET /v1/persons-seed*` journal endpoints (filter).
pub const PERSONS_SEED_OP: &str = "persons-seed";
/// Operation type of the persons→ClickHouse sync (`sync` subcommand).
pub const PERSONS_SYNC_OP: &str = "persons-sync";

@cyberantonz cyberantonz Jul 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some strings declared twice will drift.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Within the service each op-type string is declared exactly once: grep '"persons-sync"' over src/ hits only this const, and every consumer (sync_runner journal writes, the GET journal filter) goes through ops_repo::PERSONS_SYNC_OP — same for the seed const. The other spellings live across wire boundaries (the /v1/persons-sync route path, helm component labels, the e2e assertions) which can't share a Rust declaration; the contract e2e pins them together against the live service (test_persons_sync asserts operation_type == "persons-sync" end-to-end), so a drift fails CI rather than going quiet.

@mozhaev-dev
mozhaev-dev added this pull request to the merge queue Jul 31, 2026
Merged via the queue into main with commit 90a4622 Jul 31, 2026
56 checks passed
@mozhaev-dev
mozhaev-dev deleted the feat/metrics-identity-person-id-resolve branch July 31, 2026 04:56
cyberantonz pushed a commit to cyberantonz/insight that referenced this pull request Aug 3, 2026
Iteration 2 of the metrics person_id rework: the runtime switches from the
email entity key to the canonical person UUID the identity pipeline has
been resolving into the gold tables since constructorfabric#2065/constructorfabric#2066. Hard switch by
decision — no compatibility flag; rollout is gated on the dev coverage
numbers, not on code.

- validation: `entity.ids` parse as UUIDs into `person_ids: Vec<Uuid>`
  (parse-don't-validate; the pre-cutover email shape is now a loud 400,
  never a silent empty result). The email lowercasing normalizer is gone.
- compiler: every view (period/timeseries incl. capped, breakdown,
  histogram, ranking, peer) filters `person_id IN (…)`, groups and orders
  by person_id, and projects `toString(assumeNotNull(person_id))`.
  Unresolved rows (person_id NULL) fall out of every query — the epic's
  "excluded, not guessed" stance, now enforced by the key itself.
- peer: the cohort join runs person_id = person_id instead of string
  email equality — the fragile HR-email↔source-email match this rework
  exists to kill. The person grain is hardened with LIMIT 1 BY person_id
  in both cohort CTEs: the view is unique per EMAIL, and two emails
  resolving to one person must not double-weight a peer or blend pools.
- schema validator: person_id joins OBSERVATION_COLUMNS/COHORT_COLUMNS in
  this same change, as the constants' comment promised — a table without
  the column now fails the probe loudly instead of answering empty.
- naming discipline (review requirement): everything carrying the person
  UUID is named person_id/person_ids through validation, compiler rows,
  batch demux and builder; `entity_id` survives ONLY as the wire field
  name, documented as a seam at the single builder/DTO boundary.

501 unit tests green (peer-guard and ranking tests updated to the new
grain), clippy pedantic clean, fmt clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Sergei Mozhaev <mozhaev.dev@gmail.com>
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.

3 participants