Skip to content

feat(asap-tools): run sketch-bench workload tables end to end with the paced feeder - #841

Merged
milindsrivastava1997 merged 12 commits into
mainfrom
feat/e2e-paced-feeder-runner
Oct 14, 2026
Merged

milindsrivastava1997 merged 12 commits into
mainfrom
feat/e2e-paced-feeder-runner

Conversation

@zzylol

@zzylol zzylol commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Uses #828's --planner saved and controller.prebuilt_output_dir (merged); now based on main. This is #824 B4, revised: the data comes from the paced feeder instead of fake_exporter.

Summary

Runs a sketch-bench #777 workload table end to end with experiment_run_e2e.py. The paced feeder pushes the table's data in real time, and queries repeat at each RQE's interval T at evaluation times aligned to T.

  • Review fixes: rate flags per metric kind (--samples-per-sec for gauges/counters, --records-per-sec for records), time_window_seconds on SQL groups, the shared start time taken after setup, and a minimum feed duration that includes the feeder's startup check.
  • Feeder service (experiment_utils/services/paced_feeder.py), configured by exporters.exporter_list.paced_feeder: {workload_table, metrics, seed, duration_secs, samples_per_sec?, extra_flags?}:
    • One feeder process per (sink, metric), on the worker nodes (the coordinator in local mode). All processes get the same seed, table and --start-ms, so every system receives identical data.
    • sketchdb mode pushes to ASAPQuery's ingest, and to Prometheus too with query_prometheus_too; other modes push to Prometheus. In sketchdb mode Prometheus no longer forwards the fed metrics to ASAPQuery, so they don't arrive twice.
    • It starts once its sinks are ready, and fails the run if a feeder dies at startup (wrong URL, etc.).
    • Startup checks: the metric is in the table and is a gauge/counter, streaming.parallelism == 1, and duration_secs outlasts the queries.
    • setup_dependencies.sh builds the feeder.
  • Prometheus: when the feeder is configured, it starts with --web.enable-remote-write-receiver (bare metal and container). No out-of-order window is needed: the feeder keeps each series' samples of a tick in one ordered request (fix(asap-tools): send paced feeder ticks concurrently, ordered per series, in (t, t+1s] #840), and a CloudLab run (48M samples, 200,000 rows/s) had 0 rejections without one.
  • Query-time alignment: client_options.align_to_interval evaluates repetition k at floor((start − O)/T)·T + k·T and sends it O = query_time_offset seconds later, so all systems answer the same windows.
  • Config generator (sketch_bench_e2e/sketch_bench_e2e_configs.py): from a workload table (+ optional prebuilt controller output dir), it writes an experiment_type YAML. Query groups come from e2e.queries grouped by interval_secs (the same grouping as --planner saved), with metrics from e2e.metrics and the feeder block.
  • ASAPQuery's allowed lateness in these runs is the engine default, 5 s (the framework never overrides it), so the feeder's within-tick reordering is safe.

Not covered yet

  • Multigroup (records) metrics (feeder support in feat(asap-tools): generate records metrics in the paced feeder #843) are fed to ASAPQuery only, as <table>.<field> for its SQL frontend. The startup check refuses them when any mode also sends to Prometheus (baseline, or query_prometheus_too). The generator feeds them only with --sql-queries, and then writes a sketchdb-only config; the Prometheus baseline for the classic queries of an all table is a separate run.
  • SQL query groups: the query client uses one protocol per server, so multigroup's SQL queries can't be sent to ASAPQuery's SQL endpoint from this runner yet. The generator includes them only with --sql-queries FILE.
  • No cluster run yet. The end-to-end check (ASAPQuery vs Prometheus vs ClickHouse on the same data) comes with the rest of the stack.

Testing

  • Tests: tests/test_paced_feeder.py (feeder command lines, sinks per mode, startup checks, the Prometheus receiver flag), tests/test_sketch_bench_e2e_configs.py (config generation from a real classic table snippet) and queriers/prometheus-client/tests/test_query_time.py (alignment math). The experiments suite passes (89 tests) under pytest and unittest. A CI step now runs the prometheus-client tests. black/flake8 are clean.
  • Dry run: the generated YAML for sketch-bench's classic shared1 table loads through Hydra and passes the runner's startup checks; controller, client and Prometheus configs are generated. In dry-run mode the client, started at 12:00:35 with T = 60 s and a 10 s offset, evaluated at 12:00:00, 12:01:00 and 12:02:00.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

milindsrivastava1997 and others added 4 commits October 12, 2026 13:31
…l plan without re-solving

Reads a plan from sketch-bench's autosketch_vs_asap plan file and the eval
table's `e2e` block (PromQL queries, the plan RQEs each reads, metric
labels), and writes streaming and inference YAML through the same
converter as --planner milp. experiment_run_e2e.py gains
controller.prebuilt_output_dir to deploy those configs instead of running
the planner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Check saved picks against the RAQE's window rules, refuse duplicate
picks, reject avg queries before the leaf split, and take a roll-up's
grouping from the plan so the shared guard catches it. Share the flag
checks of --planner milp and saved. Copy only the two prebuilt configs
into a cleared output dir, refusing the dir itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e paced feeder

