refactor: rename pod crate to worker
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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:?}"),
|
||||
|
||||
@@ -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:?}"),
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user