rebuild_queue: dedup MetaUpdate on (kind, agent, inputs) (closes #365)

This commit is contained in:
damocles 2026-05-24 04:52:29 +02:00 committed by Mara
parent 0b03d5bcfb
commit 3fa12bf363

View file

@ -220,11 +220,18 @@ impl RebuildQueue {
/// or — on dedup — the existing entry's id with the new reason /// or — on dedup — the existing entry's id with the new reason
/// appended). /// appended).
/// ///
/// Dedup rule: a `Queued` entry with the same `(kind, agent)` swallows /// Dedup rule:
/// the new request and returns its existing id. Running and terminal /// - `Rebuild` / `Spawn` / `Destroy`: a `Queued` entry with the same
/// entries do not dedup — operators are free to re-queue a rebuild /// `(kind, agent)` swallows the new request.
/// that's currently running (something changed since it started) or /// - `MetaUpdate`: dedup ALSO requires the `inputs` field to match —
/// re-run one that just finished. /// two meta-updates with different input lists are distinct work
/// and must queue separately (closes #365: previously the second
/// meta-update collapsed into the first whenever it was still
/// `Queued`, losing the second's input set).
///
/// Running and terminal entries never dedup — operators are free
/// to re-queue a rebuild that's currently running (something
/// changed since it started) or re-run one that just finished.
pub fn enqueue( pub fn enqueue(
&self, &self,
kind: QueueKind, kind: QueueKind,
@ -238,7 +245,9 @@ impl RebuildQueue {
/// Same as `enqueue` but carries an `inputs` payload — used by /// Same as `enqueue` but carries an `inputs` payload — used by
/// `MetaUpdate` enqueues to tell the worker which meta-flake /// `MetaUpdate` enqueues to tell the worker which meta-flake
/// inputs to bump. /// inputs to bump. For `MetaUpdate` the `inputs` value is part of
/// the dedup key (two meta-updates with different inputs are
/// distinct operations, see #365).
pub fn enqueue_with_inputs( pub fn enqueue_with_inputs(
&self, &self,
kind: QueueKind, kind: QueueKind,
@ -249,9 +258,15 @@ impl RebuildQueue {
inputs: Vec<String>, inputs: Vec<String>,
) -> u64 { ) -> u64 {
let mut inner = self.inner.lock().expect("rebuild_queue mutex poisoned"); let mut inner = self.inner.lock().expect("rebuild_queue mutex poisoned");
// Dedup against a pending entry with the same (kind, agent). // Dedup against a pending entry with the same (kind, agent) —
// and, for MetaUpdate, the same `inputs` list (see method
// docstring + #365 for why).
for entry in inner.entries.iter_mut() { for entry in inner.entries.iter_mut() {
if entry.state == QueueState::Queued && entry.kind == kind && entry.agent == agent { if entry.state == QueueState::Queued
&& entry.kind == kind
&& entry.agent == agent
&& (kind != QueueKind::MetaUpdate || entry.inputs == inputs)
{
if !entry.reason.contains(&reason) { if !entry.reason.contains(&reason) {
entry.reason.push_str(&format!("\nalso requested by: {reason}")); entry.reason.push_str(&format!("\nalso requested by: {reason}"));
} }
@ -619,6 +634,62 @@ mod tests {
assert!(snap[0].reason.contains("auto sweep")); assert!(snap[0].reason.contains("auto sweep"));
} }
#[test]
fn meta_update_dedup_matches_inputs() {
// Two MetaUpdate enqueues with identical inputs → dedup (#365).
let q = RebuildQueue::new();
let a = q.enqueue_with_inputs(
QueueKind::MetaUpdate,
"hyperhive".to_owned(),
QueueSource::Manual,
"first".to_owned(),
None,
vec!["nixpkgs".to_owned()],
);
let b = q.enqueue_with_inputs(
QueueKind::MetaUpdate,
"hyperhive".to_owned(),
QueueSource::Manual,
"duplicate click".to_owned(),
None,
vec!["nixpkgs".to_owned()],
);
assert_eq!(a, b, "identical-inputs meta-updates should dedup");
assert_eq!(q.snapshot().len(), 1);
}
#[test]
fn meta_update_dedup_separates_distinct_inputs() {
// Two MetaUpdate enqueues with DIFFERENT inputs → distinct
// entries, not deduped (the actual #365 bug).
let q = RebuildQueue::new();
let a = q.enqueue_with_inputs(
QueueKind::MetaUpdate,
"hyperhive".to_owned(),
QueueSource::Manual,
"bump nixpkgs".to_owned(),
None,
vec!["nixpkgs".to_owned()],
);
let b = q.enqueue_with_inputs(
QueueKind::MetaUpdate,
"hyperhive".to_owned(),
QueueSource::Manual,
"bump bitburner-agent".to_owned(),
None,
vec!["agent-bitburner/bitburner-agent".to_owned()],
);
assert_ne!(a, b, "different-inputs meta-updates must NOT dedup");
let snap = q.snapshot();
assert_eq!(snap.len(), 2);
// Both inputs lists are preserved.
let inputs: Vec<&[String]> = snap.iter().map(|e| e.inputs.as_slice()).collect();
assert!(inputs.iter().any(|i| *i == ["nixpkgs"]));
assert!(inputs
.iter()
.any(|i| *i == ["agent-bitburner/bitburner-agent"]));
}
#[test] #[test]
fn dedup_does_not_apply_across_kinds_or_agents() { fn dedup_does_not_apply_across_kinds_or_agents() {
let q = RebuildQueue::new(); let q = RebuildQueue::new();