Skip to content

Commit 017ceb4

Browse files
tomberekclaude
andcommitted
Don't re-download a tarball we already have, even after the URL cache expires
Nix re-checks a downloaded tarball's URL every hour by default (tarball-ttl), even when the caller already pinned the exact content hash (narHash). That pinned hash is enough to know the content is still correct without contacting the server again, but nothing was checking for that case, so fetches kept hitting the network on a schedule for no reason. If the cached entry for a URL has gone stale but its content still matches the pinned hash, reuse it instead of re-downloading. This is skipped when the TTL is explicitly 0 (as with `--refresh`), since that means the caller wants the source re-verified regardless of any hash we already have on file. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 7f711be commit 017ceb4

2 files changed

Lines changed: 51 additions & 6 deletions

File tree

src/libfetchers/tarball.cc

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@ DownloadFileResult downloadFile(
105105
}
106106

107107
static DownloadTarballResult downloadTarball_(
108-
const Settings & settings, const std::string & urlS, const Headers & headers, const std::string & displayPrefix)
108+
const Settings & settings,
109+
const std::string & urlS,
110+
const Headers & headers,
111+
const std::string & displayPrefix,
112+
const std::optional<Hash> & expectedNarHash)
109113
{
110114
ParsedURL url = parseURL(urlS);
111115

@@ -150,10 +154,23 @@ static DownloadTarballResult downloadTarball_(
150154
if (cached && !settings.getTarballCache()->hasObject(getRevAttr(cached->value, "treeHash")))
151155
cached.reset();
152156

153-
if (cached && !cached->expired)
154-
/* We previously downloaded this tarball and it's younger than
155-
`tarballTtl`, so no need to check the server. */
156-
return attrsToResult(cached->value);
157+
if (cached) {
158+
if (!cached->expired)
159+
/* We previously downloaded this tarball and it's younger than
160+
`tarballTtl`, so no need to check the server. */
161+
return attrsToResult(cached->value);
162+
163+
/* The cached entry is stale, but if its content still matches
164+
the pinned NAR hash, there's no need to re-check the server
165+
either. Exception: a TTL of 0 (e.g. `--refresh`) means the
166+
caller explicitly wants us to verify against the server, so
167+
don't let a merely-matching old hash short-circuit that. */
168+
if (expectedNarHash && settings.tarballTtl.get() != 0) {
169+
auto treeHash = getRevAttr(cached->value, "treeHash");
170+
if (settings.getTarballCache()->treeHashToNarHash(settings, treeHash) == *expectedNarHash)
171+
return attrsToResult(cached->value);
172+
}
173+
}
157174

158175
auto _res = std::make_shared<Sync<FileTransferResult>>();
159176

@@ -491,7 +508,8 @@ struct TarballInputScheme : CurlInputScheme
491508
{
492509
auto input(_input);
493510

494-
auto result = downloadTarball_(settings, getStrAttr(input.attrs, "url"), {}, "«" + input.to_string() + "»");
511+
auto result = downloadTarball_(
512+
settings, getStrAttr(input.attrs, "url"), {}, "«" + input.to_string() + "»", input.getNarHash());
495513

496514
if (result.immutableUrl) {
497515
auto immutableInput = Input::fromURL(*result.immutableUrl);

tests/functional/tarball.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,33 @@ test_tarball() {
3131
nix-build -o "$TEST_ROOT"/result -E "import (fetchTree { type = \"tarball\"; url = \"file://$tarball\"; })"
3232
nix-build -o "$TEST_ROOT"/result -E "import (fetchTree { type = \"tarball\"; url = \"file://$tarball\"; narHash = \"$hash\"; })"
3333

34+
# Once a URL has been fetched with a given narHash, re-fetching the same
35+
# URL after its cache entry has genuinely expired (a nonzero TTL that has
36+
# elapsed) should not require re-reading it. Prove this by corrupting the
37+
# file in place and expecting the fetch to still succeed. (`--tarball-ttl
38+
# 0` is excluded here since that's the explicit "always re-verify"
39+
# signal used by `--refresh`, which must NOT be short-circuited.)
40+
cp "$tarball" "$tarball.bak"
41+
echo garbage > "$tarball"
42+
sleep 2
43+
nix-build -o "$TEST_ROOT"/result --tarball-ttl 1 -E "import (fetchTree { type = \"tarball\"; url = \"file://$tarball\"; narHash = \"$hash\"; })"
44+
mv "$tarball.bak" "$tarball"
45+
46+
# `--refresh` (tarball-ttl 0) must always re-verify against the source,
47+
# even if a narHash is pinned, so a mismatch must still be reported.
48+
# Use a different URL with different (but validly archived) content, so
49+
# the failure is a hash mismatch rather than an unpack error, and so the
50+
# cache entry for `$tarball` itself isn't disturbed by the expected
51+
# failure.
52+
otherRoot=$TEST_ROOT/tarball-other
53+
rm -rf "$otherRoot"
54+
mkdir -p "$otherRoot"
55+
echo "different content" > "$otherRoot/default.nix"
56+
cp "${config_nix}" dependencies.builder*.sh "$otherRoot/"
57+
otherTarball=$TEST_ROOT/tarball-other.tar$ext
58+
(cd "$TEST_ROOT" && GNUTAR_REPRODUCIBLE=1 tar --mtime="$otherRoot"/default.nix --owner=0 --group=0 --numeric-owner --sort=name -c -f - tarball-other) | $compressor > "$otherTarball"
59+
expectStderr 102 nix eval --refresh --raw --expr "(fetchTree { type = \"tarball\"; url = \"file://$otherTarball\"; narHash = \"$hash\"; }).outPath" | grepQuiet "NAR hash mismatch"
60+
3461
[[ $(nix eval --impure --expr "(fetchTree \"file://$tarball\").lastModified") = 1000000000 ]]
3562

3663
nix-instantiate --strict --eval -E "!((import (fetchTree { type = \"tarball\"; url = \"file://$tarball\"; narHash = \"$hash\"; })) ? submodules)" >&2

0 commit comments

Comments
 (0)