feat: resume時のscope claimを過去の有効scopeに揃える
This commit is contained in:
@@ -32,7 +32,7 @@ pub(crate) fn rules_overlap(a: &ScopeRule, b: &ScopeRule) -> bool {
|
||||
}
|
||||
|
||||
/// Does `cover` fully contain `inner`'s claimed paths?
|
||||
fn covers_fully(cover: &ScopeRule, inner: &ScopeRule) -> bool {
|
||||
pub(crate) fn covers_fully(cover: &ScopeRule, inner: &ScopeRule) -> bool {
|
||||
if cover.permission < inner.permission {
|
||||
return false;
|
||||
}
|
||||
@@ -44,8 +44,9 @@ fn covers_fully(cover: &ScopeRule, inner: &ScopeRule) -> bool {
|
||||
}
|
||||
|
||||
/// Check whether `rule` is contained in `parent`'s effective write
|
||||
/// scope: its allow set covers `rule`, and no child of `parent` has
|
||||
/// already taken a piece that would overlap `rule`.
|
||||
/// scope: its allow set covers `rule`, no deny rule caps it, and no
|
||||
/// child of `parent` has already taken a piece that would overlap
|
||||
/// `rule`.
|
||||
pub fn is_within_effective_write(lock: &LockFile, parent: &str, rule: &ScopeRule) -> bool {
|
||||
let Some(alloc) = lock.find(parent) else {
|
||||
return false;
|
||||
@@ -61,6 +62,14 @@ pub fn is_within_effective_write(lock: &LockFile, parent: &str, rule: &ScopeRule
|
||||
if !covered {
|
||||
return false;
|
||||
}
|
||||
let denied = alloc
|
||||
.scope_deny
|
||||
.iter()
|
||||
.filter(|r| r.permission == Permission::Write)
|
||||
.any(|r| rules_overlap(r, rule));
|
||||
if denied {
|
||||
return false;
|
||||
}
|
||||
let child_conflict = lock
|
||||
.allocations
|
||||
.iter()
|
||||
@@ -71,7 +80,14 @@ pub fn is_within_effective_write(lock: &LockFile, parent: &str, rule: &ScopeRule
|
||||
!child_conflict
|
||||
}
|
||||
|
||||
/// Find the Pod that actually owns a write scope overlapping `rule`.
|
||||
/// The Pod and rule that actually own a conflicting write scope.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConflictOwner {
|
||||
pub pod_name: String,
|
||||
pub rule: ScopeRule,
|
||||
}
|
||||
|
||||
/// Find the Pod/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
|
||||
@@ -82,38 +98,47 @@ pub fn find_conflict_owner(
|
||||
lock: &LockFile,
|
||||
rule: &ScopeRule,
|
||||
exempt: Option<&str>,
|
||||
) -> Option<String> {
|
||||
) -> Option<ConflictOwner> {
|
||||
find_conflict_owners(lock, rule, exempt).into_iter().next()
|
||||
}
|
||||
|
||||
/// Find every top-level delegation tree owner that conflicts with `rule`.
|
||||
pub fn find_conflict_owners(
|
||||
lock: &LockFile,
|
||||
rule: &ScopeRule,
|
||||
exempt: Option<&str>,
|
||||
) -> Vec<ConflictOwner> {
|
||||
if rule.permission != Permission::Write {
|
||||
return None;
|
||||
return Vec::new();
|
||||
}
|
||||
for alloc in lock
|
||||
.allocations
|
||||
lock.allocations
|
||||
.iter()
|
||||
.filter(|a| a.delegated_from.is_none())
|
||||
{
|
||||
if let Some(owner) = find_conflict_in_subtree(lock, alloc, rule) {
|
||||
if Some(owner.as_str()) == exempt {
|
||||
continue;
|
||||
}
|
||||
return Some(owner);
|
||||
}
|
||||
}
|
||||
None
|
||||
.filter_map(|alloc| find_conflict_in_subtree(lock, alloc, rule))
|
||||
.filter(|owner| Some(owner.pod_name.as_str()) != exempt)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn find_conflict_in_subtree(
|
||||
lock: &LockFile,
|
||||
alloc: &Allocation,
|
||||
rule: &ScopeRule,
|
||||
) -> Option<String> {
|
||||
let overlaps_here = alloc
|
||||
) -> Option<ConflictOwner> {
|
||||
let overlapping_rule = alloc
|
||||
.scope_allow
|
||||
.iter()
|
||||
.filter(|r| r.permission == Permission::Write)
|
||||
.any(|r| rules_overlap(r, rule));
|
||||
if !overlaps_here {
|
||||
.find(|r| rules_overlap(r, rule))?;
|
||||
|
||||
let fully_denied_here = alloc
|
||||
.scope_deny
|
||||
.iter()
|
||||
.filter(|r| r.permission == Permission::Write)
|
||||
.any(|r| covers_fully(r, rule));
|
||||
if fully_denied_here {
|
||||
return None;
|
||||
}
|
||||
|
||||
for child in lock
|
||||
.allocations
|
||||
.iter()
|
||||
@@ -123,14 +148,17 @@ fn find_conflict_in_subtree(
|
||||
return Some(owner);
|
||||
}
|
||||
}
|
||||
Some(alloc.pod_name.clone())
|
||||
Some(ConflictOwner {
|
||||
pod_name: alloc.pod_name.clone(),
|
||||
rule: overlapping_rule.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_util::*;
|
||||
use crate::{ScopeLockError, delegate_scope, register_pod};
|
||||
use crate::{ScopeLockError, delegate_scope, register_pod, register_pod_with_deny};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
@@ -200,4 +228,69 @@ mod tests {
|
||||
other => panic!("expected WriteConflict, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn denied_write_region_is_not_claimed_by_restored_parent() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("pods.json");
|
||||
let mut g = open_empty(&path);
|
||||
register_pod_with_deny(
|
||||
&mut g,
|
||||
"parent".into(),
|
||||
std::process::id(),
|
||||
sock("parent"),
|
||||
vec![write_rule("/src", true)],
|
||||
vec![write_rule("/src/core", true)],
|
||||
sid(),
|
||||
)
|
||||
.unwrap();
|
||||
register_pod(
|
||||
&mut g,
|
||||
"child".into(),
|
||||
std::process::id(),
|
||||
sock("child"),
|
||||
vec![write_rule("/src/core", true)],
|
||||
sid(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_deny_does_not_hide_parent_conflict() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("pods.json");
|
||||
let mut g = open_empty(&path);
|
||||
register_pod_with_deny(
|
||||
&mut g,
|
||||
"parent".into(),
|
||||
std::process::id(),
|
||||
sock("parent"),
|
||||
vec![write_rule("/src", true)],
|
||||
vec![write_rule("/src/core", true)],
|
||||
sid(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let err = register_pod(
|
||||
&mut g,
|
||||
"other".into(),
|
||||
std::process::id(),
|
||||
sock("other"),
|
||||
vec![write_rule("/src", true)],
|
||||
sid(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
match err {
|
||||
ScopeLockError::WriteConflict {
|
||||
competitor,
|
||||
competitor_rule,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(competitor, "parent");
|
||||
assert_eq!(competitor_rule.target, std::path::PathBuf::from("/src"));
|
||||
}
|
||||
other => panic!("expected WriteConflict, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,12 @@ pub enum ScopeLockError {
|
||||
Io(#[from] io::Error),
|
||||
#[error("pod name `{0}` is already registered")]
|
||||
DuplicatePodName(String),
|
||||
#[error("requested scope `{}` conflicts with pod `{competitor}`", .rule.target.display())]
|
||||
WriteConflict { competitor: String, rule: ScopeRule },
|
||||
#[error("requested scope `{}` conflicts with pod `{competitor}` rule `{}`", .rule.target.display(), .competitor_rule.target.display())]
|
||||
WriteConflict {
|
||||
competitor: String,
|
||||
rule: ScopeRule,
|
||||
competitor_rule: ScopeRule,
|
||||
},
|
||||
#[error(
|
||||
"requested scope `{}` is not within spawner `{spawner}`'s effective scope",
|
||||
.rule.target.display()
|
||||
|
||||
@@ -22,11 +22,16 @@ mod table;
|
||||
#[cfg(test)]
|
||||
mod test_util;
|
||||
|
||||
pub use conflict::{find_conflict_owner, is_within_effective_write};
|
||||
pub use conflict::{
|
||||
ConflictOwner, find_conflict_owner, find_conflict_owners, is_within_effective_write,
|
||||
};
|
||||
pub use error::ScopeLockError;
|
||||
pub use lifecycle::{
|
||||
ScopeAllocationGuard, SessionLockInfo, adopt_allocation, install_top_level, lookup_session,
|
||||
update_session,
|
||||
ScopeAllocationGuard, SessionLockInfo, adopt_allocation, install_top_level,
|
||||
install_top_level_with_deny, lookup_session, update_session,
|
||||
};
|
||||
pub use mutate::{
|
||||
delegate_scope, reclaim_stale, reclaim_stale_with, register_pod, register_pod_with_deny,
|
||||
release_pod,
|
||||
};
|
||||
pub use mutate::{delegate_scope, reclaim_stale, reclaim_stale_with, register_pod, release_pod};
|
||||
pub use table::{Allocation, LockFile, LockFileGuard, default_registry_path};
|
||||
|
||||
@@ -8,7 +8,7 @@ use manifest::ScopeRule;
|
||||
use session_store::SessionId;
|
||||
|
||||
use crate::error::ScopeLockError;
|
||||
use crate::mutate::{register_pod, release_pod};
|
||||
use crate::mutate::release_pod;
|
||||
use crate::table::{LockFileGuard, default_registry_path};
|
||||
|
||||
/// Owned allocation: on drop, opens the lock file and releases this
|
||||
@@ -46,15 +46,30 @@ pub fn install_top_level(
|
||||
socket: PathBuf,
|
||||
scope_allow: Vec<ScopeRule>,
|
||||
session_id: SessionId,
|
||||
) -> Result<ScopeAllocationGuard, ScopeLockError> {
|
||||
install_top_level_with_deny(pod_name, pid, socket, scope_allow, Vec::new(), session_id)
|
||||
}
|
||||
|
||||
/// Open the default lock file, register a top-level Pod 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,
|
||||
pid: u32,
|
||||
socket: PathBuf,
|
||||
scope_allow: Vec<ScopeRule>,
|
||||
scope_deny: Vec<ScopeRule>,
|
||||
session_id: SessionId,
|
||||
) -> Result<ScopeAllocationGuard, ScopeLockError> {
|
||||
let lock_path = default_registry_path()?;
|
||||
let mut guard = LockFileGuard::open(&lock_path)?;
|
||||
register_pod(
|
||||
crate::mutate::register_pod_with_deny(
|
||||
&mut guard,
|
||||
pod_name.clone(),
|
||||
pid,
|
||||
socket,
|
||||
scope_allow,
|
||||
scope_deny,
|
||||
session_id,
|
||||
)?;
|
||||
Ok(ScopeAllocationGuard {
|
||||
@@ -176,6 +191,7 @@ mod tests {
|
||||
pid: placeholder_pid,
|
||||
socket: sock(pod_name),
|
||||
scope_allow: vec![write_rule("/tmp/child", true)],
|
||||
scope_deny: Vec::new(),
|
||||
delegated_from: None,
|
||||
session_id: None,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::path::PathBuf;
|
||||
use manifest::{Permission, ScopeRule};
|
||||
use session_store::SessionId;
|
||||
|
||||
use crate::conflict::{find_conflict_owner, is_within_effective_write};
|
||||
use crate::conflict::{find_conflict_owner, find_conflict_owners, is_within_effective_write};
|
||||
use crate::error::ScopeLockError;
|
||||
use crate::table::{Allocation, LockFileGuard};
|
||||
|
||||
@@ -25,6 +25,28 @@ pub fn register_pod(
|
||||
socket: PathBuf,
|
||||
scope_allow: Vec<ScopeRule>,
|
||||
session_id: SessionId,
|
||||
) -> Result<(), ScopeLockError> {
|
||||
register_pod_with_deny(
|
||||
guard,
|
||||
pod_name,
|
||||
pid,
|
||||
socket,
|
||||
scope_allow,
|
||||
Vec::new(),
|
||||
session_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Register a top-level Pod with explicit deny rules that reduce the
|
||||
/// claimed effective write scope.
|
||||
pub fn register_pod_with_deny(
|
||||
guard: &mut LockFileGuard,
|
||||
pod_name: String,
|
||||
pid: u32,
|
||||
socket: PathBuf,
|
||||
scope_allow: Vec<ScopeRule>,
|
||||
scope_deny: Vec<ScopeRule>,
|
||||
session_id: SessionId,
|
||||
) -> Result<(), ScopeLockError> {
|
||||
reclaim_stale(guard);
|
||||
if guard.data().find(&pod_name).is_some() {
|
||||
@@ -41,10 +63,22 @@ pub fn register_pod(
|
||||
.iter()
|
||||
.filter(|r| r.permission == Permission::Write)
|
||||
{
|
||||
if let Some(competitor) = find_conflict_owner(guard.data(), rule, None) {
|
||||
let conflicts = find_conflict_owners(guard.data(), rule, None);
|
||||
let all_denied = !conflicts.is_empty()
|
||||
&& conflicts.iter().all(|owner| {
|
||||
scope_deny
|
||||
.iter()
|
||||
.filter(|r| r.permission == Permission::Write)
|
||||
.any(|deny| crate::conflict::covers_fully(deny, &owner.rule))
|
||||
});
|
||||
if all_denied {
|
||||
continue;
|
||||
}
|
||||
if let Some(competitor) = conflicts.into_iter().next() {
|
||||
return Err(ScopeLockError::WriteConflict {
|
||||
competitor,
|
||||
competitor: competitor.pod_name,
|
||||
rule: rule.clone(),
|
||||
competitor_rule: competitor.rule,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -53,6 +87,7 @@ pub fn register_pod(
|
||||
pid,
|
||||
socket,
|
||||
scope_allow,
|
||||
scope_deny,
|
||||
delegated_from: None,
|
||||
session_id: Some(session_id),
|
||||
});
|
||||
@@ -88,8 +123,9 @@ 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: competitor.pod_name,
|
||||
rule: rule.clone(),
|
||||
competitor_rule: competitor.rule,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -99,6 +135,7 @@ pub fn delegate_scope(
|
||||
pid,
|
||||
socket,
|
||||
scope_allow,
|
||||
scope_deny: Vec::new(),
|
||||
delegated_from: Some(spawner.into()),
|
||||
// Pre-reservation. The child fills in its own session_id when
|
||||
// it calls `adopt_allocation` after the worker is built.
|
||||
|
||||
@@ -35,6 +35,11 @@ pub struct Allocation {
|
||||
pub socket: PathBuf,
|
||||
/// Allow rules granted to this Pod (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
|
||||
/// 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.
|
||||
pub delegated_from: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user