- exporters.exporter_list.paced_feeder: experiment_run_e2e.py runs one
  paced_feeder process per (sink, metric) on the worker nodes, pushing over
  remote write to the query engine's ingest (sketchdb mode) and/or
  Prometheus (other modes, or query_prometheus_too). It starts once its
  sinks accept writes, shares one --start-ms and seed, and fails the run if
  a feeder dies. Prometheus does not forward fed metrics to the query engine.
- Prometheus (bare-metal and containerized) starts with
  --web.enable-remote-write-receiver when the feeder is configured.
- client_options.align_to_interval: repetition k evaluates at
  floor((start - query_time_offset) / T) * T + k * T and is sent
  query_time_offset after that time.
- sketch_bench_e2e/sketch_bench_e2e_configs.py writes an experiment_type
  config from a table's e2e block: one query group per interval, the feeder
  block, metrics, and controller.prebuilt_output_dir; structured queries
  only with a SQL file.
- setup_dependencies.sh builds the feeder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
…aced feeder

The feeder sends each tick's requests concurrently, so a series' samples
can reach Prometheus' remote-write receiver out of order within a second,
and Prometheus rejects them without an out-of-order window. When the paced
feeder is configured, the generated prometheus.yml (used by bare-metal and
containerized Prometheus) sets storage.tsdb.out_of_order_time_window: 5s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
zzylol and others added 3 commits October 13, 2026 01:52
Records metrics (http, flows) reach remote write as one sample per value
field, many sharing a series and timestamp, which Prometheus' receiver
rejects. check_paced_feeder_config now takes the experiment modes: it
accepts records metrics when every mode feeds ASAPQuery only and refuses
them when a mode feeds Prometheus (baseline or query_prometheus_too).

The config generator feeds a table's records metrics only with
--sql-queries (only the structured queries read them); the config then
has no baseline mode, and records metrics stay out of `metrics`, which
with prebuilt planner configs only keeps fed metrics out of Prometheus'
forwarding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
- Pass --samples-per-sec only to gauge and counter feeders and the new
  records_per_sec as --records-per-sec only to records feeders; the
  feeder exits on the other kind's flag.
- SQL query groups get time_window_seconds: 0; their templates read only
  {{ end_time }} / {{ end_datetime }}, but the client refuses to render
  templates without a window.
- Take the shared --start-ms after the per-node binary check and table
  copy, so the 5 s lead covers only the launches.
- duration_secs must also cover the feeder's startup check before
  steady_state_wait and the queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
…d feeder

The feeder now keeps each series' samples of a tick in one ordered
request, and Prometheus 2.53's remote-write receiver accepted a full feed
(48M samples) with no out-of-order window and no rejections. This reverts
f7e5a66; Prometheus keeps --web.enable-remote-write-receiver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
Brings in #828 (the base, merged with later review changes), #860 and the
rest of main. Planner files, experiment_utils/config.py and
test_prebuilt_controller_output.py take main's versions (this branch never
changed them). experiment_run_e2e.py takes main's
copy_prebuilt_controller_output call. main_prometheus_client.py keeps both
align_to_interval and main's simulated_start_time, refusing a group that
sets both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae
@milindsrivastava1997

Copy link
Copy Markdown
Contributor

Code review

