Skip to content

fix: read the tenant from _org_id alone - #268

Merged
catinspace-au merged 9 commits into
mainfrom
fix/ga-rules-org-reader
Oct 21, 2026
Merged

catinspace-au merged 9 commits into
mainfrom
fix/ga-rules-org-reader

Conversation

@catinspace-au

@catinspace-au catinspace-au commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

The loader now takes the tenant from _org_id and nothing else. A record with no org, or no source, lands in the deployment's default org and default table instead of in no org or the DLQ. A routing edit reaches a running loader without a restart.

What changes:

  • Tenant
    • _org_id reads routing.org_id_field (default _org_id) alone. No column directive and no sender field such as org_id can set it.
    • New routing.default_org_id (default default, the same string every DFE component uses). An absent, null or "" org lands there.
    • routing.org_routes is keyed on that tenant.
  • Source
    • An absent, null or "" _source counts as absent, so the record falls through to the topic's source, then default_table. It never reaches the DLQ.
  • A record whose top-level object carries _org_id or _source more than once, an escaped spelling included, is dead-lettered at intake on every pipeline mode with a reason naming the key, and counted on dfe_loader_duplicate_header_total. JSON readers disagree on which value wins, so the loader trusts neither. No \ byte and at most one quoted spelling of each key skips the scan.
  • Removed keys: routing.rules (CEL), routing.db_fields, routing.compat_v2_source and routing.mapping_file, which nothing read.
  • Hot reload
    • hot_reload.enabled defaults to true.
    • A reload rebuilds the router, the header extractor and the default table, pre-warms any uncached table the new routing names, and stops holding the new default table absent. The router reads the configured metadata.
    • The deployment contract lists routing in hot_paths, so a routing edit no longer rolls the pods. routing.dlq, field_mapping and enrichment are built at startup and log a restart warning.
  • _timestamp_received reads only the first hop's _timestamp_received ahead of its directive. _timestamp_receiver, timestamp_received and received_at are read only when the column's directive names them.
  • GeoIP databases are provisioned by scalo's geoip_download. The maxminddb lookup is unchanged.
    • New geoip.auto_download.max_database_bytes (default 512 MiB). A file that is not an MMDB is refused.
    • maxmind_account_id and maxmind_license_key are secrets now, redacted in logs and config dumps. Same YAML shape.
    • ip_locate downloads IPLocate's own ASN database. sapics downloads DB-IP's ASN data to sapics-dbip-asn.mmdb.
    • The direct reqwest, flate2 and tar dependencies are gone.
  • scalo 2.14.19.
  • Two flaky tests fixed: the gRPC backend tests bind port 0 and dial the address the server reports, and apply_results_memory_released_on_error asserts the lease ledger rather than host memory.

Ack and commit timing are unchanged. tests/integration/hot_reload.rs streams 600 records, edits the default table and default org mid-stream, and checks nothing is lost or written twice and the group commits past the last offset.

Consumers need lockstep edits: dfe-engine's loader model and default configs still render the removed keys, dfe-infra's apps.yaml says the watcher ships off, and dfe-docker's detection e2e sets the tenant through a payload org_id, which no longer works.

Evidence:

  • hyperi-ci check: 1849 tests run, 1849 passed, 19 skipped, testcontainers included. fmt, clippy per feature, audit, deny and the feature matrix pass.
  • On 4df0014 (main) the tenancy, hot-reload and repeated-key container tests fail: the _source: "" record goes to the DLQ, after the edit a record for an absent table falls back to the old default table, and all three repeated-key records land. All three pass here.
  • A probe through the public API fails all 7 of its checks on main and passes on this branch: sender org_id, null and empty org, empty _source, _timestamp_receiver, the hot_reload default, hot_paths and the MaxMind key in a config dump.

Done when PR CI is green and the engine, dfe-infra and dfe-docker edits land with it.

