September 4, 2019·5 min read·engineeringnixstoremaintenance

Nix 2.3 changes the locking protocol for a shared store

Nix 2.3 switches from POSIX to BSD file locks. The release requires consistent store users and adds more explicit build logs and post-build hook behavior.

Two programs can use the same lock filename without participating in the same locking protocol. Nix 2.3 changes its file locking from POSIX locks to BSD locks, and the September 4 release notes consequently say not to use this release and earlier versions simultaneously on one Nix store. That is a compatibility condition for concurrent access, not merely a change to an internal helper.

The important operational unit is the shared store. Upgrading one executable does not by itself establish that every process accessing that store has adopted the new mechanism. A mixture of old and new participants violates the coordination assumption stated by the release.

I am drawn to Nix because the declared inputs make system construction more explicit. This change concerns the machinery that allows those constructions to share storage safely. Deterministic package identities still need a coherent protocol for processes that create and manage their paths.

The new helper uses flock

The path-lock implementation in 2.3 maps read, write and unlock requests to LOCK_SH, LOCK_EX and LOCK_UN. It then uses flock on the open descriptor. A non-blocking attempt adds LOCK_NB and reports contention when the call returns EWOULDBLOCK.

This makes the release-note description concrete. The changed mechanism is the operating-system locking operation used on a descriptor. The path of a lock file is not a sufficient description of the protocol, because opening the same file does not establish that two callers requested compatible forms of locking.

The helper also distinguishes waiting from a non-blocking attempt. The surrounding path-lock code can first try to acquire an exclusive lock, then wait if the caller permits it. That is part of coordinating access to a path already being handled elsewhere.

The release does not say existing store contents must all be rebuilt because the locking API changes. Its explicit restriction is simultaneous access by different release generations. That scope matters because it identifies the condition an upgrade needs to avoid without inventing a package-format migration.

Lock files have their own lifetime problem

The implementation acquires multiple path locks in sorted order. Its comment explains that a consistent order prevents deadlocks. Two operations that need several locks should not each retain one while waiting for another held by the other operation.

After acquiring a lock, the code also checks whether the file has become stale. When deleting a lock file, the helper unlinks the pathname and writes a marker through the existing descriptor. A waiter that later acquires the old file can inspect its size and retry when the marker indicates deletion.

That detail addresses a different race from the old-versus-new locking protocol. A descriptor can still refer to a file after its pathname has been unlinked. Meanwhile another process can create a new file at that pathname. Without recognizing the stale object, a waiter could believe its old lock protected the newly created pathname's activity.

The code's comments make clear why a lock's identity and lifetime need attention. Correct mutual exclusion involves more than selecting a familiar filename. Processes need the same locking operation and a way to recognize that the object they locked still belongs to the current coordination scheme.

Build logs become explicit output

Nix 2.3 also adds --print-build-logs, abbreviated -L, to the nix command. The release notes say it prints build-log output to standard error instead of showing only the last line in the progress bar. Package names prefix the lines so concurrent builds can be distinguished.

The flag can be used with the ordinary build interface.

nix build -L -f '<nixpkgs>' hello

This example selects hello from the Nixpkgs expression found through the search path. Its purpose is to show the log flag on a build command. It does not pin the selected package set, so a repeated invocation depends on the caller's search-path configuration.

The release also runs builds in a pseudo-terminal and sets TERM to xterm-256color. That lets programs such as GCC, Clang and CMake produce colored output when they recognize a terminal. Printing the full log and allowing terminal-aware formatting are related usability changes with different mechanisms.

I find the package-name prefixes particularly useful as a design choice. A stream from several jobs needs enough origin information to associate each message with its build. Color can improve presentation, but the prefix is what supplies that attribution in the documented output.

A post-build hook extends the execution contract

The new post-build-hook setting invokes a program after a successful build. The configuration manual for 2.3 adds details that matter when using it for follow-up work.

The hook runs synchronously and blocks other builds from progressing while it executes. If it fails, the completed build remains successful, but no further builds execute. A hook's failure therefore has a different result from the builder failing to produce its output.

The manual also says substituted paths do not trigger the hook. Downloading an existing result is not treated as a new build for this purpose. A process that expects notification for every path becoming available cannot assume this setting covers both routes.

The privilege context is explicit. With nix-daemon, the daemon executes the hook as root. Without the daemon, it runs as the user invoking nix-build. The setting is restricted to global configuration or trusted users on the command line, which corresponds to its ability to select a program for that execution context.

Offline behavior groups several choices

The --no-net flag disables substituters and treats previously downloaded files as current by setting tarball-ttl to infinity. It also disables download retries and minimizes the connection timeout. The release enables the flag automatically when no non-loopback interfaces are configured.

This is a convenience setting for the client's network-related behavior. It should be understood through those stated effects rather than as a substitute for a builder's sandbox network policy. A disabled substituter and an isolated build namespace concern different stages of obtaining a result.

The release also defaults max-jobs to one. The configuration manual defines that as the number of jobs Nix tries to build in parallel, with auto as an explicit choice based on CPU count. That controls build scheduling, while the locking change controls coordination around shared paths. For an installation moving to 2.3, the shared-store requirement remains the critical compatibility fact. All concurrent participants need to use a release compatible with the new lock protocol.