Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,192 @@
|
||||
//! 新场景沿用同一个 compare 构建器;只替换测试入口名,保留正式生成的认证和 Job 分派。
|
||||
|
||||
use dever::compiler::{emit, model::Program};
|
||||
use std::path::Path;
|
||||
#[path = "../job/build.rs"]
|
||||
mod jobs;
|
||||
|
||||
pub fn run(core: &Path) -> bool {
|
||||
if std::env::args().any(|value| value == "--rest-json") {
|
||||
crate::compare(
|
||||
core,
|
||||
"auth",
|
||||
"rest-json",
|
||||
&(include_str!("../samples.rs").to_owned()
|
||||
+ include_str!("../application/rest_json.rs")
|
||||
+ "\nmod output { use super::*;\n"
|
||||
+ include_str!("../application/rest_output.rs")
|
||||
+ "\n}\n"),
|
||||
&[("GENERATED_RAW_ENCODE", "demo.document.app.encode")],
|
||||
&[],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if std::env::args().any(|value| value == "--rest-wire") {
|
||||
crate::compare(
|
||||
core,
|
||||
"auth",
|
||||
"rest-wire",
|
||||
&(include_str!("../samples.rs").to_owned() + include_str!("../application/rest.rs")),
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if std::env::args().any(|value| value == "--api-wire") {
|
||||
crate::compare(
|
||||
core,
|
||||
"api-wire",
|
||||
"api-wire",
|
||||
&(include_str!("../samples.rs").to_owned() + include_str!("../application/http.rs")),
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
let http = std::env::args().any(|value| value == "--auth-http");
|
||||
if http || std::env::args().any(|value| value == "--auth") {
|
||||
let reference = [
|
||||
include_str!("shared.rs"),
|
||||
include_str!("reference.rs"),
|
||||
include_str!("measure.rs"),
|
||||
"mod protected_http { use super::*;\n",
|
||||
include_str!("http/reference.rs"),
|
||||
include_str!("http/client.rs"),
|
||||
include_str!("http/check.rs"),
|
||||
include_str!("http/measure.rs"),
|
||||
"}\n",
|
||||
&format!("const HTTP_BENCH: bool = {http};\n"),
|
||||
]
|
||||
.concat();
|
||||
crate::compare(
|
||||
core,
|
||||
"auth-performance",
|
||||
if http { "auth-http" } else { "auth" },
|
||||
&reference,
|
||||
&[
|
||||
("GENERATED_SEED", "bench.account.app.seed"),
|
||||
("GENERATED_PERMISSIONS", "bench.account.app.permissions"),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
true
|
||||
} else if std::env::args().any(|value| value == "--jobs") {
|
||||
jobs::run(core);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata(name: &str, program: &Program, reference: &str) -> String {
|
||||
match name {
|
||||
"rest-json" => {
|
||||
let index = program
|
||||
.rest
|
||||
.iter()
|
||||
.position(|entry| {
|
||||
program.packages
|
||||
[program.definitions[program.models[entry.model].definition].source]
|
||||
== "demo.document.model"
|
||||
})
|
||||
.unwrap();
|
||||
let source = emit::application::generate(program);
|
||||
let route = source
|
||||
.split_once(&format!("async fn rest{index}("))
|
||||
.unwrap()
|
||||
.1;
|
||||
let detail = route
|
||||
.split_once("let row = rows.pop().ok_or(dever_runtime::api::Failure(404))?; ")
|
||||
.unwrap()
|
||||
.1;
|
||||
let page = route
|
||||
.split_once("let page = executor.page(")
|
||||
.unwrap()
|
||||
.1
|
||||
.split_once(".await.map_err(rest_error)?; ")
|
||||
.unwrap()
|
||||
.1;
|
||||
let definition = program.models[program.rest[index].model].definition;
|
||||
reference
|
||||
.replace("RAW_ARGS", &format!("RestCreate{index}"))
|
||||
.replace("RAW_MODEL_ID", &definition.to_string())
|
||||
.replace("RAW_MODEL", &format!("Type{definition}"))
|
||||
.replace(
|
||||
"REST_DETAIL_ENCODE",
|
||||
detail.split_once(".map_err(Into::into)").unwrap().0,
|
||||
)
|
||||
.replace(
|
||||
"REST_PAGE_ENCODE",
|
||||
page.split_once(".map_err(Into::into)").unwrap().0,
|
||||
)
|
||||
}
|
||||
"rest-wire" => {
|
||||
// 比较实际 REST 输入类型及其解码入口,不把数据库和认证计入 codec 耗时。
|
||||
let source = emit::application::generate(program);
|
||||
let (_, input) = source
|
||||
.split_once("let args: RestCreate0 = input.decode(")
|
||||
.unwrap();
|
||||
let decoder = input.split_once(')').unwrap().0;
|
||||
reference.replace("REST_DECODER", decoder)
|
||||
}
|
||||
"api-wire" => [
|
||||
("GENERATED_HTTP", "/bench/wire/echo"),
|
||||
("GENERATED_LIST", "/bench/wire/count"),
|
||||
]
|
||||
.into_iter()
|
||||
.fold(reference.to_owned(), |source, (marker, path)| {
|
||||
let index = program
|
||||
.http
|
||||
.iter()
|
||||
.position(|entry| entry.path == path && entry.method == "POST")
|
||||
.unwrap();
|
||||
source.replace(marker, &format!("api{index}"))
|
||||
}),
|
||||
"auth" | "auth-http" => {
|
||||
assert_eq!(program.verifiers.len(), 1);
|
||||
let verifier = &program.verifiers[0];
|
||||
let account = program
|
||||
.models
|
||||
.iter()
|
||||
.position(|model| {
|
||||
program.packages[program.definitions[model.definition].source]
|
||||
== "bench.account.model"
|
||||
})
|
||||
.unwrap();
|
||||
reference
|
||||
.replace("ACCOUNT_BINDING", &account.to_string())
|
||||
.replace("AUTH_BINDING", &(program.models.len() + 1).to_string())
|
||||
.replace("AUTH_USER_MODEL", &verifier.user.1.to_string())
|
||||
.replace("AUTH_TENANT_MODEL", &verifier.tenant.1.to_string())
|
||||
.replace(
|
||||
"GENERATED_HTTP",
|
||||
&format!(
|
||||
"api{}",
|
||||
program
|
||||
.http
|
||||
.iter()
|
||||
.position(|entry| entry.path == "/bench/account/admin/permissions"
|
||||
&& entry.method == "GET")
|
||||
.unwrap()
|
||||
),
|
||||
)
|
||||
}
|
||||
"jobs" => jobs::metadata(program, reference),
|
||||
_ => reference.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn application(name: &str, program: &Program) -> String {
|
||||
if matches!(
|
||||
name,
|
||||
"auth" | "auth-http" | "jobs" | "api-wire" | "rest-wire" | "rest-json"
|
||||
) {
|
||||
let source = emit::application::generate(program);
|
||||
assert_eq!(source.matches("fn main() {").count(), 1);
|
||||
source.replacen("fn main() {", "fn application_main() {", 1)
|
||||
} else if !program.commands.is_empty() {
|
||||
emit::application::functions(program)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// 正确性检查在计时外覆盖凭据、Provider、角色、路由及严格输入,不能用空 200 掩盖漏做。
|
||||
fn check_requests(connection: &mut HttpConnection, input: &AuthInput) {
|
||||
for request in &input.requests[..16] {
|
||||
check_reply(&connection.request(&request_bytes(request)), 200);
|
||||
}
|
||||
check_reply(
|
||||
&connection.request(&request_bytes(&input.requests[16])),
|
||||
401,
|
||||
);
|
||||
let valid = &input.requests[0];
|
||||
let mut missing = valid.clone();
|
||||
missing.headers_mut().remove("cookie");
|
||||
let mut invalid = valid.clone();
|
||||
invalid
|
||||
.headers_mut()
|
||||
.insert("cookie", "session=v1.invalid-jwt".parse().unwrap());
|
||||
let mut host = valid.clone();
|
||||
host.headers_mut()
|
||||
.insert("host", "other.test".parse().unwrap());
|
||||
let mut path = valid.clone();
|
||||
*path.uri_mut() = format!("{}-missing", path.uri()).parse().unwrap();
|
||||
let mut query = valid.clone();
|
||||
*query.uri_mut() = format!("{}?unexpected=1", query.uri()).parse().unwrap();
|
||||
for (request, status) in [
|
||||
(missing, 401),
|
||||
(invalid, 401),
|
||||
(host, 404),
|
||||
(path, 404),
|
||||
(query, 400),
|
||||
] {
|
||||
check_reply(&connection.request(&request_bytes(&request)), status);
|
||||
}
|
||||
}
|
||||
|
||||
fn membership(runtime: &Runtime, registry: &Arc<Registry>, input: &AuthInput, grant: bool) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let (mut context, claims) = input
|
||||
.service
|
||||
.prepare("admin", &input.requests[0], false, input.now)
|
||||
.unwrap();
|
||||
rust_verify(&mut context, claims.unwrap(), scope, registry)
|
||||
.await
|
||||
.map_err(|error| error.0)
|
||||
.unwrap();
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(AUTH_BINDING, scope).await.unwrap();
|
||||
if grant {
|
||||
authorization::grant(executor, scope, &context, 16, "reader")
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
authorization::revoke(executor, scope, &context, 16, "reader")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn check(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
config: &Arc<api::Config>,
|
||||
input: &AuthInput,
|
||||
) {
|
||||
for generated in [true, false] {
|
||||
with_server(runtime, registry, config, input, generated, |port| {
|
||||
check_requests(&mut HttpConnection::connect(port), input);
|
||||
});
|
||||
}
|
||||
membership(runtime, registry, input, false);
|
||||
for generated in [true, false] {
|
||||
with_server(runtime, registry, config, input, generated, |port| {
|
||||
let mut connection = HttpConnection::connect(port);
|
||||
check_reply(
|
||||
&connection.request(&request_bytes(&input.requests[15])),
|
||||
403,
|
||||
);
|
||||
});
|
||||
}
|
||||
membership(runtime, registry, input, true);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// 客户端只连接本轮持有的回环监听器,使用同一 HTTP/1.1 长连接顺序发送请求。
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read, Write},
|
||||
net::TcpStream,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const ROUTE: &str = "/bench/account/admin/permissions";
|
||||
const CONTENT_TYPE: &str = "application/json; charset=utf-8";
|
||||
const SUCCESS: &str = "{\"code\":0,\"message\":\"ok\",\"data\":[{\"key\":\"bench.account.admin.permissions\",\"component\":\"bench\",\"domain\":\"account\",\"site\":\"admin\",\"action\":\"permissions\",\"method\":\"GET\",\"kind\":\"http\"}]}";
|
||||
|
||||
struct HttpReply {
|
||||
status: u16,
|
||||
content_type: String,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
struct HttpConnection(BufReader<TcpStream>);
|
||||
|
||||
impl HttpConnection {
|
||||
fn connect(port: u16) -> Self {
|
||||
let socket = TcpStream::connect(("127.0.0.1", port)).unwrap();
|
||||
socket.set_nodelay(true).unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
socket
|
||||
.set_write_timeout(Some(Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
Self(BufReader::new(socket))
|
||||
}
|
||||
|
||||
fn request(&mut self, request: &[u8]) -> HttpReply {
|
||||
self.0.get_mut().write_all(request).unwrap();
|
||||
let mut line = String::new();
|
||||
self.0.read_line(&mut line).unwrap();
|
||||
assert!(line.starts_with("HTTP/1.1 "));
|
||||
let status = line.split_whitespace().nth(1).unwrap().parse().unwrap();
|
||||
let mut length = None;
|
||||
let mut content_type = String::new();
|
||||
loop {
|
||||
line.clear();
|
||||
assert!(self.0.read_line(&mut line).unwrap() > 0);
|
||||
if line == "\r\n" {
|
||||
break;
|
||||
}
|
||||
let (name, value) = line.trim_end().split_once(':').unwrap();
|
||||
match name.to_ascii_lowercase().as_str() {
|
||||
"content-length" => {
|
||||
assert!(length.is_none());
|
||||
length = Some(value.trim().parse::<usize>().unwrap());
|
||||
}
|
||||
"content-type" => content_type = value.trim().into(),
|
||||
"connection" => assert_ne!(value.trim(), "close"),
|
||||
"transfer-encoding" | "set-cookie" => panic!("unexpected response header"),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
let mut body = vec![0; length.unwrap()];
|
||||
self.0.read_exact(&mut body).unwrap();
|
||||
HttpReply {
|
||||
status,
|
||||
content_type,
|
||||
body,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn request_bytes(request: &Request) -> Vec<u8> {
|
||||
assert!(request.body().is_empty());
|
||||
let mut bytes = format!("{} {} HTTP/1.1\r\n", request.method(), request.uri()).into_bytes();
|
||||
for (name, value) in request.headers() {
|
||||
bytes.extend_from_slice(name.as_str().as_bytes());
|
||||
bytes.extend_from_slice(b": ");
|
||||
bytes.extend_from_slice(value.as_bytes());
|
||||
bytes.extend_from_slice(b"\r\n");
|
||||
}
|
||||
bytes.extend_from_slice(b"Connection: keep-alive\r\nContent-Length: 0\r\n\r\n");
|
||||
bytes
|
||||
}
|
||||
|
||||
fn check_reply(reply: &HttpReply, status: u16) {
|
||||
assert_eq!(reply.status, status);
|
||||
assert_eq!(reply.content_type, CONTENT_TYPE);
|
||||
let expected = if status == 200 {
|
||||
SUCCESS.as_bytes().to_vec()
|
||||
} else {
|
||||
api::error(status).body().to_vec()
|
||||
};
|
||||
assert_eq!(reply.body, expected);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 两侧复用相同 HTTP 引擎、限额和调度策略;计时包含真实 JWT 校验、三次数据库读取及响应。
|
||||
fn with_server<Value>(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
config: &Arc<api::Config>,
|
||||
input: &AuthInput,
|
||||
generated: bool,
|
||||
client: impl FnOnce(u16) -> Value,
|
||||
) -> Value {
|
||||
let registry = registry.clone();
|
||||
let config = config.clone();
|
||||
let authentication = input.service.clone();
|
||||
std::thread::scope(|threads| {
|
||||
let (started, port) = std::sync::mpsc::sync_channel(0);
|
||||
let (finished, completion) = tokio::sync::oneshot::channel();
|
||||
let serving = threads.spawn(move || {
|
||||
runtime
|
||||
.run(async move |scope| {
|
||||
let listener = dever_runtime::net::listen("127.0.0.1", 0, scope)
|
||||
.await
|
||||
.unwrap();
|
||||
let port = listener.port().unwrap() as u16;
|
||||
let incoming = listener.clone();
|
||||
let server = scope
|
||||
.spawn(move |child| async move {
|
||||
let limits = dever_runtime::http::Limits {
|
||||
connections: 1,
|
||||
..Default::default()
|
||||
};
|
||||
api::serve::<_, _, true>(
|
||||
&incoming,
|
||||
&limits,
|
||||
&child,
|
||||
None,
|
||||
move |request, scope, peer| {
|
||||
let config = config.clone();
|
||||
let registry = registry.clone();
|
||||
let authentication = authentication.clone();
|
||||
async move {
|
||||
let path = request.uri().path();
|
||||
let response = match (request.method().as_str(), path) {
|
||||
("GET", ROUTE) if config.accepts("admin", &request) => {
|
||||
if generated {
|
||||
GENERATED_HTTP(
|
||||
&request,
|
||||
peer,
|
||||
&scope,
|
||||
®istry,
|
||||
&authentication,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
rust_http(
|
||||
&request,
|
||||
peer,
|
||||
&scope,
|
||||
®istry,
|
||||
&authentication,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
_ => api::error(404),
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
})
|
||||
.unwrap();
|
||||
// 阻塞客户端独立于运行时线程,也不占用数据库的阻塞池;交接在计时外。
|
||||
started.send(port).unwrap();
|
||||
completion.await.unwrap();
|
||||
listener.close().unwrap();
|
||||
match server.timeout(5000).await.unwrap() {
|
||||
dever_runtime::task::Timed::Ready(result) => result.unwrap(),
|
||||
_ => panic!("owned HTTP benchmark server failed to drain"),
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
let result = client(port.recv().unwrap());
|
||||
finished.send(()).unwrap();
|
||||
serving.join().unwrap();
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn run(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
config: &Arc<api::Config>,
|
||||
input: &AuthInput,
|
||||
) {
|
||||
check(runtime, registry, config, input);
|
||||
eprintln!(
|
||||
"HTTP/1.1 loopback; one persistent connection; rotating 16 JWT cookies; JWT + Provider + role gate + permission list + JSON response included; TCP connect and response assertions excluded; no TLS/proxy/concurrent-load claim"
|
||||
);
|
||||
let requests = input.requests[..16]
|
||||
.iter()
|
||||
.map(request_bytes)
|
||||
.collect::<Vec<_>>();
|
||||
samples("http_jwt_provider_roles", |generated, count| {
|
||||
with_server(runtime, registry, config, input, generated, |port| {
|
||||
let mut connection = HttpConnection::connect(port);
|
||||
check_reply(&connection.request(&requests[0]), 200);
|
||||
let mut replies = Vec::with_capacity(count);
|
||||
let start = Instant::now();
|
||||
for index in 0..count {
|
||||
replies.push(connection.request(black_box(&requests[index % requests.len()])));
|
||||
}
|
||||
let elapsed = start.elapsed().as_nanos();
|
||||
let mut checksum = 0u64;
|
||||
for reply in &replies {
|
||||
check_reply(reply, 200);
|
||||
checksum += black_box(reply.body.iter().map(|byte| u64::from(*byte)).sum::<u64>());
|
||||
}
|
||||
(elapsed, checksum)
|
||||
})
|
||||
});
|
||||
check(runtime, registry, config, input);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 手写 Rust 完成同样的请求边界、Provider、权限门禁和业务返回,不调用生成 API。
|
||||
use dever_runtime::{api, authorization, wire};
|
||||
use wire::serde::{self, Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde")]
|
||||
struct RustPermission {
|
||||
key: String,
|
||||
component: String,
|
||||
domain: String,
|
||||
site: String,
|
||||
action: String,
|
||||
method: String,
|
||||
kind: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(crate = "dever_runtime::wire::serde", deny_unknown_fields)]
|
||||
struct EmptyFields {}
|
||||
|
||||
struct EmptyInput;
|
||||
|
||||
impl<'input> Deserialize<'input> for EmptyInput {
|
||||
fn deserialize<Decoder: serde::Deserializer<'input>>(
|
||||
decoder: Decoder,
|
||||
) -> Result<Self, Decoder::Error> {
|
||||
let _: EmptyFields = wire::object(decoder)?;
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
fn access_failure(error: authorization::Failure) -> ApiFailure {
|
||||
match error {
|
||||
authorization::Failure::Access(status) => ApiFailure(status),
|
||||
authorization::Failure::Database(_) => ApiFailure(500),
|
||||
}
|
||||
}
|
||||
|
||||
async fn rust_http(
|
||||
request: &Request,
|
||||
peer: std::net::SocketAddr,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
authentication: &Arc<auth::Service>,
|
||||
) -> Response {
|
||||
let now = match auth::now() {
|
||||
Ok(now) => now,
|
||||
Err(error) => return api::error(error.status()),
|
||||
};
|
||||
let (mut context, claims) = match authentication.prepare_at("admin", request, false, now, peer)
|
||||
{
|
||||
Ok(prepared) => prepared,
|
||||
Err(error) => return api::error(error.status()),
|
||||
};
|
||||
if let Some(claims) = claims {
|
||||
if let Err(error) = rust_verify(&mut context, claims, scope, registry).await {
|
||||
return error.response();
|
||||
}
|
||||
}
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let outcome = async {
|
||||
authorization::authorize(
|
||||
&mut database,
|
||||
AUTH_BINDING,
|
||||
scope,
|
||||
&context,
|
||||
"bench",
|
||||
"account",
|
||||
"permissions",
|
||||
)
|
||||
.await
|
||||
.map_err(access_failure)?;
|
||||
let input = api::input(request).map_err(ApiFailure)?;
|
||||
let _: EmptyInput = input
|
||||
.decode(dever_runtime::wire::decode)
|
||||
.map_err(|_| ApiFailure(400))?;
|
||||
let values = authorization::permissions(&mut database, AUTH_BINDING, scope, &context, true)
|
||||
.await
|
||||
.map_err(access_failure)?
|
||||
.into_iter()
|
||||
.map(|value| RustPermission {
|
||||
key: value.key,
|
||||
component: value.component,
|
||||
domain: value.domain,
|
||||
site: value.site,
|
||||
action: value.action,
|
||||
method: value.method,
|
||||
kind: value.kind,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
wire::response(&values).map_err(ApiFailure::from)
|
||||
}
|
||||
.await;
|
||||
match outcome {
|
||||
Ok(body) => {
|
||||
let mut response = api::response(body);
|
||||
context.publish(&mut response);
|
||||
response
|
||||
}
|
||||
Err(error) => error.response(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
// JWT 解析在计时前完成;本场景测 Provider 数据库复核及随后实际角色读取。
|
||||
use dever_runtime::api::{Request, Response};
|
||||
|
||||
struct AuthInput {
|
||||
service: Arc<auth::Service>,
|
||||
requests: Vec<Request>,
|
||||
claims: Vec<Claims>,
|
||||
now: i64,
|
||||
}
|
||||
|
||||
fn prepare(path: &Path) -> (Arc<dever_runtime::api::Config>, AuthInput) {
|
||||
let config = Arc::new(dever_runtime::api::Config::load(path, &["admin"]).unwrap());
|
||||
let service = Arc::new(
|
||||
auth::Service::new(
|
||||
config.clone(),
|
||||
&[auth::Verification {
|
||||
name: "bench.account.verify",
|
||||
user_model: AUTH_USER_MODEL,
|
||||
tenant_model: AUTH_TENANT_MODEL,
|
||||
}],
|
||||
&[auth::Route {
|
||||
topic: "admin",
|
||||
required: true,
|
||||
user_models: &[AUTH_USER_MODEL],
|
||||
tenant_models: &[AUTH_TENANT_MODEL],
|
||||
}],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let now = auth::now().unwrap();
|
||||
let claims = (0..17)
|
||||
.map(|index| Claims {
|
||||
subject: format!("account-{index:02} 中文\""),
|
||||
session: format!("session-{index}"),
|
||||
site: "admin".into(),
|
||||
tenant: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let requests = claims
|
||||
.iter()
|
||||
.map(|claims| {
|
||||
// 只用公开 Cookie 接口准备凭据,不读取 Secret 的私有字节。
|
||||
let mut bootstrap = Request::new(Default::default());
|
||||
*bootstrap.method_mut() = "POST".parse().unwrap();
|
||||
*bootstrap.uri_mut() = "/login".parse().unwrap();
|
||||
bootstrap
|
||||
.headers_mut()
|
||||
.insert("host", "benchmark.test".parse().unwrap());
|
||||
bootstrap
|
||||
.headers_mut()
|
||||
.insert("origin", "https://benchmark.test".parse().unwrap());
|
||||
let (mut context, _) = service.prepare("admin", &bootstrap, true, now).unwrap();
|
||||
context
|
||||
.issue_cookie(&claims.subject, &claims.session, None, now)
|
||||
.unwrap();
|
||||
let mut response = Response::new(Default::default());
|
||||
context.publish(&mut response);
|
||||
let cookie = response.headers().get("set-cookie").unwrap();
|
||||
let cookie = std::str::from_utf8(cookie.as_bytes())
|
||||
.unwrap()
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap()
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
let mut request = Request::new(Default::default());
|
||||
*request.uri_mut() = "/bench/account/admin/permissions".parse().unwrap();
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("host", "benchmark.test".parse().unwrap());
|
||||
request
|
||||
.headers_mut()
|
||||
.insert("cookie", cookie.try_into().unwrap());
|
||||
request
|
||||
})
|
||||
.collect();
|
||||
(
|
||||
config,
|
||||
AuthInput {
|
||||
service,
|
||||
requests,
|
||||
claims,
|
||||
now,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn measure_auth(
|
||||
runtime: &Runtime,
|
||||
registry: &Arc<Registry>,
|
||||
input: &AuthInput,
|
||||
generated: bool,
|
||||
roles: bool,
|
||||
count: usize,
|
||||
) -> (u128, u64) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let mut contexts = input.requests[..16]
|
||||
.iter()
|
||||
.map(|request| {
|
||||
input
|
||||
.service
|
||||
.prepare("admin", request, false, input.now)
|
||||
.unwrap()
|
||||
.0
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut checksum = 0u64;
|
||||
let start = Instant::now();
|
||||
for index in 0..count {
|
||||
let position = index % contexts.len();
|
||||
let context = &mut contexts[position];
|
||||
verify(
|
||||
generated,
|
||||
context,
|
||||
black_box(&input.claims[position]).clone(),
|
||||
scope,
|
||||
registry,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.0)
|
||||
.unwrap();
|
||||
checksum += black_box(
|
||||
context.user_id(None).unwrap().unwrap() as u64
|
||||
+ context.id().unwrap().len() as u64,
|
||||
);
|
||||
if roles {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let values =
|
||||
black_box(permissions(generated, context, scope, &mut database).await);
|
||||
for value in &values {
|
||||
for field in value {
|
||||
checksum += field.bytes().map(u64::from).sum::<u64>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(start.elapsed().as_nanos(), checksum)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn check_auth(runtime: &Runtime, registry: &Arc<Registry>, input: &AuthInput) {
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let expected = vec![
|
||||
[
|
||||
"bench.account.admin.permissions",
|
||||
"bench",
|
||||
"account",
|
||||
"admin",
|
||||
"permissions",
|
||||
"GET",
|
||||
"http",
|
||||
]
|
||||
.map(String::from),
|
||||
];
|
||||
for generated in [true, false] {
|
||||
for index in 0..16 {
|
||||
let (mut context, claims) = input
|
||||
.service
|
||||
.prepare("admin", &input.requests[index], false, input.now)
|
||||
.unwrap();
|
||||
verify(generated, &mut context, claims.unwrap(), scope, registry)
|
||||
.await
|
||||
.map_err(|error| error.0)
|
||||
.unwrap();
|
||||
assert_eq!(context.id().unwrap(), input.claims[index].subject);
|
||||
assert_eq!(
|
||||
context.user_id(Some(AUTH_USER_MODEL)).unwrap(),
|
||||
Some(index as i64 + 1)
|
||||
);
|
||||
assert!(
|
||||
context
|
||||
.tenant_id(Some(AUTH_TENANT_MODEL))
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
assert_eq!(
|
||||
permissions(generated, &mut context, scope, &mut database).await,
|
||||
expected
|
||||
);
|
||||
}
|
||||
for (index, wrong_session) in [(16, false), (0, true)] {
|
||||
let (mut context, claims) = input
|
||||
.service
|
||||
.prepare("admin", &input.requests[index], false, input.now)
|
||||
.unwrap();
|
||||
let mut claims = claims.unwrap();
|
||||
if wrong_session {
|
||||
claims.session.push_str("-revoked");
|
||||
}
|
||||
assert_eq!(
|
||||
verify(generated, &mut context, claims, scope, registry)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.0,
|
||||
401
|
||||
);
|
||||
assert!(context.id().is_err());
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (directory, path) = directory(
|
||||
"auth",
|
||||
r#""http":{"host":"127.0.0.1","port":18001},"sites":{"admin":{"path":"admin","hosts":["benchmark.test"],"auth":"session","origin":"https://benchmark.test"}},"auth":{"providers":{"session":{"verify":"bench.account.verify","jwtSecret":"benchmark-only-key-not-for-any-deployment","ttlSeconds":3600,"cookie":"session"}}}"#,
|
||||
);
|
||||
let runtime = runtime();
|
||||
let registry = runtime
|
||||
.run(async |scope| database_registry(&path, scope).await)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (config, input) = prepare(&path);
|
||||
runtime
|
||||
.run(async |scope| {
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
for (index, claims) in input.claims.iter().enumerate() {
|
||||
let account = GENERATED_SEED(
|
||||
&claims.subject,
|
||||
&claims.session,
|
||||
index < 16,
|
||||
scope,
|
||||
&mut database,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(account.field0.0, index as i64 + 1);
|
||||
}
|
||||
let (mut context, claims) = input
|
||||
.service
|
||||
.prepare("admin", &input.requests[0], false, input.now)
|
||||
.unwrap();
|
||||
drop(database);
|
||||
api_verify(&mut context, claims.unwrap(), scope, ®istry)
|
||||
.await
|
||||
.map_err(|error| error.0)
|
||||
.unwrap();
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database.on(AUTH_BINDING, scope).await.unwrap();
|
||||
dever_runtime::authorization::synchronize(
|
||||
executor,
|
||||
scope,
|
||||
&config,
|
||||
&[dever_runtime::authorization::Declaration {
|
||||
topic: "admin",
|
||||
component: "bench",
|
||||
domain: "account",
|
||||
action: "permissions",
|
||||
method: "GET",
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
dever_runtime::authorization::save(
|
||||
&mut database,
|
||||
AUTH_BINDING,
|
||||
scope,
|
||||
&context,
|
||||
"reader",
|
||||
"Benchmark reader",
|
||||
false,
|
||||
&["bench.account.admin.permissions".into()],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
for user in 1..=16 {
|
||||
let executor = database.on(AUTH_BINDING, scope).await.unwrap();
|
||||
dever_runtime::authorization::grant(executor, scope, &context, user, "reader")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
check_auth(&runtime, ®istry, &input);
|
||||
header();
|
||||
if HTTP_BENCH {
|
||||
protected_http::run(&runtime, ®istry, &config, &input);
|
||||
} else {
|
||||
for (name, roles) in [
|
||||
("auth_provider_verify", false),
|
||||
("auth_provider_role_read", true),
|
||||
] {
|
||||
samples(name, |generated, count| {
|
||||
measure_auth(&runtime, ®istry, &input, generated, roles, count)
|
||||
});
|
||||
}
|
||||
}
|
||||
check_auth(&runtime, ®istry, &input);
|
||||
drop(registry);
|
||||
drop(runtime);
|
||||
drop(directory);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// 手写 Rust 重做 Provider 的查询、完整行解码和身份判断;不调用生成函数。
|
||||
use dever_runtime::{
|
||||
api::Failure as ApiFailure,
|
||||
auth::{self, Claims, Identity},
|
||||
};
|
||||
|
||||
struct RustAccount {
|
||||
id: dever_runtime::database::Id<AUTH_USER_MODEL>,
|
||||
_created_at: dever_runtime::time::calendar::DateTime,
|
||||
subject: String,
|
||||
_session: String,
|
||||
_active: bool,
|
||||
}
|
||||
|
||||
async fn rust_verify(
|
||||
context: &mut auth::Context<'_>,
|
||||
claims: Claims,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
) -> Result<(), ApiFailure> {
|
||||
if context.verifier() != Some("bench.account.verify") {
|
||||
return Err(ApiFailure(500));
|
||||
}
|
||||
let mut database = DbContext::new(registry.clone());
|
||||
let executor = database
|
||||
.on(ACCOUNT_BINDING, scope)
|
||||
.await
|
||||
.map_err(|_| ApiFailure(500))?;
|
||||
let mut rows = executor.query(scope,
|
||||
"SELECT \"id\",\"created_at\",\"subject\",\"session\",\"active\" FROM \"bench_account_model\" WHERE (((\"subject\" IS ?1) AND (\"session\" IS ?2)) AND (\"active\" IS ?3)) ORDER BY \"id\" DESC LIMIT 1",
|
||||
vec![Parameter::Text(claims.subject), Parameter::Text(claims.session), Parameter::Integer(1)], 1,
|
||||
|row| Ok(RustAccount {
|
||||
id: dever_runtime::database::Id(row.get::<_, i64>(0)?),
|
||||
_created_at: dever_runtime::time::calendar::DateTime::from_millis(row.get::<_, i64>(1)?).map_err(|_| dever_runtime::database::Error::new(dever_runtime::database::Kind::InvalidData, "invalid stored DateTime"))?,
|
||||
subject: row.get(2)?, _session: row.get(3)?, _active: row.get(4)?,
|
||||
})
|
||||
).await.map_err(|_| ApiFailure(500))?;
|
||||
let row = rows.pop().ok_or(ApiFailure(401))?;
|
||||
context
|
||||
.establish(Identity {
|
||||
id: row.subject,
|
||||
user_id: Some(row.id.0),
|
||||
tenant_id: None,
|
||||
})
|
||||
.map_err(|error| ApiFailure(error.status()))
|
||||
}
|
||||
|
||||
// 输出所有字段,防止只读长度让优化器删掉 Dever 或 Rust 的记录转换。
|
||||
type PermissionView = [String; 7];
|
||||
|
||||
async fn rust_permissions(
|
||||
context: &mut auth::Context<'_>,
|
||||
scope: &Scope<'_>,
|
||||
database: &mut DbContext,
|
||||
) -> Vec<PermissionView> {
|
||||
dever_runtime::authorization::permissions(database, AUTH_BINDING, scope, context, true)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|value| {
|
||||
[
|
||||
value.key,
|
||||
value.component,
|
||||
value.domain,
|
||||
value.site,
|
||||
value.action,
|
||||
value.method,
|
||||
value.kind,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn permissions(
|
||||
generated: bool,
|
||||
context: &mut auth::Context<'_>,
|
||||
scope: &Scope<'_>,
|
||||
database: &mut DbContext,
|
||||
) -> Vec<PermissionView> {
|
||||
if generated {
|
||||
GENERATED_PERMISSIONS(scope, database, context)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|value| {
|
||||
[
|
||||
value.field0,
|
||||
value.field1,
|
||||
value.field2,
|
||||
value.field3,
|
||||
value.field4,
|
||||
value.field5,
|
||||
value.field6,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
rust_permissions(context, scope, database).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify(
|
||||
generated: bool,
|
||||
context: &mut auth::Context<'_>,
|
||||
claims: Claims,
|
||||
scope: &Scope<'_>,
|
||||
registry: &Arc<Registry>,
|
||||
) -> Result<(), ApiFailure> {
|
||||
if generated {
|
||||
api_verify(context, claims, scope, registry).await
|
||||
} else {
|
||||
rust_verify(context, claims, scope, registry).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// SQLite、资源作用域和优化参数两侧一致;只比较生成层,不推断完整语言或 HTTP 吞吐。
|
||||
use dever_runtime::{
|
||||
database::{Context as DbContext, Parameter, Registry},
|
||||
task::{Limits, Runtime, Scope},
|
||||
};
|
||||
use std::{
|
||||
hint::black_box,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
struct BenchDirectory(PathBuf);
|
||||
|
||||
impl Drop for BenchDirectory {
|
||||
fn drop(&mut self) {
|
||||
std::fs::remove_dir_all(&self.0).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn directory(name: &str, extra: &str) -> (BenchDirectory, PathBuf) {
|
||||
let unique = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let relative = format!("{name}-benchmark-{}-{unique}", std::process::id());
|
||||
let directory = BenchDirectory(
|
||||
std::env::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(&relative),
|
||||
);
|
||||
std::fs::create_dir(&directory.0).unwrap();
|
||||
let settings = directory.0.join("setting.json");
|
||||
std::fs::write(&settings, format!(r#"{{"database":{{"default":{{"type":"sqlite","path":"{relative}/case.db","max_connections":1,"max_page_size":2048}}}},{extra}}}"#)).unwrap();
|
||||
(directory, settings)
|
||||
}
|
||||
|
||||
fn runtime() -> Runtime {
|
||||
Runtime::new(Limits {
|
||||
threads: 1,
|
||||
workers: 1,
|
||||
tasks: 16,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn smoke() -> bool {
|
||||
std::env::args().any(|value| value == "--smoke")
|
||||
}
|
||||
|
||||
fn samples(name: &str, mut measure: impl FnMut(bool, usize) -> (u128, u64)) {
|
||||
if let Ok(selected) = std::env::var("DEVER_BENCH_CASES")
|
||||
&& !selected.split(',').any(|case| case == name)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let (rounds, count) = if smoke() { (1, 4) } else { (15, 2000) };
|
||||
if !smoke() {
|
||||
for generated in [true, false] {
|
||||
black_box(measure(generated, 200));
|
||||
}
|
||||
}
|
||||
for sample in 0..rounds {
|
||||
let mut order = [("dever", true), ("rust", false), ("control", true)];
|
||||
order.rotate_left(sample % 3);
|
||||
let mut expected = None;
|
||||
for (implementation, generated) in order {
|
||||
let (elapsed, checksum) = measure(generated, count);
|
||||
if let Some(expected) = expected {
|
||||
assert_eq!(checksum, expected);
|
||||
}
|
||||
expected = Some(checksum);
|
||||
if !smoke() {
|
||||
println!("{name},{sample},{implementation},{elapsed},{checksum}");
|
||||
}
|
||||
}
|
||||
}
|
||||
if smoke() {
|
||||
println!("correctness smoke passed: {name}; no performance result collected");
|
||||
}
|
||||
}
|
||||
|
||||
fn header() {
|
||||
eprintln!(
|
||||
"Rust 1.95.0; release fat LTO; codegen-units=1; overflow-checks=true; SQLite WAL/NORMAL; one connection; one runtime worker; setup/checks excluded"
|
||||
);
|
||||
if !smoke() {
|
||||
println!("case,sample,implementation,nanoseconds,checksum");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user