Skip to content

The Viewer's Job History reads each server's newest runs through an index (#4477) - #4493

Merged
erikdarlingdata merged 13 commits into
devfrom
fix/4477-job-history-per-server-topn
Sep 27, 2026
Merged

erikdarlingdata merged 13 commits into
devfrom
fix/4477-job-history-per-server-topn

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Refs #4477.

Why

The Viewer's Job History (ViewerDataService.BuildJobHistorySql) shows the newest 2,000 job runs across the fleet (or one server) in the chosen window.

  • How today's read does it: it reads EVERY job_history row collected in the window plus a day of clock-skew allowance, applies each server's UTC offset, sorts them all, and keeps 2,000.
  • Measured on a production store: that meant scanning 4.68 M rows in the 2-day floor. The store then held duplicate rows from a separate collector bug, since fixed; after that cleanup the same scan reads about 1.3 M rows, still for 2,000 shown.
  • job_stats re-scans the same window for the long-running check.

What changes

  • Each server's newest rows come from V150's idx_job_history_server_run (server_id, run_datetime DESC, instance_id DESC) as a per-server LATERAL … ORDER BY run_datetime DESC, instance_id DESC LIMIT n, then merge with today's exact order and tie-break (run_datetime_utc DESC, instance_id DESC).
  • Why this is exact: a server's UTC offset is constant across its rows, so "newest by run_datetime − offset" per server is "newest by run_datetime", and the merged top-n is the same set.
  • The per-server read is driven by the servers that HAVE rows in the window, not the servers registry. A server whose registry entry was removed still shows its history under its collected name, as the old LEFT JOIN servers did.
  • job_stats (the average successful duration and last success, for the long-running flag) is computed only for the (server, job) pairs in the result.

Measurements

Measured on a local PostgreSQL 18 + TimescaleDB 2.30 store planted like a busy fleet: 43 servers × 72 hours, 2,015,496 job_history rows, the two oldest daily chunks compressed, after VACUUM ANALYZE. The parameters are the Viewer's defaults: the last 24 hours, all servers, the 2,000-row cap. EXPLAIN (ANALYZE, BUFFERS), run warm in both orders, and once after a container restart. The restart empties shared_buffers but not the OS page cache, so compare the "read" counts rather than the cold milliseconds.

warm after restart
before (fleet-wide scan) 573 ms, 63,687 shared hit 575 ms, 35,407 hit + 28,280 read
this PR (per-server top-N) 300 ms, 163,424 shared hit 381 ms, 157,931 hit + 5,482 read

The per-server read touches more pages in total: each of the 43 servers' index descents fetches up to the row cap before the fleet-wide merge. Nearly all of those pages are already in memory. It reads about 5× fewer blocks from outside shared_buffers, and it's about 47% faster warm. The same measurement on a production-sized store is planned after the next install, as a confirmation.

Test plan

  • JobHistoryPerServerTopNLiveTests (live TimescaleDB rig):
    • NewSql_MatchesOldSql_ExactlyAcrossOffsetsTiesAndTheCapBoundary: 5 servers (offsets +60 / −300 / 0 / no server_properties row / deregistered), 3 days of runs with retries and failures, a cross-server tie on run_datetime_utc, a same-server tie on run_datetime, and a cap that cuts the plant. It matches dev's SQL row for row, fleet-wide and scoped, in both orders.
    • DeregisteredServersRow_StillSurfaces_LikeTheOldLeftJoin: a server removed from servers still returns its rows under its collected name.
    • ScopedRead_UsesTheIndex_WithNoExtraSort: the scoped read uses idx_job_history_server_run with no sort on run_datetime. The same assertion against dev's SQL on the same rig FAILS (no LATERAL).
  • Mutations:
    • the instance_id DESC tie-break dropped from the fleet merge → the equivalence pin RED (a wrong server at row 19);
    • the registry-driven server set restored → the deregistered-server pin RED;
    • both reverted.
  • Also green: JobHistoryGroupedStatsMatchWindowFunctionLiveTests, CollectionLogWatermarkAndJobHistoryIndexesRungTests, JobHistoryCapLabelTests, JobHistoryIdentityEpochTests and DocCommentHygieneTests. Darling.Tests builds with 0 warnings.

CHANGELOG

SECTION: Changed
ENTRY:

…-history index (#4469, #4477)

The startup watermark read looks up each collector's newest collection_log row
through a new index instead of scanning the newest chunks: idx_collection_log_watermark
on collection_log (server_id, collector_name, collection_time DESC), plus a per-collector
unnest()/LATERAL LIMIT 1 lookup replacing the GROUP BY. Also adds
idx_job_history_server_run on job_history (server_id, run_datetime DESC, instance_id DESC)
for the Viewer's Job History read.

Refs #4469
Refs #4477
…rm, the top-rung pins (#4469)

StorageVersion.SchemaVersion moves to 150. The Viewer schema probe gains a V150 sentinel (both
idx_collection_log_watermark and idx_job_history_server_run present) and its newest-first arm above
V149's, so a fully-migrated store maps to exactly 150 instead of falling through to V149 and showing a
spurious upgrade banner. QueryStoreLivenessHotTouchLiveTests' 'I am the top rung' facts move to a new
CollectionLogWatermarkAndJobHistoryIndexesRungTests class; V149's and V148's own facts are rewritten to
assert what stays true forever (registered, ladder-dense, gated below the current top's arm) rather than
'is exactly the top'.

Refs #4469, #4477
…ory-indexes' into fix/4477-job-history-per-server-topn
…rvers registry (#4477)

Correctness fix on top of the per-server LATERAL: the driving set for the
per-server top-N is now every server_id with a job_history row inside the
window, not the servers registry. A server removed from the registry after
its history was collected still surfaces (raw collected name, matching the
old LEFT JOIN's COALESCE fallback).

Adds live equivalence, plan-shape and mutation pins.
…equivalence pin

The two rows meant to land on the same run_datetime_utc instant did not:
their raw run_datetime values were only 6 minutes apart while their
server offsets differ by 360 minutes, so they never actually tied, and
their instance_id values were assigned in the wrong order for the
instance_id DESC tie-break the assertion expects. Widened the raw gap
to 360 minutes and swapped the instance_id values so the fixture
produces the tie it claims to.
…-pass key-then-row read (#4477)

job_stats now scans job_history once for the whole fleet, restricted to the
(server_id, job_id) pairs base already kept via a semi-join, instead of a
per-server LATERAL that re-scanned each server's whole success-row window.

top_by_server now runs a key-only, index-only pass first (run_datetime,
instance_id -- the two columns idx_job_history_server_run carries) to find
the fleet-wide n-th cutoff, then a second pass that fetches full rows only
at or past that cutoff. Same tie-break, same LIMIT, same result set.
…as a two-pass key-then-row read (#4477)"

This reverts commit cd450b9.
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 27, 2026 20:51
@erikdarlingdata
erikdarlingdata merged commit d4ccb2e into dev Sep 27, 2026
15 of 16 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/4477-job-history-per-server-topn branch September 27, 2026 20:51
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