Skip to content

Commit fd3543d

Browse files
committed
fix(queue-runner): don't parse buildproducts.path as a StorePath
`buildproducts.path` is a full filesystem path, not a bare store path: Hydra's `$out/nix-support/hydra-build-products` lets a product point at a sub-path of an output (e.g. `doc manual $doc/share/doc/nix/manual index.html`), so the column legitimately contains a trailing `/...` after the `<hash>-<name>`. `OwnedBuildProduct::parse_paths` fed the whole string to `StoreDir::parse`, which died with `invalid store path / symbol at 50` on any such value. That wedged every cached-build fast path touching a product with a subpath — notably the `nix-manual-2.31.4/share/doc/nix/manual` products from `roots.nix` builds, leaving ~200 builds per eval stuck as 'unfinished' even though every output was already in the store. Even had the parse succeeded, `BuildProduct::from_db` was hard-coding `relative_path: \"\".into()`, so the sibling constructors (`from_grpc`, `from_shared`) and `from_db` disagreed on what the field meant. Fix by: - dropping the `StorePath` generic + `parse_paths` from `OwnedBuildProduct` (path stays as `Option<String>` — it's opaque to the db crate), and - routing `BuildProduct::from_db` through the same `RelativeStorePath::from_path` splitter `from_grpc`/`from_shared` already use. The db-side write path calls `RelativeStorePath::print`, so `from_path` is now its exact inverse. Adds unit tests covering the subpath product, a bare store path, and the print/from_path round-trip.
1 parent e8b7bd8 commit fd3543d

4 files changed

Lines changed: 84 additions & 37 deletions

File tree

