You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
perf(dq): batch location gap-fills, events rollup, re-decode tool, mem cap (load review) (#15)
* perf(dq): batch segment/daily location gap-fills into one as-of join (#8)
Segment and daily-activity enrichment gap-filled each boundary's location
with a separate LocationAt reverse-scan point query — O(2*segments) serial
queries per request (up to thousands on a sparse-GPS vehicle with the idling
candidate cap), each holding a pooled connection.
Add duck.Queries.LocationsAt: one ASOF LEFT JOIN resolves the nearest
non-origin fix at or before every requested boundary timestamp, index-aligned.
It is exactly equivalent to per-point LocationAt — the right side excludes
(0,0) fixes before deduping (subject,name,timestamp) to the lowest
cloud_event_id, so the as-of match is tie-free and deterministic — proven by
TestLakeQueries_LocationsAt_MatchesLocationAt across the (0,0)-skip,
tie-break, and lookback-floor cases.
Also add a 90d lookback floor to both LocationAt and LocationsAt so a
fix-less vehicle no longer full-reverse-scans its retained partition; the
floor is shared so the two paths stay equivalent.
Wire GetSegments and GetDailyActivity to collect every fix-less boundary and
resolve them in a single batched pass (BatchLocationAtSource), falling back
to the (0,0) sentinel unchanged. TestGetSegments_BatchesLocationGapFill and
TestGetDailyActivity_BatchesLocationGapFill prove O(1) location queries (one
batched call, zero point calls) and correct scatter-by-index.
* perf(dq): cap the idle query DuckDB on a materializer pod (#7)
A materializer release always builds the query DuckDB backend (main.go serves
the query HTTP/gRPC unconditionally) even though it serves no reads: the
overlay disables ingress and the query fleet is a separate release with its
own Service selector, so no query/fetch traffic is routed to it. That idle
instance still honored DUCKDB_MEMORY_LIMIT=6GiB, so decode(6) + query(6) could
reach 12GiB on an 8Gi pod and OOM.
Fully skipping the query backend is entangled (main.go serves the query
HTTP/gRPC and probes duckSvc for readiness unconditionally) and higher-risk,
so take the lower-risk config route: add DUCKDB_QUERY_MEMORY_LIMIT, applied
via queryDuckConfig ONLY when MATERIALIZER_ENABLED, to cap the idle query
instance; the decode instance keeps the full DUCKDB_MEMORY_LIMIT. Set it to
1GiB in values-materializer.yaml so the two limits sum to 7GiB < 8Gi. A query
pod (MATERIALIZER_ENABLED=false) ignores the override entirely.
TestQueryDuckConfig_MaterializerMemoryCap pins all three cases.
* feat(dq): events_latest rollup for GetEventSummaries; #5b floor deferred (#5)
(a) GetEventSummaries full-history-scanned lake.events per dataSummary, with no
rollup — asymmetric with the now-cheap signal side. Add lake.events_latest, a
per-(subject,name) count + first/last-seen rollup maintained by the materializer
exactly like signals_latest: a dirtyEventSubjects set marked post-commit,
FlushEventRollup recomputing only dirty subjects bucket-chunked off the decode
commit, RecomputeEventRollup for the disaster-recovery / first-create backfill,
and an orphan-prune in PruneDecoded. eventRollupSelectSQL mirrors
GetEventSummaries' (subject,timestamp,name,source) dedup + GROUP BY name, so the
rollup is a materialized view by construction. GetEventSummaries serves from it,
falling back to the base scan until the table exists (self-healing, guards a
rollout where a query pod predates the materializer creating the new table).
ensureSchema escalates the FIRST FlushEventRollup to a full rebuild when
events_latest is created over a pre-existing lake.events (the migration case) so
dormant vehicles are backfilled and never read an empty summary; a fresh catalog
skips it (no snapshot churn). LAKE_REBUILD_ROLLUP_ON_BOOT rebuilds both rollups.
Proven by tests/ducklake_event_rollup_test.go: end-to-end incremental via the
real decode path (with a cross-cloud_event_id duplicate to prove read-dedup),
full-rebuild == per-batch parity, and first-create dormant-subject backfill.
(b) The signals rollup recompute timestamp floor is DEFERRED as
TODO(load-review #5b): a naive floor undercounts count/first_seen (full-history
aggregates) and an incremental count fold can't stay exact because the write
anti-join keys on cloud_event_id, so a different-id duplicate is stored and only
the read QUALIFY dedup collapses it — an incremental += would double-count it,
breaking the rollup-exactness invariant the tests assert. The code carries a
precise split-recency/cumulative-column design for a proven follow-up.
The commit/cursor-advance exactly-once path is untouched; both rollups are
materialized views maintained off the commit.
* feat(dq): re-decode backfill tool + cursor-reset alert; #1c deferred (#1)
(a) When the decode cursor lags past LAKE_SNAPSHOT_RETENTION, maybeRecoverExpired
skips the unretained prefix WITHOUT decoding it and only cursorResetsTotal records
the loss — no tool existed to recover the gap. Add BackfillTimeRange: it reads
raw_events in [from, to) DIRECTLY from the base table (not the expired change feed;
the rows survive din's separate row retention), re-decodes via the existing decode
path, and idempotent-inserts into lake.signals/events WITHOUT touching the
ingest_progress cursor (out-of-band repair). Idempotency uses the same
cloud_event_id anti-join but with the UNCLAMPED [min,max] timestamp window
(minMaxTime, factored out of timeRange) so re-decoding arbitrarily old data still
finds and skips existing rows — the steady-state 30d probe-floor clamp would miss
old duplicates and double-insert. Exposed as `dq -backfill-from <RFC3339>
-backfill-to <RFC3339>` (runs once and exits), which flushes both rollups after.
Proven idempotent + cursor-untouched + skipped-range-recovery by
tests/ducklake_backfill_test.go.
(b) The DQMaterializerCursorReset alert already existed; corrected its stale
"reset to head" wording (the code skips only the unretained prefix) and made it
actionable — it now names the backfill invocation to recover the gap.
(c) The per-pass byte budget is DEFERRED as TODO(load-review #1c): the real OOM is
one oversized snapshot (span can't drop below 1), which only intra-snapshot
row-key-window pagination can bound — and that decouples the atomic
insert+cursor-advance the chaos-proven exactly-once protocol relies on, so it must
be re-proven under SIGKILL, not just unit-tested. The code carries the precise
pagination design (page by (subject,timestamp,cloud_event_id), idempotent
intermediate windows, cursor coupled only to the final window's insert).
The commit/cursor-advance exactly-once path is untouched; readDelta only gained a
shared scan helper (readRawByTime reuses it).
// Sharding is not honored on the DuckLake path — readDelta reads the whole
148
190
// raw_events delta and the single global ingest_progress cursor makes exactly
149
191
// one logical processor (extra replicas just lose the cursor CAS and roll
150
192
// back). Refuse the config rather than silently ignore it: run the
151
193
// materializer as a single replicaCount=1 release (SR review #8).
152
194
ifsettings.MaterializerShardCount>1 {
153
-
returnnil, fmt.Errorf(
195
+
returnnil, nil, nil, fmt.Errorf(
154
196
"MATERIALIZER_SHARD_COUNT=%d is not supported on the DuckLake path: the global ingest_progress cursor allows only one materializer; run a single replicaCount=1 release",
0 commit comments