Snix's nar-bridge[1] stores NARs under a different URL, i.e. `nar/snix-castore/<hash>.nar` rather than `nar/<filehash>.nar`. Right now, when copying into such a store via the http binary-cache, we'd end up with wrong cache entries that point to the wrong NAR URL. On Hydra, this is a fatal error, i.e. builds that depend on previously built paths (that were written to the cache before by the queue runner) would be aborted because of that. This patch removes the caching since we'd have to re-fetch the narinfo to do taht and this can also happen the next time, the narinfo is queried. Also, removes the negative cache entry indicating that the store-path doesn't exist in the store. We don't have any coverage for http-stores so far, so I wrote a small testcase for the "default" case and the nar-bridge case in functional2 since it has a very nice fixture for an HTTP server ready. I'm aware that there's a CL for a nicer cache server[2], but given I'm down a pretty deep rabbit hole by playing around with Snix, I decided to not finish the CL and write something small for the tests in here. This can be replaced by the fixtures from that CL later on as well. [1] https://snix.dev/docs/components/overview/#nar-bridge [2] https://gerrit.lix.systems/c/lix/+/2431/1 Change-Id: I4fcdf47a6bf9c3c8fbeb235eeca7a48914a4d693
150 lines
5.3 KiB
Python
150 lines
5.3 KiB
Python
from pathlib import Path
|
|
import sqlite3
|
|
|
|
import aiohttp.web as web
|
|
import pytest
|
|
|
|
from functional2.testlib.fixtures.file_helper import File
|
|
from functional2.testlib.fixtures.http_server import http_server
|
|
from functional2.testlib.fixtures.nix import Nix
|
|
|
|
|
|
class HTTPStore:
|
|
def __init__(self):
|
|
self.uploaded_nars = {}
|
|
self.known_nar_hashes = set()
|
|
|
|
async def upload_narinfo(self, req: web.Request) -> web.Response:
|
|
self.uploaded_nars[req.match_info["hash"]] = self._parse_narinfo(await req.text())
|
|
return web.Response(text="")
|
|
|
|
async def upload_nar(self, req: web.Request) -> web.Response:
|
|
self.known_nar_hashes.add(req.match_info["narhash"])
|
|
return web.Response(text="")
|
|
|
|
async def nix_cache_info(self, _: web.Request) -> web.Response:
|
|
return web.Response(text="StoreDir: /nix/store")
|
|
|
|
async def get_narinfo(self, req: web.Request) -> web.Response:
|
|
narinfo_hash = req.match_info["hash"]
|
|
if narinfo_hash not in self.uploaded_nars:
|
|
return web.Response(text="", status=404)
|
|
return web.Response(
|
|
text="\n".join(f"{k}: {v}" for k, v in self.uploaded_nars[narinfo_hash].items()) + "\n"
|
|
)
|
|
|
|
async def nar_head(self, req: web.Request) -> web.Response:
|
|
return web.Response(
|
|
text="", status=200 if req.match_info["narhash"] in self.known_nar_hashes else 404
|
|
)
|
|
|
|
def _parse_narinfo(self, text: str) -> dict[str, str]:
|
|
narinfo = {}
|
|
for line in text.splitlines():
|
|
key, value = line.split(": ", 1)
|
|
narinfo[key] = value
|
|
_, hashpart = narinfo["FileHash"].split(":", 1)
|
|
assert hashpart in self.known_nar_hashes
|
|
return narinfo
|
|
|
|
|
|
class FakeNARBridge(HTTPStore):
|
|
"""
|
|
HTTP Store that mutates the URL field of the narinfo, just like
|
|
nar-bridge from snix.dev.
|
|
|
|
Testcase to ensure that the correct URL to the nar (i.e. nar/snix-castore/...)
|
|
ends up in the disk-cache.
|
|
"""
|
|
|
|
async def upload_narinfo(self, req: web.Request) -> web.Response:
|
|
narinfo = self._parse_narinfo(await req.text())
|
|
narinfo["URL"] = (
|
|
f"nar/snix-castore/00000000000000000000000000000000000000000000000000000?narsize=f{narinfo['FileSize']}"
|
|
)
|
|
|
|
self.uploaded_nars[req.match_info["hash"]] = narinfo
|
|
return web.Response(text="")
|
|
|
|
|
|
@pytest.fixture(params=[HTTPStore, FakeNARBridge])
|
|
def store(request: pytest.FixtureRequest) -> HTTPStore:
|
|
store_class = request.param
|
|
return store_class()
|
|
|
|
|
|
def start_server(store: HTTPStore) -> web.Application:
|
|
app = web.Application()
|
|
app.add_routes(
|
|
[
|
|
web.put("/{hash}.narinfo", store.upload_narinfo),
|
|
web.get("/{hash}.narinfo", store.get_narinfo),
|
|
web.put("/nar/{narhash}.nar", store.upload_nar),
|
|
web.get("/nix-cache-info", store.nix_cache_info),
|
|
web.head("/nar/{narhash}.nar", store.nar_head),
|
|
]
|
|
)
|
|
|
|
return app
|
|
|
|
|
|
def nars_from_narinfo_cache(db_path: Path) -> list[dict[str, str | bool]]:
|
|
assert db_path.exists()
|
|
db = sqlite3.connect(db_path)
|
|
rows = db.execute(
|
|
"SELECT n.present, n.hashPart, n.namePart, n.url FROM NARs n INNER JOIN BinaryCaches b ON n.cache = b.id WHERE b.url LIKE '%localhost%'"
|
|
)
|
|
return [
|
|
{"present": bool(present), "hashPart": hashPart, "namePart": namePart, "url": url}
|
|
for present, hashPart, namePart, url in rows
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("files", [{"test-file": File("hello world")}], indirect=True)
|
|
def test_http_simple(nix: Nix, store: HTTPStore, files: Path):
|
|
test_file = files / "test-file"
|
|
result = nix.nix(cmd=["store", "add-file", test_file], flake=True).run()
|
|
result.ok()
|
|
store_path = result.stdout_plain
|
|
hash_part, _ = Path(store_path).stem.split("-", 1)
|
|
|
|
nar_info_cache = nix.test_root / "test-home" / ".cache" / "nix" / "binary-cache-v6.sqlite"
|
|
|
|
app = start_server(store)
|
|
with http_server(app) as httpd:
|
|
url = f"http://localhost:{httpd.port}?compression=none"
|
|
nix.nix(cmd=["store", "ping", "--store", url], flake=True).run().ok()
|
|
|
|
# Narinfo shouldn't exist yet
|
|
nix.nix(
|
|
cmd=["path-info", "--store", f"http://localhost:{httpd.port}", store_path], flake=True
|
|
).run().expect(1)
|
|
cache_entries = nars_from_narinfo_cache(nar_info_cache)
|
|
|
|
assert len(cache_entries) == 1
|
|
assert not cache_entries[0]["present"]
|
|
assert cache_entries[0]["hashPart"] == hash_part
|
|
|
|
# Successful upload
|
|
nix.nix(
|
|
cmd=["copy", "--from", nix.test_root / "store", "--to", url, store_path], flake=True
|
|
).run().ok()
|
|
assert hash_part in store.uploaded_nars
|
|
|
|
# Make sure the negative entry got removed
|
|
assert not nars_from_narinfo_cache(nar_info_cache)
|
|
|
|
# Ensure that the narinfo can be found now.
|
|
nix.nix(
|
|
cmd=["path-info", "--store", f"http://localhost:{httpd.port}", store_path], flake=True
|
|
).run().ok()
|
|
|
|
# Ensure local narinfo cache is up-to-date.
|
|
nar_entries = nars_from_narinfo_cache(nar_info_cache)
|
|
assert len(nar_entries) == 1
|
|
|
|
assert nar_entries[0]["present"]
|
|
assert nar_entries[0]["hashPart"] == hash_part
|
|
assert nar_entries[0]["namePart"] == "test-file"
|
|
assert nar_entries[0]["url"] == store.uploaded_nars[hash_part]["URL"]
|