Note: relies on #840's feeder behavior (per-series ordered requests within a tick).

  1. High — sketch_bench_e2e_configs.py:59 + core.py:204-207: uncovered queries silently return empty in sketchdb mode. The generated SKETCHDB_MODE has no query_prometheus_too, so fed metrics never reach Prometheus, but streaming.forward_unsupported_queries defaults to true (config/config.yaml:54). Any query the planner didn't cover is forwarded to an empty Prometheus and recorded as ASAPQuery's (empty) result. Suggest setting forward_unsupported_queries=false in the generator or rejecting the combination at startup.
  2. High — paced_feeder.py:208 vs query_time.py (first_aligned_eval_time): sketchdb and baseline runs see different data windows. start_ms = ceil(now) + 5s is not aligned to T, while eval times are wall-clock multiples of T. Each mode runs at a different wall time, so the window ending at aligned t_k covers a different slice of the generated data per mode. Suggest aligning start_ms to the LCM of group intervals (or largest T).
  3. Medium — core.py:204: fed_metrics comes from experiment_params.metrics entries with exporter: paced_feeder, but the feeder sends exporter_list.paced_feeder.metrics. If a hand-written config lists them only in the feeder block, fed_metrics is empty and with query_prometheus_too ASAPQuery receives every sample twice (inflated sums/counts). Derive from the feeder block or assert they agree.
  4. Medium — paced_feeder.py:208-239: feeders launch via sequential SSH after start_ms is fixed, with a fixed 5s lead; with ~20 jobs the later feeders start after start_ms. Launch in parallel or scale the lead.
  5. Medium — main_prometheus_client.py:334: first eval time floor((now - O)/T)*T can precede the feed start. With starting_delay=120 and T=3600s, repetition 0 evaluates an empty/partial window. Use the first multiple of T ≥ feeder start.
  6. Medium — experiment_run_e2e.py:520/:606: no try/finally between feeder start and stop; an exception leaves feeders pushing for duration_secs into the next run's sinks.
  7. Low-medium — experiment_run_e2e.py:327: with controller_auto_discover_labels, label inference runs against Prometheus before the feeder starts (and in sketchdb mode Prometheus never gets the metrics). Generated configs avoid it; hand-written ones don't.
  8. Low — paced_feeder.py:202: in local mode a relative workload_table passes check_paced_feeder_config (resolved from the Python cwd) but the feeder runs in data-sources/paced-feeder, so it can't open the table.
  9. Low — paced_feeder.py:106: the streaming.parallelism == 1 check fires even for baseline-only configs.

Design: the workload table is parsed in two places (paced_feeder.py:70 table_metric_types and load_e2e) and re-read on every PacedFeederService.start; a shared loader would help.

Tests: no coverage for findings 1, 2, 3, 5.

@milindsrivastava1997

milindsrivastava1997 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Plan for review findings #2 and #5

Real numbers from the sketch-bench tables: queries repeat every T = 60s (most), 300s or 1s, with ranges of 15m / 1h / 6h / 24h. The feeder sends one sample per second per series. The first query goes out ~3 min after the feeder starts.

Problem

Take sum(rate(x[15m])) repeating every minute. The baseline and sketchdb modes run at different times, each with its own feeder:

Every query compares answers over windows shifted by 19 samples, because the feeders started at different seconds within the minute. The difference comes from the input, not the sketches. (#2)

With T = 300s, the first query rounds down to the previous 5-minute mark, which falls before the feed started roughly 40% of the time, so it sees no data. (#5)

Why not just query at "feeder start + k minutes" (10:03:23, 10:04:23, ...)? ASAPQuery precomputes windows on whole minutes, so those times wouldn't line up with them.

Proposed fix: backdate the data to the last 5-minute mark

The feeder stamps its first sample with the most recent multiple of the largest T (5 min) instead of the current time. Every timestamp then runs a fixed few minutes behind real time.

Each query is sent once its data has arrived (at real time eval + delay + query_time_offset). Both modes now see the same samples in every query, queries stay on whole minutes / 5-minute marks, query k always starts k·T after the data begins (no empty first window), and nothing waits.

Risks

  • Will Prometheus accept backdated samples? We only backdate by < 5 min. Tested locally on v2.53.2 and v3.9.1 with samples ~1h old: all were accepted and queryable.
  • Work needed. The feeder needs a "backdate by X" option, so feeders launched a few seconds apart over SSH use the same timestamps. The client needs to be told the backdated start, so it knows which times to evaluate and when to send.
  • SQL queries are fine, because they use the time the client passes in. The exception is a hand-written query that calls NOW() itself.
  • Not addressed here: 1h / 6h / 24h ranges are mostly empty during a ~1h run (the data is only minutes to an hour old). After this fix both modes see the same partial data, so the comparison is fair, but those queries don't exercise a full window.

milindsrivastava1997 and others added 2 commits October 14, 2026 09:57
Each mode's feeder now stamps its data from the last multiple of the
aligned query intervals (sending it in real time from a shared send
time), and aligned query groups evaluate at that origin + k*T, sent once
the data has arrived. Every mode then answers over the same samples,
evaluation times stay on ASAPQuery's windows, and the first window is
never empty.

Also from review:
- refuse forward_unsupported_queries when sketchdb mode gives Prometheus
  no data (the generator sets it false), and auto_discover_labels
- take the fed metrics from the feeder block
- launch feeders with one SSH per node and fail if that overruns the lead
- stop feeders at exit after a failed run
- resolve a relative workload_table; check parallelism only for sketchdb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-runner

# Conflicts:
#	asap-tools/experiments/experiment_utils/services/docker_prometheus.py
#	asap-tools/experiments/experiment_utils/services/prometheus.py
milindsrivastava1997 added a commit that referenced this pull request Oct 14, 2026
…844)

* feat(experiments): fake_exporter-equivalent dataset for ClickHouse generated2 workloads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(experiments): translate generated2 PromQL workloads to ClickHouse SQL configs

Adds a client_options.simulated_start_time so templated SQL can be evaluated
against bulk-loaded data with fixed timestamps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(asap-tools): translate sketch-bench e2e PromQL to ClickHouse SQL configs

sketch_bench_workloads.py reads a sketch-bench table's workloads[0].e2e block
and writes, per table, the metric tables' init SQL, one SQL file per query
interval and a baseline experiment_run_clickhouse.py config whose query
groups repeat every interval.

Queries keep generate_workloads.py's Prometheus 2.53 semantics (range
vectors read (T - d, T], extrapolatedRate for rate/increase,
quantileExactInclusive) and add the classic template shapes: topk over
sum by, sum/count/quantile by over instant vectors (latest sample per series
in (T - 1s, T]) and rates, and the ratio of two quantile_over_time. Eval
time is {{ end_time }} floored to the interval. Unsupported shapes and
structured (non-PromQL) entries raise.

