jobq: make NodeGuid an actual guid

Third time the operator asked for a guid and got a substitute: first an
i64 index, then a {random job id, per-builder counter} pair. The pair was
defensible in isolation -- a foreign handle misses rather than colliding,
with no new dependency -- but "an equivalent that avoids a dep" is a
counter-proposal, not an implementation.

It is also simpler as a guid, which was the question asked: NodeGuid(Uuid)
drops the `job` field, the `next_seq` counter, `fresh_job_id()` and the
`Cell` import, and halves the type's doc. One random draw per node rather
than one per builder -- noise next to what a node does when it runs.

uuid 1.24 was already in Cargo.lock as a transitive dependency, so this
adds an edge rather than a package.
This commit is contained in:
atlas 2026-08-02 13:53:51 +02:00 committed by mara
commit c82853af5a
4 changed files with 15 additions and 40 deletions

1
Cargo.lock generated
View file

@ -1758,6 +1758,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror 2.0.18",
"uuid",
]
[[package]]

View file

@ -64,6 +64,7 @@ hive-sock-client = { path = "hive-sock-client" }
hive-types = { path = "hive-types" }
thiserror = "2"
tower-http = { version = "0.7", features = ["fs"] }
uuid = { version = "1", features = ["v4"] }
rmcp = { version = "2", default-features = false, features = [
"server",
"macros",

View file

@ -12,6 +12,7 @@ chrono = { workspace = true }
enumflags2.workspace = true
serde = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true }
[dev-dependencies]
serde_json = { workspace = true }

View file

@ -26,7 +26,7 @@
//! silent reorder — a builder that sorted for you would quietly accept a shape
//! the graph itself cannot express.
use std::cell::{Cell, RefCell};
use std::cell::RefCell;
use std::collections::HashMap;
use crate::{Dep, DepWhen, Graph, GraphError, NodeId, TerminalState};
@ -38,32 +38,14 @@ use crate::{Dep, DepWhen, Graph, GraphError, NodeId, TerminalState};
/// insert time and are meaningful system-wide, while this is a builder-local
/// label that stops existing once the job is inserted.
///
/// The `job` half is what makes it safe to compare. A [`NodeRef`] converts into
/// a bare `NodeGuid`, dropping the borrow that ties it to its builder — so a
/// handle *can* be carried to a different job (captured by an inner closure,
/// say). Were the identity a plain per-builder counter, the two jobs' `0`s would
/// be equal and that handle would silently resolve to an unrelated node. With a
/// per-builder random half it simply isn't found, and the insert fails by name.
/// It is a v4 uuid because it has to survive leaving its builder. A [`NodeRef`]
/// converts into a bare `NodeGuid`, dropping the borrow that tied it to the
/// builder — so a handle *can* be carried into a different job (captured by an
/// inner closure, say). Under a per-builder counter the two jobs' `0`s would be
/// equal and that handle would silently address an unrelated node; drawn at
/// random per node it simply isn't found, and the insert fails by name.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeGuid {
/// Identifies the builder that issued this handle. Random per builder, so
/// two jobs' handles never compare equal.
job: u64,
/// Position within that builder — the cheap half, since `job` already
/// separates one builder's handles from another's.
seq: u32,
}
/// A fresh identity for one builder.
///
/// `RandomState` is seeded from the process's random state and advances per
/// instance, so each builder gets a distinct, unguessable value — enough to
/// make a foreign handle a miss rather than a collision. No `rand` dependency
/// for what is a collision-avoidance job, not a cryptographic one.
fn fresh_job_id() -> u64 {
use std::hash::BuildHasher as _;
std::collections::hash_map::RandomState::new().hash_one(0_u8)
}
pub struct NodeGuid(uuid::Uuid);
/// Why a job could not be inserted.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
@ -117,10 +99,6 @@ struct Pending<N, R> {
#[derive(Debug)]
pub struct JobBuilder<N, R> {
nodes: RefCell<Vec<Pending<N, R>>>,
/// Identifies this builder in every handle it issues.
job: u64,
/// Position within this builder — the `seq` half of a [`NodeGuid`].
next_seq: Cell<u32>,
}
// Hand-written rather than derived: `#[derive(Default)]` would demand
@ -129,8 +107,6 @@ impl<N, R> Default for JobBuilder<N, R> {
fn default() -> Self {
Self {
nodes: RefCell::new(Vec::new()),
job: fresh_job_id(),
next_seq: Cell::new(0),
}
}
}
@ -160,11 +136,7 @@ impl<N, R> JobBuilder<N, R> {
/// The returned handle is where those are declared; it is [`Copy`], so it
/// can be named as a dependency as many times as needed.
pub fn node(&self, payload: N) -> NodeRef<'_, N, R> {
let guid = NodeGuid {
job: self.job,
seq: self.next_seq.get(),
};
self.next_seq.set(guid.seq + 1);
let guid = NodeGuid(uuid::Uuid::new_v4());
self.nodes.borrow_mut().push(Pending {
guid,
payload,
@ -558,9 +530,9 @@ mod tests {
/// job — an inner closure capturing an outer handle, say — must not
/// silently address whichever node happens to sit at the same position.
///
/// This is what the random half of a [`NodeGuid`] buys: with a plain
/// per-builder counter both jobs' first handles would be equal, and the
/// edge below would resolve, wrongly, to the second job's own node.
/// This is what a random [`NodeGuid`] buys: with a plain per-builder
/// counter both jobs' first handles would be equal, and the edge below
/// would resolve, wrongly, to the second job's own node.
#[test]
fn a_handle_from_another_job_is_not_silently_resolved() {
let mut g = graph();