Store::queryMissing¶
Source: src/libstore/misc.cc:102-315
Purpose¶
Given a set of top-level derived paths (derivations + output names, or opaque store paths), determines what will need to be built vs substituted vs is unknown. Called by:
nix build --dry-runviaprintMissing(store, pathsToBuild)atsrc/libcmd/installables.cc:628The
Workerat the start of every build to pre-populate substitute info (src/libstore/build/worker.cc:339-340):/* Call queryMissing() to efficiently query substitutes. */ store.queryMissing(topPaths);
Return type¶
struct MissingPaths {
StorePathSet willBuild; // derivations that must be built locally
StorePathSet willSubstitute; // output paths that can be fetched from substituters
StorePathSet unknown; // paths no one has
uint64_t downloadSize; // total bytes to download for willSubstitute
uint64_t narSize; // total uncompressed NAR size
};
Algorithm¶
Walks the transitive dependency closure of the given targets using a ThreadPool.
DerivedPath::Built handling (a derivation with specific wanted outputs)¶
doPath(Built{drvPath, outputs}):
if already seen → return
if !isValidPath(drvPath):
→ mark drvPath as "unknown"
→ return
// Check which outputs are known and which are valid
for each output in queryPartialDerivationOutputMap(drvPath):
if outputPath is null → knownOutputPaths = false
if output is wanted AND !isValidPath(outputPath) → add to invalid set
if knownOutputPaths AND invalid is empty → return (all good)
// Read the derivation to check allowSubstitutes
drv = derivationFromPath(drvPath)
drvOptions = derivationOptionsFromStructuredAttrs(drv)
if CA derivation with unknown outputs AND useSubstitutes:
→ check each output's Realisation against every substituter
→ if any output has no realisation → knownOutputPaths = false
if knownOutputPaths AND useSubstitutes AND substitutesAllowed:
→ for each invalid output, query ALL substituters via querySubstitutablePathInfos
→ if substituter has it: account downloadSize/narSize, mark willSubstitute
→ if no substituter has it: mark willBuild, recursively enqueue all inputDrvs
else:
→ mark willBuild, recursively enqueue all inputDrvs
DerivedPath::Opaque handling (a concrete store path)¶
doPath(Opaque{path}):
if already seen → return
if isValidPath(path) → return
query all substituters via querySubstitutablePathInfos
if any has it: → mark willSubstitute, account sizes, enqueue references
else: → mark unknown
Recursive expansion¶
When a derivation is marked willBuild, all its inputDrvs are enqueued (line 146-148):
for (const auto & [inputDrv, inputNode] : drv.inputDrvs.map) {
enqueueDerivedPaths(makeConstantStorePathRef(inputDrv), inputNode);
}
querySubstitutablePathInfos (called for each invalid output)¶
In src/libstore/store-api.cc:444-493:
void Store::querySubstitutablePathInfos(const StorePathCAMap & paths, SubstitutablePathInfos & infos) {
if (!useSubstitutes) return;
for (auto & path : paths) {
for (auto & sub : getDefaultSubstituters()) {
auto info = sub->queryPathInfo(subPath);
// record downloadSize, narSize, references
}
}
}
Iterates every configured substituter per path, calling queryPathInfo on each.
Daemon protocol path¶
Client calls RemoteStore::queryMissing (src/libstore/remote-store.cc:751-772):
Sends
WorkerProto::Op::QueryMissingto the daemonDaemon handler (
src/libstore/daemon.cc:946-956):case WorkerProto::Op::QueryMissing: { auto targets = WorkerProto::Serialise<DerivedPaths>::read(*store, rconn); logger->startWork(); auto missing = store->queryMissing(targets); // calls LocalStore::queryMissing logger->stopWork(); WorkerProto::write(*store, wconn, missing.willBuild); WorkerProto::write(*store, wconn, missing.willSubstitute); WorkerProto::write(*store, wconn, missing.unknown); conn.to << missing.downloadSize << missing.narSize; break; }
The daemon runs
Store::queryMissing(the default implementation frommisc.cc) — there is noLocalStoreoverride.If the daemon is too old (protocol < 1.19),
RemoteStore::queryMissingfalls back to the default implementation which runs locally over the connection.
Why it can take 30+ seconds¶
Factor |
Detail |
|---|---|
Walks the full dependency closure |
For a derivation like |
N+1 substituter queries per output |
Every invalid output path triggers |
|
Reads the |
|
Full deserialization of the derivation to check structured attrs and |
Daemon is synchronous |
The daemon handler holds the connection for the entire duration — one |
No caching across invocations |
Every |
No substituter-level batching |
|
ThreadPool parallelism is limited |
The thread pool only parallelizes checking multiple invalid outputs of the same derivation against substituters ( |
A realised output, and a sibling that is not¶
queryMissing reads every output of a derivation to decide
knownOutputPaths, and it ignores the outputs that the caller wants. The
loop is at misc.cc:217-225:
for (auto & [outputName, pathOpt] : queryPartialDerivationOutputMap(drvPath)) {
if (!pathOpt) {
knownOutputPaths = false;
break;
}
if (bfd.outputs.contains(outputName) && !isValidPath(*pathOpt))
invalid.insert(*pathOpt);
}
The break runs for an output that the caller did not ask for. The next line
reads bfd.outputs, and the substituter loop at misc.cc:250 reads
bfd.outputs as well. So two of the three tests honour the wanted outputs and
the first one does not. The comment above the loop states the intent that the
code misses: "CA derivations for which we have a trust mapping for all wanted
outputs".
A content-addressed derivation reaches this state after one ordinary
build. DerivationGoal holds one wantedOutput, and it registers a
realisation for that output alone, at derivation-goal.cc:228-236. The build
of the resolved derivation makes every output, and Nix maps one of them back
to the original derivation.
The measurement¶
ca/content-addressed.nix gives rootCA the outputs out, dev and foo.
This runs against Nix 2.34.8, with no daemon and no pynixd:
$ nix build -f content-addressed.nix rootCA^out
$ sqlite3 db.sqlite 'select drvPath, outputName from Realisations'
sha256:e76acd40...|out # the original rootCA.drv, one output
sha256:39850e18...|dev
sha256:39850e18...|foo # the resolved derivation, all three
sha256:39850e18...|out
$ nix build -f content-addressed.nix rootCA^out --dry-run
this derivation will be built:
.../gfzi77x6b81brl6b3lmpgmi2vrn6wp1f-rootCA.drv
The output out is realised, and its path is valid. Nix still names the
derivation. After nix build -f content-addressed.nix 'rootCA^*' registers
dev and foo as well, the same --dry-run command names nothing. So the
siblings are the cause.
What pynixd does¶
QueryMissingPlanGoal._classify_derivation reads the wanted outputs alone,
through selected_output_paths. pynixd also registers a realisation for every
output that the build made, in _register_realisations, and not for the
wanted one alone. Each half makes the answer of pynixd complete where the
answer of Nix is not, so pynixd leaves rootCA.drv out of willBuild and
nix-daemon puts it in.
pynixd keeps its answer. Issue #26 holds the difference, and issue #23 holds the convention for a defect of Nix that pynixd copies. This is not one of those: pynixd does not copy this defect.
Lillecarl/nix#312 reports the defect, and it gives the correction: read
bfd.outputs in the first test as well, which makes the three tests of the
function agree.