generate_workloads.extrapolated_rate now builds on rate_from_samples, which
the new module reuses; its SQL output is unchanged.

The test compares every generated statement in clickhouse-local against a
Python implementation of the Prometheus semantics at several eval times,
covering empty windows and every extrapolation branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* fix(asap-tools): use Prometheus 2.53's closed windows in sketch-bench SQL

Experiments run Prometheus 2.53.2 (installation/prometheus/install.sh),
whose range selectors and instant lookback are closed, [T - d, T]; left-open
windows arrived in 3.0. Aligned eval times put a sample on the left
boundary, so the SQL now reads ts >= T - d. The Python reference selects
[T - d, T] as well.

generate_workloads.py's generated2 SQL is unchanged; only the shared
rate_from_samples docstring no longer names a window shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): multi-grouping sketch-bench queries in ClickHouse SQL

Structured e2e entries {table, value, capability, grouping, quantile,
window_secs, interval_secs} become pooled queries over a records table.
Cardinality is uniqExact of the value field per group, and quantile is
quantileExactInclusive over all of a group's samples. Both read the same
closed [T - W, T] window, with T floored to the interval as the PromQL
queries are. init.sql now creates records tables, with one Float64 column
per value field. Each table's structured queries are also written in
ASAPQuery's SQL frontend dialect (COUNT(DISTINCT col), QUANTILE(q, col),
BETWEEN DATEADD(s, -W, NOW()) AND NOW()) to asapquery.sql.

The clickhouse-local test adds a skewed records dataset in which endpoint is
a child of service. It checks template 16's CUBE over (region, service,
endpoint) and several cardinality groupings against a Python reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): ASAPQuery's left-open window for multigroup queries

Multigroup (structured) queries involve no Prometheus, so they now read
ASAPQuery's left-open window (E - W, E] instead of the closed [E - W, E]
that classic PromQL keeps to match Prometheus 2.53. ASAPQuery's SQL
frontend and precompute windows are (start, end], so ClickHouse and
ASAPQuery compute over the same samples. Query gets a `left_open` flag that
structured entries set; the raw SQL uses `ts > E - W`.

The ASAPQuery-dialect file ends its window at the floored eval time instead
of NOW(). The frontend cannot floor a literal, so the client's
{{ end_datetime }} is moved back by {{ end_time % T }} inside DATEADD:

  ts BETWEEN DATEADD(s, -{{ end_time % T + W }}, '{{ end_datetime }}')
         AND DATEADD(s, -{{ end_time % T }}, '{{ end_datetime }}')

which the frontend parses to (start, duration) = (E - W, W).

The Python reference for multigroup now pools records in (t - w, t].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): request aligned eval times in sketch-bench ClickHouse configs

The SQL floors {{ end_time }} to T, but the current query client sleeps T
after each repetition's queries finish, so its eval times drift by the
query latency and the arms answer different windows. Each query group now
sets `align_to_interval: true` and `query_time_offset: 10` (PR #841's
default). With #841's client, repetition k is evaluated at
floor((start - O) / T) * T + k * T and sent O seconds later.

The current client ignores `align_to_interval` (unknown client options
become unused attributes) and shifts its eval time back by the offset. The
module docstring records the drift until #841's client lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* fix(asap-tools): compare ts with DateTime64 bounds in sketch-bench raw SQL

ClickHouse 26.10 turns `ts > <integer>` on a DateTime64 key column into
`ts in [<integer> + 1, +Inf)` for index pruning, so the left-open multigroup
SQL dropped the samples in the window's first second from the result (A6:
raw vs MV relative differences of 1e-6 to 2.6e-3 on 49 of 52 queries).
The raw SQL now compares ts with toDateTime64(<bound>, 3) on both ends, for
classic and multigroup queries.

