feat: add programmatic control of precompute engine pipelines from asap-query-engine - #287
Conversation
…-programmatic-control-of-asap-summary-ingest-pipelines
Issue #242: Design Decisions LogThis document records the design questions and decisions that shaped the implementation plan for issue #242 (programmatic control of precompute engine pipelines). It is intended to explain why the plan looks the way it does, not just what it does. Problem Statement
The goal: start What Already ExistsAfter exploring the codebase:
Decision 1: When to trigger planningQuestion: "Once on startup" — but at startup there are no intercepted queries yet. What does the planner plan? Options considered:
Decision: (b) — wait for the first real observation window, plan with actual queries. More meaningful input to the planner. During the observation window, all queries fall through to Prometheus via the existing fallback mechanism, so users see no gap in Grafana. Future: Subsequent observation windows will eventually trigger replanning (repeated reconfiguration). The design accommodates this without structural changes. Decision 2: One-shot vs. repeated loopQuestion: After the first plan is applied, should the Decision: Keep the loop running, but only apply the config on the first successful plan (via an Decision 3: Runtime reconfiguration strategy (Option A vs. B)Question: How should the engine be reconfigured at runtime? Option A — Lazy initialization (two-phase startup):
Option B — Start with empty configs, hot-swap internals:
Decision: Option B — chosen because it is more amenable to future repeated reconfiguration. Option A loses all precomputed sketch data on every config update, which is wasteful once the engine has been running for multiple windows. Option B preserves historical precomputed data and allows incremental updates. Decision 4: Config replacement vs. mergingQuestion: When new planner output arrives, does it replace the existing config entirely, or merge with it? Decision: Replace — the new planner output becomes the complete truth. However, the input to the planner includes the current This means: if a metric was being precomputed and the planner decides it still should be, it will appear in the new config. If it doesn't appear, it is dropped — data for that aggregation expires naturally via the existing cleanup policy (see Decision 8). Decision 5: Passing existing configs to the plannerQuestion: Decision: Add Decision 6: Who applies the planner resultQuestion: Options considered:
Decision: (a) — keeps Decision 7: Atomic vs. non-atomic config updateQuestion: Decision: Accept the brief inconsistency. During the transition window, a query might be matched by the old inference config against data computed by the new streaming config (or vice versa), which could produce a miss and fall back to Prometheus. This is acceptable — it is transient and self-correcting within one query cycle. Implementation note: The inconsistency window is marked with a Decision 8: Stale precomputed data and in-flight state after config replaceQuestion: When In-flight data in precompute engine workers
Consequences:
The data committed to the store under old agg IDs becomes orphaned: the query engine now uses new inference/streaming configs pointing to new agg IDs, so those store entries are never queried and eventually expire via the cleanup policy. Implementation: On
The final flush happens before Stale data already committed to
|
| Component | Before | After |
|---|---|---|
streaming_config in store |
Arc<StreamingConfig> |
RwLock<Arc<StreamingConfig>> (brief lock to clone pointer, iterate without lock) |
inference_config in SimpleEngine |
InferenceConfig (owned) |
RwLock<InferenceConfig> (SimpleEngine is already behind Arc) |
agg_configs in IngestState |
Vec<Arc<AggregationConfig>> |
ArcSwap<Vec<Arc<AggregationConfig>>> (lock-free reads on hot path) |
| Worker config updates | impossible | WorkerMessage::UpdateAggConfigs |
| Planner output | logged and discarded | sent via watch channel, applied by main.rs task |
PrecomputeEngine::run() |
creates channels internally, consumes self | channels created in new(), handle() extracted before run() |
ControllerConfig |
queries only | queries + existing_streaming_config + existing_inference_config |
No description provided.