Skip to content

fix: archive tier covers all 5 sketched metrics (HLL/CS/CMS/KLL/DDSketch all in Thanos) - #350

Merged
zzylol merged 1 commit into
mainfrom
fix/archive-tier-covers-all-sketched-metrics
May 8, 2026
Merged

zzylol merged 1 commit into
mainfrom
fix/archive-tier-covers-all-sketched-metrics

Conversation

@zzylol

@zzylol zzylol commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the post-#345 demo's archive_miss=1370 failure mode for the four counter-sketch metrics (unique_users_per_min, top_endpoint_qps, endpoint_request_freq, request_size_bytes).

Root cause (a): the backend's POST /api/v1/storage_routing handler is an atomic per-tenant SWAP — every push replaces the whole tenant's routing table. handle_plan emitted a single-element metrics:[…] document per call, so when the demo POSTed /api/v1/plan for each of the 5 sketched contract metrics in sequence, only the last metric's entry survived in the backend. The other 4 metrics defaulted to sketch_warm_tier (which has no warm-tier sketch state for archive-shape queries: count, topk, rate_post_hoc, histogram_quantile, delta, deriv, absent) → backend returned empty / 404 → accuracy_reduce.py logged archive_miss even though gorillas3 did write their TSDB blocks to MinIO and Thanos had them indexed.

Fix: thread per-metric BackendStageConfig through a new AppState.backend_routing_cache: Arc<Mutex<HashMap<String, BackendStageConfig>>>. Each plan-emit cycle updates the cache for the metric being planned and re-emits the storage-routing JSON from the union of all known plans. The next swap then preserves every previously seen metric's routing entry.

Scope: controller-only (controller/src/main.rs). Doesn't touch gorillas3, deploy/configs, or backend.

Evidence

Before (live demo stack with all 5 producers emitting):

$ curl -s 'http://localhost:19092/api/v1/label/__name__/values' | jq .data
[
  "http_freshness_probe_archive",
  "http_freshness_probe_raw",
  "http_freshness_probe_warm",
  "http_requests_total",
  "http_requests_total_latency_ms"
]

$ curl -s 'http://localhost:19091/api/v1/storage_routing' | jq .metrics_count
4   # only bootstrap YAML; controller's per-metric pushes erase each other

After (same stack, fix applied, controller rebuilt):

$ for m in http_requests_total_latency_ms request_size_bytes unique_users_per_min top_endpoint_qps endpoint_request_freq; do
    curl -s -X POST -H 'Content-Type: application/json' \
      -d "{\"metric_name\":\"$m\",...}" 'http://localhost:18080/api/v1/plan'
  done

$ curl -s 'http://localhost:19091/api/v1/storage_routing' | jq '{entries: .metrics_count, hash: .table_hash}'
{ "entries": 5, "hash": "a54a679c395b57f1" }

$ docker logs backend | grep "swap completed"
... entries=1 table_hash=...   # 1st metric posted
... entries=2 table_hash=...   # 2nd, cumulative
... entries=3 table_hash=...
... entries=4 table_hash=...
... entries=5 table_hash=a54a679c395b57f1   # 5th — all metrics preserved

$ curl -s 'http://localhost:19092/api/v1/label/__name__/values' | jq .data
[
  "endpoint_request_freq",         # ← CMS metric, archive-tier visible
  "http_freshness_probe_archive",
  "http_freshness_probe_raw",
  "http_freshness_probe_warm",
  "http_requests_total",
  "http_requests_total_latency_ms",
  "request_size_bytes",            # ← KLL metric
  "top_endpoint_qps",              # ← CountSketch metric
  "unique_users_per_min"           # ← HLL metric
]

Regression test

api_tests::storage_routing_cumulative_push_covers_all_5_sketched_metrics (in controller/src/main.rs):

  • Spins up an axum mock backend that captures every POST /api/v1/storage_routing body.
  • Posts /api/v1/plan 5 times for the 5 sketched contract metrics.
  • Asserts the last body's metrics:[…] array contains all 5 metrics, each carrying a thanos_archive target.
  • Asserts the cumulative count grows monotonically across pushes (1 → 2 → 3 → 4 → 5).
