mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 01:48:20 -10:00
coordination: mount the TUI runtime shared, keep serve exclusive (#7042)
`new_shared_runtime_subagent_manager` set `coordination_exclusive = true` and
held the per-workspace coordination flock for the life of the process. The
embedded TUI background runtime and the standalone host both went through that
one constructor, so a second `codewhale` in the same folder could not take the
lock shared and every append in it was refused:
another Codewhale process owns delegated coordination for <workspace>:
Resource temporarily unavailable (os error 11)
`docs/architecture/delegated-coordination.md` reserves the exclusive holder for
`codewhale serve` and the other standalone Runtime hosts, where the whole
process is the writer.
Split the constructor: `new_shared_runtime_subagent_manager` keeps the shared
posture the TUI runtime mount is documented to take, and the new
`new_exclusive_runtime_subagent_manager` carries the exclusive one for
`runtime_api_sub_agent_manager`, i.e. the HTTP/serve host. After this,
`coordination_exclusive = true` is reachable only from that host call site.
The architecture doc claimed the standalone host "refuses to start" without the
lock. It does not: `ensure_coordination_process_lock` failing sets
`coordination_snapshot_read_only`, logs, and the ledger is picked up on the next
start. The doc now says what the code does instead of the reverse.
Regression source: 23e9119caf.
Verification (frozen tree, `CODEWHALE_CACHE_ROOT=/Volumes/VIXinSSD/cw-build-cache`):
./scripts/dev-cargo.sh test -p codewhale-tui --lib -- \
runtime_mount_coordination_lock_is_shared_with_parallel_sessions \
headless_worker_registration_enforces_live_claims_and_projects_context \
one_actor_keeps_original_claim_bytes_and_admits_disjoint_owner_roots \
mounted_history
-> 17 passed; 0 failed; 15369 filtered out; 1m17s compile + 2.27s run
The new `runtime_mount_coordination_lock_is_shared_with_parallel_sessions`
fails on the previous constructor (the second shared holder's
`ensure_coordination_process_lock` returns EWOULDBLOCK) and passes now.
`headless_worker_registration_enforces_live_claims_and_projects_context` keeps
its `independent` refusal by moving to the exclusive constructor, which is the
posture its assertion describes.
Not run: hosted CI, full suite, release gates.
This commit is contained in:
1 parent
6249919458
commit
842d2cecf4
4 files changed
+47
-9
No files matched your search
@@ -88,7 +88,7 @@ use crate::task_manager::{
|
||||
};
|
||||
use crate::tools::subagent::{
|
||||
AgentWorkerRecord, AgentWorkerStatus, SharedSubAgentManager, SubAgentStatus,
|
||||
new_shared_runtime_subagent_manager,
|
||||
new_exclusive_runtime_subagent_manager,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(super) use codewhale_models::{ContentBlock, Message};
|
||||
@@ -1386,7 +1386,7 @@ fn runtime_api_sub_agent_manager(
|
||||
workspace: &FsPath,
|
||||
workers: usize,
|
||||
) -> Result<SharedSubAgentManager> {
|
||||
new_shared_runtime_subagent_manager(workspace.to_path_buf(), workers)
|
||||
new_exclusive_runtime_subagent_manager(workspace.to_path_buf(), workers)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
||||
@@ -11230,6 +11230,21 @@ pub fn new_shared_subagent_manager_with_timeout(
|
||||
pub(crate) fn new_shared_runtime_subagent_manager(
|
||||
workspace: PathBuf,
|
||||
workers: usize,
|
||||
) -> Result<SharedSubAgentManager> {
|
||||
new_runtime_subagent_manager(workspace, workers, false)
|
||||
}
|
||||
|
||||
pub(crate) fn new_exclusive_runtime_subagent_manager(
|
||||
workspace: PathBuf,
|
||||
workers: usize,
|
||||
) -> Result<SharedSubAgentManager> {
|
||||
new_runtime_subagent_manager(workspace, workers, true)
|
||||
}
|
||||
|
||||
fn new_runtime_subagent_manager(
|
||||
workspace: PathBuf,
|
||||
workers: usize,
|
||||
exclusive: bool,
|
||||
) -> Result<SharedSubAgentManager> {
|
||||
type RuntimeManagerRegistry =
|
||||
parking_lot::Mutex<HashMap<PathBuf, (usize, std::sync::Weak<RwLock<SubAgentManager>>)>>;
|
||||
@@ -11254,7 +11269,7 @@ pub(crate) fn new_shared_runtime_subagent_manager(
|
||||
.with_admission_limit(workers)
|
||||
.with_launch_concurrency(workers)
|
||||
.with_state_path(state_path);
|
||||
manager.coordination_exclusive = true;
|
||||
manager.coordination_exclusive = exclusive;
|
||||
manager = manager.require_coordination_process_lock();
|
||||
if let Err(error) = manager.ensure_coordination_process_lock() {
|
||||
manager.coordination_snapshot_read_only = true;
|
||||
@@ -19768,7 +19783,7 @@ mod owner_scoped_coordination_tests {
|
||||
let selected = root.join("selected");
|
||||
fs::create_dir_all(original.join("src")).unwrap();
|
||||
fs::create_dir_all(selected.join("src")).unwrap();
|
||||
let authority = new_shared_runtime_subagent_manager(original.clone(), 2).unwrap();
|
||||
let authority = new_exclusive_runtime_subagent_manager(original.clone(), 2).unwrap();
|
||||
let mut manager = authority.try_write().unwrap();
|
||||
admit(&mut manager, &original);
|
||||
admit(&mut manager, &selected);
|
||||
|
||||
@@ -1194,12 +1194,33 @@ fn coordination_hot_paths_count_only_active_authoritative_owners() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_mount_coordination_lock_is_shared_with_parallel_sessions() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let runtime_authority = new_shared_runtime_subagent_manager(tmp.path().to_path_buf(), 8)
|
||||
.expect("admit TUI runtime writer");
|
||||
let session =
|
||||
SubAgentManager::new(tmp.path().to_path_buf(), 8).require_coordination_process_lock();
|
||||
session
|
||||
.ensure_coordination_process_lock()
|
||||
.expect("a second session appends beside the runtime");
|
||||
assert!(!session.coordination_snapshot_read_only);
|
||||
session
|
||||
.persist_state_synchronously()
|
||||
.expect("session append");
|
||||
let runtime = runtime_authority.try_write().expect("runtime lock");
|
||||
assert!(!runtime.coordination_snapshot_read_only);
|
||||
runtime
|
||||
.persist_state_synchronously()
|
||||
.expect("runtime append");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headless_worker_registration_enforces_live_claims_and_projects_context() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let authority_a = new_shared_runtime_subagent_manager(tmp.path().to_path_buf(), 8)
|
||||
let authority_a = new_exclusive_runtime_subagent_manager(tmp.path().to_path_buf(), 8)
|
||||
.expect("admit Runtime writer");
|
||||
let authority_b = new_shared_runtime_subagent_manager(tmp.path().join("."), 8)
|
||||
let authority_b = new_exclusive_runtime_subagent_manager(tmp.path().join("."), 8)
|
||||
.expect("reuse canonical Runtime writer");
|
||||
assert!(Arc::ptr_eq(&authority_a, &authority_b));
|
||||
let mut manager = authority_a.try_write().expect("first authority lock");
|
||||
|
||||
@@ -117,9 +117,11 @@ whole-document atomic replace. One per-workspace advisory flock
|
||||
fleet`, `codewhale resume`/`fork` mounts and the TUI's background task runtime
|
||||
(`/task`, `task_create`, automations) take the flock shared, so several
|
||||
sessions in one folder hold it at once. Failing to get it only logs a warning.
|
||||
- **Exclusive, fatal on failure.** `codewhale serve` and the other standalone
|
||||
Runtime hosts take it exclusively and refuse to start without it. Beside such an
|
||||
owner, shared holders cannot append.
|
||||
- **Exclusive, degrade read-only.** `codewhale serve` and the other standalone
|
||||
Runtime hosts take it exclusively and refuse every durable ledger append when
|
||||
another process already holds it; they log the refusal and pick the ledger back
|
||||
up when the process restarts after that owner exits. Beside such an owner,
|
||||
shared holders cannot append.
|
||||
|
||||
Every shared holder may append, and each append replaces the whole file, so the
|
||||
last rename wins. The ledger is loaded once at boot; agents another process
|
||||
|
||||
Reference in new issue
Block a user