A regression test runs a left-open and a closed query in clickhouse-local
over a table with 8-row granules and samples every 7 ms around the window
start. Without the cast it reproduces the loss (8435 of 8571 rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): take ASAPQuery SQL from the table's structured entries

sketch-bench's exporter now writes each structured e2e entry's ASAPQuery
SQL (`sql`, e.g. `... WHERE ts BETWEEN DATEADD(s, -300, NOW()) AND NOW()
...`), so it is the single ASAPQuery SQL renderer. asapquery.sql now takes
that SQL as written and only replaces NOW() with the floored eval time; this
module's own dialect renderer is gone. The ClickHouse raw SQL still comes
from the structured fields.

The frontend's DATEADD takes only NOW(), a literal or a CAST as its base, so
NOW() becomes a datetime literal floored in the client's Jinja template:
{{ end_datetime }} (UTC) with its seconds floored for T dividing 60, or its
minutes for 60 | T | 3600. Other intervals raise.

All 936 distinct structured queries in sketch-bench cd51be9's tables
(T = 60 s and 300 s), rendered through the client's QueryTemplate at three
end times, parse in ASAPQuery's SQL frontend to (start, duration) =
(floor(E) - W, W) and pass its matcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* fix(asap-tools): address #844 review on sketch-bench SQL rendering

- Render an empty structured grouping as the grand total, no GROUP BY.
- Comment PromQL on one line so multi-line PromQL stays a comment.
- Reject PromQL over a records metric, and ASAPQuery sql without NOW().
- Document the 1 s instant lookback and the UTC server requirement.
- Drop PR references from comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(asap-tools): validate sketch-bench table inputs at generation time

- Raise on an unknown structured table, a label-less metric, more than one
  workload, and an interval longer than --duration-secs.
- Warn when --starting-delay-secs is shorter than offset + longest window.
- Strip a trailing ';' from ASAPQuery sql; reuse convert()'s label map.
- Correct the comment on which tables the runner fills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Milind Srivastava <milindsrivastava1997@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@milindsrivastava1997 milindsrivastava1997 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated code review: 10 findings (inline).