subprojects/crates/db/src/connection.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,9 @@ impl Connection {
261261
pub async fn get_build_products_for_build_id(
262262
&mut self,
263263
build_id: i32,
264-
store_dir: &StoreDir,
265-
) -> anyhow::Result<Vec<crate::models::OwnedBuildProduct>> {
266-
let rows = sqlx::query_as!(
267-
crate::models::OwnedBuildProduct::<String>,
264+
) -> sqlx::Result<Vec<crate::models::OwnedBuildProduct>> {
265+
sqlx::query_as!(
266+
crate::models::OwnedBuildProduct,
268267
r#"
269268
SELECT
270269
type,
@@ -279,10 +278,7 @@ impl Connection {
279278
build_id
280279
)
281280
.fetch_all(&mut *self.conn)
282-
.await?;
283-
rows.into_iter()
284-
.map(|r| Ok(r.parse_paths(store_dir)?))
285-
.collect()
281+
.await
286282
}
287283

288284
pub async fn get_build_metrics_for_build_id(

subprojects/crates/db/src/models.rs

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -221,34 +221,22 @@ pub struct BuildOutput {
221221
pub size: Option<i64>,
222222
}
223223

224+
// `buildproducts.path` is an opaque filesystem path, not a store path: Hydra's
225+
// `$out/nix-support/hydra-build-products` lets a product point at a sub-path of
226+
// an output (e.g. `doc manual $doc/share/doc/nix/manual index.html`), so the
227+
// value can legitimately contain a trailing `/...` after the store path. Keep
228+
// it as a raw `String` here; splitting into base + relative is the caller's job.
224229
#[derive(Debug)]
225-
pub struct OwnedBuildProduct<StorePath = harmonia_store_core::store_path::StorePath> {
230+
pub struct OwnedBuildProduct {
226231
pub r#type: String,
227232
pub subtype: String,
228233
pub filesize: Option<i64>,
229234
pub sha256hash: Option<String>,
230-
pub path: Option<StorePath>,
235+
pub path: Option<String>,
231236
pub name: String,
232237
pub defaultpath: Option<String>,
233238
}
234239

235-
impl OwnedBuildProduct<String> {
236-
pub fn parse_paths(
237-
self,
238-
store_dir: &StoreDir,
239-
) -> Result<OwnedBuildProduct, ParseStorePathError> {
240-
Ok(OwnedBuildProduct {
241-
r#type: self.r#type,
242-
subtype: self.subtype,
243-
filesize: self.filesize,
244-
sha256hash: self.sha256hash,
245-
path: self.path.map(|p| store_dir.parse(&p)).transpose()?,
246-
name: self.name,
247-
defaultpath: self.defaultpath,
248-
})
249-
}
250-
}
251-
252240
#[derive(Debug)]
253241
pub struct BuildProduct<'a> {
254242
pub r#type: &'a str,

subprojects/hydra-queue-runner/src/state/build.rs

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -388,12 +388,15 @@ pub struct BuildProduct {
388388
}
389389

390390
impl BuildProduct {
391-
pub fn from_db(v: db::models::OwnedBuildProduct) -> Self {
392-
Self {
393-
path: v.path.map(|p| RelativeStorePath {
394-
base_path: p,
395-
relative_path: "".into(),
396-
}),
391+
pub fn from_db(
392+
store_dir: &nix_utils::StoreDir,
393+
v: db::models::OwnedBuildProduct,
394+
) -> anyhow::Result<Self> {
395+
Ok(Self {
396+
path: v
397+
.path
398+
.map(|p| RelativeStorePath::from_path(store_dir, &p))
399+
.transpose()?,
397400
default_path: v.defaultpath,
398401
r#type: v.r#type,
399402
subtype: v.subtype,
@@ -402,7 +405,7 @@ impl BuildProduct {
402405
sha256hash: v.sha256hash,
403406
#[allow(clippy::cast_sign_loss)]
404407
file_size: v.filesize.map(|v| v as u64),
405-
}
408+
})
406409
}
407410

408411
pub fn from_grpc(
@@ -739,3 +742,63 @@ impl Builds {
739742
builds.remove(&id);
740743
}
741744
}
745+
746+
#[cfg(test)]
747+
mod tests {
748+
use super::*;
749+
750+
fn test_store_dir() -> nix_utils::StoreDir {
751+
nix_utils::StoreDir::new("/nix/store").unwrap()
752+
}
753+
754+
// Regression: `buildproducts.path` produced by e.g. a `doc manual …` entry in
755+
// `$out/nix-support/hydra-build-products` points at a sub-path of an output.
756+
// Parsing the full value as a bare `StorePath` used to fail with
757+
// `invalid store path / symbol at 50`, wedging every "cached" Hydra build
758+
// whose outputs included a doc/manual or similar sub-path product.
759+
#[test]
760+
fn relative_store_path_splits_product_subpath() {
761+
let dir = test_store_dir();
762+
let rel = RelativeStorePath::from_path(
763+
&dir,
764+
"/nix/store/bwqqp42xqn37z31dapi7jrhy8iwc2zsx-nix-manual-2.31.4/share/doc/nix/manual",
765+
)
766+
.expect("subpath product must parse");
767+
assert_eq!(
768+
rel.base_path.to_string(),
769+
"bwqqp42xqn37z31dapi7jrhy8iwc2zsx-nix-manual-2.31.4"
770+
);
771+
assert_eq!(&*rel.relative_path, "share/doc/nix/manual");
772+
}
773+
774+
#[test]
775+
fn relative_store_path_accepts_bare_store_path() {
776+
let dir = test_store_dir();
777+
let rel = RelativeStorePath::from_path(
778+
&dir,
779+
"/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-example-1.0",
780+
)
781+
.expect("bare store path must parse");
782+
assert_eq!(
783+
rel.base_path.to_string(),
784+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-example-1.0"
785+
);
786+
assert!(rel.relative_path.is_empty());
787+
}
788+
789+
// The DB write path calls `RelativeStorePath::print` and the DB read path
790+
// (after this fix) calls `RelativeStorePath::from_path`. They must be inverses
791+
// for both shapes of `buildproducts.path`.
792+
#[test]
793+
fn relative_store_path_roundtrips() {
794+
let dir = test_store_dir();
795+
for original in [
796+
"/nix/store/bwqqp42xqn37z31dapi7jrhy8iwc2zsx-nix-manual-2.31.4/share/doc/nix/manual",
797+
"/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-example-1.0",
798+
] {
799+
let rel = RelativeStorePath::from_path(&dir, original)
800+
.unwrap_or_else(|e| panic!("parse {original}: {e}"));
801+
assert_eq!(rel.print(&dir), original);
802+
}
803+
}
804+
}

subprojects/hydra-queue-runner/src/state/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,11 +2057,11 @@ impl State {
20572057
};
20582058

20592059
res.products = db
2060-
.get_build_products_for_build_id(build_id, self.store.store_dir())
2060+
.get_build_products_for_build_id(build_id)
20612061
.await?
20622062
.into_iter()
2063-
.map(build::BuildProduct::from_db)
2064-
.collect();
2063+
.map(|p| build::BuildProduct::from_db(self.store.store_dir(), p))
2064+
.collect::<anyhow::Result<_>>()?;
20652065
res.metrics = db
20662066
.get_build_metrics_for_build_id(build_id)
20672067
.await?

0 commit comments

Comments
 (0)