$ cargo test --bin controller storage_routing
running 16 tests
... all passing, including new test
test result: ok. 16 passed

What this PR does NOT fix

Out-of-scope failures observed during diagnosis but not within this PR's blast radius:

  • OpAMP RemoteConfig push isn't applied by the agent (effective-config preview shows the static bootstrap YAML, not the controller-pushed routing-connector YAML). Agent stays on [gorillas3, ddsketch, batch] single-pipeline. This is upstream of the routing fix and orthogonal — the bootstrap config has gorillas3 first so all metrics still flow through it.
  • Producer ResourceExhausted on default cardinality (grpc: received message after decompression larger than max 4194304): with EXPORTER_FIVE_SKETCH_USER_POOL=1000 × EXPORTER_CARDINALITY=500 the OTLP delta export overflows the agent's 4 MiB max_recv_msg_size. Tuning belongs in deploy/configs/asap-otel-agent-*.yaml (parallel agent's scope).

Test plan

  • cargo test --bin controller storage_routing (all 16 pass, including new regression)
  • Live stack: 5 sequential POST /api/v1/plan → backend storage_routing shows metrics_count=5 (was 1 pre-fix)
  • docker exec ... mc ls -r local/asap-gorilla-tsdb/ shows TSDB blocks (gorillas3 unchanged, still writes for all metrics)
  • curl thanos /api/v1/label/__name__/values shows all 5 sketched metrics plus freshness probes
  • docker logs backend | grep "swap completed" shows entries grow monotonically across cumulative posts

Refs #46.

🤖 Generated with Claude Code

