Skip to content

Merge master into nix-next#1717

Open
github-actions[bot] wants to merge 254 commits into
nix-nextfrom
master
Open

Merge master into nix-next#1717
github-actions[bot] wants to merge 254 commits into
nix-nextfrom
master

Conversation

@github-actions

@github-actions github-actions Bot commented May 6, 2026

Copy link
Copy Markdown

Automated PR to keep nix-next in sync with master.

Ericson2314 and others added 30 commits May 7, 2026 17:31
hydra-tests: Extract `ProcessGroup` and shared test helpers
Apply [semantic line breaks](https://sembr.org/) (one sentence per line)
across all markdown files in the manual and repository. This makes diffs
easier to review since changing one sentence no longer reflows an entire
paragraph.

Also add `CONTRIBUTING.md` documenting the convention, and clean up
various pandoc artifacts along the way: escaped quotes (`\'`), escaped
underscores (`\_`), broken multi-line list items, and non-breaking
spaces.
Reformat markdown to use semantic line breaks
Forgot to do this when bumping harmonia. The commented stuff will return
when we merge `nix-next`.
`buildproducts.path` can contain a sub-path below a store output
(e.g. `/nix/store/…-nix-manual-2.31.4/share/doc/nix/manual`), but
`OwnedBuildProduct::parse_paths` was calling `StoreDir::parse` which
only accepts bare store paths. Any path with a `/` after the store
path name failed with `invalid store path / symbol`, breaking the
cached-build fast path for those builds.

Move `RelativeStorePath` from the queue-runner into a new
`store-path-utils` crate so the DB crate can use it directly. Change
`OwnedBuildProduct`'s default type parameter from `StorePath` to
`RelativeStorePath` and have `parse_path()` call
`RelativeStorePath::from_path`, which correctly splits the full path
into a `StorePath` base and a relative suffix.
When `create_step` encounters an existing step (`is_new=false`), it now
re-checks whether the step's outputs have appeared in the local nix
store since the step was first created. Without this check, builds
whose outputs became available between poll cycles (e.g. via
concurrent builds, substitution, or external upload) would get stuck
in an infinite re-load loop: the DB says `finished=0`, the step already
exists in memory so `create_build` inserts the build but never routes
it to `handle_cached_build`.

The fix adds two checks before returning `Valid(step)`:
1. If the step is already marked finished, return `None` immediately.
2. Query missing outputs from the local store; if all are present,
   mark the step finished, insert into `finished_drvs`, and return
   `None` so `create_build` routes to `handle_cached_build`.
Nix writes `Compression: bzip2` in narinfo files (see
`src/libstore/nar-info.cc`), but hydra's parser only accepted `bz2`
(the file extension, not the compression name). This caused errors
like `invalid value for Compression: bzip2` when reading old narinfo
entries from `cache.nixos.org`, making the queue runner treat those
store paths as missing and rebuild them.

Fix `FromStr` to accept `bzip2`, and fix `as_str` to write `bzip2`
(matching nix). The file extension method `ext()` correctly uses
`.nar.bz2` and is unchanged.

Examples of affected narinfo files:

- https://cache.nixos.org/2crhfrb8pqy61d32jaajpw33jjmny6xz.narinfo
- https://cache.nixos.org/9fggf2bx5p29da89jrbmdh2fcd68m1mx.narinfo
- https://cache.nixos.org/0xs59grdbgz7z2ci2ff4yfwb6jha1qaa.narinfo
The architecture doc was sitting outside `src/`, presumably because it
was mostly notes and not yet cleaned up enough to be part of the
user-facing docs. We will do that clean up next, but before we do that,
move the file and expose it, so the subsequent diff is readable and not
confused by the move.
- Organize components by deployment topology: master machine vs builder
  machines vs destination store, reflecting which components share a Nix
  store and which don't
- Add `hydra-builder`, `hydra-notify`, `hydra-eval-jobset` — components
  that weren't documented before
- Update `hydra-queue-runner` language from C++ to Rust
- Add source layout and build system overview
- Add database schema section with pointers to the SQL and migration docs
- Update FAQ: both Nix and Hydra now have experimental dynamic derivations
- Fix typos (`hydra-build-producs`, `obsolute`)
Add updated architecture section to the manual
Pick up latest changes from nix-community/harmonia.
Both `hydra-builder` and `hydra-queue-runner` had near-identical
`build.rs` files that compiled the same `.proto` file and generated
the same version constant. This consolidates that into a single
`hydra-proto` crate with feature flags:

- `client` — generates tonic client stubs (used by `hydra-builder`)

- `server` — generates tonic server traits (used by `hydra-queue-runner`)

- `db` — `From` impls bridging proto enums to `db::models` types

Proto ↔ native type conversions are also consolidated here:

- `ProtoStorePath` moved from `shared::proto`, now depends on
  `harmonia-store-core` directly rather than `nix-utils`

- `Pressure`/`PressureState` — both components now use the generated
  proto types directly, eliminating duplicate structs and field-by-field
  conversion boilerplate

- `PresignedUploadOpts`, `ConfigUpdate` — queue-runner now uses the
  proto types directly instead of maintaining trivial local wrappers

- `BuildResultState` — restructured as a nested enum wrapping
  `hydra_proto::BuildResultState` with extra `Aborted`/`Cancelled`
  variants for queue-runner-internal states

- `RelativeStorePath` — new proto message type making the store path
  vs sub-path split explicit on the wire; `From`/`TryFrom` conversions
  in `hydra-proto` bridge to `store_path_utils::RelativeStorePath`

- `BuildProduct.path` changed from opaque `string` to structured
  `RelativeStorePath` message

Also:

- Groups all local workspace crates under `[workspace.dependencies]`
  with `workspace = true` references instead of relative paths

- Updates the architecture doc with descriptions of each shared crate
Extract hydra-proto crate for shared `gRPC` code generation
Introduces `store.proto` (`nix.store.v1` package) containing types that
are generic to any Nix store, not specific to Hydra:

- `StorePath`, `StorePaths`, `RelativeStorePath`

- `ValidPathInfo` — store path with NAR hash/size, references, deriver, CA

- `NarInfo` — wraps `ValidPathInfo` with cache-specific fields
  (url, compression, file hash/size)

`streaming.proto` (`runner.v1`) imports these and uses them for
`BuildMessage.drv`, `BuildProduct.path`, `PresignedUploadComplete`, etc.

`PresignedUploadComplete` is restructured to contain a `NarInfo` message
instead of flattening all fields, making the layering explicit.
Split `.proto` into generic Nix store types and Hydra-specific types
The `binary-cache` crate had its own `NarInfo` struct that duplicated
what harmonia now provides as `harmonia_store_nar_info::NarInfo`. This
replaces it with the upstream type, which also brings in
`harmonia-store-path-info` for `ValidPathInfo`/`UnkeyedValidPathInfo`.

The local struct was flat; the harmonia type is nested:
`NarInfo { path, info: UnkeyedNarInfo { info: UnkeyedValidPathInfo, url, compression, ... } }`.
All construction and field access sites are updated accordingly.

`From<harmonia_store_nar_info::NarInfo>` and `TryFrom<proto::NarInfo>`
conversions are added in the proto crate so both components can convert
between native and wire representations without manual field-by-field
marshalling. Round-trip unit tests verify the conversion is lossless.

Also adds `store_dir` field to proto `ValidPathInfo` to match the
harmonia type. The queue-runner now asserts the store dir matches
rather than silently overriding it.
Replace local `NarInfo` with `harmonia-store-nar-info`
Consumers now import `StorePath`, `StoreDir`, `OutputName`, etc.
directly from `harmonia-store-core` rather than through `nix-utils`
re-exports. This makes the actual dependency on harmonia explicit
at each use site.

`nix-utils` retains its own local functionality (`parse_store_path`,
`BaseStore`, `LocalStore`, `realise_drv`, etc.) but no longer acts as
a forwarding layer for harmonia types.
Remove harmonia re-exports from `nix-utils`
Replace stringly-typed fields in `store.proto` with proper message types:

- `Hash` — algorithm enum + raw digest bytes
- `Signature` — key name + signature string
- `ContentAddress` — method enum + hash

`UnkeyedValidPathInfo.nar_hash` is now `Hash` (not string),
`signatures` is `repeated Signature` (not `repeated string`),
`ca` is `optional ContentAddress` (not `optional string`).
`UnkeyedNarInfo.file_hash`/`file_size` renamed to
`download_hash`/`download_size` with proper `Hash` type.

All `From`/`TryFrom` impls use `&` for harmonia→proto direction
(no need to consume the source) and by-value for proto→harmonia
(proto types are consumed). Round-trip tests cover every layer.
Use structured proto types for `Hash`, `Signature`, and `ContentAddress`
Move each test from the monolithic `nixos-tests.nix` into its own file
under `nixos-tests/`, with shared VM configuration extracted to
`common.nix`. No functional changes to existing tests.
Split NixOS tests into separate files
Tests the full Hydra pipeline against a real S3-compatible store
(garage): evaluate a jobset via the web API, build it, and verify
that outputs are uploaded to the S3 bucket.

Currently checks NAR listings (`.ls` files): the test builds a
derivation that produces regular files, an executable, a symlink,
and a subdirectory, then does an exact JSON comparison of the
uploaded listing against the expected structure.

Uses `services.garage` NixOS module on a dedicated `s3` VM, and
`POST /api/push` to trigger evaluation explicitly rather than
relying on `checkinterval` polling.
Replace the `list_nar_deep` FFI call (which used Nix's C++
`getFSAccessor` and `listNarDeep`) with harmonia's pure-Rust
`parse_nar_listing`. The new `nar_listing()` helper streams NAR bytes
from `nar_from_path` through `harmonia-file-nar`'s parser, producing a
`FileTree<NarFileInfo>` that serializes to the same JSON format.

This removes the last use of `list_nar_deep` from `binary-cache`, so
the FFI function and its `BaseStore` trait method are deleted along
with the C++ implementation and the `list_nar` example.

Also adapts `parse_drv` to take `&[u8]` instead of `&str`, matching
upstream harmonia's `parse_derivation_aterm` signature change.

The previous commit's NixOS test ensures we get this right.
Use harmonia for NAR listings instead of Nix C++ FFI
Mic92 and others added 30 commits July 14, 2026 18:50
The presence cache used the default rollback journal with synchronous=FULL,
so every write fsynced, which on ZFS is a ZIL commit and dominated ingestion
time. It is only a cache, so open it in WAL mode with synchronous=NORMAL and
trade the per-write sync for durability.
Ideally we could switch soon to a store that queue-runner owns itself that
is not shared with the system, as we are now bottlenecked again on daemon
interactions. But that is a bigger change.

For now, restore some of the performance lost when switching store reads
from SQLite to the nix-daemon: create_step opens a single daemon connection
and uses it for all of a step's output-validity checks rather than
handshaking per output. The connection lives on the stack for that step and
closes on return, so nothing is held open between ingestion passes.
Build with swc:

`swc compile ./ansi_up.ts --config-json '{"module": {"type": "umd"}, "minify": true, "jsc": {"parser": {"syntax": "typescript"}}}' --out-file ansi_up.js`
hydra-builder pins each build's inputs with AddTempRoot on a per-build
daemon connection. Nothing exercised that, so a regression to the pinning
would only show up as the production-only "input missing" flake from
#1806.

Add a test that runs nix-store --gc on the builder store while a build is
inside its builder script and asserts the input survives and the build
succeeds. The build handshakes with the test through two sentinel files
(sandbox is off), so the GC point is deterministic. The input is referenced
only via args so it stays out of /proc/*/environ, which the GC scans for
runtime roots and would otherwise mask a broken pin.
request_build builds the derivation on its own daemon connection. The
daemon roots a build's outputs via the connection that ran it, so
dropping that connection releases them. Outputs were only re-pinned onto
the persistent roots connection after request_build returned, so they
were rooted by nothing in between.

A GC in that window deletes the outputs, and the following
query_path_info and upload fail with "path is not valid". This does not
depend on the daemon honoring temp roots added mid-collection: the
outputs are simply unrooted for the duration of the gap.

Pin the outputs onto the roots connection before dropping the build
connection.
Services log to journald, not a terminal, so the ANSI escape codes were noise
and the coloring was wasted work.
The dispatcher calls get_required_features per runnable step each cycle, so
cloning the feature Vec churned the allocator. Expose the StepDrvInfo Arc and
let callers borrow the slice.
create_build and create_step threaded a growing list of shared Arcs. Gather
the pass-wide state into one InjectCtx and split the GC-abort, dependency
injection, missing-output and substitution logic into helpers. No behaviour
change.
create_step opened a daemon connection per step for output-validity reads,
and the recursion fans out, so an ingestion burst opened thousands. The
daemon forks a worker per connection on a single accept thread, hammering one
nix-daemon.

Carry a per-pass DaemonConnPool in the ingestion context that caps live
connections and reuses them, so a pass costs a fixed number of handshakes
instead of one per step. The pool drops when the pass returns.
A connection left desynced by a failed query is dropped rather than returned.
Profiling showed propagate_priorities at ~37% of CPU in the queue monitor
loop, ~25% of all samples in StorePath hashing.

The walk dedups visited steps with HashSet<Arc<Step>>, hashing each Step by
its drv_path (20-byte hash plus name string) on every dependency edge. The
Steps registry only yields one Arc<Step> per derivation, so pointer identity
matches content identity. Key the set by Arc pointer instead, reducing each
membership check to a usize hash.

ByPtr holds the Arc rather than a raw pointer to pin the address for the
walk; a freed step's address could otherwise be reused by a concurrently
created step and cause a false "already visited" hit.
add_root() created a gcroot with a bare symlink() and was never called. A
bare symlink can land after a running collector has scanned the gcroots
directory, so the rooted output races that collection and can be deleted.

Route rooting through the daemon's addPermRoot, which pins the path with a
temp root (synchronising with a running GC over its socket), writes the
symlink atomically and registers the indirect root, under the GC lock.

Call it on build success, but only without presigned uploads: with them
the builder pushes NARs straight to the remote cache and downstream
builders fetch inputs from there, so outputs need not live locally.
Ordering by step numer is local to a build closure and jumps around a
lot.
complete_build ran the finalize in a detached task and acked the builder
immediately. If that task failed, the error was only logged: remove_job
never ran, so the machine slot stayed allocated until the next restart.
On a long-running queue runner these dead slots accumulate on idle
builders.

Run the finalize inline and return a non-OK status on failure. The
builder retries complete_build, so a failed finalize reschedules instead
of leaking the slot.
acquire() used sqlx's 30s default and surfaced a momentary pool spike as a
hard error, failing callers on the critical path (build finalization).
Shorten the per-attempt timeout and retry a bounded number of times so a
spike is waited out; only sustained exhaustion fails. Other errors return
immediately.
create_step recurses with per-level buffered concurrency, so a deep build
graph fans out into many more in-flight prefetches than one level, each
acquiring a DB connection. A large graph drains the pool and starves build
finalization, which then times out on acquire.

Cap prefetches with a semaphore sized to a quarter of the pool. The permit
is released before create_step recurses, so it cannot deadlock.
The queue runner pins non-presigned build outputs with a GC root until a
dependent build consumes them. This test simulates the output going
missing, so nix-store --delete hits that live root and fails. Delete with
--ignore-liveness, matching the test's intent of forcefully removing it.
The evaluator updates builds rows in a large transaction while the queue
runner marks builds finished, so Postgres occasionally aborts one side
of the lock cycle. The evaluator already retries; the queue runner did
not, so a completion that lost the deadlock was rolled back and the
build stayed unfinished.

Add retry_serialization_failures in the db crate (retries on 40001/40P01)
and wrap the queue runner's build-finishing transactions in it.
The evaluator held Builds row locks for the entire evaluation (observed
~13 minutes on nixpkgs) because it repointed builds at their new
derivation inline in the large evaluation transaction. That lock window
deadlocked the queue runner finishing builds of the same jobset, and a
contended jobset could exhaust the retry budget and lose the evaluation.

Defer the repoint: record which builds need it and apply the updates
after the evaluation transaction commits, in a short id-ordered
transaction with its own deadlock retry. The evaluation transaction no
longer writes existing Builds rows.
succeed_step and fail_step update several build rows per transaction but
iterated them in arbitrary order (Vec in weak-ref order, HashSet). Two
concurrent completions with overlapping build sets could lock the same
rows in opposite orders and deadlock.

Sort both by build id so completion transactions lock rows in a
consistent order.
get_jobset_build_steps wrapped stopTime in to_timestamp() before
comparing against the time window. The planner cannot use
indexbuildstepsonstoptime as a range bound on a transformed value, so it
scans the whole index. On nixpkgs jobsets this took over a minute and
blocked queue-runner startup.

Compare stopTime directly against an integer epoch so the index becomes
a range scan.

Before:
  Parallel Index Scan ... indexbuildstepsonstoptime
    Filter: to_timestamp(stoptime) > now() - '240:00:00'
    Rows Removed by Filter: 57685544
  Execution Time: 74536 ms

After:
  Parallel Index Scan ... indexbuildstepsonstoptime
    Index Cond: stoptime > EXTRACT(epoch FROM now())::bigint - 864000
  Execution Time: 2204 ms

Also drop the accidental "* 10" on the scheduling window: those extra 9
days are pruned back to SCHEDULING_WINDOW anyway.
"starting dispatch", "create_step: ... already finished ... skipping",
and "step ... is now runnable" fire for every dispatch loop and every
step scanned, flooding the journal on large jobsets. They are routine
scan progress, not notable events, so log them at debug.
Pulls in the harmonia-store-remote change that logs the daemon handshake
at debug instead of info, which the queue runner triggered on every RPC.
The daemon's error_msg appends the last N build-log lines and a "For full
logs" footer. Hydra already stores and links the full log, so this only
bloated the errormsg shown in the web UI.

Build the errormsg from the FailureStatus instead.
This makes nixos-unstable with the many big test closures progress very
slowly.
The client options sent "max-log-size", which is not a Nix setting name
(the setting is max-build-log-size, alias build-max-log-size). The daemon
ignores unknown settings, so the log size limit was never enforced.
latestbuilds/nrbuilds warn "Use of uninitialized value" whenever the
optional project/jobset/job/system parameters are omitted. Check
definedness before comparing against the empty string.
Cover meta.maxSilent, meta.timeout, maxLogSize and maxOutputSize through
the full evaluator -> queue-runner -> builder -> daemon path, and verify
that a job's meta.maxSilent overrides the queue-runner's maxSilentTime
default instead of being capped by it.
hydra-eval-jobset baked 7200/36000 into every build without
meta.maxSilent/meta.timeout, so the queue-runner's maxSilentTime and
buildTimeout settings never applied to top-level jobs. Store NULL
instead and fall back to the queue-runner's configured defaults.
Hydra.nixos.org: fixes from nixos build infra
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.

10 participants