Skip to content

Commit ba888fb

Browse files
committed
queue-runner: resolve drv output chains one level at a time in SQL
Replace the recursive SQL CTE in `resolve_drv_output_chains` (which cannot see the in-memory resolved-drv map) with a Rust loop that calls a new depth-1 `resolve_drv_output` query at each level, translating intermediate drv paths through `resolved_drv_map` between steps. This fixes resolution of dynamic derivation chains where an intermediate result is an unresolved drv (status=13): the recursive SQL required `status = 0` at every level and had no way to follow the in-memory mapping, so chains like `make-derivations.drv.drv^out^out` would fail when the intermediate `make-derivations.drv` had been resolved to a different path. Results are memoized in a temporary `HashMap` scoped to the `try_resolve` call, so repeated lookups of the same (drv, output_name) pair hit the cache. Once we have the resolved derivation mapping in the database, we can go back to the all-SQL approach, saving ourselves some app <-> DB round trips. This situation is explicitly tested.
1 parent 3cbcdbe commit ba888fb

2 files changed

Lines changed: 213 additions & 31 deletions

File tree

subprojects/crates/db/src/connection.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,41 @@ impl Connection {
395395
Ok(results)
396396
}
397397

398+
/// Look up a single output of a derivation from the most recent
399+
/// successful buildstep.
400+
pub async fn resolve_drv_output(
401+
&mut self,
402+
store_dir: &StoreDir,
403+
drv_path: &StorePath,
404+
output_name: &OutputName,
405+
) -> sqlx::Result<Option<StorePath>> {
406+
let drv_display = store_dir.display(drv_path).to_string();
407+
let output_name_str: &str = output_name.as_ref();
408+
let row: Option<(String,)> = sqlx::query_as(
409+
r"SELECT o.path
410+
FROM buildsteps s
411+
JOIN buildstepoutputs o
412+
ON s.build = o.build AND s.stepnr = o.stepnr
413+
WHERE s.drvPath = $1
414+
AND o.name = $2
415+
AND o.path IS NOT NULL
416+
AND s.status = 0
417+
ORDER BY s.build DESC
418+
LIMIT 1",
419+
)
420+
.bind(&drv_display)
421+
.bind(output_name_str)
422+
.fetch_optional(&mut *self.conn)
423+
.await?;
424+
425+
row.map(|(path,)| {
426+
store_dir
427+
.parse(&path)
428+
.map_err(|e| sqlx::Error::Decode(Box::new(e)))
429+
})
430+
.transpose()
431+
}
432+
398433
#[tracing::instrument(skip(self), err)]
399434
pub async fn get_status(&mut self) -> sqlx::Result<Option<serde_json::Value>> {
400435
Ok(
@@ -1461,4 +1496,142 @@ mod tests {
14611496
]
14621497
);
14631498
}
1499+
1500+
// -- resolve_drv_output (depth-1) tests ------------------------------------
1501+
1502+
#[tokio::test]
1503+
async fn resolve_drv_output_basic() {
1504+
let (_pg, mut conn) = setup().await;
1505+
insert_step(&mut conn, 1, 1, &sp("foo.drv")).await;
1506+
insert_output(&mut conn, 1, 1, "out", &sp("result")).await;
1507+
1508+
let result = conn
1509+
.resolve_drv_output(&test_store_dir(), &sp("foo.drv"), &on("out"))
1510+
.await
1511+
.unwrap();
1512+
assert_eq!(result, Some(sp("result")));
1513+
}
1514+
1515+
#[tokio::test]
1516+
async fn resolve_drv_output_missing() {
1517+
let (_pg, mut conn) = setup().await;
1518+
let result = conn
1519+
.resolve_drv_output(&test_store_dir(), &sp("nonexistent.drv"), &on("out"))
1520+
.await
1521+
.unwrap();
1522+
assert_eq!(result, None);
1523+
}
1524+
1525+
#[tokio::test]
1526+
async fn resolve_drv_output_picks_latest_build() {
1527+
let (_pg, mut conn) = setup().await;
1528+
insert_step(&mut conn, 1, 1, &sp("foo.drv")).await;
1529+
insert_output(&mut conn, 1, 1, "out", &sp("old-result")).await;
1530+
insert_step(&mut conn, 5, 1, &sp("foo.drv")).await;
1531+
insert_output(&mut conn, 5, 1, "out", &sp("new-result")).await;
1532+
1533+
let result = conn
1534+
.resolve_drv_output(&test_store_dir(), &sp("foo.drv"), &on("out"))
1535+
.await
1536+
.unwrap();
1537+
assert_eq!(result, Some(sp("new-result")));
1538+
}
1539+
1540+
// -- Simulate the Rust-side loop that replaces the recursive SQL ----------
1541+
//
1542+
// These mirror the resolved-step tests from the DB-column approach,
1543+
// but use resolve_drv_output + an in-memory map instead of
1544+
// resolvedDrvPath in the SQL.
1545+
1546+
/// Helper: resolve a chain one level at a time using `resolve_drv_output`,
1547+
/// translating through `resolved_map` between levels.
1548+
async fn resolve_chain_with_map(
1549+
conn: &mut Connection,
1550+
resolved_map: &std::collections::HashMap<StorePath, StorePath>,
1551+
root: &StorePath,
1552+
outputs: &[&OutputName],
1553+
) -> Option<StorePath> {
1554+
let sd = test_store_dir();
1555+
let mut current = root.clone();
1556+
for output_name in outputs {
1557+
let translated = resolved_map.get(&current).cloned().unwrap_or(current);
1558+
current = conn
1559+
.resolve_drv_output(&sd, &translated, output_name)
1560+
.await
1561+
.unwrap()?;
1562+
}
1563+
Some(current)
1564+
}
1565+
1566+
/// Depth-1: unresolved.drv was resolved to resolved.drv, which has
1567+
/// the outputs. The in-memory map translates before lookup.
1568+
#[tokio::test]
1569+
async fn resolve_with_map_depth_1() {
1570+
let (_pg, mut conn) = setup().await;
1571+
1572+
// resolved.drv was built successfully
1573+
insert_step(&mut conn, 2, 1, &sp("resolved.drv")).await;
1574+
insert_output(&mut conn, 2, 1, "out", &sp("result")).await;
1575+
1576+
let mut map = std::collections::HashMap::new();
1577+
map.insert(sp("unresolved.drv"), sp("resolved.drv"));
1578+
1579+
let result =
1580+
resolve_chain_with_map(&mut conn, &map, &sp("unresolved.drv"), &[&on("out")]).await;
1581+
assert_eq!(result, Some(sp("result")));
1582+
}
1583+
1584+
/// Depth-2: unresolved.drv was resolved to resolved.drv, whose output
1585+
/// is an intermediate.drv that has the final output.
1586+
#[tokio::test]
1587+
async fn resolve_with_map_depth_2() {
1588+
let (_pg, mut conn) = setup().await;
1589+
1590+
insert_step(&mut conn, 2, 1, &sp("resolved.drv")).await;
1591+
insert_output(&mut conn, 2, 1, "out", &sp("intermediate.drv")).await;
1592+
insert_step(&mut conn, 3, 1, &sp("intermediate.drv")).await;
1593+
insert_output(&mut conn, 3, 1, "out", &sp("final")).await;
1594+
1595+
let mut map = std::collections::HashMap::new();
1596+
map.insert(sp("unresolved.drv"), sp("resolved.drv"));
1597+
1598+
let result = resolve_chain_with_map(
1599+
&mut conn,
1600+
&map,
1601+
&sp("unresolved.drv"),
1602+
&[&on("out"), &on("out")],
1603+
)
1604+
.await;
1605+
assert_eq!(result, Some(sp("final")));
1606+
}
1607+
1608+
/// Depth-2 where the intermediate result was also resolved:
1609+
/// root.drv.drv (not resolved) → intermediate.drv (resolved) → final
1610+
#[tokio::test]
1611+
async fn resolve_with_map_intermediate_resolved() {
1612+
let (_pg, mut conn) = setup().await;
1613+
1614+
// root.drv.drv^out → unresolved-intermediate.drv
1615+
insert_step(&mut conn, 1, 1, &sp("root.drv.drv")).await;
1616+
insert_output(&mut conn, 1, 1, "out", &sp("unresolved-intermediate.drv")).await;
1617+
1618+
// resolved-intermediate.drv^out → final-result
1619+
insert_step(&mut conn, 2, 1, &sp("resolved-intermediate.drv")).await;
1620+
insert_output(&mut conn, 2, 1, "out", &sp("final-result")).await;
1621+
1622+
let mut map = std::collections::HashMap::new();
1623+
map.insert(
1624+
sp("unresolved-intermediate.drv"),
1625+
sp("resolved-intermediate.drv"),
1626+
);
1627+
1628+
let result = resolve_chain_with_map(
1629+
&mut conn,
1630+
&map,
1631+
&sp("root.drv.drv"),
1632+
&[&on("out"), &on("out")],
1633+
)
1634+
.await;
1635+
assert_eq!(result, Some(sp("final-result")));
1636+
}
14641637
}

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

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -73,46 +73,55 @@ impl StepInfo {
7373

7474
let mut conn = db.get().await.ok()?;
7575

76+
// Memoize depth-1 lookups across all chains resolved in this call.
77+
let mut memo = std::collections::HashMap::<
78+
(nix_utils::StorePath, nix_utils::OutputName),
79+
Option<nix_utils::StorePath>,
80+
>::new();
81+
7682
drv.try_resolve(store_dir, &mut |inputs| {
7783
tokio::task::block_in_place(|| {
78-
// Flatten each SingleDerivedPath chain into (root, [outputs...])
79-
// and resolve everything in a single recursive SQL query.
84+
let rt = tokio::runtime::Handle::current();
85+
8086
let chains: Vec<_> = inputs
8187
.iter()
8288
.map(|(drv_path, output_name)| flatten_chain(drv_path, output_name))
8389
.collect();
8490

85-
// Translate unresolved drv paths to resolved ones using the
86-
// in-memory map, so the SQL query finds outputs under the
87-
// resolved drv path directly.
88-
let translated_roots: Vec<_> = chains
89-
.iter()
90-
.map(|(root, _)| {
91-
resolved_drv_map
92-
.get(root)
93-
.cloned()
94-
.unwrap_or_else(|| root.clone())
95-
})
96-
.collect();
97-
98-
// SQL needs forward order; OutputNameChain stores reversed.
99-
let chain_refs: Vec<_> = chains
91+
// Resolve each chain one level at a time, translating
92+
// through the in-memory resolved-drv map between levels.
93+
chains
10094
.iter()
101-
.zip(&translated_roots)
102-
.map(|((_, chain), root)| (root, chain.0.iter().rev().collect::<Vec<_>>()))
103-
.collect();
104-
105-
let sql_input: Vec<_> = chain_refs
106-
.iter()
107-
.map(|(root, outputs)| (*root, outputs.as_slice()))
108-
.collect();
109-
110-
tokio::runtime::Handle::current()
111-
.block_on(conn.resolve_drv_output_chains(store_dir, &sql_input))
112-
.unwrap_or_else(|e| {
113-
tracing::warn!("resolve_drv_output_chains failed: {e}");
114-
vec![None; inputs.len()]
95+
.map(|(root, chain)| {
96+
let mut current = root.clone();
97+
// OutputNameChain is in stack order; iterate
98+
// reversed for forward (root-to-leaf) order.
99+
for output_name in chain.0.iter().rev() {
100+
let translated = resolved_drv_map
101+
.get(&current)
102+
.cloned()
103+
.unwrap_or_else(|| current.clone());
104+
let key = (translated, output_name.clone());
105+
let result = match memo.get(&key) {
106+
Some(cached) => cached.clone(),
107+
None => {
108+
let r = rt
109+
.block_on(
110+
conn.resolve_drv_output(store_dir, &key.0, &key.1),
111+
)
112+
.unwrap_or_else(|e| {
113+
tracing::warn!("resolve_drv_output failed: {e}");
114+
None
115+
});
116+
memo.insert(key, r.clone());
117+
r
118+
}
119+
};
120+
current = result?;
121+
}
122+
Some(current)
115123
})
124+
.collect()
116125
})
117126
})
118127
}

0 commit comments

Comments
 (0)