Maintenance Daemon¶
telemetry-maintenance-srv is the maintenance daemon that keeps the data lake
healthy. It is a long-running service you deploy alongside
ingestion and FlightSQL: it materializes views
on a schedule and runs retention cleanup.
Ad-hoc administration — inspecting partitions, retiring incompatible ones — is done through SQL and the Python API, not by driving this binary. See Admin SQL Functions.
It reads the lake from the environment:
| Variable | Required | Description |
|---|---|---|
MICROMEGAS_SQL_CONNECTION_STRING |
Yes | PostgreSQL connection for lake metadata |
MICROMEGAS_OBJECT_STORE_URI |
Yes | Object store holding the partitions |
MICROMEGAS_DEFAULT_AUDIENCE |
No | The audience a credential with no bound ingestion audience is stamped with at write time (default public). Set it identically on every role that builds a lakehouse — FlightSQL, this one, the monolith, and ingestion — since this role bakes the value into partitions and the ingestion role stamps new rows with it; setting it on only some of them materializes and stamps under mismatched defaults. See Audience stamping |
MICROMEGAS_DATAFUSION_MEMORY_BUDGET_MB |
No | Query engine memory budget in MB; unset means an unbounded pool (the local-development default). This is set in real deployments. Unlike flight-sql-srv, the daemon's merges and materialization run on the shared, unscoped pool — there is no per-query audit record here, so this budget is a process-wide ceiling rather than something attributable to one query. Merge scans open one reader per source file group -- one reader total for the concatenating path, or one per input partition for the ordered sort-merge path -- so merge memory does not scale with host core count |
MICROMEGAS_DATAFUSION_MAX_TEMP_DIRECTORY_MB |
No | Cap on total spill-file bytes across all concurrent work, in MB; default 100 GB (DataFusion's own default), far larger than a typical Fargate container's local disk. Applies the same way as on flight-sql-srv: exceeding the cap fails whichever task's spill write pushes past it, process-wide and shared across all concurrent work, not necessarily the task that consumed most of the budget |
MICROMEGAS_STATIC_TABLES_URL |
No | Auto-discovery source for static JSON/CSV tables (see FlightSQL). Set it identically on every role that builds a lakehouse — this daemon resolves it too, so a materialized view whose extract query reads a static table builds the same way here as it does on flight-sql-srv |
MICROMEGAS_VIEW_DEFINITION_REFRESH_SECONDS |
No | How often the daemon reloads DDL-defined materialized views from Postgres (default 60). The daemon now picks up a view set created, replaced, or dropped by CREATE/DROP MATERIALIZED VIEW without a restart, bounded by this interval |
Lakehouse schema migrations run automatically at service startup. The v7
migration runs CREATE EXTENSION IF NOT EXISTS btree_gist (required by the
partition-overlap exclusion constraint). On PostgreSQL 13+ — including Amazon
Aurora and RDS — btree_gist is a trusted extension, so the service's
database role only needs the CREATE privilege on the database it already
owns. On PostgreSQL 12 or older, or if the role lacks that privilege, have a
superuser pre-create the extension once; the migration then proceeds
unchanged.
Running the daemon¶
The Docker image (maintenance.Dockerfile) runs telemetry-maintenance-srv as
its entrypoint; no arguments are required.
| Flag | Default | Description |
|---|---|---|
--shutdown-grace-period-seconds |
25 |
Seconds to let in-flight tasks finish on SIGTERM |
--retention-days |
90 |
Delete lake data older than this many days (retention horizon) |
On SIGTERM the daemon stops scheduling new tasks and drains those already
running, up to the grace period. See
Service Lifecycle & Shutdown. Interrupted materialization
is safe to redo — a task that doesn't finish simply leaves its partition
unwritten, and the next scheduled run redoes it.
A view that fails to materialize (e.g. a bad SqlBatchView query) does not
starve the views ordered after it: each view's materialization is attempted
and its failure isolated independently, so every other view still gets
materialized in the same pass. A failing view is logged and counted (see
materialize_view_failure below); if any view
failed, the pass as a whole is still reported as failed so the failure
surfaces in the daemon's logs, but that failure is attributed per view rather
than to whichever view happened to fail first.
materialize_view_failure¶
Each of the four materialization tasks (every second/minute/hour/day) emits
materialize_view_failure (count, tags {view_set_name,
view_instance_id}) as one event with value = 1 per failed view per
materialization pass. To count failures over a window, use count(*) or
sum(value).
Run a single telemetry-maintenance-srv instance per lake. The scheduled
tasks are not partitioned across instances, so multiple daemons would
redundantly materialize the same partitions. Materialization is idempotent, so
this is wasteful rather than corrupting — but there is no benefit to more than
one.
What it does¶
The daemon keeps materialized views current by running several scheduled tasks. The four materialization tasks each work a trailing window at their own granularity, so recent data lands in fine-grained partitions quickly while older data is consolidated into coarser ones:
| Task | Period | Work |
|---|---|---|
| Every second | 1 s | Materialize the newest 1-second partitions. Skipped when the daemon is more than 10 s behind — the minute task backfills the gap. |
| Every minute | 1 min | Materialize 1-minute partitions. |
| pg_stats | 1 min | Sample the metadata Postgres's pg_stat_* views (see below). |
| Every hour | 1 h | Retention cleanup (see below), the blocks/streams/audience integrity counts (see below), then materialize 1-hour partitions. |
| Every day | 1 day | Materialize 1-day partitions. |
Retention¶
The hourly task performs cleanup automatically:
- Deletes lake data older than the retention horizon — blocks, streams, and processes past the horizon are removed.
- Deletes expired temporary files left behind by query execution.
Integrity counts¶
Alongside retention cleanup, the hourly task also counts two blocks-row anomalies over the
last hour, directly against Postgres. Both cost nothing on the ingestion hot path — that is
precisely why they run here instead of at write time.
block_stream_process_id_mismatch¶
count, no tags: the number of blocks rows in the last hour whose process_id disagrees with
their stream's own process_id. No longer security-critical under per-row audience stamping — a
block's own audience column governs its label regardless of what process_id it claims — so
this is a plain data-integrity signal. Healthy baseline: a flat zero. Every non-zero reading
is a bug or an attack; a hard-reject at write time is a deferred follow-up pending data from this
counter.
block_audience_mismatch_rows¶
count, no tags: the number of blocks rows in the last hour whose own audience disagrees
with their stream's or process's audience — built from the same NULL-tolerant comparison the
blocks_view.rs materialization-time exclusion predicate uses, so the two can never drift apart.
A non-zero reading is not necessarily a bug — it may reflect a re-pointed ingestion
credential — but it always means telemetry was silently dropped from blocks (and so from
log_entries/measures/log_stats and every other view built from it) by that exclusion. A
deployment should watch this metric read a flat zero for a representative period before trusting
that the exclusion is only ever discarding attacker-injected blocks and not legitimate telemetry;
a nonzero, non-attack reading is a sign to fix the underlying cause (e.g. stop re-pointing a
credential's audience mid-stream) rather than to treat the drop as expected.
Kept separate from, and never summed with, block_audience_mismatch_excluded below — both run in
this same maintenance-role process, so an identically-named counter from both sites would land in
the same process's measures stream with no tag to tell them apart, and any query summing them
would be summing two incompatible quantities: this one is a live Postgres row count over the last
hour, the other is a per-partition exclusion count at materialization time.
block_audience_mismatch_excluded¶
count, no tags: emitted from MetadataPartitionSpec::write, not from the hourly task — only
when a blocks partition is actually written (never on a scheduled pass that decides nothing
needs writing), naming the count of rows the audience-mismatch predicate excluded from that
partition. It still double-counts across a materialization retry, or a fresh
CreateFromSource re-write of a range whose source rows changed, since each such write re-runs
the comparison — but not across a re-merge, which writes through the view's PartitionMerger and
never calls MetadataPartitionSpec::write. So it is a "some write saw a mismatch" signal rather
than a running total of distinct excluded blocks — trust block_audience_mismatch_rows above for
sizing the drop.
The retention horizon defaults to 90 days and is configurable via
--retention-days or the MICROMEGAS_RETENTION_DAYS environment variable:
The flag takes precedence over the environment variable, which in turn takes precedence over the default.
Metadata Postgres self-observability¶
Once a minute, the daemon samples the metadata Postgres's standard pg_stat_*
catalog views (plus index/table sizes) and emits the readings as micromegas
metrics through its own tracing sink, so they land in the lake's measures
view like any other telemetry — no extra wiring or credentials required. This
turns questions that could previously only be answered by connecting to the DB
directly (e.g. which indexes are dead weight) into evidence queryable via
FlightSQL.
All counters are emitted raw and cumulative, exactly as Postgres reports
them — the collector never calls pg_stat_reset* and holds no state between
ticks. Deltas and rates are a query-time concern.
| Metric family | Tags | Source |
|---|---|---|
pg_index_scans, pg_index_tuples_read, pg_index_tuples_fetched, pg_index_size_bytes |
{table, index} |
pg_stat_user_indexes + pg_relation_size |
pg_table_seq_scans, pg_table_idx_scans, pg_table_live_tuples, pg_table_dead_tuples, pg_table_tuples_inserted, pg_table_tuples_updated, pg_table_tuples_deleted, pg_table_seconds_since_autovacuum |
{table} |
pg_stat_user_tables |
pg_db_blocks_hit, pg_db_blocks_read, pg_db_xact_commit, pg_db_xact_rollback, pg_db_deadlocks, pg_db_temp_bytes, pg_db_stats_reset_timestamp |
— | pg_stat_database |
pg_activity_connections |
{state} |
pg_stat_activity, grouped |
pg_activity_oldest_xact_age_seconds |
— | pg_stat_activity |
pg_pool_size, pg_pool_idle |
— | the daemon's own sqlx::PgPool (client-side, no query) |
Tags are read with the property_get SQL function, e.g.
property_get(properties, 'index').
pg_db_stats_reset_timestamp marks counter-reset boundaries (a clean restart on
PG15+, a crash, or an Aurora failover/patch/instance replacement) — segment on
it rather than assuming counters only ever increase.
Sample queries¶
Indexes with zero scans over the observed window (candidates for removal):
SELECT property_get(properties, 'table') AS table,
property_get(properties, 'index') AS index,
max(value) AS idx_scans
FROM measures
WHERE name = 'pg_index_scans'
GROUP BY 1, 2
HAVING max(value) = 0
ORDER BY 1, 2;
Cache-hit ratio over a window (as a delta, since the counters are cumulative):
WITH bounds AS (
SELECT min(value) FILTER (WHERE name = 'pg_db_blocks_hit') AS hit_start,
max(value) FILTER (WHERE name = 'pg_db_blocks_hit') AS hit_end,
min(value) FILTER (WHERE name = 'pg_db_blocks_read') AS read_start,
max(value) FILTER (WHERE name = 'pg_db_blocks_read') AS read_end
FROM measures
WHERE name IN ('pg_db_blocks_hit', 'pg_db_blocks_read')
)
SELECT (hit_end - hit_start)::float
/ nullif((hit_end - hit_start) + (read_end - read_start), 0) AS cache_hit_ratio
FROM bounds;
Out of scope (follow-ups)¶
pg_stat_statements— needsshared_preload_librariesandCREATE EXTENSIONon the Aurora cluster parameter group.- Aurora/CloudWatch-only signals (
ACUUtilization, Performance Insightsdb.load.avg) — need the AWS SDK and IAM credentials, unlike the in-DB views above which require none.
Ad-hoc administration¶
Manual maintenance — backfilling a time range, retiring stale or schema-incompatible partitions — runs through the FlightSQL server, not this binary:
- SQL functions such as
materialize_partitions()(backfill a time range),regenerate_partitions()(force-rebuild existing partitions directly from source data, bypassing the freshness checkmaterialize_partitions()stops at),retire_partitions(), andretire_partition_by_metadata(). All four require an authenticated admin — see Authentication. - Python helpers such as
micromegas.admin.list_incompatible_partitions()andmicromegas.admin.retire_incompatible_partitions()(the latter internally callsretire_partition_by_metadata()and so also requires admin).
Both are documented in Admin SQL Functions.