The thin chart now renders on scalo-service 2.14.19 as well. The library pin had stayed at 2.14.15, which hashes the whole config into the pod and ignores the contract's hot_paths, so every config edit rolled the pods.
The first hop now stamps _org_id and _source, so the loader only reads them.

- _org_id reads routing.org_id_field and nothing else. A column directive can no longer point it at a field the sender writes, so a sender's org_id never sets the tenant.
- An absent, null or empty _org_id lands in routing.default_org_id, default 'default', the same string in every component. It used to land as '', which is in no org.
- A null or empty _source counts as absent: the next routing field, then the topic, then default_table. An empty one used to go to the DLQ.
- org_routes keys on the tenant the record resolves to.
- routing.rules (CEL), routing.db_fields, routing.compat_v2_source and routing.mapping_file are gone. Nothing set them, and mapping_file had no reader.
- On the legacy_flatten path the routing fields are stripped before the header is written, so routing on _source no longer strips the _source header it just wrote.
A config reload now rebuilds the router, the header extractor and the default table from the new routing and metadata, pre-warms any uncached table the new routing names, and stops holding the new default table absent. The router reads the configured metadata instead of the default.

hot_reload.enabled defaults to true and the deployment contract lists routing in hot_paths, so a routing edit reaches running pods without rolling them. routing.dlq, field_mapping and enrichment are built once at startup and now log a restart warning.

tests/integration/hot_reload.rs edits the default table and default org mid-stream and proves no record is lost or duplicated and the commit passes the last offset.
The header pass took _timestamp_receiver, timestamp_received and received_at as receive stamps when the column carried no directive. The first hop now stamps _timestamp_received alone, so that is the only key read ahead of the directive, and any other spelling is read only when the column's directive names it.
The loader's own downloader is replaced by scalo's geoip_download, which caps each download at max_database_bytes, refuses a file that is not an MMDB and keeps the IPinfo token out of the logged URL. The maxminddb lookup is unchanged.

geoip.auto_download gains max_database_bytes (default 512 MiB), and maxmind_account_id and maxmind_license_key become secrets, redacted in logs and config dumps. data_dir keeps its /var/lib/dfe/geoip default. The ip_locate provider now downloads IPLocate's own ASN database, and sapics downloads DB-IP's ASN data to sapics-dbip-asn.mmdb.

The direct reqwest, flate2 and tar dependencies go with the old module.
The gRPC backend tests picked a port from the process id and bound it twice, so a port taken between the probe and the bind failed the test. The listener now binds port 0 and the client dials the address it reports.

apply_results_memory_released_on_error compared two current_bytes readings, which follow the host's own memory use. It now asserts the lease ledger drops by the failed message's 50 bytes.
The capability catalogue still named routing rules, which no longer exist, and mounted MMDB readers, which startup now downloads. Both descriptions now say what the services do.
Comment thread tests/integration/geoip_provisioning.rs Fixed
Comment thread tests/integration/geoip_provisioning.rs Fixed
The GeoIP redaction test printed its fixture credentials in its failure messages, which CodeQL reads as cleartext logging. The messages now name the field and never the value or the dump.
A record whose top-level object carries _org_id or _source more than once, an escaped spelling included, is taken out at intake and dead-lettered with a reason naming the key, before any pipeline mode reads it. JSON readers disagree on which value wins and ClickHouse refuses the row under full capture, so neither value is trusted.

A record with no backslash and at most one quoted spelling of each key is passed without a scan. Counted on dfe_loader_duplicate_header_total.
@catinspace-au
catinspace-au marked this pull request as draft October 21, 2026 07:49
@catinspace-au
catinspace-au marked this pull request as ready for review October 21, 2026 21:54
@catinspace-au
catinspace-au merged commit 0656104 into main Oct 21, 2026
17 checks passed
@catinspace-au
catinspace-au deleted the fix/ga-rules-org-reader branch October 21, 2026 21:55
@github-actions

Copy link
Copy Markdown

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