core: sausage-vfs per process (/, /proc, /self, /run), activation, graph, watcher, upgrades
- /proc is a core layer: core/ (PROTOCOL.md, graph.json, graph.dot, locks.json, plugins/<name>/discover.json and client.rs) and one folder per process (info.json, log, and its /self files). - Locks are released only after the task ended, the store dropped, and in-flight file operations returned. - core.activate and core.revert with copies, discovery, compatibility checks of imports and consumers, groups, force. An admission gate makes the snapshot of old processes complete. - Graph of discover documents by hash, refreshed by a file watcher. - Upgrades: grace periods (x-sausage-grace), stop, service restarts, rpc.retired. - SDK plugins in the real core (cli/tests/sdk.rs).
This commit is contained in:
+257
-3
@@ -190,7 +190,7 @@ async fn unimplemented_core_methods_return_method_not_found() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
assert_eq!(
|
||||
h.call("core.activate", json!({})).await.unwrap_err()["code"],
|
||||
h.call("core.unknown", json!({})).await.unwrap_err()["code"],
|
||||
-32601
|
||||
);
|
||||
|
||||
@@ -1083,8 +1083,9 @@ async fn boot_services_are_detached() {
|
||||
.unwrap();
|
||||
let h = w.start().await;
|
||||
let processes = h.processes();
|
||||
assert_eq!(processes.len(), 1);
|
||||
assert!(processes.values().next().unwrap().detached);
|
||||
let services: Vec<_> = processes.values().filter(|p| p.method == "serve").collect();
|
||||
assert_eq!(services.len(), 1);
|
||||
assert!(services[0].detached);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
@@ -1110,3 +1111,256 @@ async fn core_lifetime_events_enter_the_journal() {
|
||||
}
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 4: the root merges world files and mounts, and cross-mount rename fails.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn filesystem_root_and_mount_boundaries() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.filesystem", json!({"topic":"root"})).await;
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(w.0.join("state/root")).unwrap(),
|
||||
"world"
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 4: a process publishes self files read-only under its own live proc folder.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn filesystem_self_and_proc_lifetime() {
|
||||
use sausage_vfs::Layer;
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let view = h.filesystem().await.unwrap();
|
||||
let pid = call(&h, "probe.filesystem", json!({"topic":"self"})).await;
|
||||
call(&h, "core.stop", json!({"pid":pid})).await;
|
||||
assert!(
|
||||
view.stat(&format!("/proc/{}", pid.as_str().unwrap()), true)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
let live = call(&h, "probe.self_serve", json!({})).await;
|
||||
call(&h, "probe.filesystem", json!({"topic":"isolated"})).await;
|
||||
assert_eq!(
|
||||
call(
|
||||
&h,
|
||||
"probe.read",
|
||||
json!({"path":format!("/proc/{}/endpoint",live.as_str().unwrap())})
|
||||
)
|
||||
.await,
|
||||
"endpoint"
|
||||
);
|
||||
call(&h, "core.stop", json!({"pid":live})).await;
|
||||
assert!(
|
||||
view.stat(&format!("/proc/{}", live.as_str().unwrap()), true)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 4: core-owned self names reject creation and replacement.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn filesystem_reserved_self_names() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.filesystem", json!({"topic":"reserved"})).await;
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 4: generated logs capture a finite snapshot at open.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn filesystem_log_snapshot_ends() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.filesystem", json!({"topic":"snapshot"})).await;
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 4: proc/core provides protocol, graph, lock, and plugin inspection files.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn filesystem_core_inspection_files() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(&h, "probe.filesystem", json!({"topic":"core"})).await;
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 4: proc/core exposes each discovered document and its generated Rust client.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn filesystem_discovery_document_and_client() {
|
||||
use sausage_vfs::{Layer, Open};
|
||||
let w = TestWorld::new();
|
||||
let document =
|
||||
json!({"methods":[{"name":"echo","params":[],"result":{"schema":{"type":"string"}}}]});
|
||||
std::fs::create_dir_all(w.0.join("state/probe")).unwrap();
|
||||
std::fs::write(w.0.join("state/probe/discover.json"), document.to_string()).unwrap();
|
||||
let h = w.start().await;
|
||||
let view = h.filesystem().await.unwrap();
|
||||
tokio::time::timeout(std::time::Duration::from_secs(20), async {
|
||||
loop {
|
||||
if view
|
||||
.open("/proc/core/plugins/probe/discover.json", Open::default())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
break;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let published = call(
|
||||
&h,
|
||||
"probe.read",
|
||||
json!({"path":"/proc/core/plugins/probe/discover.json"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(published.as_str().unwrap()).unwrap(),
|
||||
document
|
||||
);
|
||||
let client = call(
|
||||
&h,
|
||||
"probe.read",
|
||||
json!({"path":"/proc/core/plugins/probe/client.rs"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
client,
|
||||
sausage_interface::client_rs("probe", &document).unwrap()
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 4: run is shared across processes but empty after a host restart.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn filesystem_run_is_shared_and_ephemeral() {
|
||||
use sausage_vfs::Layer;
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
call(
|
||||
&h,
|
||||
"probe.write",
|
||||
json!({"path":"/run/shared","text":"yes"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
call(&h, "probe.read", json!({"path":"/run/shared"})).await,
|
||||
"yes"
|
||||
);
|
||||
h.stop().await;
|
||||
let h = w.start().await;
|
||||
assert!(
|
||||
h.filesystem()
|
||||
.await
|
||||
.unwrap()
|
||||
.list("/run")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 6: no write from a killed process lands after the next holder acquires its lock.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn filesystem_kill_drains_writes_before_lock_transfer() {
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
let pid = call(&h, "probe.write_loop", json!({})).await;
|
||||
let next = h.clone();
|
||||
let waiter = tokio::spawn(async move { call(&next, "probe.locked_write", json!({})).await });
|
||||
use sausage_vfs::{Layer, Open};
|
||||
let view = h.filesystem().await.unwrap();
|
||||
tokio::time::timeout(std::time::Duration::from_secs(20), async {
|
||||
loop {
|
||||
let file = view
|
||||
.open("/proc/core/locks.json", Open::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let locks: Value = serde_json::from_slice(&file.read(0, 65536).await.unwrap()).unwrap();
|
||||
if locks["/state/loop"]["waiting"]
|
||||
.as_array()
|
||||
.is_some_and(|queue| !queue.is_empty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
call(&h, "core.kill", json!({"pid":pid})).await;
|
||||
waiter.await.unwrap();
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(w.0.join("state/loop")).unwrap(),
|
||||
"new"
|
||||
);
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 4: native processes appear in proc and locks.json identifies holders and waiters.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn filesystem_native_processes_and_lock_snapshot() {
|
||||
use sausage_vfs::{Layer, Open};
|
||||
let w = TestWorld::new();
|
||||
let h = w.start().await;
|
||||
h.register_native(
|
||||
"native",
|
||||
std::sync::Arc::new(|ctx, _, _| {
|
||||
Box::pin(async move {
|
||||
ctx.call("core.lock", json!({"path":"live"})).await.unwrap();
|
||||
ctx.notify("ready", json!({"pid":ctx.pid}));
|
||||
std::future::pending().await
|
||||
})
|
||||
}),
|
||||
);
|
||||
let mut events = h.subscribe();
|
||||
let host = h.clone();
|
||||
let task = tokio::spawn(async move { host.call("native.wait", json!({})).await });
|
||||
let event = events.recv().await.unwrap();
|
||||
let pid = event["params"]["params"]["pid"].as_str().unwrap();
|
||||
let view = h.filesystem().await.unwrap();
|
||||
assert!(
|
||||
view.list("/proc")
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|e| e.name == pid)
|
||||
);
|
||||
let info = call(
|
||||
&h,
|
||||
"probe.read",
|
||||
json!({"path":format!("/proc/{pid}/info.json")}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(info.as_str().unwrap()).unwrap()["version"],
|
||||
"native"
|
||||
);
|
||||
let host = h.clone();
|
||||
let waiter = tokio::spawn(async move { host.call("core.lock", json!({"path":"live"})).await });
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
loop {
|
||||
let file = view
|
||||
.open("/proc/core/locks.json", Open::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let locks: Value = serde_json::from_slice(&file.read(0, 65536).await.unwrap()).unwrap();
|
||||
if locks["live"]["waiting"] == json!(["console"]) {
|
||||
assert_eq!(locks["live"]["owner"], pid);
|
||||
break;
|
||||
}
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
call(&h, "core.kill", json!({"pid":pid})).await;
|
||||
assert_eq!(waiter.await.unwrap().unwrap(), json!({"held":true}));
|
||||
assert!(task.await.unwrap().is_err());
|
||||
assert!(view.stat(&format!("/proc/{pid}"), true).await.is_err());
|
||||
h.stop().await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user