Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,228 @@
|
||||
//! 真实命令的字节管道、非零退出、超时和输出预算;所有写入都在当前测试目录。
|
||||
|
||||
use super::{Asset, Bundle, Directory, Limits, Runtime};
|
||||
use dever_runtime::worker::command::{self, Limits as CommandLimits};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{fs, path::PathBuf, process::Command, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn command_runs_owned_native_fixture_without_shell_and_drains_on_failure() {
|
||||
let _execution = super::EXECUTOR.lock().unwrap();
|
||||
let directory = Directory::new();
|
||||
let fixture = directory.0.join("fixture");
|
||||
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../../test/rust/runtime/worker/fixture.rs");
|
||||
let result = Command::new("rustup")
|
||||
.args(["run", "1.95.0", "rustc", "--edition=2024", "-O"])
|
||||
.arg(source)
|
||||
.arg("-o")
|
||||
.arg(&fixture)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
let runner = directory.0.join("runner");
|
||||
fs::copy(std::env::current_exe().unwrap(), &runner).unwrap();
|
||||
let result = Command::new(runner)
|
||||
.args(["--exact", "command::owned_command_case", "--nocapture"])
|
||||
.env_clear()
|
||||
.env("DEVER_COMMAND_FIXTURE", fixture)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&result.stdout),
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_command_case() {
|
||||
let Some(fixture) = std::env::var_os("DEVER_COMMAND_FIXTURE") else {
|
||||
return;
|
||||
};
|
||||
let bytes = fs::read(&fixture).unwrap();
|
||||
fs::remove_file(fixture).unwrap();
|
||||
let entry = "module/demo/tool/command";
|
||||
let launch = serde_json::to_vec(&serde_json::json!({"format":"dever-command-launch-v1","ecosystem":"command","entry":entry,"executable":entry,"arguments":[],"working_directory":"module/demo/tool"})).unwrap();
|
||||
let files = [
|
||||
(entry, bytes, true),
|
||||
("module/demo/tool/command.dever-command.json", launch, false),
|
||||
]
|
||||
.map(|(path, bytes, executable)| Asset {
|
||||
path,
|
||||
sha256: Box::leak(format!("{:x}", Sha256::digest(&bytes)).into_boxed_str()),
|
||||
bytes: Box::leak(bytes.into_boxed_slice()),
|
||||
executable,
|
||||
});
|
||||
let bundle: &'static Bundle = Box::leak(Box::new(Bundle {
|
||||
files: Box::leak(Box::new(files)),
|
||||
}));
|
||||
let root = std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
let runtime = Runtime::new(Limits {
|
||||
threads: 2,
|
||||
workers: 1,
|
||||
tasks: 32,
|
||||
})
|
||||
.unwrap();
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let arguments = ["--echo", "a b", "$(touch never)", "\"quoted\""].map(str::to_owned);
|
||||
let output = command::run(
|
||||
scope,
|
||||
bundle,
|
||||
entry,
|
||||
&arguments,
|
||||
Some(&[0, 255, 1]),
|
||||
CommandLimits::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(output.code, 7);
|
||||
assert_eq!(output.stdout, [0, 255, 1]);
|
||||
assert_eq!(output.stderr, b"a b\n$(touch never)\n\"quoted\"");
|
||||
let empty = command::run(
|
||||
scope,
|
||||
bundle,
|
||||
entry,
|
||||
&["--echo".into()],
|
||||
None,
|
||||
CommandLimits::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(empty.stdout.is_empty());
|
||||
let error = command::run(
|
||||
scope,
|
||||
bundle,
|
||||
entry,
|
||||
&["--flood".into()],
|
||||
None,
|
||||
CommandLimits {
|
||||
timeout_ms: 1000,
|
||||
output_limit: 100,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
assert!(error.contains("limit"));
|
||||
let marker = root.join("timed-pid");
|
||||
let error = command::run(
|
||||
scope,
|
||||
bundle,
|
||||
entry,
|
||||
&["--hang".into(), marker.to_str().unwrap().into()],
|
||||
None,
|
||||
CommandLimits {
|
||||
timeout_ms: 100,
|
||||
output_limit: 100,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
assert!(error.contains("timed out"));
|
||||
let pid = fs::read_to_string(marker).unwrap();
|
||||
assert!(!PathBuf::from(format!("/proc/{pid}")).exists());
|
||||
let marker = root.join("cancelled-pid");
|
||||
let arguments = ["--hang".into(), marker.to_str().unwrap().into()];
|
||||
let running = command::run(
|
||||
scope,
|
||||
bundle,
|
||||
entry,
|
||||
&arguments,
|
||||
None,
|
||||
CommandLimits::default(),
|
||||
);
|
||||
tokio::pin!(running);
|
||||
tokio::select! {
|
||||
_ = &mut running => panic!("hanging command completed before cancellation"),
|
||||
ready = tokio::time::timeout(Duration::from_secs(3), async {
|
||||
while !marker.is_file() { tokio::time::sleep(Duration::from_millis(5)).await; }
|
||||
}) => ready.unwrap(),
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let pid = fs::read_to_string(root.join("cancelled-pid")).unwrap();
|
||||
assert!(!PathBuf::from(format!("/proc/{pid}")).exists());
|
||||
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let (started, ready) = tokio::sync::oneshot::channel();
|
||||
let (release, receive) = std::sync::mpsc::channel();
|
||||
let blocker = scope
|
||||
.spawn_sync(move || {
|
||||
started.send(()).unwrap();
|
||||
receive.recv().unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
ready.await.unwrap();
|
||||
let queued = scope
|
||||
.spawn(move |child| async move {
|
||||
command::run(
|
||||
&child,
|
||||
bundle,
|
||||
entry,
|
||||
&["--echo".into()],
|
||||
None,
|
||||
CommandLimits::default(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.unwrap();
|
||||
let stopped =
|
||||
tokio::time::timeout(Duration::from_millis(300), queued.timeout(30)).await;
|
||||
release.send(()).unwrap();
|
||||
blocker.wait().await.unwrap();
|
||||
assert!(matches!(
|
||||
stopped,
|
||||
Ok(Ok(dever_runtime::task::Timed::Expired(None)))
|
||||
));
|
||||
worker_finish(scope).await;
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let marker = root.join("child-cancelled-pid");
|
||||
let arguments = vec!["--hang".into(), marker.to_str().unwrap().into()];
|
||||
let child = scope
|
||||
.spawn(move |child| async move {
|
||||
command::run(
|
||||
&child,
|
||||
bundle,
|
||||
entry,
|
||||
&arguments,
|
||||
None,
|
||||
CommandLimits::default(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.unwrap();
|
||||
tokio::time::timeout(Duration::from_secs(3), async {
|
||||
while !marker.is_file() {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
child.stop().await.unwrap();
|
||||
let pid = fs::read_to_string(marker).unwrap();
|
||||
assert!(!PathBuf::from(format!("/proc/{pid}")).exists());
|
||||
worker_finish(scope).await;
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn worker_finish(scope: &dever_runtime::task::Scope<'_>) {
|
||||
dever_runtime::worker::finish(scope, Ok(())).await.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
//! 独立 std-only 协议对端;测试从固定 Rust 工具链构建,不调用宿主语言环境。
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
fn read() -> Option<String> {
|
||||
let mut prefix = [0; 4];
|
||||
std::io::stdin().read_exact(&mut prefix).ok()?;
|
||||
let mut bytes = vec![0; u32::from_be_bytes(prefix) as usize];
|
||||
std::io::stdin().read_exact(&mut bytes).unwrap();
|
||||
Some(String::from_utf8(bytes).unwrap())
|
||||
}
|
||||
|
||||
fn send(value: &str) {
|
||||
let mut output = std::io::stdout().lock();
|
||||
output
|
||||
.write_all(&(value.len() as u32).to_be_bytes())
|
||||
.unwrap();
|
||||
output.write_all(value.as_bytes()).unwrap();
|
||||
output.flush().unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
if let Some(mode) = args.first() {
|
||||
match mode.as_str() {
|
||||
"--echo" => {
|
||||
let mut bytes = Vec::new();
|
||||
std::io::stdin().read_to_end(&mut bytes).unwrap();
|
||||
std::io::stdout().write_all(&bytes).unwrap();
|
||||
std::io::stderr()
|
||||
.write_all(args[1..].join("\n").as_bytes())
|
||||
.unwrap();
|
||||
std::process::exit(7);
|
||||
}
|
||||
"--hang" => {
|
||||
std::fs::write(&args[1], std::process::id().to_string()).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||
return;
|
||||
}
|
||||
"--flood" => {
|
||||
std::io::stdout().write_all(&[1; 4096]).unwrap();
|
||||
std::io::stderr().write_all(&[2; 4096]).unwrap();
|
||||
return;
|
||||
}
|
||||
_ => panic!("unknown test command"),
|
||||
}
|
||||
}
|
||||
let Some(hello) = read() else {
|
||||
return;
|
||||
};
|
||||
assert!(hello.contains("\"kind\":\"hello\""));
|
||||
let (fields, setting) = hello.rsplit_once(",\"setting\":").unwrap();
|
||||
if setting.contains("sensitive") {
|
||||
assert!(setting.contains("sensitive\\\"\\\\雪"));
|
||||
}
|
||||
send(&format!("{fields}}}").replace("\"kind\":\"hello\"", "\"kind\":\"ready\""));
|
||||
let health = read().unwrap();
|
||||
assert!(health.contains("\"kind\":\"health\""));
|
||||
send(&health);
|
||||
let mut bad_shutdown = false;
|
||||
while let Some(request) = read() {
|
||||
if request == "{\"kind\":\"shutdown\"}" {
|
||||
if bad_shutdown {
|
||||
send("{}");
|
||||
std::process::exit(7);
|
||||
}
|
||||
send(&request);
|
||||
return;
|
||||
}
|
||||
let id = request
|
||||
.split("\"id\":")
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split([',', '}'])
|
||||
.next()
|
||||
.unwrap();
|
||||
if request.contains("\"kind\":\"cancel\"") {
|
||||
send(&format!(
|
||||
"{{\"kind\":\"error\",\"id\":{id},\"error\":\"dever.cancelled\",\"payload\":null}}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if request.contains("\"operation\":\"measure\"") {
|
||||
let payload = request
|
||||
.split_once(",\"payload\":")
|
||||
.unwrap()
|
||||
.1
|
||||
.strip_suffix('}')
|
||||
.unwrap();
|
||||
let length = payload.len();
|
||||
let checksum: u64 = payload.bytes().map(u64::from).sum();
|
||||
send(&format!(
|
||||
"{{\"kind\":\"result\",\"id\":{id},\"payload\":{{\"length\":{length},\"checksum\":{checksum}}}}}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if request.contains("\"operation\":\"crash\"") {
|
||||
std::process::exit(7);
|
||||
}
|
||||
if request.contains("\"operation\":\"hang\"") {
|
||||
continue;
|
||||
}
|
||||
if request.contains("\"operation\":\"ignore\"") {
|
||||
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||
continue;
|
||||
}
|
||||
if request.contains("\"operation\":\"shutdown_bad\"") {
|
||||
bad_shutdown = true;
|
||||
}
|
||||
if request.contains("\"operation\":\"native\"") {
|
||||
if request.contains("\"input\":-4") || request.contains("\"input\":-5") {
|
||||
bad_shutdown = true;
|
||||
}
|
||||
if request.contains("\"input\":-1") {
|
||||
send(&format!(
|
||||
"{{\"kind\":\"result\",\"id\":{id},\"payload\":{{\"value\":\"wrong-type\"}}}}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if request.contains("\"input\":-2")
|
||||
|| request.contains("\"input\":-3")
|
||||
|| request.contains("\"input\":-5")
|
||||
{
|
||||
let error = if request.contains("\"input\":-2") {
|
||||
"not.declared.Failed"
|
||||
} else {
|
||||
"dever.api.Error.Invalid"
|
||||
};
|
||||
send(&format!(
|
||||
"{{\"kind\":\"error\",\"id\":{id},\"error\":\"{error}\",\"payload\":{{}}}}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let value = if request.contains("\"input\":99}") {
|
||||
std::process::id()
|
||||
} else {
|
||||
42
|
||||
};
|
||||
send(&format!(
|
||||
"{{\"kind\":\"result\",\"id\":{id},\"payload\":{{\"value\":{value}}}}}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if request.contains("\"operation\":\"bad\"") {
|
||||
send("{\"kind\":\"result\",\"id\":900,\"payload\":{}}");
|
||||
continue;
|
||||
}
|
||||
if request.contains("\"operation\":\"error\"") {
|
||||
send(&format!(
|
||||
"{{\"kind\":\"error\",\"id\":{id},\"error\":\"demo.test.Error.Failed\",\"payload\":{{\"message\":\"failed\"}}}}"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if let Some((_, fields)) = [
|
||||
("null_error", "\"kind\":\"result\",\"error\":null"),
|
||||
("missing_error", "\"kind\":\"error\""),
|
||||
("extra_field", "\"kind\":\"result\",\"unexpected\":true"),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|(operation, _)| request.contains(&format!("\"operation\":\"{operation}\"")))
|
||||
{
|
||||
send(&format!("{{{fields},\"id\":{id},\"payload\":null}}"));
|
||||
continue;
|
||||
}
|
||||
let response = format!(
|
||||
"{{\"kind\":\"result\",\"id\":{id},\"payload\":{{\"value\":42,\"pid\":{}}}}}",
|
||||
std::process::id()
|
||||
);
|
||||
if request.contains("\"operation\":\"fragment\"") {
|
||||
let prefix = (response.len() as u32).to_be_bytes();
|
||||
std::io::stdout().write_all(&prefix[..2]).unwrap();
|
||||
std::io::stdout().flush().unwrap();
|
||||
assert!(read().unwrap().contains("\"kind\":\"cancel\""));
|
||||
std::io::stdout().write_all(&prefix[2..]).unwrap();
|
||||
std::io::stdout().write_all(response.as_bytes()).unwrap();
|
||||
std::io::stdout().flush().unwrap();
|
||||
} else {
|
||||
send(&response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
//! 逐调用取消与校验失败必须完成自己的清理,不等待另一个调用或阻塞任务。
|
||||
|
||||
use super::{CONTRACT, Directory, ENTRY, EXECUTOR, bundle, call, pid};
|
||||
use dever_runtime::{
|
||||
task::{Limits, Runtime, Timed},
|
||||
wire::json::Json,
|
||||
worker::{self, Setting},
|
||||
};
|
||||
use std::{fs, path::PathBuf, process::Command, time::Duration};
|
||||
|
||||
#[test]
|
||||
fn worker_validation_cancellation_and_cleanup_are_owned() {
|
||||
let _execution = EXECUTOR.lock().unwrap();
|
||||
let directory = Directory::new();
|
||||
let fixture = directory.0.join("fixture");
|
||||
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../../test/rust/runtime/worker/fixture.rs");
|
||||
let built = Command::new("rustup")
|
||||
.args(["run", "1.95.0", "rustc", "--edition=2024", "-O"])
|
||||
.arg(source)
|
||||
.arg("-o")
|
||||
.arg(&fixture)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
built.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&built.stderr)
|
||||
);
|
||||
let runner = directory.0.join("runner");
|
||||
fs::copy(std::env::current_exe().unwrap(), &runner).unwrap();
|
||||
let result = Command::new(runner)
|
||||
.args(["--exact", "lifecycle::owned_lifecycle_case", "--nocapture"])
|
||||
.env_clear()
|
||||
.env("DEVER_LIFECYCLE_FIXTURE", fixture)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&result.stdout),
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_lifecycle_case() {
|
||||
let Some(fixture) = std::env::var_os("DEVER_LIFECYCLE_FIXTURE") else {
|
||||
return;
|
||||
};
|
||||
let bundle = bundle(fs::read(&fixture).unwrap());
|
||||
fs::remove_file(fixture).unwrap();
|
||||
let runtime = Runtime::new(Limits {
|
||||
threads: 2,
|
||||
workers: 1,
|
||||
tasks: 32,
|
||||
})
|
||||
.unwrap();
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let first = pid(call(scope, bundle, "echo").await.unwrap());
|
||||
let held = worker::call(
|
||||
scope,
|
||||
bundle,
|
||||
ENTRY,
|
||||
CONTRACT,
|
||||
&Setting::empty(),
|
||||
"echo",
|
||||
Json::parse("{}").unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let queued = scope
|
||||
.spawn(move |child| async move { call(&child, bundle, "echo").await })
|
||||
.unwrap();
|
||||
let stopped =
|
||||
tokio::time::timeout(Duration::from_millis(300), queued.timeout(30)).await;
|
||||
assert!(matches!(stopped, Ok(Ok(Timed::Expired(None)))));
|
||||
assert_eq!(first, pid(held.resolve(Ok).await.unwrap()));
|
||||
|
||||
let held = worker::call(
|
||||
scope,
|
||||
bundle,
|
||||
ENTRY,
|
||||
CONTRACT,
|
||||
&Setting::empty(),
|
||||
"echo",
|
||||
Json::parse("{}").unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let queued = scope
|
||||
.spawn(move |child| async move { call(&child, bundle, "echo").await })
|
||||
.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(30)).await;
|
||||
let failed = held
|
||||
.resolve::<()>(|_| Err("invalid typed payload".into()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(failed.contains("invalid typed payload"));
|
||||
assert!(!PathBuf::from(format!("/proc/{first}")).exists());
|
||||
assert!(
|
||||
queued
|
||||
.wait()
|
||||
.await
|
||||
.unwrap()
|
||||
.err()
|
||||
.unwrap()
|
||||
.contains("queued")
|
||||
);
|
||||
let second = pid(call(scope, bundle, "echo").await.unwrap());
|
||||
assert_ne!(first, second);
|
||||
let held = worker::call(
|
||||
scope,
|
||||
bundle,
|
||||
ENTRY,
|
||||
CONTRACT,
|
||||
&Setting::empty(),
|
||||
"echo",
|
||||
Json::parse("{}").unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let queued = scope
|
||||
.spawn(move |child| async move { call(&child, bundle, "echo").await })
|
||||
.unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(30)).await;
|
||||
drop(held);
|
||||
let failed = tokio::time::timeout(Duration::from_secs(3), queued.wait())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.err()
|
||||
.unwrap();
|
||||
assert!(failed.contains("queued"));
|
||||
assert!(!PathBuf::from(format!("/proc/{second}")).exists());
|
||||
let third = pid(call(scope, bundle, "echo").await.unwrap());
|
||||
assert_ne!(second, third);
|
||||
let panicked = scope
|
||||
.spawn(move |child| async move {
|
||||
worker::call(
|
||||
&child,
|
||||
bundle,
|
||||
ENTRY,
|
||||
CONTRACT,
|
||||
&Setting::empty(),
|
||||
"echo",
|
||||
Json::parse("{}").unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.resolve::<()>(|_| panic!("typed decoder panicked"))
|
||||
.await
|
||||
})
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
tokio::time::timeout(Duration::from_secs(3), panicked.wait()).await,
|
||||
Ok(Err(dever_runtime::task::Error::Panicked))
|
||||
));
|
||||
assert!(!PathBuf::from(format!("/proc/{third}")).exists());
|
||||
worker::finish(scope, Ok(())).await.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Config {
|
||||
#[serde(rename = "token")]
|
||||
_token: dever_runtime::value::secret::Secret,
|
||||
}
|
||||
let document = dever_runtime::adapter::Document::parse(
|
||||
r#"{"adapter":{"demo.test":{"setting":{"token":"sensitive\"\\雪"}}}}"#.into(),
|
||||
)
|
||||
.unwrap();
|
||||
let schemas = [dever_runtime::adapter::Schema {
|
||||
name: "demo.test",
|
||||
implementations: &["default"],
|
||||
}];
|
||||
let selected = document.select(&schemas).unwrap();
|
||||
let setting = selected[0].worker_setting::<Config>().unwrap();
|
||||
let input = Json::parse("{}").unwrap();
|
||||
let process = pid(worker::call(
|
||||
scope,
|
||||
bundle,
|
||||
ENTRY,
|
||||
CONTRACT,
|
||||
&setting,
|
||||
"echo",
|
||||
input.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.resolve(Ok)
|
||||
.await
|
||||
.unwrap());
|
||||
let error = worker::call(scope, bundle, ENTRY, CONTRACT, &setting, "bad", input)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
assert!(!error.contains("sensitive"));
|
||||
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
|
||||
worker::finish(scope, Ok(())).await.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let (started, ready) = tokio::sync::oneshot::channel();
|
||||
let (release, receive) = std::sync::mpsc::channel();
|
||||
let blocking = scope
|
||||
.spawn_sync(move || {
|
||||
started.send(()).unwrap();
|
||||
receive.recv().unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
ready.await.unwrap();
|
||||
let queued = scope
|
||||
.spawn(move |child| async move { call(&child, bundle, "echo").await })
|
||||
.unwrap();
|
||||
let cancelled =
|
||||
tokio::time::timeout(Duration::from_millis(300), queued.timeout(30)).await;
|
||||
release.send(()).unwrap();
|
||||
blocking.wait().await.unwrap();
|
||||
assert!(matches!(cancelled, Ok(Ok(Timed::Expired(None)))));
|
||||
worker::finish(scope, Ok(())).await.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let process = pid(call(scope, bundle, "echo").await.unwrap());
|
||||
let hanging = scope
|
||||
.spawn(move |child| async move { call(&child, bundle, "ignore").await })
|
||||
.unwrap();
|
||||
let result = tokio::time::timeout(Duration::from_secs(3), hanging.timeout(50))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(matches!(result, Timed::Expired(None)));
|
||||
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
|
||||
worker::finish(scope, Ok(())).await.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let process = pid(call(scope, bundle, "shutdown_bad").await.unwrap());
|
||||
let error = worker::finish::<()>(scope, Err("primary failure".into()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error.contains("primary failure") && error.contains("shutdown"),
|
||||
"{error}"
|
||||
);
|
||||
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
//! 真实受管 Worker 与隔离资源目录;不访问已安装服务或生产缓存。
|
||||
|
||||
use dever_runtime::{
|
||||
task::{Limits, Runtime},
|
||||
wire::json::Json,
|
||||
worker::{self, Asset, Bundle, Reply, Setting},
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
fs,
|
||||
os::unix::fs::{DirBuilderExt, PermissionsExt},
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const ENTRY: &str = "module/demo/test/worker";
|
||||
static EXECUTOR: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
mod command;
|
||||
mod lifecycle;
|
||||
mod performance;
|
||||
const CONTRACT: &str = r#"{"port":"demo.test","adapter":"default","ecosystem":"exec","entry":"module/demo/test/worker","schema":"owned-fixture","capabilities":[],"operations":["bad","crash","echo","error","extra_field","fragment","hang","ignore","measure","missing_error","null_error","shutdown_bad"],"inputs":{},"outputs":{},"errors":{"demo.test.Error.Failed":{}},"setting":null}"#;
|
||||
|
||||
struct Directory(PathBuf);
|
||||
|
||||
impl Directory {
|
||||
fn new() -> Self {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"dever-worker-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
fs::DirBuilder::new().mode(0o700).create(&path).unwrap();
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Directory {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn bundle(bytes: Vec<u8>) -> &'static Bundle {
|
||||
let launch = serde_json::to_vec(&serde_json::json!({"format":"dever-worker-launch-v1","ecosystem":"exec","entry":ENTRY,"executable":ENTRY,"arguments":[],"working_directory":"module/demo/test"})).unwrap();
|
||||
let files = [
|
||||
(ENTRY, bytes, true),
|
||||
("module/demo/test/worker.dever-worker.json", launch, false),
|
||||
]
|
||||
.map(|(path, bytes, executable)| Asset {
|
||||
path,
|
||||
sha256: Box::leak(format!("{:x}", Sha256::digest(&bytes)).into_boxed_str()),
|
||||
bytes: Box::leak(bytes.into_boxed_slice()),
|
||||
executable,
|
||||
});
|
||||
Box::leak(Box::new(Bundle {
|
||||
files: Box::leak(Box::new(files)),
|
||||
}))
|
||||
}
|
||||
|
||||
fn pid(reply: Reply) -> u32 {
|
||||
let Reply::Result(value) = reply else {
|
||||
panic!("expected result")
|
||||
};
|
||||
let value: serde_json::Value = serde_json::from_str(&value).unwrap();
|
||||
assert_eq!(value["value"], 42);
|
||||
value["pid"].as_u64().unwrap() as u32
|
||||
}
|
||||
|
||||
async fn call(
|
||||
scope: &dever_runtime::task::Scope<'_>,
|
||||
bundle: &'static Bundle,
|
||||
operation: &str,
|
||||
) -> Result<Reply, String> {
|
||||
worker::call(
|
||||
scope,
|
||||
bundle,
|
||||
ENTRY,
|
||||
CONTRACT,
|
||||
&Setting::empty(),
|
||||
operation,
|
||||
Json::parse("{}").unwrap(),
|
||||
)
|
||||
.await?
|
||||
.resolve(Ok)
|
||||
.await
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_worker_runs_from_a_relocated_owned_executable() {
|
||||
let _execution = EXECUTOR.lock().unwrap();
|
||||
let directory = Directory::new();
|
||||
let fixture = directory.0.join("fixture");
|
||||
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../../test/rust/runtime/worker/fixture.rs");
|
||||
let result = Command::new("rustup")
|
||||
.args(["run", "1.95.0", "rustc", "--edition=2024", "-O"])
|
||||
.arg(source)
|
||||
.arg("-o")
|
||||
.arg(&fixture)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
let runner = directory.0.join("runner");
|
||||
fs::copy(std::env::current_exe().unwrap(), &runner).unwrap();
|
||||
let result = Command::new(runner)
|
||||
.args(["--exact", "owned_process_protocol_case", "--nocapture"])
|
||||
.env_clear()
|
||||
.env("DEVER_WORKER_FIXTURE", &fixture)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&result.stdout),
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_process_protocol_case() {
|
||||
let Some(fixture) = std::env::var_os("DEVER_WORKER_FIXTURE") else {
|
||||
return;
|
||||
};
|
||||
let bundle = bundle(fs::read(&fixture).unwrap());
|
||||
fs::remove_file(fixture).unwrap();
|
||||
let runtime = Runtime::new(Limits {
|
||||
threads: 2,
|
||||
workers: 1,
|
||||
tasks: 32,
|
||||
})
|
||||
.unwrap();
|
||||
let process = runtime
|
||||
.run(async |scope| {
|
||||
let first = pid(call(scope, bundle, "echo").await.unwrap());
|
||||
assert_eq!(first, pid(call(scope, bundle, "echo").await.unwrap()));
|
||||
for source in [
|
||||
"null",
|
||||
" \n{\"text\":\"雪\\u96ea\\n🙂\",\"value\":1e+02} \t",
|
||||
] {
|
||||
let response = worker::call(
|
||||
scope,
|
||||
bundle,
|
||||
ENTRY,
|
||||
CONTRACT,
|
||||
&Setting::empty(),
|
||||
"measure",
|
||||
Json::parse(source).unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.resolve(Ok)
|
||||
.await
|
||||
.unwrap();
|
||||
let Reply::Result(payload) = response else {
|
||||
panic!("expected measured result")
|
||||
};
|
||||
let measured: serde_json::Value = serde_json::from_str(&payload).unwrap();
|
||||
let expected = source.trim_ascii();
|
||||
assert_eq!(measured["length"], expected.len());
|
||||
assert_eq!(
|
||||
measured["checksum"],
|
||||
expected.bytes().map(u64::from).sum::<u64>()
|
||||
);
|
||||
}
|
||||
let Reply::Error { identity, payload } = call(scope, bundle, "error").await.unwrap()
|
||||
else {
|
||||
panic!("business error")
|
||||
};
|
||||
assert_eq!(identity, "demo.test.Error.Failed");
|
||||
assert_eq!(payload, "{\"message\":\"failed\"}");
|
||||
for operation in ["hang", "fragment"] {
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(30), call(scope, bundle, operation))
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(first, pid(call(scope, bundle, "echo").await.unwrap()));
|
||||
}
|
||||
assert!(call(scope, bundle, "crash").await.is_err());
|
||||
let second = pid(call(scope, bundle, "echo").await.unwrap());
|
||||
assert_ne!(first, second);
|
||||
assert!(call(scope, bundle, "bad").await.is_err());
|
||||
let third = pid(call(scope, bundle, "echo").await.unwrap());
|
||||
assert_ne!(second, third);
|
||||
worker::finish(scope, Ok(third)).await.unwrap()
|
||||
})
|
||||
.unwrap();
|
||||
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
|
||||
for operation in ["null_error", "missing_error", "extra_field"] {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let process = pid(call(scope, bundle, "echo").await.unwrap());
|
||||
assert!(call(scope, bundle, operation).await.is_err());
|
||||
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
|
||||
worker::finish(scope, Ok(())).await.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_publication_reuses_only_exact_owned_bytes_and_modes() {
|
||||
let directory = Directory::new();
|
||||
let resources = bundle(b"not executed".to_vec());
|
||||
fs::create_dir(directory.0.join("data")).unwrap();
|
||||
fs::set_permissions(directory.0.join("data"), fs::Permissions::from_mode(0o755)).unwrap();
|
||||
let root = resources.prepare_in(&directory.0).unwrap();
|
||||
assert_eq!(root, resources.prepare_in(&directory.0).unwrap());
|
||||
fs::write(root.join("unlocked.py"), b"not part of bundle").unwrap();
|
||||
assert!(
|
||||
resources
|
||||
.prepare_in(&directory.0)
|
||||
.unwrap_err()
|
||||
.contains("unexpected")
|
||||
);
|
||||
fs::remove_file(root.join("unlocked.py")).unwrap();
|
||||
fs::set_permissions(root.join(ENTRY), fs::Permissions::from_mode(0o700)).unwrap();
|
||||
assert!(
|
||||
resources
|
||||
.prepare_in(&directory.0)
|
||||
.unwrap_err()
|
||||
.contains("mode")
|
||||
);
|
||||
fs::write(root.join(ENTRY), b"bad resource").unwrap();
|
||||
fs::set_permissions(root.join(ENTRY), fs::Permissions::from_mode(0o500)).unwrap();
|
||||
assert!(resources.prepare_in(&directory.0).is_err());
|
||||
let duplicate = Bundle {
|
||||
files: Box::leak(
|
||||
vec![Asset {
|
||||
path: "../escape",
|
||||
bytes: b"",
|
||||
sha256: "",
|
||||
executable: false,
|
||||
}]
|
||||
.into_boxed_slice(),
|
||||
),
|
||||
};
|
||||
assert!(
|
||||
duplicate
|
||||
.prepare_in(&directory.0)
|
||||
.unwrap_err()
|
||||
.contains("path")
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//! 真实复用 Worker 的逐调用成本;输入编码、进程启动和结果核验不计入样本。
|
||||
|
||||
use super::{CONTRACT, ENTRY, EXECUTOR, bundle};
|
||||
use dever_runtime::{
|
||||
task::{Limits, Runtime, Scope},
|
||||
wire::json::Json,
|
||||
worker::{self, Bundle, Reply, Setting},
|
||||
};
|
||||
use std::{fs, path::PathBuf, time::Instant};
|
||||
mod native;
|
||||
|
||||
async fn request(scope: &Scope<'_>, bundle: &'static Bundle, input: Json) -> (usize, u64) {
|
||||
let response = worker::call(
|
||||
scope,
|
||||
bundle,
|
||||
ENTRY,
|
||||
CONTRACT,
|
||||
&Setting::empty(),
|
||||
"measure",
|
||||
input,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.resolve(Ok)
|
||||
.await
|
||||
.unwrap();
|
||||
let Reply::Result(payload) = response else {
|
||||
panic!("expected measured result")
|
||||
};
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Output {
|
||||
length: usize,
|
||||
checksum: u64,
|
||||
}
|
||||
let output: Output = serde_json::from_str(&payload).unwrap();
|
||||
(output.length, output.checksum)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn worker_input_cost() {
|
||||
let _execution = EXECUTOR.lock().unwrap();
|
||||
let fixture = PathBuf::from(
|
||||
std::env::var_os("DEVER_WORKER_BENCH_FIXTURE")
|
||||
.expect("DEVER_WORKER_BENCH_FIXTURE must name the precompiled Worker fixture"),
|
||||
);
|
||||
let bundle = bundle(fs::read(&fixture).unwrap());
|
||||
let runtime = Runtime::new(Limits {
|
||||
threads: 1,
|
||||
workers: 1,
|
||||
tasks: 32,
|
||||
})
|
||||
.unwrap();
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let mut native = native::Native::start(&fixture, CONTRACT).await.unwrap();
|
||||
eprintln!(
|
||||
"worker_input: 15 fixed rounds; one async worker; owned inputs prepared before timing; \
|
||||
same executable, payload, frame protocol, deadlines and reply checks; \
|
||||
rust_independent uses Tokio directly without Dever Worker; \
|
||||
it excludes shared-session admission, caller-cancellation handshake and Scope cleanup; \
|
||||
no compiler runs in this probe"
|
||||
);
|
||||
println!("\ncase,sample,implementation,iterations,elapsed_ns,checksum");
|
||||
for text in ["null", " \n{\"text\":\"雪\\u96ea\\n🙂\",\"value\":1e+02} \t"] {
|
||||
let trimmed = text.trim_ascii();
|
||||
let expected = (trimmed.len(), trimmed.bytes().map(u64::from).sum::<u64>());
|
||||
assert_eq!(
|
||||
request(scope, bundle, Json::parse(text).unwrap()).await,
|
||||
expected
|
||||
);
|
||||
assert_eq!(native.request(trimmed).await.unwrap(), expected);
|
||||
}
|
||||
for size in [64, 65_536, 1_048_576] {
|
||||
let iterations = match size {
|
||||
64 => 4096,
|
||||
65_536 => 512,
|
||||
_ => 64,
|
||||
};
|
||||
let input = Json::encode(&serde_json::json!({"text": "a".repeat(size)})).unwrap();
|
||||
let expected = (
|
||||
input.as_str().len(),
|
||||
input.as_str().bytes().map(u64::from).sum::<u64>(),
|
||||
);
|
||||
for _ in 0..16 {
|
||||
assert_eq!(request(scope, bundle, input.clone()).await, expected);
|
||||
assert_eq!(native.request(input.as_str()).await.unwrap(), expected);
|
||||
}
|
||||
for sample in 0..15 {
|
||||
let mut order = ["dever", "rust_independent", "control"];
|
||||
order.rotate_left(sample % 3);
|
||||
for label in order {
|
||||
let mut results = Vec::with_capacity(iterations);
|
||||
let elapsed = if label == "rust_independent" {
|
||||
let inputs = (0..iterations)
|
||||
.map(|_| input.as_str().to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
let started = Instant::now();
|
||||
for payload in inputs {
|
||||
results.push(native.request(&payload).await.unwrap());
|
||||
}
|
||||
started.elapsed().as_nanos()
|
||||
} else {
|
||||
let inputs = vec![input.clone(); iterations];
|
||||
let started = Instant::now();
|
||||
for payload in inputs {
|
||||
results.push(request(scope, bundle, payload).await);
|
||||
}
|
||||
started.elapsed().as_nanos()
|
||||
};
|
||||
assert!(results.iter().all(|&result| result == expected));
|
||||
let checksum = expected.1 * results.len() as u64;
|
||||
println!("worker_input_{size},{sample},{label},{iterations},{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
worker::finish(scope, Ok(())).await.unwrap();
|
||||
native.finish().await.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe"]
|
||||
fn validated_reply_decode_cost() {
|
||||
#[derive(Debug, PartialEq, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Output {
|
||||
text: String,
|
||||
values: Vec<i64>,
|
||||
}
|
||||
for size in [64, 4096, 65_536] {
|
||||
let payload =
|
||||
serde_json::json!({"text": "a".repeat(size), "values": [1, 2, 3]}).to_string();
|
||||
// Channel::read 的完整帧验证已完成;只计量其后的载荷解码,不代表 IPC 往返。
|
||||
let input = Json::parse(&payload).unwrap();
|
||||
let expected = dever_runtime::wire::decode::<Output>(input.as_str()).unwrap();
|
||||
assert_eq!(
|
||||
expected,
|
||||
serde_json::from_str::<Output>(input.as_str()).unwrap()
|
||||
);
|
||||
for sample in 0..15 {
|
||||
for before in if sample % 2 == 0 {
|
||||
[true, false]
|
||||
} else {
|
||||
[false, true]
|
||||
} {
|
||||
let mut checksum = 0;
|
||||
let started = Instant::now();
|
||||
for _ in 0..2000 {
|
||||
let text = std::hint::black_box(input.as_str());
|
||||
let value = if before {
|
||||
dever_runtime::wire::decode::<Output>(text).unwrap()
|
||||
} else {
|
||||
serde_json::from_str::<Output>(text).unwrap()
|
||||
};
|
||||
checksum += std::hint::black_box(&value).text.len() + value.values.len();
|
||||
}
|
||||
let elapsed = started.elapsed().as_nanos();
|
||||
assert_eq!(checksum, 2000 * (size + 3));
|
||||
let label = if before { "before" } else { "after" };
|
||||
println!("worker_reply_{size},{sample},{label},{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
//! 独立 Tokio IPC 下界:同一对端、帧格式、期限和响应校验,不调用 Dever Worker。
|
||||
//! 直连不包含共享会话排队、调用方取消握手和 Scope 回收,不能据此宣称完整语义追平。
|
||||
|
||||
use bytes::Buf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{io, path::Path, process::Stdio, time::Duration};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
process::{Child, ChildStdin, ChildStdout},
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub(super) struct Native {
|
||||
child: Child,
|
||||
input: ChildStdin,
|
||||
output: ChildStdout,
|
||||
next: u64,
|
||||
}
|
||||
|
||||
impl Native {
|
||||
pub(super) async fn start(executable: &Path, contract: &str) -> io::Result<Self> {
|
||||
let mut child = tokio::process::Command::new(executable)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
let mut value = Self {
|
||||
input: child.stdin.take().unwrap(),
|
||||
output: child.stdout.take().unwrap(),
|
||||
child,
|
||||
next: 1,
|
||||
};
|
||||
let contract: serde_json::Value = serde_json::from_str(contract)?;
|
||||
let mut hello = serde_json::json!({"kind":"hello","version":"dever-component-1"});
|
||||
for name in ["port", "schema", "adapter", "capabilities", "operations"] {
|
||||
hello[name] = contract[name].clone();
|
||||
}
|
||||
let mut expected = hello.clone();
|
||||
expected["kind"] = "ready".into();
|
||||
let mut header = serde_json::to_string(&hello)?;
|
||||
header.pop();
|
||||
header.push_str(",\"setting\":null}");
|
||||
value.write(&header, "", "").await?;
|
||||
let ready: serde_json::Value = serde_json::from_str(&value.read().await?)?;
|
||||
assert_eq!(ready, expected);
|
||||
value.write(r#"{"kind":"health","id":0}"#, "", "").await?;
|
||||
assert_eq!(value.read().await?.as_str(), r#"{"kind":"health","id":0}"#);
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
async fn write(&mut self, header: &str, payload: &str, suffix: &str) -> io::Result<()> {
|
||||
let length = header.len() + payload.len() + suffix.len();
|
||||
if length > 16 * 1024 * 1024 {
|
||||
return Err(io::Error::other("frame too large"));
|
||||
}
|
||||
let prefix = (length as u32).to_be_bytes();
|
||||
let mut bytes = Buf::chain(prefix.as_slice(), header.as_bytes())
|
||||
.chain(payload.as_bytes())
|
||||
.chain(suffix.as_bytes());
|
||||
tokio::time::timeout(Duration::from_secs(2), async {
|
||||
self.input.write_all_buf(&mut bytes).await?;
|
||||
self.input.flush().await
|
||||
})
|
||||
.await
|
||||
.map_err(io::Error::other)?
|
||||
}
|
||||
|
||||
async fn read(&mut self) -> io::Result<Zeroizing<String>> {
|
||||
tokio::time::timeout(Duration::from_secs(2), async {
|
||||
let mut prefix = [0; 4];
|
||||
self.output.read_exact(&mut prefix).await?;
|
||||
let length = u32::from_be_bytes(prefix) as usize;
|
||||
if length == 0 || length > 16 * 1024 * 1024 {
|
||||
return Err(io::Error::other("invalid frame length"));
|
||||
}
|
||||
let mut bytes = vec![0; length];
|
||||
self.output.read_exact(&mut bytes).await?;
|
||||
String::from_utf8(bytes)
|
||||
.map(Zeroizing::new)
|
||||
.map_err(io::Error::other)
|
||||
})
|
||||
.await
|
||||
.map_err(io::Error::other)?
|
||||
}
|
||||
|
||||
pub(super) async fn request(&mut self, payload: &str) -> io::Result<(usize, u64)> {
|
||||
#[derive(Serialize)]
|
||||
struct Header<'text> {
|
||||
kind: &'text str,
|
||||
id: u64,
|
||||
operation: &'text str,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Output {
|
||||
length: usize,
|
||||
checksum: u64,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Frame {
|
||||
kind: String,
|
||||
id: u64,
|
||||
payload: Output,
|
||||
}
|
||||
let id = self.next;
|
||||
self.next = id
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| io::Error::other("request id exhausted"))?;
|
||||
let mut header = serde_json::to_string(&Header {
|
||||
kind: "call",
|
||||
id,
|
||||
operation: "measure",
|
||||
})?;
|
||||
header.pop();
|
||||
header.push_str(",\"payload\":");
|
||||
self.write(&header, payload, "}").await?;
|
||||
let frame: Frame = serde_json::from_str(&self.read().await?)?;
|
||||
if frame.kind != "result" || frame.id != id {
|
||||
return Err(io::Error::other("unexpected response"));
|
||||
}
|
||||
Ok((frame.payload.length, frame.payload.checksum))
|
||||
}
|
||||
|
||||
pub(super) async fn finish(mut self) -> io::Result<()> {
|
||||
self.write(r#"{"kind":"shutdown"}"#, "", "").await?;
|
||||
assert_eq!(self.read().await?.as_str(), r#"{"kind":"shutdown"}"#);
|
||||
let status = tokio::time::timeout(Duration::from_secs(2), self.child.wait())
|
||||
.await
|
||||
.map_err(io::Error::other)??;
|
||||
assert!(status.success());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//! 出站帧保留 JSON 原文,并在写出长度前验证合并后的预算。
|
||||
use super::*;
|
||||
use crate::wire::json::Json;
|
||||
use std::{hint::black_box, process::Stdio, time::Instant};
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Call<'input> {
|
||||
kind: &'static str,
|
||||
id: u64,
|
||||
operation: &'input str,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_payload_preserves_spelling_and_combined_limits() {
|
||||
let header = Call {
|
||||
kind: "call",
|
||||
id: 1,
|
||||
operation: "雪\"\\",
|
||||
};
|
||||
for source in ["null", " \n{\"雪\":[1e9999,-0,\"\\u96ea\"]}\t "] {
|
||||
let value = Json::parse(source).unwrap();
|
||||
let head = payload_header(
|
||||
serde_json::to_string(&header).unwrap(),
|
||||
4,
|
||||
",\"payload\":",
|
||||
value.shape().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let frame = format!("{head}{}}}", source.trim_ascii());
|
||||
#[derive(serde::Serialize)]
|
||||
struct Reference<'input> {
|
||||
#[serde(flatten)]
|
||||
header: &'input Call<'input>,
|
||||
payload: &'input RawValue,
|
||||
}
|
||||
assert_eq!(
|
||||
frame,
|
||||
serde_json::to_string(&Reference {
|
||||
header: &header,
|
||||
payload: serde_json::from_str(source).unwrap(),
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
crate::wire::raw::validate(&frame).unwrap();
|
||||
}
|
||||
for count in [document::MAX_VALUES - 5, document::MAX_VALUES - 4] {
|
||||
let value = Json::encode(&vec![0; count]).unwrap();
|
||||
let encoded = payload_header(
|
||||
serde_json::to_string(&header).unwrap(),
|
||||
4,
|
||||
",\"payload\":",
|
||||
value.shape().unwrap(),
|
||||
);
|
||||
assert_eq!(encoded.is_ok(), count == document::MAX_VALUES - 5);
|
||||
}
|
||||
for depth in [document::MAX_DEPTH - 1, document::MAX_DEPTH] {
|
||||
let value = Json::parse(&("[".repeat(depth) + &"]".repeat(depth))).unwrap();
|
||||
assert_eq!(
|
||||
payload_header(
|
||||
serde_json::to_string(&header).unwrap(),
|
||||
4,
|
||||
",\"payload\":",
|
||||
value.shape().unwrap()
|
||||
)
|
||||
.is_ok(),
|
||||
depth < document::MAX_DEPTH
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_frame_does_not_write_a_partial_prefix() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
runtime.block_on(async {
|
||||
let mut child = tokio::process::Command::new("cat")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut channel = Channel::new(child.stdin.take().unwrap(), child.stdout.take().unwrap());
|
||||
let payload = Json::string(&"a".repeat(LIMIT - 2)).unwrap();
|
||||
let header = Call {
|
||||
kind: "call",
|
||||
id: 1,
|
||||
operation: "measure",
|
||||
};
|
||||
assert!(
|
||||
channel
|
||||
.send_payload(
|
||||
serde_json::to_string(&header).unwrap(),
|
||||
4,
|
||||
",\"payload\":",
|
||||
payload.as_str(),
|
||||
payload.shape().unwrap()
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
let control = serde_json::json!({"kind":"health","id":0});
|
||||
channel.send(&control).await.unwrap();
|
||||
assert_eq!(channel.read().await.unwrap().as_str(), control.to_string());
|
||||
drop(channel);
|
||||
assert!(child.wait().await.unwrap().success());
|
||||
});
|
||||
}
|
||||
|
||||
fn reference_header(header: &Call<'_>, values: usize, depth: usize) -> Result<String, String> {
|
||||
if values + 4 > document::MAX_VALUES || depth + 1 > document::MAX_DEPTH {
|
||||
return Err("JSON budget".into());
|
||||
}
|
||||
let mut text = serde_json::to_string(header).map_err(|error| error.to_string())?;
|
||||
// 与正式通道一样,整帧字节预算在写出长度前检查,不属于帧头编码计时。
|
||||
text.pop();
|
||||
text.push_str(",\"payload\":");
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit release performance probe; header only, not complete IPC"]
|
||||
fn outgoing_header_cost() {
|
||||
let header = Call {
|
||||
kind: "call",
|
||||
id: 42,
|
||||
operation: "measure",
|
||||
};
|
||||
let shape = document::Shape::new(2, 1);
|
||||
let expected = reference_header(&header, 2, 1).unwrap();
|
||||
assert_eq!(
|
||||
payload_header(
|
||||
serde_json::to_string(&header).unwrap(),
|
||||
4,
|
||||
",\"payload\":",
|
||||
shape
|
||||
)
|
||||
.unwrap(),
|
||||
expected
|
||||
);
|
||||
for sample in 0..15 {
|
||||
for native in if sample % 2 == 0 {
|
||||
[false, true]
|
||||
} else {
|
||||
[true, false]
|
||||
} {
|
||||
let started = Instant::now();
|
||||
let mut checksum = 0;
|
||||
for _ in 0..100_000 {
|
||||
let text = if native {
|
||||
reference_header(black_box(&header), black_box(2), black_box(1))
|
||||
} else {
|
||||
payload_header(
|
||||
serde_json::to_string(black_box(&header)).unwrap(),
|
||||
4,
|
||||
",\"payload\":",
|
||||
black_box(shape),
|
||||
)
|
||||
}
|
||||
.unwrap();
|
||||
checksum += black_box(text).len();
|
||||
}
|
||||
assert_eq!(checksum, 100_000 * expected.len());
|
||||
println!(
|
||||
"worker_header,{sample},{},{},{checksum}",
|
||||
if native { "rust" } else { "dever" },
|
||||
started.elapsed().as_nanos()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user