refactor: rename pod crate to worker

This commit is contained in:
2026-06-26 00:05:57 +09:00
parent 4c677640f4
commit 6c59fe927b
194 changed files with 6637 additions and 6146 deletions
+18 -18
View File
@@ -80,18 +80,18 @@ pub fn is_within_effective_write(lock: &LockFile, parent: &str, rule: &ScopeRule
!child_conflict
}
/// The Pod and rule that actually own a conflicting write scope.
/// The Worker and rule that actually own a conflicting write scope.
#[derive(Debug, Clone)]
pub struct ConflictOwner {
pub pod_name: String,
pub worker_name: String,
pub rule: ScopeRule,
}
/// Find the Pod/rule that actually owns a write scope overlapping `rule`.
/// Find the Worker/rule that actually owns a write scope overlapping `rule`.
///
/// Walks the delegation tree: if an allocation overlaps `rule`, we
/// descend into its children and return the deepest overlapping node
/// as the true owner. `exempt` names a Pod whose ownership is
/// as the true owner. `exempt` names a Worker whose ownership is
/// permitted (used during delegation: the spawner itself is allowed
/// to still own the rule's region because it is handing it down).
pub fn find_conflict_owner(
@@ -115,7 +115,7 @@ pub fn find_conflict_owners(
.iter()
.filter(|a| a.delegated_from.is_none())
.filter_map(|alloc| find_conflict_in_subtree(lock, alloc, rule))
.filter(|owner| Some(owner.pod_name.as_str()) != exempt)
.filter(|owner| Some(owner.worker_name.as_str()) != exempt)
.collect()
}
@@ -142,14 +142,14 @@ fn find_conflict_in_subtree(
for child in lock
.allocations
.iter()
.filter(|a| a.delegated_from.as_deref() == Some(alloc.pod_name.as_str()))
.filter(|a| a.delegated_from.as_deref() == Some(alloc.worker_name.as_str()))
{
if let Some(owner) = find_conflict_in_subtree(lock, child, rule) {
return Some(owner);
}
}
Some(ConflictOwner {
pod_name: alloc.pod_name.clone(),
worker_name: alloc.worker_name.clone(),
rule: overlapping_rule.clone(),
})
}
@@ -158,7 +158,7 @@ fn find_conflict_in_subtree(
mod tests {
use super::*;
use crate::test_util::*;
use crate::{ScopeLockError, delegate_scope, register_pod, register_pod_with_deny};
use crate::{ScopeLockError, delegate_scope, register_pod, register_worker_with_deny};
use tempfile::TempDir;
#[test]
@@ -192,9 +192,9 @@ mod tests {
#[test]
fn conflict_detection_descends_to_real_owner() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -213,9 +213,9 @@ mod tests {
&delegation_scope(vec![write_rule("/src", true)]),
)
.unwrap();
// A different top-level Pod trying to register /src/core/x
// A different top-level Worker trying to register /src/core/x
// should be blamed on B (deepest owner), not A.
let err = register_pod(
let err = register_worker(
&mut g,
"x".into(),
std::process::id(),
@@ -233,9 +233,9 @@ mod tests {
#[test]
fn denied_write_region_is_not_claimed_by_restored_parent() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod_with_deny(
register_worker_with_deny(
&mut g,
"parent".into(),
std::process::id(),
@@ -245,7 +245,7 @@ mod tests {
sid(),
)
.unwrap();
register_pod(
register_worker(
&mut g,
"child".into(),
std::process::id(),
@@ -259,9 +259,9 @@ mod tests {
#[test]
fn partial_deny_does_not_hide_parent_conflict() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod_with_deny(
register_worker_with_deny(
&mut g,
"parent".into(),
std::process::id(),
@@ -272,7 +272,7 @@ mod tests {
)
.unwrap();
let err = register_pod(
let err = register_worker(
&mut g,
"other".into(),
std::process::id(),
+5 -5
View File
@@ -9,10 +9,10 @@ use session_store::SegmentId;
/// Errors raised by the mutating pod-registry operations.
#[derive(Debug, thiserror::Error)]
pub enum ScopeLockError {
#[error("I/O error on pods.json: {0}")]
#[error("I/O error on workers.json: {0}")]
Io(#[from] io::Error),
#[error("pod name `{0}` is already registered")]
DuplicatePodName(String),
DuplicateWorkerName(String),
#[error("requested scope `{}` conflicts with pod `{competitor}` rule `{}`", .rule.target.display(), .competitor_rule.target.display())]
WriteConflict {
competitor: String,
@@ -27,14 +27,14 @@ pub enum ScopeLockError {
#[error("invalid delegation scope: {source}")]
InvalidScope { source: ScopeError },
#[error("pod `{0}` is not registered")]
UnknownPod(String),
UnknownWorker(String),
#[error(
"session {segment_id} is already held by pod `{pod_name}` at {}",
"session {segment_id} is already held by pod `{worker_name}` at {}",
.socket.display()
)]
SegmentConflict {
segment_id: SegmentId,
pod_name: String,
worker_name: String,
socket: PathBuf,
},
}
+9 -9
View File
@@ -1,16 +1,16 @@
//! Machine-wide Pod allocation registry.
//! Machine-wide Worker allocation registry.
//!
//! A single JSON file at `<runtime_dir>/pods.json` records every live
//! Pod's allocation (see [`manifest::paths::pod_registry_path`] for
//! A single JSON file at `<runtime_dir>/workers.json` records every live
//! Worker's allocation (see [`manifest::paths::pod_registry_path`] for
//! how the path is resolved). File-level `flock(2)` serialises access
//! across processes so spawn sequences from unrelated Pods can't race.
//! across processes so spawn sequences from unrelated Workers can't race.
//!
//! Each Pod, when starting, acquires the lock, reclaims stale entries
//! (Pods whose PID has died), checks that its requested write scope
//! Each Worker, when starting, acquires the lock, reclaims stale entries
//! (Workers whose PID has died), checks that its requested write scope
//! does not overlap any other allocation's effective write scope, and
//! registers itself. When it exits normally, it removes its entry and
//! returns delegated scope to its `delegated_from` parent. Crash
//! recovery rides on the next Pod that opens the file — no background
//! recovery rides on the next Worker that opens the file — no background
//! reaper.
mod conflict;
@@ -31,7 +31,7 @@ pub use lifecycle::{
install_top_level_with_deny, lookup_segment, update_segment,
};
pub use mutate::{
delegate_scope, reclaim_delegated_scope, reclaim_stale, reclaim_stale_with, register_pod,
register_pod_with_deny, release_pod,
delegate_scope, reclaim_delegated_scope, reclaim_stale, reclaim_stale_with, register_worker,
register_worker_with_deny, release_worker,
};
pub use table::{Allocation, LockFile, LockFileGuard, default_registry_path};
+50 -43
View File
@@ -8,21 +8,21 @@ use manifest::ScopeRule;
use session_store::SegmentId;
use crate::error::ScopeLockError;
use crate::mutate::release_pod;
use crate::mutate::release_worker;
use crate::table::{LockFileGuard, default_registry_path};
/// Owned allocation: on drop, opens the lock file and releases this
/// Pod's entry. The guard keeps only the name + lock-file path; it
/// does not hold the `flock` for the Pod's lifetime.
/// Worker's entry. The guard keeps only the name + lock-file path; it
/// does not hold the `flock` for the Worker's lifetime.
#[derive(Debug)]
pub struct ScopeAllocationGuard {
pod_name: String,
worker_name: String,
lock_path: PathBuf,
}
impl ScopeAllocationGuard {
pub fn pod_name(&self) -> &str {
&self.pod_name
pub fn worker_name(&self) -> &str {
&self.worker_name
}
pub fn lock_path(&self) -> &Path {
@@ -33,28 +33,35 @@ impl ScopeAllocationGuard {
impl Drop for ScopeAllocationGuard {
fn drop(&mut self) {
if let Ok(mut guard) = LockFileGuard::open(&self.lock_path) {
let _ = release_pod(&mut guard, &self.pod_name);
let _ = release_worker(&mut guard, &self.worker_name);
}
}
}
/// Open the default lock file, register a top-level Pod, and return a
/// Open the default lock file, register a top-level Worker, and return a
/// guard that will release the allocation on drop.
pub fn install_top_level(
pod_name: String,
worker_name: String,
pid: u32,
socket: PathBuf,
scope_allow: Vec<ScopeRule>,
segment_id: SegmentId,
) -> Result<ScopeAllocationGuard, ScopeLockError> {
install_top_level_with_deny(pod_name, pid, socket, scope_allow, Vec::new(), segment_id)
install_top_level_with_deny(
worker_name,
pid,
socket,
scope_allow,
Vec::new(),
segment_id,
)
}
/// Open the default lock file, register a top-level Pod with explicit
/// Open the default lock file, register a top-level Worker with explicit
/// deny rules, and return a guard that will release the allocation on
/// drop.
pub fn install_top_level_with_deny(
pod_name: String,
worker_name: String,
pid: u32,
socket: PathBuf,
scope_allow: Vec<ScopeRule>,
@@ -63,9 +70,9 @@ pub fn install_top_level_with_deny(
) -> Result<ScopeAllocationGuard, ScopeLockError> {
let lock_path = default_registry_path()?;
let mut guard = LockFileGuard::open(&lock_path)?;
crate::mutate::register_pod_with_deny(
crate::mutate::register_worker_with_deny(
&mut guard,
pod_name.clone(),
worker_name.clone(),
pid,
socket,
scope_allow,
@@ -73,13 +80,13 @@ pub fn install_top_level_with_deny(
segment_id,
)?;
Ok(ScopeAllocationGuard {
pod_name,
worker_name,
lock_path,
})
}
/// Take ownership of an existing allocation that was pre-registered by
/// a spawning Pod.
/// a spawning Worker.
///
/// The spawning flow is two-stage: the spawner calls
/// [`crate::delegate_scope`] (with its own pid as a live placeholder,
@@ -88,7 +95,7 @@ pub fn install_top_level_with_deny(
/// segment_id to its own and claim the [`ScopeAllocationGuard`] so
/// the entry is released when the child exits.
pub fn adopt_allocation(
pod_name: String,
worker_name: String,
new_pid: u32,
segment_id: SegmentId,
) -> Result<ScopeAllocationGuard, ScopeLockError> {
@@ -96,24 +103,24 @@ pub fn adopt_allocation(
let mut guard = LockFileGuard::open(&lock_path)?;
let alloc = guard
.data_mut()
.find_mut(&pod_name)
.ok_or_else(|| ScopeLockError::UnknownPod(pod_name.clone()))?;
.find_mut(&worker_name)
.ok_or_else(|| ScopeLockError::UnknownWorker(worker_name.clone()))?;
alloc.pid = new_pid;
alloc.segment_id = Some(segment_id);
guard.save()?;
Ok(ScopeAllocationGuard {
pod_name,
worker_name,
lock_path,
})
}
/// Rewrite the `segment_id` recorded for `pod_name` to
/// Rewrite the `segment_id` recorded for `worker_name` to
/// `new_segment_id`.
///
/// The Pod's in-memory `segment_id` can change underneath the
/// The Worker's in-memory `segment_id` can change underneath the
/// allocation in two normal places:
///
/// - `Pod::compact` mints a fresh session and swaps it in.
/// - `Worker::compact` mints a fresh session and swaps it in.
/// - `session_store::ensure_head_or_fork` auto-forks when another
/// writer has advanced the store head behind our back.
///
@@ -121,37 +128,37 @@ pub fn adopt_allocation(
/// find the live session id, not the old one. Without this update a
/// concurrent `restore_from_manifest(new_id)` would see "no live
/// writer" and proceed to register a competing allocation on the
/// session this Pod just moved into.
/// session this Worker just moved into.
///
/// The lock is opened once and the allocation is rewritten inside the
/// guard, so the segment_id collision check is atomic with the
/// rewrite.
pub fn update_segment(pod_name: &str, new_segment_id: SegmentId) -> Result<(), ScopeLockError> {
pub fn update_segment(worker_name: &str, new_segment_id: SegmentId) -> Result<(), ScopeLockError> {
let lock_path = default_registry_path()?;
let mut guard = LockFileGuard::open(&lock_path)?;
if let Some(other) = guard.data().find_by_segment(new_segment_id) {
if other.pod_name != pod_name {
if other.worker_name != worker_name {
return Err(ScopeLockError::SegmentConflict {
segment_id: new_segment_id,
pod_name: other.pod_name.clone(),
worker_name: other.worker_name.clone(),
socket: other.socket.clone(),
});
}
}
let alloc = guard
.data_mut()
.find_mut(pod_name)
.ok_or_else(|| ScopeLockError::UnknownPod(pod_name.into()))?;
.find_mut(worker_name)
.ok_or_else(|| ScopeLockError::UnknownWorker(worker_name.into()))?;
alloc.segment_id = Some(new_segment_id);
guard.save()?;
Ok(())
}
/// Information about a Pod that currently holds an allocation for a
/// Information about a Worker that currently holds an allocation for a
/// given session.
#[derive(Debug, Clone)]
pub struct SegmentLockInfo {
pub pod_name: String,
pub worker_name: String,
pub socket: PathBuf,
pub pid: u32,
}
@@ -159,7 +166,7 @@ pub struct SegmentLockInfo {
/// Open the default lock file, reclaim stale entries, and return the
/// allocation currently writing to `segment_id`, if any.
///
/// Used by `Pod::restore_from_manifest` to refuse a resume that would
/// Used by `Worker::restore_from_manifest` to refuse a resume that would
/// race a live writer on the same source session.
pub fn lookup_segment(segment_id: SegmentId) -> Result<Option<SegmentLockInfo>, ScopeLockError> {
let lock_path = default_registry_path()?;
@@ -169,7 +176,7 @@ pub fn lookup_segment(segment_id: SegmentId) -> Result<Option<SegmentLockInfo>,
.data()
.find_by_segment(segment_id)
.map(|a| SegmentLockInfo {
pod_name: a.pod_name.clone(),
worker_name: a.worker_name.clone(),
socket: a.socket.clone(),
pid: a.pid,
}))
@@ -185,11 +192,11 @@ mod tests {
/// Mimic what the spawner does before the child comes up: push an
/// allocation for the child carrying the spawner's (live) pid as a
/// placeholder. Exists only in tests.
fn delegate_placeholder(g: &mut LockFileGuard, pod_name: &str, placeholder_pid: u32) {
fn delegate_placeholder(g: &mut LockFileGuard, worker_name: &str, placeholder_pid: u32) {
g.data_mut().allocations.push(Allocation {
pod_name: pod_name.to_string(),
worker_name: worker_name.to_string(),
pid: placeholder_pid,
socket: sock(pod_name),
socket: sock(worker_name),
scope_allow: vec![write_rule("/tmp/child", true)],
scope_deny: Vec::new(),
delegated_from: None,
@@ -202,7 +209,7 @@ mod tests {
fn scope_allocation_guard_releases_on_drop() {
let dir = TempDir::new().unwrap();
let _sandbox = RuntimeDirSandbox::new(dir.path());
let lock_path = dir.path().join("pods.json");
let lock_path = dir.path().join("workers.json");
let guard = install_top_level(
"a".into(),
std::process::id(),
@@ -226,7 +233,7 @@ mod tests {
fn adopt_allocation_rewrites_pid_and_releases_on_drop() {
let dir = TempDir::new().unwrap();
let _sandbox = RuntimeDirSandbox::new(dir.path());
let lock_path = dir.path().join("pods.json");
let lock_path = dir.path().join("workers.json");
// Pre-register an allocation under spawner's pid, as delegate_scope would.
{
let mut g = LockFileGuard::open(&lock_path).unwrap();
@@ -251,7 +258,7 @@ mod tests {
let dir = TempDir::new().unwrap();
let _sandbox = RuntimeDirSandbox::new(dir.path());
let err = adopt_allocation("ghost".into(), 42, sid()).unwrap_err();
assert!(matches!(err, ScopeLockError::UnknownPod(ref n) if n == "ghost"));
assert!(matches!(err, ScopeLockError::UnknownWorker(ref n) if n == "ghost"));
}
#[test]
@@ -268,7 +275,7 @@ mod tests {
)
.unwrap();
let info = lookup_segment(s).unwrap().expect("expected live writer");
assert_eq!(info.pod_name, "live");
assert_eq!(info.worker_name, "live");
assert_eq!(info.socket, sock("live"));
drop(guard);
// After the guard's release, the lookup goes back to None.
@@ -292,7 +299,7 @@ mod tests {
update_segment("p", updated).unwrap();
// lookup against the original is now empty, the updated id wins.
assert!(lookup_segment(original).unwrap().is_none());
assert_eq!(lookup_segment(updated).unwrap().unwrap().pod_name, "p");
assert_eq!(lookup_segment(updated).unwrap().unwrap().worker_name, "p");
}
#[test]
@@ -321,11 +328,11 @@ mod tests {
let err = update_segment("a", s_b).unwrap_err();
match err {
ScopeLockError::SegmentConflict {
pod_name,
worker_name,
segment_id,
..
} => {
assert_eq!(pod_name, "b");
assert_eq!(worker_name, "b");
assert_eq!(segment_id, s_b);
}
other => panic!("expected SegmentConflict, got {other:?}"),
+78 -78
View File
@@ -11,24 +11,24 @@ use crate::conflict::{find_conflict_owner, find_conflict_owners};
use crate::error::ScopeLockError;
use crate::table::{Allocation, LockFileGuard};
/// Register a top-level Pod (started directly by a human, no
/// Register a top-level Worker (started directly by a human, no
/// delegation parent). Reclaims stale entries before checking
/// conflicts so a crashed Pod's allocation doesn't block the new one.
/// conflicts so a crashed Worker's allocation doesn't block the new one.
///
/// Rejects when another live allocation is already writing to
/// `segment_id`, so two `restore_from_manifest` calls under different
/// `pod_name`s cannot both grab the same session log.
pub fn register_pod(
/// `worker_name`s cannot both grab the same session log.
pub fn register_worker(
guard: &mut LockFileGuard,
pod_name: String,
worker_name: String,
pid: u32,
socket: PathBuf,
scope_allow: Vec<ScopeRule>,
segment_id: SegmentId,
) -> Result<(), ScopeLockError> {
register_pod_with_deny(
register_worker_with_deny(
guard,
pod_name,
worker_name,
pid,
socket,
scope_allow,
@@ -37,21 +37,21 @@ pub fn register_pod(
)
}
/// Register a top-level Pod with explicit deny rules that reduce the
/// Register a top-level Worker with explicit deny rules that reduce the
/// claimed effective write scope.
///
/// Conflict semantics: if every Pod overlapping a requested allow rule
/// Conflict semantics: if every Worker overlapping a requested allow rule
/// is fully covered by one of `scope_deny`, the conflict is suppressed
/// and the registration proceeds. The check is structural (deny ⊇
/// competitor.rule), not relational — it does not verify that the
/// competitor actually descends from this Pod's prior delegations.
/// competitor actually descends from this Worker's prior delegations.
/// In practice this is safe because the canonical restore caller derives
/// `scope_deny` from outstanding `pod-store` child delegations, so any
/// covered competitor is expected to be a descendant of the original
/// allocation. Direct callers must uphold the same invariant.
pub fn register_pod_with_deny(
pub fn register_worker_with_deny(
guard: &mut LockFileGuard,
pod_name: String,
worker_name: String,
pid: u32,
socket: PathBuf,
scope_allow: Vec<ScopeRule>,
@@ -59,13 +59,13 @@ pub fn register_pod_with_deny(
segment_id: SegmentId,
) -> Result<(), ScopeLockError> {
reclaim_stale(guard);
if guard.data().find(&pod_name).is_some() {
return Err(ScopeLockError::DuplicatePodName(pod_name));
if guard.data().find(&worker_name).is_some() {
return Err(ScopeLockError::DuplicateWorkerName(worker_name));
}
if let Some(existing) = guard.data().find_by_segment(segment_id) {
return Err(ScopeLockError::SegmentConflict {
segment_id,
pod_name: existing.pod_name.clone(),
worker_name: existing.worker_name.clone(),
socket: existing.socket.clone(),
});
}
@@ -86,14 +86,14 @@ pub fn register_pod_with_deny(
}
if let Some(competitor) = conflicts.into_iter().next() {
return Err(ScopeLockError::WriteConflict {
competitor: competitor.pod_name,
competitor: competitor.worker_name,
rule: rule.clone(),
competitor_rule: competitor.rule,
});
}
}
guard.data_mut().allocations.push(Allocation {
pod_name,
worker_name,
pid,
socket,
scope_allow,
@@ -105,9 +105,9 @@ pub fn register_pod_with_deny(
Ok(())
}
/// Register a spawned Pod whose scope is delegated from `spawner`.
/// Register a spawned Worker whose scope is delegated from `spawner`.
/// The requested scope must be within the spawner's delegation authority;
/// overlap with any Pod other than `spawner` is a conflict.
/// overlap with any Worker other than `spawner` is a conflict.
pub fn delegate_scope(
guard: &mut LockFileGuard,
spawner: &str,
@@ -119,10 +119,10 @@ pub fn delegate_scope(
) -> Result<(), ScopeLockError> {
reclaim_stale(guard);
if guard.data().find(&spawned).is_some() {
return Err(ScopeLockError::DuplicatePodName(spawned));
return Err(ScopeLockError::DuplicateWorkerName(spawned));
}
if guard.data().find(spawner).is_none() {
return Err(ScopeLockError::UnknownPod(spawner.into()));
return Err(ScopeLockError::UnknownWorker(spawner.into()));
}
for rule in &scope_allow {
let allowed = delegation_scope
@@ -137,7 +137,7 @@ pub fn delegate_scope(
if rule.permission == Permission::Write {
if let Some(competitor) = find_conflict_owner(guard.data(), rule, Some(spawner)) {
return Err(ScopeLockError::WriteConflict {
competitor: competitor.pod_name,
competitor: competitor.worker_name,
rule: rule.clone(),
competitor_rule: competitor.rule,
});
@@ -145,7 +145,7 @@ pub fn delegate_scope(
}
}
guard.data_mut().allocations.push(Allocation {
pod_name: spawned,
worker_name: spawned,
pid,
socket,
scope_allow,
@@ -159,21 +159,21 @@ pub fn delegate_scope(
Ok(())
}
/// Remove a Pod's allocation. Surviving children are reparented to
/// the removed Pod's own `delegated_from`, so the delegation tree
/// Remove a Worker's allocation. Surviving children are reparented to
/// the removed Worker's own `delegated_from`, so the delegation tree
/// stays connected.
pub fn release_pod(guard: &mut LockFileGuard, pod_name: &str) -> Result<(), ScopeLockError> {
pub fn release_worker(guard: &mut LockFileGuard, worker_name: &str) -> Result<(), ScopeLockError> {
let idx = guard
.data()
.allocations
.iter()
.position(|a| a.pod_name == pod_name);
.position(|a| a.worker_name == worker_name);
let Some(idx) = idx else {
return Err(ScopeLockError::UnknownPod(pod_name.into()));
return Err(ScopeLockError::UnknownWorker(worker_name.into()));
};
let removed = guard.data().allocations[idx].clone();
for alloc in guard.data_mut().allocations.iter_mut() {
if alloc.delegated_from.as_deref() == Some(pod_name) {
if alloc.delegated_from.as_deref() == Some(worker_name) {
alloc.delegated_from.clone_from(&removed.delegated_from);
}
}
@@ -187,7 +187,7 @@ pub fn release_pod(guard: &mut LockFileGuard, pod_name: &str) -> Result<(), Scop
/// This is idempotent for missing deny entries. For each delegated Write rule,
/// at most one exact matching deny rule is removed from the parent's `scope_deny`
/// even when the child allocation is already absent; restore reconciliation uses
/// that case when durable Pod-state still records an outstanding delegation but
/// that case when durable Worker-state still records an outstanding delegation but
/// the live lock file no longer has a child allocation.
pub fn reclaim_delegated_scope(
guard: &mut LockFileGuard,
@@ -199,7 +199,7 @@ pub fn reclaim_delegated_scope(
.data()
.allocations
.iter()
.position(|a| a.pod_name == child);
.position(|a| a.worker_name == child);
let removed_child_parent = child_idx
.map(|idx| guard.data().allocations[idx].delegated_from.clone())
.unwrap_or(None);
@@ -229,8 +229,8 @@ pub fn reclaim_delegated_scope(
}
/// Remove allocations whose PID is dead, reparenting children to the
/// dead Pod's `delegated_from`. Idempotent and best-effort — I/O
/// errors on save are swallowed so a crashed Pod's entry never blocks
/// dead Worker's `delegated_from`. Idempotent and best-effort — I/O
/// errors on save are swallowed so a crashed Worker's entry never blocks
/// forward progress.
pub fn reclaim_stale(guard: &mut LockFileGuard) {
reclaim_stale_with(guard, pid_alive);
@@ -243,7 +243,7 @@ pub fn reclaim_stale_with(guard: &mut LockFileGuard, mut is_alive: impl FnMut(u3
.allocations
.iter()
.filter(|a| !is_alive(a.pid))
.map(|a| a.pod_name.clone())
.map(|a| a.worker_name.clone())
.collect();
if dead.is_empty() {
return;
@@ -253,7 +253,7 @@ pub fn reclaim_stale_with(guard: &mut LockFileGuard, mut is_alive: impl FnMut(u3
.data()
.allocations
.iter()
.position(|a| a.pod_name == *name)
.position(|a| a.worker_name == *name)
else {
continue;
};
@@ -294,9 +294,9 @@ mod tests {
#[test]
fn register_detects_write_conflict() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -305,7 +305,7 @@ mod tests {
sid(),
)
.unwrap();
let err = register_pod(
let err = register_worker(
&mut g,
"b".into(),
std::process::id(),
@@ -321,11 +321,11 @@ mod tests {
}
#[test]
fn duplicate_pod_name_rejected() {
fn duplicate_worker_name_rejected() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -334,7 +334,7 @@ mod tests {
sid(),
)
.unwrap();
let err = register_pod(
let err = register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -343,15 +343,15 @@ mod tests {
sid(),
)
.unwrap_err();
assert!(matches!(err, ScopeLockError::DuplicatePodName(ref n) if n == "a"));
assert!(matches!(err, ScopeLockError::DuplicateWorkerName(ref n) if n == "a"));
}
#[test]
fn delegate_must_be_subset() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -376,9 +376,9 @@ mod tests {
#[test]
fn delegate_uses_delegation_scope_not_direct_effective_write() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"orchestrator".into(),
std::process::id(),
@@ -406,9 +406,9 @@ mod tests {
#[test]
fn delegate_succeeds_within_parent_scope() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -445,9 +445,9 @@ mod tests {
#[test]
fn delegate_rejects_sibling_overlap() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -486,9 +486,9 @@ mod tests {
#[test]
fn release_reparents_children() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -517,7 +517,7 @@ mod tests {
&delegation_scope(vec![write_rule("/src/core", true)]),
)
.unwrap();
release_pod(&mut g, "b").unwrap();
release_worker(&mut g, "b").unwrap();
// D should now list A as its delegated_from.
let d = g.data().find("d").unwrap();
assert_eq!(d.delegated_from.as_deref(), Some("a"));
@@ -527,10 +527,10 @@ mod tests {
#[test]
fn reclaim_delegated_scope_removes_child_and_one_parent_deny_layer() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
let delegated_rule = write_rule("/src/core", true);
register_pod_with_deny(
register_worker_with_deny(
&mut g,
"a".into(),
std::process::id(),
@@ -540,7 +540,7 @@ mod tests {
sid(),
)
.unwrap();
register_pod(
register_worker(
&mut g,
"b".into(),
std::process::id(),
@@ -566,10 +566,10 @@ mod tests {
#[test]
fn reclaim_delegated_scope_removes_parent_deny_when_child_allocation_missing() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
let delegated_rule = write_rule("/src/core", true);
register_pod_with_deny(
register_worker_with_deny(
&mut g,
"a".into(),
std::process::id(),
@@ -595,9 +595,9 @@ mod tests {
#[test]
fn reclaim_stale_reparents_and_removes_dead_entries() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -630,7 +630,7 @@ mod tests {
// will treat as dead.
let fake_dead_pid: u32 = 0xffff_fff0;
for alloc in g.data_mut().allocations.iter_mut() {
if alloc.pod_name == "b" {
if alloc.worker_name == "b" {
alloc.pid = fake_dead_pid;
}
}
@@ -643,9 +643,9 @@ mod tests {
#[test]
fn read_rules_do_not_conflict_with_write() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -655,7 +655,7 @@ mod tests {
)
.unwrap();
// B only reads under the same tree — allowed.
register_pod(
register_worker(
&mut g,
"b".into(),
std::process::id(),
@@ -670,9 +670,9 @@ mod tests {
#[test]
fn releasing_pod_reopens_scope_for_fresh_registration() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -681,8 +681,8 @@ mod tests {
sid(),
)
.unwrap();
release_pod(&mut g, "a").unwrap();
register_pod(
release_worker(&mut g, "a").unwrap();
register_worker(
&mut g,
"b".into(),
std::process::id(),
@@ -696,9 +696,9 @@ mod tests {
#[test]
fn delegated_scope_returns_to_parent_on_release() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -722,7 +722,7 @@ mod tests {
"a",
&write_rule("/src/core", true)
));
release_pod(&mut g, "b").unwrap();
release_worker(&mut g, "b").unwrap();
// /src/core is back in A's effective write scope.
assert!(is_within_effective_write(
g.data(),
@@ -734,10 +734,10 @@ mod tests {
#[test]
fn register_pod_rejects_session_id_collision() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
let shared_session = sid();
register_pod(
register_worker(
&mut g,
"first".into(),
std::process::id(),
@@ -747,9 +747,9 @@ mod tests {
)
.unwrap();
// Second registration tries to grab the same segment_id under
// a different pod_name. Without the SegmentConflict check both
// a different worker_name. Without the SegmentConflict check both
// would succeed and race on the same jsonl.
let err = register_pod(
let err = register_worker(
&mut g,
"second".into(),
std::process::id(),
@@ -761,11 +761,11 @@ mod tests {
match err {
ScopeLockError::SegmentConflict {
segment_id,
pod_name,
worker_name,
..
} => {
assert_eq!(segment_id, shared_session);
assert_eq!(pod_name, "first");
assert_eq!(worker_name, "first");
}
other => panic!("expected SegmentConflict, got {other:?}"),
}
+32 -28
View File
@@ -22,33 +22,33 @@ pub struct LockFile {
pub allocations: Vec<Allocation>,
}
/// One Pod's scope allocation.
/// One Worker's scope allocation.
///
/// `scope_allow` is the full set of allow rules the Pod was granted.
/// Portions delegated out to child Pods are **not** subtracted in
/// `scope_allow` is the full set of allow rules the Worker was granted.
/// Portions delegated out to child Workers are **not** subtracted in
/// storage — the effective write scope is derived on the fly by
/// removing rules owned by any Pod whose `delegated_from` points to
/// removing rules owned by any Worker whose `delegated_from` points to
/// this one. Keeping the raw allow set makes reparenting (stale
/// reclaim) trivial.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Allocation {
/// Pod name — also the identity used throughout orchestration.
pub pod_name: String,
/// Worker name — also the identity used throughout orchestration.
pub worker_name: String,
/// Owning process. Checked with `kill(pid, 0)` for stale detection.
pub pid: u32,
/// Pod's Unix socket path.
/// Worker's Unix socket path.
pub socket: PathBuf,
/// Allow rules granted to this Pod (write + read).
/// Allow rules granted to this Worker (write + read).
pub scope_allow: Vec<ScopeRule>,
/// Deny rules that cap this Pod's effective scope. Normally empty for
/// fresh allocations; restored Pods use this to avoid reclaiming
/// Deny rules that cap this Worker's effective scope. Normally empty for
/// fresh allocations; restored Workers use this to avoid reclaiming
/// previously delegated write regions.
#[serde(default)]
pub scope_deny: Vec<ScopeRule>,
/// Name of the Pod that delegated scope to this one, or `None` for
/// a top-level Pod started directly by a human.
/// Name of the Worker that delegated scope to this one, or `None` for
/// a top-level Worker started directly by a human.
pub delegated_from: Option<String>,
/// Segment ID this Pod is currently writing to. `None` means this
/// Segment ID this Worker is currently writing to. `None` means this
/// is a pre-reservation made by a spawner via [`crate::delegate_scope`]
/// before the child has come up; the child fills it in at
/// [`crate::adopt_allocation`] time.
@@ -57,12 +57,16 @@ pub struct Allocation {
}
impl LockFile {
pub fn find(&self, pod_name: &str) -> Option<&Allocation> {
self.allocations.iter().find(|a| a.pod_name == pod_name)
pub fn find(&self, worker_name: &str) -> Option<&Allocation> {
self.allocations
.iter()
.find(|a| a.worker_name == worker_name)
}
pub fn find_mut(&mut self, pod_name: &str) -> Option<&mut Allocation> {
self.allocations.iter_mut().find(|a| a.pod_name == pod_name)
pub fn find_mut(&mut self, worker_name: &str) -> Option<&mut Allocation> {
self.allocations
.iter_mut()
.find(|a| a.worker_name == worker_name)
}
/// Find the allocation currently writing to `segment_id`. Skips
@@ -74,7 +78,7 @@ impl LockFile {
}
}
/// Default on-disk path: `<runtime_dir>/pods.json` resolved via
/// Default on-disk path: `<runtime_dir>/workers.json` resolved via
/// [`manifest::paths::pod_registry_path`]. Tests should point this
/// elsewhere by setting `YOI_HOME` or `YOI_RUNTIME_DIR` to a
/// tempdir.
@@ -82,7 +86,7 @@ pub fn default_registry_path() -> io::Result<PathBuf> {
paths::pod_registry_path().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"could not resolve pods.json path (no YOI_HOME / \
"could not resolve workers.json path (no YOI_HOME / \
YOI_RUNTIME_DIR / XDG_RUNTIME_DIR / HOME)",
)
})
@@ -173,7 +177,7 @@ impl LockFileGuard {
serde_json::from_str(&buf).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("pods.json parse error: {e}"),
format!("workers.json parse error: {e}"),
)
})?
};
@@ -215,7 +219,7 @@ mod tests {
#[test]
fn open_creates_empty_lock_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let guard = LockFileGuard::open(&path).unwrap();
assert!(guard.data().allocations.is_empty());
assert!(path.exists());
@@ -226,7 +230,7 @@ mod tests {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let parent = dir.path().join("yoi");
let path = parent.join("pods.json");
let path = parent.join("workers.json");
let _guard = LockFileGuard::open(&path).unwrap();
let file_mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(file_mode, 0o600, "file mode = {file_mode:o}");
@@ -237,10 +241,10 @@ mod tests {
#[test]
fn save_and_reopen_roundtrip() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
{
let mut g = open_empty(&path);
register_pod(
register_worker(
&mut g,
"a".into(),
std::process::id(),
@@ -252,19 +256,19 @@ mod tests {
}
let guard = LockFileGuard::open(&path).unwrap();
assert_eq!(guard.data().allocations.len(), 1);
assert_eq!(guard.data().allocations[0].pod_name, "a");
assert_eq!(guard.data().allocations[0].worker_name, "a");
}
#[test]
fn find_by_session_skips_none_placeholders() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("pods.json");
let path = dir.path().join("workers.json");
let mut g = open_empty(&path);
// Pre-reservation: delegate_scope leaves segment_id = None
// until adopt_allocation rewrites it. find_by_segment must not
// match those placeholders, otherwise a freshly-spawning child
// would shadow itself before it has even chosen a session.
register_pod(
register_worker(
&mut g,
"parent".into(),
std::process::id(),
@@ -292,6 +296,6 @@ mod tests {
// After adopt-style rewrite, the same allocation is now found.
g.data_mut().find_mut("child").unwrap().segment_id = Some(target_session);
let found = g.data().find_by_segment(target_session).unwrap();
assert_eq!(found.pod_name, "child");
assert_eq!(found.worker_name, "child");
}
}