Repository navigation
Conversation
Each plan root (InstallablePlan.queries) is compiled once with two kinds of inputs: stored outputs, bound at evaluation time T to their definition's summary ending at T - offset, and the plan's query-time raw windows (query_time_raw_windows), bound to the metric's samples in (T - offset - width, T - offset] fetched from Prometheus and selected by the window's RawSampleSelector. An instant query whose text is a planned query is answered with a Prometheus vector and x-asap-execution: plan. Unplanned queries are forwarded; planned ones that cannot be answered (summary missing or off the coverage grid, fetch or execution failure) are forwarded with x-asap-forward-reason. Raw-window reads carry the caller's end-to-end headers (Authorization, X-Scope-OrgID and the rest query forwarding keeps), so Prometheus authorizes and scopes them as it would the caller's own query. Answer labels come from series_labels_of_output_row.
- A query whose time is not finite or beyond MAX_TIME_MS gets 400 bad_data, as Prometheus answers; window and pane binding use checked arithmetic, so no evaluation time can overflow (time=-1e300 panicked the engine thread in debug builds). - Raw-window fetches select only the series the plan keeps: the Scan's label matchers PromQL can express go into the range selector, with metric and values escaped. The selector still applies every matcher. - Raw windows are fetched concurrently. - A forward reason with characters a header cannot carry is sanitized instead of dropped.
A plan that keeps query-run summaries installs, but this data plane does not build them yet, so queries reading them are forwarded. Say so in the log at startup.
…e closed A stored output the plan builds in a query run and keeps (StoredOutputBuild::QueryRun, #509 Example 4's B3) is now executed. The query engine builds its definition's summary from the coverage's raw samples the first time an evaluation needs it, and stores it in the SDS store under the same SummaryInstanceKey as ingestion-built summaries once its coverage is closed. Later evaluations reuse it. - Closure is judged by the ingestion progress snapshotted when the evaluation chose its fetch (raw_ranges returns it with the ranges and evaluate takes it back), not when the evaluation runs, so a partial summary built from an earlier fetch is never kept. - Eviction is by the plan's retention_ms plus one pane, measured from the newest closed coverage at or before the watermark, as for ingestion-built summaries. A far-future evaluation no longer stops keeping. - One fetch per query-run definition covers all its missing summaries. - KeptResultStats (built, kept, kept_while_metric_behind, reused) is on /api/v1/status/asap. kept_while_metric_behind counts summaries kept while their own metric's newest sample had not reached their end: the watermark is global. - A query-run definition whose selector does not build no longer fails the install: its queries are forwarded. SummaryBuild, shared by the precompute and query engines, compiles a definition's build and runs it over one coverage's samples. Samples of a metric only query-run summaries read move the watermark (counted as samples_query_run_only) without being buffered. The backend now declares query_time_results_retained: true. examples/planner-layering-example4-b3.json is a workload for which the Planner selects B3 under the default cost model.
Ports #814 to the plan-executing engine. POST /api/v1/precompute/watermark?time_ms=T: the producer states that every sample at or before T has been written. The request queues an AdvanceWatermark command behind every write already acknowledged and answers {"barrier_ms": B}, the barrier in force, once it has run: every pane with end <= T is closed, keeping the left-open (end - width, end] convention, so the last pane of a series that stopped sending is built and its window served from the plan without a later sample. - T is capped at the wall clock, like the sample watermark. - A repeated or lower T does nothing; the answer is the barrier in force. - The closing reuses the expired-pane skip and checked arithmetic, so T = i64::MAX terminates. - Input stays open; a sample arriving later for a pane the barrier closed counts as late. - A missing or non-integer time_ms gets 400 bad_data with the reason. - With several producers, the barrier must follow all of their writes through T; the README says so.
…a shared SDS Series are sharded by label set across ingestion workers that each own their shard's open panes. The global watermark is the minimum over the workers that have seen a sample; the worker or barrier that moves it closes the passed panes from every worker's samples. The event-time barrier closes every worker's panes. Planned queries run on a pool of query threads, one query per thread, over a plan compiled once and shared through Arc, and wait for the writes acknowledged before them. The SDS store holds Arc instances under a RwLock and never rewrites one. jemalloc is the data plane's allocator.
zzylol
force-pushed
the
feat/concurrent-engine
branch
from
October 20, 2026 23:52
1b5d7a9 to
a70c238
Compare
zzylol
changed the base branch from
feat/repin-plan-installable
to
feat/watermark-barrier
October 20, 2026 23:53
zzylol
force-pushed
the
feat/watermark-barrier
branch
4 times, most recently
from
October 21, 2026 00:17
4987a39 to
0a5c014
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack: #821 → #824 → #839 → this PR (deferred; see Status).
Why
One engine thread ran ingestion, pane builds and every query, because the Planner's DAG types used to hold
Rcs. A long query held up ingestion and every query behind it. ASAPPlanner main (pinnedecea897f) now compiles plans that areSend + Sync, so the data plane can run ingestion and queries on many threads over one compiled plan.What
--ingestion-workers, default one per core). Remote Write series are sharded by a hash of their label set; each worker owns its shard's open panes.--query-threads, default one per core). One query per thread; queries no longer run on an ingestion thread. Before reading, a query waits until every worker has run every write acknowledged before it.IngestionandQueryEngineare compiled when the plan is installed and shared throughArc.SummaryStoreholdsArcsummary instances under aRwLock. An instance is inserted once and never rewritten (insertreturns the stored one); readers clone theArcunder the read lock. Query-run summaries (StoredOutputBuild::QueryRun) go to the same store from any query thread.tikv-jemallocator) is the data plane's global allocator.How
precompute::Sharedis what ingestion threads share: the compiledIngestion, each worker's open panes (Mutex), each worker's newest sample, the barrier, per-definition first pane and built-through end, the SDS and the counters.Shared::ingest_at(worker, …)takes a shard, then closes panes.Precomputeruns all workers on the caller's thread, for tests.QueryEngine::evaluatetakes&self; its counters are atomics (kept_stats()).Before this PR
One
enginethread: a planned query waited behind ingestion and every other query; the SDS was owned by that thread.After this PR
Ingestion runs on N workers and queries on M threads over one compiled plan; queries read summary instances concurrently by
Arc.Verification
cargo test --workspace --lockedpasses (fmt, clippy and the benefit harness were not run for this draft). New tests:sds::store_shares_instances_and_never_rewrites_them,sds::store_is_read_concurrently;precompute::a_pane_closes_once_every_shard_has_passed_it(minimum watermark over 2 shards),precompute::a_barrier_closes_every_shards_panes,precompute::sharded_panes_answer_like_one_shard(4 shards against 1, Example 4b's KLL and Example 1's rates and sums);answers::answers_concurrent_queries_like_one_at_a_time(32 concurrent queries, 4 workers, 4 query threads),answers::a_query_reads_the_panes_of_the_writes_before_it(8 runs, no wait between write and query).Existing tests changed only where the API did:
kept_statsis a method, andSummaryStore::getreturns anArc.Status: deferred (MVP is single-threaded)
Concurrency is deferred for the MVP. This branch stays at
a70c2382, stacked on the old #839 head7fbd02a6. These review findings are still open:try_ingestrecords each shard's oldest routed sample as queued for its worker before sending, and that holds the watermark until the worker has taken it. Add a test with real worker threads (1, 2 and 4 workers, several runs) asserting zero late samples and answers equal toquery_time_reference. The "allowed lateness covers that" claim in the code doc is wrong and must go.built_throughunder the locks, then build and insert after releasing them.QueryRunoutput. Countbuiltandstoredonce, and single-flight the build.samples_queuedtoo high for the shards that were not sent.query_runnames throughout instead of "kept".