service

Data Ingestion Service

Scheduled pipeline that extracts 29 tables from Actian CRM365 over JDBC, vaults PII, and loads S3, ClickHouse and RDS.

Service

Service Overview

The CRM (Actian CRM365) is RIO’s system of record, but it is not built for analytics and the RIO application cannot query it directly. This service copies the CRM’s data — on a schedule and incrementally — into three purpose-built stores:

  • S3 (bronze) — a durable archive of every table.
  • ClickHouse — the analytics warehouse behind dashboards and the Cube semantic layer.
  • RDS PostgreSQL — the transactional store the RIO application reads.

Along the way it standardises each table, writes an HMAC-SHA256 snapshot of every PII row into a DynamoDB vault, and emits an audit event at every stage so a run can be traced end to end.

Note that vaulting is an index, not a redaction. vault_pii_batch() writes the token and the lowercased values to DynamoDB and returns the dataframe unchanged, so plaintext PII still reaches S3, ClickHouse and RDS.

The full step-by-step walkthrough is documented as a flow: RIO Ingestion Service — CRM Data Sync Pipeline.

It is not event-driven

Despite publishing eight events, this service subscribes to no RIO event. It is started by a clock:

TriggerDetail
EventBridge Scheduler croncron(0 * * * ? *) in prod, cron(0 0/6 * * ? *) elsewhere → Step Functions → ECS Fargate task
S3 object uploadaws.s3 / Object Created on the default bus, filtered to the product-master bucket and the reference/product_master/ prefix (infrastructure/modules/ecs/crm-data-sync.yaml:368-390)

The second is the only rule in the repo, and it listens to AWS, not to RIO. See AWS Default Event Bus.

Two event families, two envelopes

Audit familyHierarchy event
sourcerio.platform (dependencies/audit_events.py:36)rio.glue.crm_sync (dependencies/constants.py:57)
Events8 (CRM Sync Started, ETL Batch Completed, …)1 (Hierarchy Updated)
PayloadAuditEventDetail dataclass (audit_events.py:100-136)business event with actor / context / data
PurposeObservability and the audit trailTrigger hierarchy recalculation

Audit event reference

All eight share source rio.platform. Enum definitions at audit_events.py:43-51 (detail-types) and :54-62 (event names).

Eventdetail-typedetail.event_nameEmitter
ExternalSignalReceivedExternal Signal Receivedrio.platform.ingestion.external_signal.received:351
CRMSyncStartedCRM Sync Startedrio.platform.ingestion.crm_sync.started:284
CRMSyncCompletedCRM Sync Completedrio.platform.ingestion.crm_sync.completed:305
CRMSyncFailedCRM Sync Failedrio.platform.ingestion.crm_sync.failed:327
ETLBatchStartedETL Batch Startedrio.platform.ingestion.etl_batch.started:372
ETLBatchCompletedETL Batch Completedrio.platform.ingestion.etl_batch.completed:392
ETLBatchFailedETL Batch Failedrio.platform.ingestion.etl_batch.failed:414

Data stores

dependencies/table_registry.py maps 29 CRM source tables to their targets. Every table is archived to S3; 16 of them also land in ClickHouse and 8 in RDS. Five are archive-only.

  • ClickHouse (16 source tables)dim_account, dim_campaign, dim_contract, dim_product, dim_territory, fact_bpf_history, fact_entitlement_header, fact_entitlement_line, fact_lead_snapshot, fact_opportunity_product, fact_opportunity_stage_history, fact_quote, fact_quote_approval_history, fact_quote_detail_history, fact_sales_order, fact_salesorder_detail_history.
  • RDS Postgres (8 source tables → 5 target tables)person, tenant_role, opportunity, account_team, deal_team, upserted through staging.temp_* staging tables. Three source tables fan out to two Postgres tables each via rds_destinations.
  • DynamoDB — watermark table (how far each table has synced per destination) and the PII vault.

Observability

The task exports OpenTelemetry metrics over OTLP/HTTP when OTEL_ENABLED=true and OTEL_EXPORTER_ENDPOINT is set (both default to on in crm-data-sync.yaml), flushed every 15 seconds. init_metrics() runs at the top of main() and shutdown_metrics() in the closing finally.

MetricTypeMeaningAttributes
rio.ingestion.s3_queue_rows_sentCounterRows written to the S3 gold bucket for ClickHousetable_name, tenant_id
rio.ingestion.clickhouse_rows_receivedCounterRows confirmed in ClickHouse after S3Queue ingestiontable_name, tenant_id

Comparing the two counters is how a silent S3Queue ingestion failure is caught — the audit events cannot show it, since ETLBatchCompleted always reports zero rows. System-level CPU, memory, disk and network metrics come from SystemMetricsInstrumentor.

A note on this repo’s own docs

events/contracts.md claims source rio.glue.crm_sync and a single detail-type RIO Ingestion Event for the whole audit family. The code uses source rio.platform and eight distinct Title Case detail-types. contracts.md also lists action and status values (sync_started, SUCCESS) that do not match the code enums (started, success). The code is authoritative and is what this catalog records.

Also note the README describes glue-jobs/, but the code actually lives in jobs/crm-data-sync/ and now runs on ECS, not Glue.

Inbound and Outbound Message Flow

Event-driven architecture documentation: RIO