(by_interval(promql), {}),
# The SQL templates read only {{ end_time }} / {{ end_datetime }} (the
# window is in the SQL); the client requires a window to render them.
(by_interval(sql), {"time_window_seconds": 0}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQL query groups get a group-level time_window_seconds: 0, but generate_controller_client_configs only strips client_options, and the planner's QueryGroup is #[serde(deny_unknown_fields)]. A run with --sql-queries and no --prebuilt-output-dir fails because asap-planner rejects the config with 'unknown field time_window_seconds'.


local_config_dir = os.path.join(local_experiment_dir, "paced_feeder_config")
os.makedirs(local_config_dir, exist_ok=True)
with open(os.path.join(local_config_dir, "cmds.sh"), "w") as f:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PACED_FEEDER_START_LEAD_SECS (5 s) has to cover SSH launches that run one node at a time, and run_cmd_with_retry sleeps 5 s on each rc=255 retry. With roughly 8 or more worker nodes, or a single retried SSH, the launches pass send_at_ms and the run aborts. Launch each node with Popen and wait on all of them, or scale the lead with the node count.

# One shared send time for every process of this mode, taken after the
# SSH prep above so the lead covers only the launches.
send_at_ms = (math.ceil(time.time()) + PACED_FEEDER_START_LEAD_SECS) * 1000
start_ms = backdated_start_ms(send_at_ms, align_secs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start_ms is backdated to a multiple of the LCM of the aligned intervals, so the lag can approach the full LCM. Prometheus rejects samples older than its newest sample minus 1 h, and its newest samples come from wall-clock scrapes. With intervals of 420 s and 3600 s, the LCM is 25200 s and every fed sample is rejected, so the baseline comes back empty. Consider capping or rejecting an LCM above Prometheus's out-of-order window.

"""LCM, in seconds, of the repeat intervals of the client config's
`align_to_interval` groups; 1 if there are none."""
intervals = [
g["repetition_delay_ms"] // 1000

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repetition_delay_ms // 1000 turns a sub-second interval into 0, so the LCM is 0 and backdated_start_ms divides by zero (repetition_delay_ms: 500 raises ZeroDivisionError). The client's 'whole-second repetition_delay_ms' error is never reached. A value like 1500 ms is silently truncated to 1 s. Validate that the value is a whole number of seconds here.

]
# With only fed metrics there is nothing for Prometheus to forward. (An
# empty list without the feeder keeps meaning "forward everything".)
forward_to_query_engine = bool(metrics_to_remote_write) or not fed_metrics

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a paced feeder is configured and the filtered remote-write list is empty, all forwarding to ASAPQuery is now disabled. Before this change, an empty list meant 'forward everything'. With a paced feeder plus another scraped exporter whose metrics aren't listed in experiment_params.metrics, ASAPQuery never receives that exporter's data.

)
paced_feeder_service = PacedFeederService(provider, args)
# Otherwise a failed run leaves feeders pushing into the next run's sinks.
atexit.register(paced_feeder_service.stop)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

atexit.register(paced_feeder_service.stop) runs on every exit, even when flow.no_teardown is set. Prometheus and ASAPQuery are left running for inspection, but the feeders are killed, so data stops arriving.

)
logger.debug("Current unix time: {}", int(current_time.timestamp()))
lag_secs = float(data_lag_secs)
offset_secs = getattr(query_group, "query_time_offset", 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aligned groups default query_time_offset to 0. Each repetition then fires at t_k + L, before ASAPQuery's 5 s allowed lateness closes the window, so sketchdb returns partial results. Consider requiring an offset for aligned groups, or defaulting it to at least the allowed lateness.

# No $(...): the SSH command is double-quoted on this side.
result = self.provider.execute_command(
node_idx=node_idx,
cmd=f"pgrep -fc '{PACED_FEEDER_PROCESS_PATTERN}' | grep -qx {expected}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pgrep -fc counts every paced_feeder process on the node and requires the count to equal exactly this run's job count. A stray or concurrent feeder on a shared node (count 3, expected 2) fails a healthy run. Checking the PIDs of the launched processes (e.g. echo $! per launch) would be more robust.

elif config.get("samples_per_sec") is not None:
cmd += f" --samples-per-sec {config['samples_per_sec']}"
if config.get("extra_flags"):
cmd += f" {config['extra_flags']}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra_flags and other config values are inserted unescaped into a command that get_ssh_cmd wraps in double quotes. A value like --label "a b", or one containing $, breaks the quoting, and the only symptom is the vague 'expected N processes' error later. Consider shlex.quote.


import constants
from .base import BaseService
from .fake_exporters import get_exporter_target_nodes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: get_exporter_target_nodes now has a generic name but still lives in fake_exporters.py, so the paced feeder depends on the fake-exporter module. Consider moving it to a shared module.

milindsrivastava1997 added a commit that referenced this pull request Oct 14, 2026
…eries (#845)

* feat(experiments): fake_exporter-equivalent dataset for ClickHouse generated2 workloads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(experiments): translate generated2 PromQL workloads to ClickHouse SQL configs

Adds a client_options.simulated_start_time so templated SQL can be evaluated
against bulk-loaded data with fixed timestamps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(asap-tools): translate sketch-bench e2e PromQL to ClickHouse SQL configs

sketch_bench_workloads.py reads a sketch-bench table's workloads[0].e2e block
and writes, per table, the metric tables' init SQL, one SQL file per query
interval and a baseline experiment_run_clickhouse.py config whose query
groups repeat every interval.

Queries keep generate_workloads.py's Prometheus 2.53 semantics (range
vectors read (T - d, T], extrapolatedRate for rate/increase,
quantileExactInclusive) and add the classic template shapes: topk over
sum by, sum/count/quantile by over instant vectors (latest sample per series
in (T - 1s, T]) and rates, and the ratio of two quantile_over_time. Eval
time is {{ end_time }} floored to the interval. Unsupported shapes and
structured (non-PromQL) entries raise.

generate_workloads.extrapolated_rate now builds on rate_from_samples, which
the new module reuses; its SQL output is unchanged.

The test compares every generated statement in clickhouse-local against a
Python implementation of the Prometheus semantics at several eval times,
covering empty windows and every extrapolation branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* fix(asap-tools): use Prometheus 2.53's closed windows in sketch-bench SQL

Experiments run Prometheus 2.53.2 (installation/prometheus/install.sh),
whose range selectors and instant lookback are closed, [T - d, T]; left-open
windows arrived in 3.0. Aligned eval times put a sample on the left
boundary, so the SQL now reads ts >= T - d. The Python reference selects
[T - d, T] as well.

generate_workloads.py's generated2 SQL is unchanged; only the shared
rate_from_samples docstring no longer names a window shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): multi-grouping sketch-bench queries in ClickHouse SQL

Structured e2e entries {table, value, capability, grouping, quantile,
window_secs, interval_secs} become pooled queries over a records table.
Cardinality is uniqExact of the value field per group, and quantile is
quantileExactInclusive over all of a group's samples. Both read the same
closed [T - W, T] window, with T floored to the interval as the PromQL
queries are. init.sql now creates records tables, with one Float64 column
per value field. Each table's structured queries are also written in
ASAPQuery's SQL frontend dialect (COUNT(DISTINCT col), QUANTILE(q, col),
BETWEEN DATEADD(s, -W, NOW()) AND NOW()) to asapquery.sql.

The clickhouse-local test adds a skewed records dataset in which endpoint is
a child of service. It checks template 16's CUBE over (region, service,
endpoint) and several cardinality groupings against a Python reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): baseline_mv arm for sketch-bench e2e queries

The sketch-bench ClickHouse configs now run baseline_mv beside baseline:
exact mergeable states in materialized views, no sketches. There is one
AggregatingMergeTree view per (metric, value, statistic, interval T). Its
key is the T-second bucket plus the union of the queries' groupings and their
child_of parents (every label for per-series statistics). The states are
sumState, countState, quantilesExactInclusiveState over the queries' levels,
argMaxState per series for instant vectors, and per-series first/last
sample, min/max ts and count for rate/increase. Those feed the same
extrapolatedRate SQL.

Bucket b holds (b - T, b], and an `edge` flag keeps samples at exactly b
apart. A closed window [E - W, E] therefore merges the buckets in
(E - W, E] plus the edge rows of bucket E - W. A query whose window is not a
multiple of T raises, so instant vectors are only served at T = 1 s. The
init SQL creates the views after their tables and before any data arrives.

The test runs every MV query next to its raw SQL in clickhouse-local. Results
match exactly, except sums, which agree within 1e-9 relative.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): ASAPQuery's left-open window for multigroup queries

Multigroup (structured) queries involve no Prometheus, so they now read
ASAPQuery's left-open window (E - W, E] instead of the closed [E - W, E]
that classic PromQL keeps to match Prometheus 2.53. ASAPQuery's SQL
frontend and precompute windows are (start, end], so ClickHouse and
ASAPQuery compute over the same samples. Query gets a `left_open` flag that
structured entries set; the raw SQL uses `ts > E - W`.

The ASAPQuery-dialect file ends its window at the floored eval time instead
of NOW(). The frontend cannot floor a literal, so the client's
{{ end_datetime }} is moved back by {{ end_time % T }} inside DATEADD:

  ts BETWEEN DATEADD(s, -{{ end_time % T + W }}, '{{ end_datetime }}')
         AND DATEADD(s, -{{ end_time % T }}, '{{ end_datetime }}')

which the frontend parses to (start, duration) = (E - W, W).

The Python reference for multigroup now pools records in (t - w, t].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): per-arm init SQL so baseline ingest skips the views

Each sketch-bench table now gets one config per arm. The baseline config
(init.sql) creates only the tables. The baseline_mv config (init_mv.sql)
creates the tables and their views. Both files drop the views first, so
none left by an earlier baseline_mv run keeps feeding on the recreated
tables. Both configs list both arms' SQL files, so saved results label every
query by its baseline text.

experiment_run_clickhouse.py monitored ingest under the baseline mode's
client config and output dir. A baseline_mv-only run has neither, so ingest
now uses baseline when the run has it, and otherwise the run's first mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): left-open bucket selection for multigroup views

A multigroup query's window is now ASAPQuery's (E - W, E], so its
baseline_mv SQL merges just the buckets in (E - W, E] and skips the edge
rows that hold a closed window's left boundary. Classic PromQL queries keep
the closed window and its edge rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): request aligned eval times in sketch-bench ClickHouse configs

The SQL floors {{ end_time }} to T, but the current query client sleeps T
after each repetition's queries finish, so its eval times drift by the
query latency and the arms answer different windows. Each query group now
sets `align_to_interval: true` and `query_time_offset: 10` (PR #841's
default). With #841's client, repetition k is evaluated at
floor((start - O) / T) * T + k * T and sent O seconds later.

The current client ignores `align_to_interval` (unknown client options
become unused attributes) and shifts its eval time back by the offset. The
module docstring records the drift until #841's client lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* fix(asap-tools): skip the baseline_mv arm when its views can't serve a query

convert() planned views for every query, and plan_views raises when a
window is not a multiple of the interval (e.g. an instant vector, which
reads [T - 1s, T], at T > 1 s). So such a table failed to convert even for
the baseline arm, which needs no views.

Now a table with such a query gets only the baseline arm: init.sql, the
raw SQL and the baseline config, plus a note on stderr naming the queries.
Dropping just those queries from the MV arm would shift its query indices
against the baseline SQL that labels both arms' results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* fix(asap-tools): compare ts with DateTime64 bounds in sketch-bench raw SQL

ClickHouse 26.10 turns `ts > <integer>` on a DateTime64 key column into
`ts in [<integer> + 1, +Inf)` for index pruning, so the left-open multigroup
SQL dropped the samples in the window's first second from the result (A6:
raw vs MV relative differences of 1e-6 to 2.6e-3 on 49 of 52 queries).
The raw SQL now compares ts with toDateTime64(<bound>, 3) on both ends, for
classic and multigroup queries.

A regression test runs a left-open and a closed query in clickhouse-local
over a table with 8-row granules and samples every 7 ms around the window
start. Without the cast it reproduces the loss (8435 of 8571 rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* fix(asap-tools): cast view bucket bounds; regression test covers the MV SQL

The views' `bucket` column is a DateTime (whole seconds), so integer bounds
already filter it exactly. They are now toDateTime(<bound>) anyway, to match
the column type, as the raw SQL's ts bounds now are.

The key-pruning regression test, merged from feat/e2e-sql-from-table, now
passes the views to load_script (this branch's signature). It also runs each
query's MV SQL over views with 1-row granules and checks raw and MV against
the expected counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* feat(asap-tools): take ASAPQuery SQL from the table's structured entries

sketch-bench's exporter now writes each structured e2e entry's ASAPQuery
SQL (`sql`, e.g. `... WHERE ts BETWEEN DATEADD(s, -300, NOW()) AND NOW()
...`), so it is the single ASAPQuery SQL renderer. asapquery.sql now takes
that SQL as written and only replaces NOW() with the floored eval time; this
module's own dialect renderer is gone. The ClickHouse raw SQL still comes
from the structured fields.

The frontend's DATEADD takes only NOW(), a literal or a CAST as its base, so
NOW() becomes a datetime literal floored in the client's Jinja template:
{{ end_datetime }} (UTC) with its seconds floored for T dividing 60, or its
minutes for 60 | T | 3600. Other intervals raise.

All 936 distinct structured queries in sketch-bench cd51be9's tables
(T = 60 s and 300 s), rendered through the client's QueryTemplate at three
end times, parse in ASAPQuery's SQL frontend to (start, duration) =
(floor(E) - W, W) and pass its matcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnJ7LocWxguh3q4QWkCYae

* fix(asap-tools): address #845 review on sketch-bench materialized views

- Keep every view in one database that each init drops, so views another
  workload left on the metric tables stop firing on inserts.
- Clear the table's generated files before writing, so a skipped
  baseline_mv arm or a removed interval leaves no stale SQL or config.
- Render views with no label keys (empty grouping) as valid DDL.
- Raise ValueError when child_of names an unknown label.
- Share one mv_servable() check between convert and plan_views.
- Docstring: E for the eval time, T for the interval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(asap-tools): render sketch-bench outputs before clearing old ones

A re-run that failed validation or rendering had already deleted the
earlier SQL files, leaving its config pointing at missing files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Milind Srivastava <milindsrivastava1997@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Feeders now start at the next multiple of the aligned intervals' LCM
(at most 600 s) rather than stamping data in the past, so the client
sends each query at its eval time + query_time_offset and the feeder
loses --send-at-ms.

Also from review:
- refuse sub-second aligned intervals and query_time_offset below
  ASAPQuery's 5 s allowed lateness
- give feeder launches 10 s per node on top of the 5 s lead
- keep feeders running under flow.no_teardown
- refuse --sql-queries without --prebuilt-output-dir
- refuse other exporters next to the feeder with no listed metrics

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@milindsrivastava1997

Copy link
Copy Markdown
Contributor

Review follow-up (06289b2)

Backdating is gone. It needed too many guards (Prometheus' sample-age limit, client lag plumbing). Instead, each mode's feeders now wait for the next multiple of the aligned intervals' LCM and start there. For today's tables (T = 1, 60, 300 s) that's a wait of at most 5 min per mode. Every mode's data still starts on the same boundary, so each query covers the same samples in every mode. The client sends each query at its eval time + query_time_offset. --send-at-ms is removed from the feeder.

Addressed:

  • LCM too large: refused above 600 s (that's how long the feeder would wait).
  • Sub-second intervals: aligned groups must use whole-second repetition_delay_ms.
  • query_time_offset of 0: aligned groups need at least 5 s (ASAPQuery's allowed lateness). The generator uses 10 s.
  • 5 s launch lead: now 5 s + 10 s per node, which covers one SSH retry each. An overrun still stops the feeders and fails the run.
  • atexit under no_teardown: only registered when teardown is on.
  • SQL groups without prebuilt configs: the generator refuses --sql-queries without --prebuilt-output-dir.
  • Forwarding disabled: errors when other exporters run next to the feeder but none of their metrics are listed.

Not changed: the pgrep exact count (only matters when experiments share nodes), extra_flags quoting (quoting would break passing several flags), and moving get_exporter_target_nodes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants