nix-store --delete
scans full .links/ directory on every invocation #758
Labels
No labels
Affects/CppNix
Affects/Nightly
Affects/Only nightly
Affects/Stable
Area/build-packaging
Area/cli
Area/evaluator
Area/fetching
Area/flakes
Area/language
Area/lix ci
Area/nix-eval-jobs
Area/profiles
Area/protocol
Area/releng
Area/remote-builds
Area/repl
Area/repl/debugger
Area/store
bug
Context
contributors
Context
drive-by
Context
maintainers
Context
RFD
crash 💥
Cross Compilation
devx
docs
Downstream Dependents
E/easy
E/hard
E/help wanted
E/reproducible
E/requires rearchitecture
imported
Language/Bash
Language/C++
Language/NixLang
Language/Python
Language/Rust
Needs Langver
OS/Linux
OS/macOS
performance
regression
release-blocker
stability
Status
blocked
Status
invalid
Status
postponed
Status
wontfix
testing
testing/flakey
Topic/Large Scale Installations
ux
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lix-project/lix#758
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Describe the bug
When running
nix-store --delete
even on a single store path, it will always scan the full.links
directory for any hardlinks it can remove (because only one storepath remains that references the hardlink)./* Unlink all files in /nix/store/.links that have a link count of 1,
which indicates that there are no other links and so they can be
safely deleted. FIXME: race condition with optimisePath(): we
might see a link count of 1 just before optimisePath() increases
the link count. */
if (options.action == GCOptions::gcDeleteDead || deleteSpecific) {
printInfo("deleting unused links...");
AutoCloseDir dir(opendir(linksDir.c_str()));
if (!dir) throw SysError("opening directory '%1%'", linksDir);
int64_t actualSize = 0, unsharedSize = 0;
struct dirent * dirent;
while (errno = 0, dirent = readdir(dir.get())) {
checkInterrupt();
std::string name = dirent->d_name;
if (name == "." || name == "..") continue;
Path path = linksDir + "/" + name;
auto st = lstat(path);
if (st.st_nlink != 1) {
actualSize += st.st_size;
unsharedSize += (st.st_nlink - 1) * st.st_size;
continue;
}
printMsg(lvlTalkative, "deleting unused link '%1%'", path);
if (unlink(path.c_str()) == -1)
throw SysError("deleting '%1%'", path);
/* Do not accound for deleted file here. Rely on deletePath()
accounting. */
}
struct stat st;
if (stat(linksDir.c_str(), &st) == -1)
throw SysError("statting '%1%'", linksDir);
int64_t overhead = st.st_blocks * 512ULL;
printInfo("note: currently hard linking saves %.2f MiB",
((unsharedSize - actualSize - overhead) / (1024.0 * 1024.0)));
}
Steps To Reproduce
nix-store --add
nix-store --delete
and check the strace. It will take linear time to the number of files in.links
, about 1.5 million files on my developer machine.Expected behavior
The
--delete
operation should take more-or-less constant time.nix --version
outputAdditional context
This is kind of in-between a bug report and a request to improve the optimization feature.
My first idea was “some kind of bloom filter”, but really, we know which store paths we are deleting, the
links
directory is hashed over the inodes, shouldn’t we just be able to check exactly the files we are deleting? So the operation shouldn’t ever have to take longer thanO(n)
where n is the amount of files in the storepaths to delete.There's still the small challenge of reference scanning the entire system though; you need to know it's not alive in any process environment or whatever. But yea this sounds suboptimal.
In case this is not optimal, it could be argued that a full scan should only be necessary for operations like
nix-store --gc
, which will guarantee optimal cleanup of the links, and for everything else it can be a best-effort operation.that requires hashing every single file we're about to delete, and that can be much more expensive than stating all .links entries. hardlink-based deduplication of store objects is unsound, if your goal is to have quick single-path deletes you're better off disabling automatic store optimization and running something like duperemove to deduplicate data properly. (we have plans to eventually add reflink-based deduplication to lix, but it's very far down the list)
Ah, I didn’t read far enough into the code to see that it’s actually hashing the files.
fwiw, even just listing
.links/
on my machine withls -f >/dev/null
takes about 1 second, and nix takes about 5 seconds to do all the stats. And I’m pretty sure it’s not a very involved setup, my store is maybe 200GB.I see the problem that fully hashing arbitrary store paths can be pretty expensive in the worst case.
Using a counting bloom filter could be a solution, but as you said it’s probably not worth the conceptual complexity this would introduce.
Edit: really not sure about this, don’t quote me :P
we're not touching this part of the code to fix something that's already unsound, if anything we'll be moving to a reflink-based system that has none of the soundness problems and doesn't need a hardlink directory either. also not that bloom filters are the wrong tool for this job since we still need to know the hash of the link we're trying to delete