…plans (#46)

The backend's POST /api/v1/storage_routing handler is an atomic
per-tenant SWAP — every push replaces the whole tenant's routing
table. The pre-existing handle_plan path emitted a single-element
metrics:[…] document per call, so when the demo POSTed /api/v1/plan
for each of the 5 sketched contract metrics in sequence only the
LAST metric's entry survived in the backend. The other 4 defaulted
to sketch_warm_tier (no warm-tier sketch state for archive-shape
queries) → accuracy_reduce.py logged archive_miss for those metrics
even though gorillas3 wrote their TSDB blocks to MinIO and Thanos
had them indexed.

Wire AppState.backend_routing_cache so every plan-emit cycle posts
the union of all metrics planned to date. Verified end-to-end against
a live stack: storage_routing entries=5 after the per-metric POSTs,
Thanos /api/v1/label/__name__/values lists all 5 sketched metrics
plus the freshness probes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 0bd2e71 into main May 8, 2026
zzylol added a commit that referenced this pull request May 8, 2026
…#351)

PR #112 (freshness probe cache) and PR #350 (storage routing cumulative)
both flagged producer→agent gRPC failures with high-cardinality batches:

  rpc error: code = ResourceExhausted desc = grpc: received message after
  decompression larger than max (X vs. 4194304)

The gateway already has `max_recv_msg_size_mib: 64` (controller emit at
stage_config.rs:428 + the corresponding gateway placeholder), but the
AGENT receiver inherits the 4 MiB grpc default. With 5K series × 500
user_ids on the new HLL workload, batches exceed 4 MiB and the agent
silently drops them — no metric flow → no sketch state → no probe data.

Three sites:
- `deploy/configs/asap-otel-agent-b6-asap-single-sketch.yaml` (static
  placeholder loaded via volume mount)
- `controller/src/config/stage_config.rs:128` (typed Edge emit, the
  legacy single-pipeline arm)
- `controller/src/config/stage_config.rs:852` (typed Edge emit, the
  5-sketch routing-connector arm)

OTAP variant (`stage_config_otap.rs`) uses a different listener_addr
shape; left untouched — needs a separate look.

Refs #46.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 9, 2026
…e (was single ddsketch) (#353)

PR #350's report flagged that OpAMP RemoteConfig push isn't applied by
the agent at runtime — the effective-config preview keeps showing the
static bootstrap YAML loaded from the volume mount, so the running
pipeline is whatever this static file declares.

Previously the placeholder declared a single
[gorillas3, ddsketch, batch] pipeline, so agents only ran ONE sketch
(DDSketch) regardless of the controller plan. KLL / HLL / CountSketch /
CountMinSketch processors never ran.

This PR rewrites the static placeholder to mirror the typed-emit shape
from controller/src/config/stage_config.rs::emit_edge_yaml_5sketch_routing:
all 5 sketch processors loaded at top level, the OTel v0.106 routing
connector under connectors: (NOT processors:), and 7 named pipelines
(entry + raw_passthrough + 5 per-family paths). Each per-sketch
pipeline runs gorillas3 first so the cold-tier write happens on raw
samples before the sketch processor mutates the stream.

The agent boots cleanly with no config-load errors — verified with
`asap-otel validate` (exit 0) and a minimal compose stack run that
reports "Everything is ready. Begin running and processing data."
with all 5 sketch processors listed under their respective pipelines.

Smoke test in deploy/configs/tests/ parses the YAML and pins:
  - All 5 sketch processors present at top level
  - routing in connectors: (not processors:)
  - All named pipelines (entry + raw_passthrough + 5 per-family)
  - Each per-sketch pipeline has gorillas3 first
  - routing.default_pipelines == [metrics/raw_passthrough]
  - context: metric on every routing table entry

Refs #46.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zzylol added a commit that referenced this pull request May 9, 2026
#356)

PR #355 lowered gorillas3 `window_interval` to 5 s to keep the in-memory
windowState within the agent's 1.5 GiB cgroup, but under sustained load
six per-family pipelines still overshoot the ceiling and the agent gets
OOM-killed (exit 137) after roughly three minutes — the demo confirmed
both `agent-a-1` and `agent-b-1` Exited (137) two minutes into Phase 2,
and the backend's `MEMORY_DIAG` reported `group_states_len=0` for every
worker because nothing ever ingested.

The OpenTelemetry Collector ships a `memory_limiter` processor that
refuses incoming data when RSS crosses a soft threshold, applying
backpressure to upstream senders rather than crashing. The gateway
already uses it (see compose log: `Memory limiter configured
limit_mib=1536 spike_limit_mib=256 check_interval=1`); the agent didn't.

This change inserts `memory_limiter` AS THE FIRST processor in every
per-sketch pipeline (and the default `metrics/raw_passthrough`):

    metrics/ddsketch_path:
      processors: [memory_limiter, gorillas3, ddsketch, batch]

Same for `kll_path`, `hll_path`, `countsketch_path`, `countminsketch_path`,
and `raw_passthrough`. The entry pipeline (which has `processors: []`
because routing fan-out is the connector's job) is unchanged.

Threshold rationale: agent cgroup is 1536 MiB, so we pin `limit_mib:
1280` (≈ 80 % of cgroup) and `spike_limit_mib: 256` — leaving 256 MiB
headroom under the cgroup ceiling for short bursts. Mirrors the gateway
shape but scaled to the agent's smaller cgroup.

Updates both the static placeholder
(`deploy/configs/asap-otel-agent-b6-asap-single-sketch.yaml`) and the
controller's typed emit (`emit_edge_yaml_5sketch_routing` in
`controller/src/config/stage_config.rs`) so once the OpAMP-push gap
flagged by PR #350 lands, the runtime swap stays in shape.

Tests:
- New `mvp46_per_sketch_pipelines_have_memory_limiter_first` Rust test
  asserts every per-family pipeline + raw_passthrough lists
  `memory_limiter` first.
- Python smoke `test_static_placeholder_5sketch_routing.py` gains
  `test_memory_limiter_processor_block_present` and updated
  `test_each_per_sketch_pipeline_has_memory_limiter_first` /
  `test_raw_passthrough_pipeline_shape` covering the new shape.

Refs #46, follow-up to #355.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol deleted the fix/archive-tier-covers-all-sketched-metrics branch May 9, 2026 18:00
zzylol added a commit that referenced this pull request May 17, 2026
…art (ASAPCollector#381) (#383)

Patches the upstream opampextension's `onMessage` callback to handle
`msg.RemoteConfig` — closes the residual half of PR #350.

Background. The agent connects to the controller's OpAMP server,
receives `AgentRemoteConfig` messages, but the upstream extension's
onMessage handler only processes `AgentIdentification` and
`CustomMessage`. RemoteConfig is silently dropped. Result: the
controller's elaborate typed-stage-split runtime YAML (routing
connector + 5 sketch processors + gorillas3) is generated, pushed,
and ignored — every MVP demo since runs the agent's static
bootstrap YAML, the central blocker tracked in this repo's #381.

Patch (overlay at `opentelemetry-collector-contrib-patch/extension/opampextension/`):

  * `config.go` — new `RemoteConfigPath string` field (mapstructure
    key `remote_config_path`). When empty, the extension stays
    "report-only" (upstream behavior preserved). When set, the
    extension applies pushed configs by writing to that path.
  * `opamp_agent.go`:
      - `onMessage` now dispatches `msg.RemoteConfig` to a new
        `processRemoteConfig` method when both `msg.RemoteConfig`
        and `cfg.RemoteConfigPath` are non-nil/non-empty.
      - `Start` advertises `AcceptsRemoteConfig` capability when
        `RemoteConfigPath` is set. The OpAMP client library at
        `client/internal/receivedprocessor.go:83` silently drops
        `RemoteConfig` without this capability — that was the
        first dead-end on debug.
      - `processRemoteConfig` picks the first non-empty
        ConfigFile body (preferring `""` / `"asap-otel"` keys),
        YAML-validates it, writes directly to disk (NOT
        write-tmp+rename — Linux returns EBUSY on `renameat2`
        for Docker bind-mounted files), reports
        `RemoteConfigStatuses_APPLIED`, sleeps 200ms to flush
        the status, and `os.Exit(0)`. Docker `restart=unless-stopped`
        / systemd `Restart=always` is expected to bring the
        collector back up loading the freshly-written YAML.

Non-code files (`auth.go`, `factory.go`, `logger.go`, etc.) are
verbatim copies of the upstream extension. OCB's per-extension
module replace requires the WHOLE module directory under the
replace path; we can't just override two files.

builder-config.yaml: adds `path: ./extension/opampextension`
beneath the existing `gomod:` line so OCB redirects to the patch
overlay. Mirrors the pattern every other patched processor uses
(ddsketchprocessor, kllprocessor, …).

End-to-end verification (single-node MVP smoke test under
/mydata/mvp-smoke-test/):

  * Before patch: agent stays on bootstrap YAML; `restartCount=0`;
    backend sees sketches keyed by full wire-attr fingerprint.
  * After patch: agent applies pushed config and exits;
    `restartCount=1+`; host-side mounted `agent.yaml` is
    overwritten with the controller-emitted typed-stage-split
    runtime YAML.

The agent's post-restart behavior surfaces downstream issues
(gorillas3 S3 bucket not provisioned in smoke setup, memory
limiter trips on the 5-sketch-pipeline workload) — those are
separate blockers, not in scope for this PR. The OpAMP-apply
half of #381 is what this PR closes.

Companion: ASAPQuery-backend `b1-5-controller-emit-literal-env-vars`
branch — switches the controller's gorillas3 YAML emit from
bash-style `${VAR:-default}` (rejected by OTel's confmap URI
parser) to literal values resolved from the controller's env at
emit time, and threads `remote_config_path: /etc/otel/config.yaml`
into every emitted opamp extension block so OpAMP-apply persists
across the restart loop.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant