Skip to content

v1.3.42.3: the long-range readers move to the hourly rollup (unreleased)

Planning: docs/planning/v1.3.42.3-rollup-readers-phase0.md (operator's conditions of 2026-09-28: histogram percentiles say so on the card and api.md lists the edges; deploy only after drift verdicts 7 and 8 at 0). No schema, no long write, no bouncer change.

What ships

  • One reader in internal/db/rollup_read.go: RollupSpanFor splits a window into the head edge, the closed hours the rollup holds and the live tail; ReadRollupHours / ReadRollupPaths read the closed span through the rollup indexes; Hist merges the 8-bucket duration histogram of rollup rows and live rows and resolves nearest-rank percentiles to the bucket edge (the merged maximum above 5,000 ms).
  • Dashboard traffic: 1h and 6h unchanged on rows (percentile_method: exact); 24h, 7d and the new 30d stitched (histogram): the 24 h status series stays at 15 min from an index-only scan of the status index, response times one point per hour (6 h on 30d), top hosts, top paths and bandwidth over the whole range. Overview counters the same way. The pinned 24 h refresh no longer visits the rows that carry raw.
  • logs/stats and logs/timeseries (hourly buckets) on time-only filters with at least one closed hour in the window: stitched; the covering-index path of v1.3.38.4 stays only as the fallback while the rollup has no hours (percentile_method: sample).
  • X-Argos-Path: rollup | rows on overview, traffic, stats and timeseries; percentile_method in the traffic and stats bodies. Gone: detail_window, detail_from, series_covers_range, sample_n, the "(last N h of range)" card titles.
  • Endpoints take the URL vocabulary: range on /api/logs, /api/logs/stats, /api/logs/timeseries, /api/logs/export.csv, /api/notifications/deliveries (alternative to from/to) and /api/appsec/metrics (alias of window); q on /api/threats/decisions. lib/filters.ts lost rangeFrom, rangeToDash and rangeToAppSecWindow; the pages send range as is. Dashboard presets 1h / 6h / 24h / 7d / 30d (the security section clamps 30d to 7d, it stays on rows); Logs presets gain 30d; the p95 cards say "histogram (bucket edges)" with the edges in the tooltip.

  • Monthly VACUUM on the calendar (1st of the month, 04:00 UTC) with a catch-up at boot when the recorded run is older than the last slot; the first boot with this code only records the last slot, so on prod the first run is 2026-10-01 04:00 UTC (the sampler scripts/ops/vacuum-sample.sh is for that run). The old check ran on a 24 h ticker from boot and never fired unless the panel had started inside hour 04 UTC.

  • read-pool.sh: STRIP_WAIT_S (120 s) and an explicit PASS "nothing to strip" instead of waiting CAP_S for a purge line the panel only writes when it had work.

Verified on the dense demo (rc3 image, 2026-09-28)

Demo stack with scripts/demo/seed-dense.sh (500,000 rows over the 7 days ending at the seed time, 480k access rows of 1.5 KB raw), the rollup reset by hand and re-filled by the boot backfill (168 h in 40 s), rc3 = commit 1415af4 of the branch plus the docs commits.

check result
unit, internal/db + internal/dashboard + internal/api on dbtest ok: stitched traffic (24h / 7d / 30d, with and without host), overview, stats (6h / 24h / 7d, all sources and access only) and timeseries equal the row computation, tolerance 0; histogram percentiles equal the ones rebuilt from the raw durations; planner pins for the rollup reads, the edge passes, the 15 min index-only series
rollup-readers.sh PASS: stats over a closed 7 d window 499,867 rows, 2xx 378,222 / 3xx 45,334 / 4xx 54,891 / 5xx 1,343 / other 20,077 equal through the volume; timeseries sum equal (169 points); range=1h on rows; traffic 7d 169 buckets and 30d 121 buckets, X-Argos-Path: rollup, histogram, no bridge field
latency, cold, rc3 (panel at 1 CPU) logs/stats 7d 0.20 s (0.7 s before the one-pass edge, 2.6 s on the first call after the backfill), 24h 0.11 s, 30d 0.20 s; logs/timeseries 7d 0.08 s; traffic 7d miss 0.36 s, 30d miss 0.20 s, 7d with host 0.05 s; traffic 24h with the busiest host 2.6 s (its rows, on demand, unchanged behaviour)
read-pool.sh gates 0-3, IO_PRESSURE=1, STRIP_HOURS=600 PASS: idle p99 5.1 ms (n=1,093); strip of 144,779 rows in 118 s with /api/hosts p99 5.2 ms, max 31.6 ms; export x3 (67,043 rows, 15.8 MB each) aggregated p99 6.5 ms, max 7.6 ms (<= 50): the v1.3.41.1 gate 3 that failed on prod by the pinned-refresh overlap (p99 63 ms) is green with the pinned 24 h refresh on the rollup
range-7d-latency.sh (MAX_SECONDS=1) PASS: traffic 7d, stats 7d 0.24 s, traffic 7d with host 0.03 s
dashboard-latency.sh PASS
captures (/tmp/argos-captures-pending/dashboard-{before,after}-*.png) before (1.3.42.1): 7d response times and top paths "last 24 h of range"; after (rc3): 7d response times over the whole week labelled "histogram (bucket edges)", top paths over the range (61.1k vs 8.9k for the first path), 30d at 6 h buckets with the security section clamped to 7d, 24h unchanged to the eye

Found and fixed during the run: with an empty rollup (the minutes before the first fill, or a reset) 30d fell back to the row path and hit the loader deadline on the dense seed; long ranges without a rollup hour now answer the series and top hosts from the indexes with a note (commit 2c134cd). The demo also showed that read-pool.sh waits forever for retention purge done when the strip has nothing to do (the panel logs the line only when it removed or stripped rows); with raw_hours 720 on the demo the default STRIP_HOURS=6 strips nothing, hence STRIP_HOURS=600.

Deploy

Drift verdicts 7 and 8 landed at 0 (2026-09-28 11:56 and 19:58 UTC, hours 08:00 and 16:00, 2,239 and 1,685 rows, both sides equal on the volume); the gate is met. With the operator's OK: make deploy-prod from main once this branch is merged; rollback = repin 1.3.42.2. After-smokes: rollup-readers.sh, rollup-agree.sh, read-pool.sh gates 0-3, dashboard-latency.sh, panel-boot.sh, the next two drift verdicts.

Results (deployed 2026-09-29 19:26 UTC)

Verdicts 9 to 11 also landed at 0 before the deploy (01:58, 07:58, 13:58 UTC on the 29th). make deploy-prod from main (fb58d0a) at 19:25:43 UTC: sync of 62 files, image build, recreate at 19:26:59, healthy 10 s later, /healthz 200, no rollback. Boot log: read pool enabled, boot purge at 19:29:07 UTC (14,751 removed, 9,759 stripped), boot backfill of the one hour the restart cut (18:00 UTC, 24 ms), vacuum schedule initialised with last slot 2026-09-01 and next 2026-10-01 04:00 UTC (the first boot records, it does not run).

smoke result
rollup-readers.sh PASS: 7 d stats on the rollup path equal the rows (422,230 requests, five classes, tolerance 0) in 310 ms; timeseries sum equal (169 points); 1h on the rows path; traffic 7d 63 ms and 30d 113 ms cold, percentile_method: histogram, 169 / 121 buckets, no bridge fields
rollup-agree.sh PASS: lag 0 h; hour 16:00 UTC 1,611 rows and 14,794,784 bytes on both sides, four classes equal; ticker verdict 0
panel-boot.sh PASS: boot-to-listen 0.105 s, first purge 127.9 s after listen
range-7d-latency.sh PASS: traffic 7d 1.8 ms cold, logs stats 7d 57 ms cold, traffic 7d with the busiest host 21 ms
dashboard-latency.sh (MAX_SECONDS=1) PASS: health 1.1 ms cold, security overview 34 ms cold
read-pool.sh gates 0-3 PASS: idle p99 4.4 ms (n=1,085); strip of 12,352 rows in 9.9 s with /api/hosts p99 3.9 ms; export x3 of 44,573 rows (12.9 MB, 3.2-3.4 s each) p99 26.5 ms, max 70.2 ms. Gate 3 closes (31.0 ms on 42.2, 61.5 ms on 42.0)
captures /tmp/argos-captures-pending/prod/dashboard-{before,after}-{24h,7d}.png (before on 42.2 at 2026-09-28 16:26 UTC, after at 19:35 UTC on the 29th; dashboard only, one goto per page)

Found on the prod 7d capture (not on the dense demo): the p99 line of the response-time card reads up to 457,244,622 ms. Hist.Percentile returns the hour's maximum duration when the rank falls in the open bucket above 5,000 ms, and on prod that maximum is the lifetime of a long-lived connection: /api/websocket rows of 5.3 days, /socket.io/ of 80 min, camera-proxy streams of 35 s. Ten of the 169 hours in the window have more than 1 % of their requests above 5 s (all on 09-25 and 09-26), so ten p99 points are between 373,940 and 457,244,622 ms and the chart's axis scales to 60,000,000. The rollup's exact dur_p99_ms column does not help: the websocket-only group carries the same maximum. The 24h card shows the documented quantisation (50 / 100 / 250 / 500 / 1,000 / 2,500 / 5,000) as flat steps where the rows path drew a line. Reported to the operator before the tag.

Known issues carried

From v1.3.42.2: AppSec fail-open under a scanner burst at 1 CPU (stream pre-flight data point), Until on imported decisions, blocked_requests_24h mixing, dashboard/cache_test timing flake. The VACUUM schedule bound to the boot hour is fixed here. New: traffic 24h with a host filter still reads that host's rows (2.6 s cold for the busiest host on the dense demo; on demand, not pinned); hourly from the rollup in 43.x. On the rollup path top_hosts counts rows with a host link and top_paths counts access rows (api.md).