feat: publish Dever core with project scaffolding and versioned AI skills

This commit is contained in:
Antigravity committed 2026-10-04 13:37:30 +00:00
1 parent 70fa73cdc2
commit ac458ffa7f
960 files changed
+221319 -1163

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
//! Same structured run/wait workload as the native Dever task fixture.
use std::io::{self, Write};
use std::process::ExitCode;
use std::time::Instant;
use dever_runtime::{bytes::Bytes, number, task};
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("async_bench: {error}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), String> {
let arguments: Vec<_> = std::env::args().skip(1).collect();
let [mode, workers, capacity, iterations] = arguments.as_slice() else {
return Err("usage: async_bench root|worker WORKERS TASK_CAPACITY ITERATIONS".into());
};
let config = task::RuntimeConfig {
worker_threads: bounded(workers, 64)?,
max_blocking_threads: 2,
task_capacity: bounded(capacity, task::MAX_TASKS)?,
};
let iterations = bounded(iterations, 1_048_576)?;
match mode.as_str() {
"root" => task::run_entry_with(config, batches(iterations)),
// One outer structured task moves the batch onto a Tokio worker. It is
// outside each timed sample and uses the unchanged production runtime.
"worker" => task::run_entry_with(config, async move {
let job = task::run(batches(iterations)).await?;
task::wait(job).await
}),
_ => Err("mode must be root or worker".into()),
}
}
fn bounded(value: &str, maximum: usize) -> Result<usize, String> {
let number = value.parse::<usize>().map_err(|error| error.to_string())?;
if !(1..=maximum).contains(&number) {
return Err(format!("value must be between 1 and {maximum}"));
}
Ok(number)
}
async fn identity(value: i64) -> Result<i64, String> {
Ok(value)
}
async fn task_sum(value: i64, state: i64) -> Result<i64, String> {
let job = task::run(identity(value)).await?;
number::int_add(state, task::wait(job).await?).map_err(str::to_owned)
}
fn print_line(line: &str) -> Result<(), String> {
let mut output = io::stdout().lock();
writeln!(output, "{line}").map_err(|error| error.to_string())?;
output.flush().map_err(|error| error.to_string())
}
async fn batches(iterations: usize) -> Result<(), String> {
let input = Bytes::new(vec![b'x'; iterations]);
task::blocking(|| print_line("READY|0")).await?;
for _ in 0..10 {
let begin = Instant::now();
let mut checksum = 0;
for value in input.values() {
checksum = task_sum(i64::from(*value), checksum).await?;
}
let elapsed = begin.elapsed().as_nanos();
// As in the source fixture, all output and blocking dispatch are outside
// the clock window. The caller independently verifies the checksum.
task::blocking(move || {
print_line(&format!("SAMPLE|tasks|{iterations}|{elapsed}|{checksum}"))
})
.await?;
}
Ok(())
}
+33
View File
@@ -0,0 +1,33 @@
#[path = "live_bench/config.rs"]
mod config;
#[path = "live_bench/protocol.rs"]
mod protocol;
#[path = "live_bench/run.rs"]
mod run;
#[path = "support/benchmark_settings.rs"]
mod settings;
use std::process::ExitCode;
fn main() -> ExitCode {
match execute() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("live_bench: {error}");
ExitCode::FAILURE
}
}
}
fn execute() -> Result<(), String> {
let arguments: Vec<_> = std::env::args().skip(1).collect();
if arguments == ["check-config"] {
return settings::Settings::check();
}
let arguments = config::Arguments::parse(arguments)?;
run::run(arguments)
}
pub(crate) fn usage() -> String {
"usage: live_bench PROTOCOL URL CONNECTIONS CYCLES HOLD_MS TIMEOUT_MS\n live_bench check-config".into()
}
@@ -0,0 +1,125 @@
use std::time::Duration;
use hyper::Uri;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Protocol {
Tcp,
WebSocket,
Sse,
}
impl Protocol {
fn parse(value: &str) -> Result<Self, String> {
match value {
"tcp" => Ok(Self::Tcp),
"ws" => Ok(Self::WebSocket),
"sse" => Ok(Self::Sse),
_ => Err("PROTOCOL must be tcp, ws, or sse".into()),
}
}
pub fn name(self) -> &'static str {
match self {
Self::Tcp => "tcp",
Self::WebSocket => "ws",
Self::Sse => "sse",
}
}
fn scheme(self) -> &'static str {
match self {
Self::Tcp => "tcp",
Self::WebSocket => "ws",
Self::Sse => "http",
}
}
fn path(self) -> &'static str {
match self {
Self::Tcp => "/",
Self::WebSocket => "/ws",
Self::Sse => "/events",
}
}
}
#[derive(Clone, Debug)]
pub struct Target {
pub port: u16,
pub path: String,
pub url: String,
}
impl Target {
fn parse(protocol: Protocol, value: &str) -> Result<Self, String> {
let uri = value
.parse::<Uri>()
.map_err(|error| format!("URL is invalid: {error}"))?;
if uri.scheme_str() != Some(protocol.scheme()) {
return Err(format!(
"{} URL must use the {} scheme",
protocol.name(),
protocol.scheme()
));
}
if uri.host() != Some("127.0.0.1") {
return Err("live benchmark URL must use 127.0.0.1".into());
}
let port = uri.port_u16().ok_or("URL must include an explicit port")?;
if uri.path() != protocol.path() || uri.query().is_some() {
return Err(format!(
"{} URL must use the {} path without a query",
protocol.name(),
protocol.path()
));
}
Ok(Self {
port,
path: uri.path().to_owned(),
url: value.to_owned(),
})
}
}
#[derive(Clone, Debug)]
pub struct Arguments {
pub protocol: Protocol,
pub target: Target,
pub connections: usize,
pub cycles: usize,
pub hold: Duration,
pub timeout: Duration,
}
impl Arguments {
pub fn parse(values: Vec<String>) -> Result<Self, String> {
let [protocol, url, connections, cycles, hold_ms, timeout_ms] = values.as_slice() else {
return Err(crate::usage());
};
let protocol = Protocol::parse(protocol)?;
let target = Target::parse(protocol, url)?;
let connections = number(connections, "CONNECTIONS", 1, 4_096)? as usize;
let cycles = number(cycles, "CYCLES", 1, 100)? as usize;
let hold_ms = number(hold_ms, "HOLD_MS", 300, 60_000)?;
let timeout_ms = number(timeout_ms, "TIMEOUT_MS", 1, 300_000)?;
Ok(Self {
protocol,
target,
connections,
cycles,
hold: Duration::from_millis(hold_ms),
timeout: Duration::from_millis(timeout_ms),
})
}
}
fn number(value: &str, name: &str, minimum: u64, maximum: u64) -> Result<u64, String> {
let value = value
.parse::<u64>()
.map_err(|_| format!("{name} must be an integer"))?;
if !(minimum..=maximum).contains(&value) {
return Err(format!("{name} must be between {minimum} and {maximum}"));
}
Ok(value)
}
@@ -0,0 +1,325 @@
use std::time::Duration;
use bytes::Bytes;
use futures_util::{SinkExt, StreamExt};
use http_body_util::{BodyExt, Empty};
use hyper::body::Incoming;
use hyper::client::conn::http1;
use hyper::header::{CONTENT_TYPE, HOST};
use hyper::{Request, StatusCode};
use hyper_util::rt::TokioIo;
use tokio::net::TcpStream;
use tokio::task::JoinHandle;
use tokio::time::{Instant, timeout, timeout_at};
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::{WebSocketStream, client_async};
use crate::config::{Protocol, Target};
const INTERACTION_INTERVAL: Duration = Duration::from_millis(100);
const MAX_SSE_BUFFER: usize = 8_192;
const SSE_EVENT: &[u8] = b"event: live\nid: 1\ndata: ready\n\n";
const SSE_HEARTBEAT: &[u8] = b":\n\n";
pub struct Session {
connection: Connection,
cycle: usize,
index: usize,
timeout: Duration,
messages: u64,
}
enum Connection {
Tcp(TcpStream),
WebSocket(Box<WebSocketStream<TcpStream>>),
Sse(SseConnection),
}
struct SseConnection {
body: Incoming,
driver: ConnectionDriver,
pending: Vec<u8>,
}
struct ConnectionDriver(Option<JoinHandle<Result<(), hyper::Error>>>);
impl ConnectionDriver {
fn new(driver: JoinHandle<Result<(), hyper::Error>>) -> Self {
Self(Some(driver))
}
async fn close(&mut self) {
if let Some(driver) = self.0.take() {
driver.abort();
let _ = driver.await;
}
}
}
impl Drop for ConnectionDriver {
fn drop(&mut self) {
if let Some(driver) = &self.0 {
driver.abort();
}
}
}
impl Session {
pub async fn connect(
protocol: Protocol,
target: &Target,
cycle: usize,
index: usize,
operation_timeout: Duration,
) -> Result<Self, String> {
let stream = timeout(
operation_timeout,
TcpStream::connect(("127.0.0.1", target.port)),
)
.await
.map_err(|_| "connection timed out".to_owned())?
.map_err(|error| error.to_string())?;
stream
.set_nodelay(true)
.map_err(|error| error.to_string())?;
let connection = match protocol {
Protocol::Tcp => Connection::Tcp(stream),
Protocol::WebSocket => {
let (socket, response) =
timeout(operation_timeout, client_async(target.url.as_str(), stream))
.await
.map_err(|_| "WebSocket handshake timed out".to_owned())?
.map_err(|error| error.to_string())?;
if response.status() != StatusCode::SWITCHING_PROTOCOLS {
return Err(format!(
"WebSocket handshake returned {}",
response.status()
));
}
Connection::WebSocket(Box::new(socket))
}
Protocol::Sse => {
Connection::Sse(SseConnection::open(stream, target, operation_timeout).await?)
}
};
let mut session = Self {
connection,
cycle,
index,
timeout: operation_timeout,
messages: 0,
};
session.exchange(0).await?;
Ok(session)
}
pub async fn hold(&mut self, duration: Duration) -> Result<(), String> {
let deadline = Instant::now() + duration;
let mut sequence = 1_u64;
while Instant::now() < deadline {
if let Connection::Sse(connection) = &mut self.connection {
let heartbeats = connection.read_heartbeats(deadline).await?;
if heartbeats == 0 {
return Err("SSE hold ended without a heartbeat".into());
}
self.messages += heartbeats;
break;
}
self.exchange(sequence).await?;
sequence += 1;
let next = (Instant::now() + INTERACTION_INTERVAL).min(deadline);
tokio::time::sleep_until(next).await;
}
Ok(())
}
pub async fn close(mut self) -> Result<u64, String> {
match &mut self.connection {
Connection::Tcp(_) => {}
Connection::WebSocket(socket) => timeout(self.timeout, socket.as_mut().close(None))
.await
.map_err(|_| "WebSocket close timed out".to_owned())?
.map_err(|error| error.to_string())?,
Connection::Sse(connection) => connection.close().await,
}
Ok(self.messages)
}
async fn exchange(&mut self, sequence: u64) -> Result<(), String> {
match &mut self.connection {
Connection::Tcp(stream) => {
let payload = payload(self.cycle, self.index, sequence);
timeout(self.timeout, write_all(stream, &payload))
.await
.map_err(|_| "TCP write timed out".to_owned())??;
let mut received = vec![0; payload.len()];
timeout(self.timeout, read_exact(stream, &mut received))
.await
.map_err(|_| "TCP read timed out".to_owned())??;
if received != payload {
return Err("TCP echo body mismatch".into());
}
}
Connection::WebSocket(socket) => {
let payload = String::from_utf8(payload(self.cycle, self.index, sequence))
.expect("benchmark payload is ASCII");
timeout(
self.timeout,
socket.send(Message::Text(payload.clone().into())),
)
.await
.map_err(|_| "WebSocket write timed out".to_owned())?
.map_err(|error| error.to_string())?;
let message = timeout(self.timeout, socket.next())
.await
.map_err(|_| "WebSocket read timed out".to_owned())?
.ok_or("WebSocket ended before echo")?
.map_err(|error| error.to_string())?;
if !matches!(message, Message::Text(value) if value.as_str() == payload) {
return Err("WebSocket echo body mismatch".into());
}
}
Connection::Sse(connection) => connection.read_event(self.timeout).await?,
}
self.messages += 1;
Ok(())
}
}
impl SseConnection {
async fn open(
stream: TcpStream,
target: &Target,
operation_timeout: Duration,
) -> Result<Self, String> {
let (mut sender, connection) =
timeout(operation_timeout, http1::handshake(TokioIo::new(stream)))
.await
.map_err(|_| "SSE HTTP handshake timed out".to_owned())?
.map_err(|error| error.to_string())?;
let driver = ConnectionDriver::new(tokio::spawn(connection));
let request = Request::builder()
.method("GET")
.uri(&target.path)
.header(HOST, format!("127.0.0.1:{}", target.port))
.body(Empty::<Bytes>::new())
.map_err(|error| error.to_string())?;
let response = timeout(operation_timeout, sender.send_request(request))
.await
.map_err(|_| "SSE request timed out".to_owned())?
.map_err(|error| error.to_string())?;
if response.status() != StatusCode::OK {
return Err(format!("SSE request returned {}", response.status()));
}
let content_type = response
.headers()
.get(CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or_default();
if content_type.split(';').next().map(str::trim) != Some("text/event-stream") {
return Err("SSE response has the wrong Content-Type".into());
}
Ok(Self {
body: response.into_body(),
driver,
pending: Vec::new(),
})
}
async fn read_event(&mut self, operation_timeout: Duration) -> Result<(), String> {
while self.pending.len() < SSE_EVENT.len() {
let frame = timeout(operation_timeout, self.body.frame())
.await
.map_err(|_| "SSE event timed out".to_owned())?
.ok_or("SSE body ended before the initial event")?
.map_err(|error| error.to_string())?;
if let Ok(bytes) = frame.into_data() {
self.extend(bytes.as_ref())?;
}
if self.pending.len() < SSE_EVENT.len() && !SSE_EVENT.starts_with(&self.pending) {
return Err("SSE initial event body mismatch".into());
}
}
if !self.pending.starts_with(SSE_EVENT) {
return Err("SSE initial event body mismatch".into());
}
self.pending.drain(..SSE_EVENT.len());
Ok(())
}
async fn read_heartbeats(&mut self, deadline: Instant) -> Result<u64, String> {
let mut messages = consume_heartbeats(&mut self.pending)?;
loop {
let frame = match timeout_at(deadline, self.body.frame()).await {
Err(_) => break,
Ok(None) => return Err("SSE body ended during hold".into()),
Ok(Some(Err(error))) => return Err(error.to_string()),
Ok(Some(Ok(frame))) => frame,
};
if let Ok(bytes) = frame.into_data() {
self.extend(bytes.as_ref())?;
messages += consume_heartbeats(&mut self.pending)?;
}
}
Ok(messages)
}
fn extend(&mut self, bytes: &[u8]) -> Result<(), String> {
if self.pending.len().saturating_add(bytes.len()) > MAX_SSE_BUFFER {
return Err("SSE validation buffer exceeded its limit".into());
}
self.pending.extend_from_slice(bytes);
Ok(())
}
async fn close(&mut self) {
self.driver.close().await;
}
}
fn consume_heartbeats(buffer: &mut Vec<u8>) -> Result<u64, String> {
let mut messages = 0;
while buffer.len() >= SSE_HEARTBEAT.len() {
if !buffer.starts_with(SSE_HEARTBEAT) {
return Err("SSE hold data was not a heartbeat".into());
}
buffer.drain(..SSE_HEARTBEAT.len());
messages += 1;
}
if !SSE_HEARTBEAT.starts_with(buffer) {
return Err("SSE heartbeat body mismatch".into());
}
Ok(messages)
}
fn payload(cycle: usize, index: usize, sequence: u64) -> Vec<u8> {
format!("live:{cycle}:{index}:{sequence}\n").into_bytes()
}
async fn write_all(stream: &TcpStream, bytes: &[u8]) -> Result<(), String> {
let mut offset = 0;
while offset < bytes.len() {
stream.writable().await.map_err(|error| error.to_string())?;
match stream.try_write(&bytes[offset..]) {
Ok(0) => return Err("TCP write returned zero bytes".into()),
Ok(length) => offset += length,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
Err(error) => return Err(error.to_string()),
}
}
Ok(())
}
async fn read_exact(stream: &TcpStream, bytes: &mut [u8]) -> Result<(), String> {
let mut offset = 0;
while offset < bytes.len() {
stream.readable().await.map_err(|error| error.to_string())?;
match stream.try_read(&mut bytes[offset..]) {
Ok(0) => return Err("TCP connection ended before the echo completed".into()),
Ok(length) => offset += length,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
Err(error) => return Err(error.to_string()),
}
}
Ok(())
}
+284
View File
@@ -0,0 +1,284 @@
use std::io::{self, Write};
use std::time::Duration;
use tokio::runtime::Builder;
use tokio::sync::{mpsc, watch};
use tokio::task::JoinSet;
use tokio::time::{Instant, timeout_at};
use crate::config::Arguments;
use crate::protocol::Session;
const SETTLE_TIME: Duration = Duration::from_millis(300);
#[derive(Default)]
struct Counters {
attempted_connections: u64,
connected: u64,
disconnected: u64,
messages: u64,
errors: u64,
}
pub fn run(arguments: Arguments) -> Result<(), String> {
let workers = crate::settings::Settings::load()?.workers()?;
let runtime = Builder::new_multi_thread()
.worker_threads(workers)
.max_blocking_threads(1)
.enable_io()
.enable_time()
.build()
.map_err(|error| format!("cannot start live benchmark runtime: {error}"))?;
let started = std::time::Instant::now();
let result = runtime.block_on(execute(&arguments));
let (counters, failure) = match result {
Ok(counters) => (counters, None),
Err((counters, error)) => (counters, Some(error)),
};
println!(
"{{\"protocol\":\"{}\",\"connections\":{},\"cycles\":{},\"attempted_connections\":{},\"connected\":{},\"disconnected\":{},\"messages\":{},\"errors\":{},\"elapsed_ms\":{}}}",
arguments.protocol.name(),
arguments.connections,
arguments.cycles,
counters.attempted_connections,
counters.connected,
counters.disconnected,
counters.messages,
counters.errors,
started.elapsed().as_millis()
);
io::stdout().flush().map_err(|error| error.to_string())?;
match failure {
Some(error) => Err(error),
None if counters.errors != 0 => Err("live benchmark recorded connection errors".into()),
None => Ok(()),
}
}
async fn execute(arguments: &Arguments) -> Result<Counters, (Counters, String)> {
let mut counters = Counters::default();
for cycle in 0..arguments.cycles {
if let Err(error) = execute_cycle(arguments, cycle, &mut counters).await {
counters.errors += 1;
return Err((counters, error));
}
}
Ok(counters)
}
async fn execute_cycle(
arguments: &Arguments,
cycle: usize,
counters: &mut Counters,
) -> Result<(), String> {
phase(cycle, "connecting")?;
counters.attempted_connections += arguments.connections as u64;
let (ready_sender, mut ready_receiver) = mpsc::channel(arguments.connections);
let (held_sender, mut held_receiver) = mpsc::channel(arguments.connections);
let (failure_sender, mut failure_receiver) = mpsc::channel(arguments.connections);
let (hold_sender, hold_receiver) = watch::channel(false);
let (close_sender, close_receiver) = watch::channel(false);
let mut sessions = JoinSet::new();
for index in 0..arguments.connections {
sessions.spawn(report_session_failure(
session_task(
arguments.clone(),
cycle,
index,
ready_sender.clone(),
held_sender.clone(),
hold_receiver.clone(),
close_receiver.clone(),
),
failure_sender.clone(),
));
}
drop(ready_sender);
drop(held_sender);
drop(failure_sender);
if let Err(error) = wait_for_phase(
&mut ready_receiver,
arguments.connections,
Instant::now() + arguments.timeout,
"connections",
&mut failure_receiver,
)
.await
{
abort_sessions(&mut sessions).await;
return Err(error);
}
counters.connected += arguments.connections as u64;
if let Err(error) = phase(cycle, "connected") {
abort_sessions(&mut sessions).await;
return Err(error);
}
if hold_sender.send(true).is_err() {
abort_sessions(&mut sessions).await;
return Err("connection tasks ended before hold started".into());
}
let hold_deadline = Instant::now() + arguments.hold + arguments.timeout;
if let Err(error) = wait_for_phase(
&mut held_receiver,
arguments.connections,
hold_deadline,
"held connections",
&mut failure_receiver,
)
.await
{
let _ = close_sender.send(true);
abort_sessions(&mut sessions).await;
return Err(error);
}
if let Err(error) = phase(cycle, "closing") {
abort_sessions(&mut sessions).await;
return Err(error);
}
if close_sender.send(true).is_err() {
abort_sessions(&mut sessions).await;
return Err("connection tasks ended before close started".into());
}
let close_deadline = Instant::now() + arguments.timeout;
while !sessions.is_empty() {
let result = match timeout_at(close_deadline, sessions.join_next()).await {
Err(_) => {
abort_sessions(&mut sessions).await;
return Err("connection shutdown timed out".into());
}
Ok(None) => {
abort_sessions(&mut sessions).await;
return Err("connection task set ended unexpectedly".into());
}
Ok(Some(Err(error))) => {
abort_sessions(&mut sessions).await;
return Err(format!("connection task failed: {error}"));
}
Ok(Some(Ok(result))) => result,
};
match result {
Ok(messages) => {
counters.disconnected += 1;
counters.messages += messages;
}
Err(error) => {
abort_sessions(&mut sessions).await;
return Err(error);
}
}
}
phase(cycle, "disconnected")?;
tokio::time::sleep(SETTLE_TIME).await;
Ok(())
}
async fn session_task(
arguments: Arguments,
cycle: usize,
index: usize,
ready: mpsc::Sender<()>,
held: mpsc::Sender<()>,
mut hold: watch::Receiver<bool>,
mut close: watch::Receiver<bool>,
) -> Result<u64, String> {
let mut session = Session::connect(
arguments.protocol,
&arguments.target,
cycle,
index,
arguments.timeout,
)
.await
.map_err(|error| connection_error(cycle, index, error))?;
ready.send(()).await.map_err(|_| "ready receiver closed")?;
drop(ready);
wait_for_signal(&mut hold, arguments.timeout, "hold").await?;
session
.hold(arguments.hold)
.await
.map_err(|error| connection_error(cycle, index, error))?;
held.send(()).await.map_err(|_| "held receiver closed")?;
drop(held);
wait_for_signal(&mut close, arguments.timeout, "close").await?;
session
.close()
.await
.map_err(|error| connection_error(cycle, index, error))
}
async fn report_session_failure(
session: impl std::future::Future<Output = Result<u64, String>>,
failure: mpsc::Sender<String>,
) -> Result<u64, String> {
let result = session.await;
if let Err(error) = &result {
let _ = failure.send(error.clone()).await;
}
result
}
async fn wait_for_phase(
receiver: &mut mpsc::Receiver<()>,
expected: usize,
deadline: Instant,
name: &str,
failures: &mut mpsc::Receiver<String>,
) -> Result<(), String> {
for completed in 0..expected {
tokio::select! {
biased;
failure = failures.recv() => {
return Err(failure.unwrap_or_else(|| format!(
"only {completed} of {expected} {name} reached the phase"
)));
}
signal = timeout_at(deadline, receiver.recv()) => match signal {
Ok(Some(())) => {}
Ok(None) => {
return Err(format!(
"only {completed} of {expected} {name} reached the phase"
));
}
Err(_) => return Err(format!("timed out waiting for {name}")),
}
}
}
Ok(())
}
async fn wait_for_signal(
receiver: &mut watch::Receiver<bool>,
operation_timeout: Duration,
name: &str,
) -> Result<(), String> {
if *receiver.borrow() {
return Ok(());
}
tokio::time::timeout(operation_timeout, receiver.changed())
.await
.map_err(|_| format!("timed out waiting for {name} signal"))?
.map_err(|_| format!("{name} signal closed"))?;
if !*receiver.borrow() {
return Err(format!("{name} signal did not start the phase"));
}
Ok(())
}
async fn abort_sessions(sessions: &mut JoinSet<Result<u64, String>>) {
sessions.abort_all();
while sessions.join_next().await.is_some() {}
}
fn phase(cycle: usize, name: &str) -> Result<(), String> {
let mut output = io::stdout().lock();
writeln!(output, "PHASE|{cycle}|{name}").map_err(|error| error.to_string())?;
output.flush().map_err(|error| error.to_string())
}
fn connection_error(cycle: usize, index: usize, error: String) -> String {
format!("cycle {cycle} connection {index}: {error}")
}
@@ -0,0 +1,58 @@
#[path = "network_bench/config.rs"]
mod config;
#[path = "network_bench/load.rs"]
mod load;
#[path = "network_bench/metrics.rs"]
mod metrics;
#[path = "network_bench/response.rs"]
mod response;
#[path = "network_bench/service.rs"]
mod service;
#[path = "support/benchmark_settings.rs"]
mod settings;
#[path = "network_bench/tls.rs"]
mod tls;
use std::process::ExitCode;
use config::ServiceKind;
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("network_bench: {error}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), String> {
let mut arguments = std::env::args().skip(1);
let Some(command) = arguments.next() else {
return Err(usage());
};
if command == "check-config" && arguments.next().is_none() {
return settings::Settings::check();
}
if command == "load" {
let arguments = load::Arguments::parse(arguments.collect())?;
return load::run(arguments);
}
let kind = ServiceKind::parse(&command)?;
if arguments.next().is_some() {
return Err(usage());
}
service::run(kind)
}
fn usage() -> String {
concat!(
"usage: network_bench runtime-http|runtime-https|runtime-http2|runtime-https2|",
"hyper-http|hyper-https|hyper-http2|hyper-https2\n",
" network_bench check-config\n",
" network_bench load h1|h2 URL PATH RATE SECONDS PHYSICAL_CONNECTIONS ",
"STREAMS_PER_CONNECTION TIMEOUT_MS REUSE CA"
)
.into()
}
@@ -0,0 +1,227 @@
use std::time::Duration;
use crate::settings::Settings;
pub const HEADER_BYTES: usize = 8_192;
pub const BODY_BYTES: usize = 65_536;
pub const HTTP2_FRAME_BYTES: u32 = 16_384;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HttpVersion {
H1,
H2,
}
impl HttpVersion {
pub fn parse(value: &str) -> Result<Self, String> {
match value {
"h1" => Ok(Self::H1),
"h2" => Ok(Self::H2),
_ => Err("HTTP_VERSION must be 'h1' or 'h2'".into()),
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::H1 => "h1",
Self::H2 => "h2",
}
}
pub fn alpn(self) -> &'static [u8] {
match self {
Self::H1 => b"http/1.1",
Self::H2 => b"h2",
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct ConnectionShape {
pub physical_connections: usize,
pub streams_per_connection: usize,
pub request_slots: usize,
}
impl ConnectionShape {
pub fn new(
http_version: HttpVersion,
physical_connections: usize,
streams_per_connection: usize,
) -> Result<Self, String> {
if !(1..=65_536).contains(&physical_connections) {
return Err("PHYSICAL_CONNECTIONS must be between 1 and 65536".into());
}
match http_version {
HttpVersion::H1 if streams_per_connection != 1 => {
return Err("HTTP/1 requires STREAMS_PER_CONNECTION to be 1".into());
}
HttpVersion::H2 if !(1..=65_536).contains(&streams_per_connection) => {
return Err(
"HTTP/2 requires STREAMS_PER_CONNECTION to be between 1 and 65536".into(),
);
}
_ => {}
}
let request_slots = physical_connections
.checked_mul(streams_per_connection)
.ok_or("connection shape is too large")?;
if request_slots > 65_536 {
return Err(
"PHYSICAL_CONNECTIONS * STREAMS_PER_CONNECTION must not exceed 65536".into(),
);
}
Ok(Self {
physical_connections,
streams_per_connection,
request_slots,
})
}
}
#[derive(Clone, Copy, Debug)]
pub struct Http2Config {
pub streams: usize,
pub stream_window_bytes: u32,
pub connection_window_bytes: u32,
}
impl Http2Config {
pub fn from_settings(settings: &Settings, streams: usize) -> Result<Self, String> {
Ok(Self {
streams,
stream_window_bytes: settings.integer(
"http2_stream_window_bytes",
65_535,
1,
i32::MAX as usize,
)? as u32,
connection_window_bytes: settings.integer(
"http2_connection_window_bytes",
262_144,
65_535,
i32::MAX as usize,
)? as u32,
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ServiceKind {
RuntimeHttp,
RuntimeHttps,
RuntimeHttp2,
RuntimeHttps2,
HyperHttp,
HyperHttps,
HyperHttp2,
HyperHttps2,
}
impl ServiceKind {
pub fn parse(value: &str) -> Result<Self, String> {
match value {
"runtime-http" => Ok(Self::RuntimeHttp),
"runtime-https" => Ok(Self::RuntimeHttps),
"runtime-http2" => Ok(Self::RuntimeHttp2),
"runtime-https2" => Ok(Self::RuntimeHttps2),
"hyper-http" => Ok(Self::HyperHttp),
"hyper-https" => Ok(Self::HyperHttps),
"hyper-http2" => Ok(Self::HyperHttp2),
"hyper-https2" => Ok(Self::HyperHttps2),
_ => Err(format!("unknown service mode '{value}'")),
}
}
pub fn uses_runtime(self) -> bool {
matches!(
self,
Self::RuntimeHttp | Self::RuntimeHttps | Self::RuntimeHttp2 | Self::RuntimeHttps2
)
}
pub fn uses_tls(self) -> bool {
matches!(
self,
Self::RuntimeHttps | Self::RuntimeHttps2 | Self::HyperHttps | Self::HyperHttps2
)
}
pub fn http_version(self) -> HttpVersion {
match self {
Self::RuntimeHttp | Self::RuntimeHttps | Self::HyperHttp | Self::HyperHttps => {
HttpVersion::H1
}
Self::RuntimeHttp2 | Self::RuntimeHttps2 | Self::HyperHttp2 | Self::HyperHttps2 => {
HttpVersion::H2
}
}
}
}
#[derive(Clone, Debug)]
pub struct ServiceConfig {
pub workers: usize,
pub connections: usize,
pub task_capacity: usize,
pub timeout: Duration,
pub lifetime: Duration,
pub cert_path: Option<String>,
pub key_path: Option<String>,
pub http2: Option<Http2Config>,
}
impl ServiceConfig {
pub fn from_settings(settings: &Settings, kind: ServiceKind) -> Result<Self, String> {
let workers = settings.workers()?;
let connections = settings.integer("connections", 32, 1, 65_536)?;
let default_tasks = connections
.saturating_mul(2)
.saturating_add(16)
.clamp(272, 65_536);
let task_capacity = settings.integer("task_capacity", default_tasks, 1, 65_536)?;
let timeout_ms = settings.integer("timeout_ms", 2_000, 1, 300_000)?;
let lifetime_ms = settings.integer("lifetime_ms", 10_000, 1, 86_400_000)?;
let (cert_path, key_path) = if kind.uses_tls() {
(
Some(required_text(settings, "cert_path")?),
Some(required_text(settings, "key_path")?),
)
} else {
(None, None)
};
let http2 = if kind.http_version() == HttpVersion::H2 {
let streams = settings.integer("http2_streams", 16, 1, 65_536)?;
let request_slots = connections
.checked_mul(streams)
.ok_or("benchmark.connections * benchmark.http2_streams is too large")?;
if request_slots > 65_536 {
return Err(
"benchmark.connections * benchmark.http2_streams must not exceed 65536".into(),
);
}
Some(Http2Config::from_settings(settings, streams)?)
} else {
None
};
Ok(Self {
workers,
connections,
task_capacity,
timeout: Duration::from_millis(timeout_ms as u64),
lifetime: Duration::from_millis(lifetime_ms as u64),
cert_path,
key_path,
http2,
})
}
}
fn required_text(settings: &Settings, name: &str) -> Result<String, String> {
settings
.value(name)
.and_then(serde_json::Value::as_str)
.filter(|value| !value.is_empty())
.map(str::to_owned)
.ok_or_else(|| format!("benchmark.{name} must be nonempty Text"))
}
@@ -0,0 +1,767 @@
use std::collections::VecDeque;
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use http_body_util::{BodyExt, Empty};
use hyper::body::Incoming;
use hyper::client::conn::{http1, http2};
use hyper::header::{CONTENT_TYPE, HOST, HeaderValue};
use hyper::{Request, StatusCode, Uri, Version};
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
use tokio::runtime::Builder;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tokio::time::Instant;
use crate::config::{ConnectionShape, HEADER_BYTES, HTTP2_FRAME_BYTES, Http2Config, HttpVersion};
use crate::metrics::Histogram;
use crate::{response, tls};
const MAX_RATE: u64 = 1_000_000;
const MAX_SECONDS: f64 = 3_600.0;
#[derive(Clone, Debug)]
pub struct Arguments {
http_version: HttpVersion,
target: Target,
path: String,
expected: response::ExpectedResponse,
rate: u64,
seconds: Duration,
shape: ConnectionShape,
timeout: Duration,
reuse: bool,
ca_path: String,
}
impl Arguments {
pub fn parse(values: Vec<String>) -> Result<Self, String> {
let [
http_version,
url,
path,
rate,
seconds,
physical_connections,
streams_per_connection,
timeout_ms,
reuse,
ca,
] = values.as_slice()
else {
return Err(crate::usage());
};
let http_version = HttpVersion::parse(http_version)?;
let target = Target::parse(url)?;
let orm_count = if path == response::ORM_COUNT_PATH {
let settings = crate::settings::Settings::load()?;
if settings.value("orm_count").is_none() {
return Err("benchmark.orm_count is required for ORM count requests".into());
}
Some(settings.integer("orm_count", 0, 1, usize::MAX)?)
} else {
None
};
let expected = response::ExpectedResponse::for_path(path, orm_count)?;
let rate = number(rate, "RATE", 1, MAX_RATE)?;
let seconds = decimal_duration(seconds, "SECONDS", MAX_SECONDS)?;
let physical_connections =
number(physical_connections, "PHYSICAL_CONNECTIONS", 1, 65_536)? as usize;
let streams_per_connection =
number(streams_per_connection, "STREAMS_PER_CONNECTION", 1, 65_536)? as usize;
let shape =
ConnectionShape::new(http_version, physical_connections, streams_per_connection)?;
let timeout_ms = number(timeout_ms, "TIMEOUT_MS", 1, 300_000)?;
let reuse = match reuse.as_str() {
"true" => true,
"false" => false,
_ => return Err("REUSE must be 'true' or 'false'".into()),
};
if http_version == HttpVersion::H2 && !reuse {
return Err("HTTP/2 requires REUSE to be 'true'".into());
}
if target.tls && ca == "-" {
return Err("CA must name a root PEM file for HTTPS".into());
}
Ok(Self {
http_version,
target,
path: path.clone(),
expected,
rate,
seconds,
shape,
timeout: Duration::from_millis(timeout_ms),
reuse,
ca_path: ca.clone(),
})
}
}
#[derive(Clone, Debug)]
struct Target {
tls: bool,
port: u16,
authority: String,
}
impl Target {
fn parse(value: &str) -> Result<Self, String> {
let uri = value
.parse::<Uri>()
.map_err(|error| format!("URL is invalid: {error}"))?;
let scheme = uri
.scheme_str()
.ok_or("URL must include http:// or https://")?;
let host = uri.host().ok_or("URL must include a host")?;
let tls = match scheme {
"http" => false,
"https" => true,
_ => return Err("URL scheme must be http or https".into()),
};
if !matches!(host, "127.0.0.1" | "localhost" | "::1") {
return Err("benchmark URL must use a loopback host".into());
}
if tls && host != "localhost" {
return Err(
"HTTPS benchmark URL must use localhost for certificate verification".into(),
);
}
if uri.path() != "/" || uri.query().is_some() {
return Err(
"URL must contain only the scheme, loopback host and port; pass PATH separately"
.into(),
);
}
let port = uri.port_u16().ok_or("URL must include an explicit port")?;
let authority = uri
.authority()
.ok_or("URL must include an authority")?
.to_string();
Ok(Self {
tls,
port,
authority,
})
}
}
pub fn run(arguments: Arguments) -> Result<(), String> {
let settings = crate::settings::Settings::load()?;
let workers = settings.workers()?;
let runtime = Builder::new_multi_thread()
.worker_threads(workers)
.max_blocking_threads(1)
.enable_io()
.enable_time()
.build()
.map_err(|error| format!("cannot start load runtime: {error}"))?;
runtime.block_on(execute(arguments, &settings))
}
async fn execute(arguments: Arguments, settings: &crate::settings::Settings) -> Result<(), String> {
let tls = arguments
.target
.tls
.then(|| tls::client(&arguments.ca_path, arguments.http_version))
.transpose()?;
let http2 = (arguments.http_version == HttpVersion::H2)
.then(|| Http2Config::from_settings(settings, arguments.shape.streams_per_connection))
.transpose()?;
let client = Arc::new(ClientConfig {
target: arguments.target.clone(),
path: arguments.path.clone(),
expected: arguments.expected,
timeout: arguments.timeout,
reuse: arguments.reuse,
tls,
http2,
});
let mut http2_connections = if arguments.http_version == HttpVersion::H2 {
Http2Connections::connect(&client, arguments.shape).await?
} else {
Http2Connections::default()
};
let event_capacity = arguments.shape.request_slots.saturating_mul(2).max(1);
let (events, mut event_receiver) = mpsc::channel(event_capacity);
let mut commands = Vec::with_capacity(arguments.shape.request_slots);
let mut workers = Vec::with_capacity(arguments.shape.request_slots);
for id in 0..arguments.shape.request_slots {
let (sender, receiver) = mpsc::channel(1);
commands.push(sender);
let connection = match arguments.http_version {
HttpVersion::H1 => WorkerConnection::Http1(None),
HttpVersion::H2 => {
let connection = id / arguments.shape.streams_per_connection;
WorkerConnection::Http2(http2_connections.sender(connection))
}
};
workers.push(tokio::spawn(worker(
id,
receiver,
events.clone(),
client.clone(),
connection,
)));
}
drop(events);
let mut ready = VecDeque::with_capacity(arguments.shape.request_slots);
while ready.len() < arguments.shape.request_slots {
let event = tokio::time::timeout(arguments.timeout, event_receiver.recv())
.await
.map_err(|_| "load workers did not become ready before TIMEOUT_MS".to_owned())?
.ok_or("load workers stopped before becoming ready")?;
match event {
Event::Ready(id) => ready.push_back(id),
Event::Complete(_) => {
return Err("load worker completed before receiving a request".into());
}
}
}
let scheduled_float = arguments.rate as f64 * arguments.seconds.as_secs_f64();
if !scheduled_float.is_finite() || scheduled_float > u64::MAX as f64 {
return Err("RATE * SECONDS is too large".into());
}
let scheduled = scheduled_float.ceil() as u64;
let start = Instant::now();
let end = start + arguments.seconds;
let mut result = Results::new(
scheduled,
arguments.http_version,
arguments.shape,
http2_connections.successful_handshakes(),
);
for sequence in 0..scheduled {
let planned = start + Duration::from_secs_f64(sequence as f64 / arguments.rate as f64);
loop {
tokio::select! {
event = event_receiver.recv() => {
let event = event.ok_or("all load workers stopped unexpectedly")?;
result.observe(event, &mut ready);
}
_ = tokio::time::sleep_until(planned) => break,
}
}
while let Ok(event) = event_receiver.try_recv() {
result.observe(event, &mut ready);
}
if Instant::now() >= end {
result.dropped += 1;
result.dispatch_expired += 1;
continue;
}
let Some(worker) = ready.pop_front() else {
result.dropped += 1;
result.dropped_capacity += 1;
continue;
};
let job = Job { planned };
if commands[worker].try_send(job).is_ok() {
result.sent += 1;
} else {
result.dropped += 1;
result.dropped_capacity += 1;
}
}
while Instant::now() < end {
tokio::select! {
event = event_receiver.recv() => {
let event = event.ok_or("all load workers stopped unexpectedly")?;
result.observe(event, &mut ready);
}
_ = tokio::time::sleep_until(end) => break,
}
}
let drain_deadline = end + arguments.timeout;
while result.completed < result.sent {
match tokio::time::timeout_at(drain_deadline, event_receiver.recv()).await {
Ok(Some(event)) => result.observe(event, &mut ready),
_ => break,
}
}
drop(commands);
let pending = result.sent - result.completed;
result.completed += pending;
result.errors += pending;
for worker in &workers {
if !worker.is_finished() {
worker.abort();
}
}
for worker in workers {
let _ = worker.await;
}
http2_connections.close().await;
result.elapsed = start.elapsed();
println!("{}", result.json());
Ok(())
}
struct Job {
planned: Instant,
}
struct Completion {
succeeded: bool,
successful_handshakes: u64,
latency_us: u64,
dispatch_lag_us: u64,
request_latency_us: u64,
}
enum Event {
Ready(usize),
Complete(Completion),
}
async fn worker(
id: usize,
mut jobs: mpsc::Receiver<Job>,
events: mpsc::Sender<Event>,
config: Arc<ClientConfig>,
mut connection: WorkerConnection,
) {
if events.send(Event::Ready(id)).await.is_err() {
return;
}
while let Some(job) = jobs.recv().await {
let dispatched = Instant::now();
let outcome = request(&config, &mut connection).await;
let completed = Instant::now();
let completion = Completion {
succeeded: outcome.result.is_ok(),
successful_handshakes: outcome.successful_handshakes,
latency_us: micros(completed.saturating_duration_since(job.planned)),
dispatch_lag_us: micros(dispatched.saturating_duration_since(job.planned)),
request_latency_us: micros(completed.saturating_duration_since(dispatched)),
};
if events.send(Event::Complete(completion)).await.is_err()
|| events.send(Event::Ready(id)).await.is_err()
{
break;
}
}
if let WorkerConnection::Http1(Some(connection)) = connection {
connection.close().await;
}
}
struct RequestOutcome {
result: Result<(), String>,
successful_handshakes: u64,
}
async fn request(config: &ClientConfig, connection: &mut WorkerConnection) -> RequestOutcome {
let mut successful_handshakes = 0;
let result = match tokio::time::timeout(config.timeout, async {
match connection {
WorkerConnection::Http1(connection) => {
if connection.is_none() {
*connection = Some(Http1Connection::connect(config).await?);
successful_handshakes = 1;
}
connection
.as_mut()
.expect("connection established")
.request(config)
.await
}
WorkerConnection::Http2(sender) => http2_request(sender, config).await,
}
})
.await
{
Ok(result) => result,
Err(_) => Err("request timed out".to_owned()),
};
if let WorkerConnection::Http1(connection) = connection
&& (result.is_err() || !config.reuse)
&& let Some(connection) = connection.take()
{
connection.close().await;
}
RequestOutcome {
result,
successful_handshakes,
}
}
struct ClientConfig {
target: Target,
path: String,
expected: response::ExpectedResponse,
timeout: Duration,
reuse: bool,
tls: Option<Arc<rustls::ClientConfig>>,
http2: Option<Http2Config>,
}
enum WorkerConnection {
Http1(Option<Http1Connection>),
Http2(http2::SendRequest<Empty<Bytes>>),
}
struct Http1Connection {
sender: http1::SendRequest<Empty<Bytes>>,
driver: Option<JoinHandle<Result<(), hyper::Error>>>,
}
impl Http1Connection {
async fn connect(config: &ClientConfig) -> Result<Self, String> {
let stream = tls::connect(
config.target.port,
config.timeout,
config.tls.as_ref(),
HttpVersion::H1,
)
.await?;
let (sender, connection) =
tokio::time::timeout(config.timeout, http1::handshake(TokioIo::new(stream)))
.await
.map_err(|_| "HTTP handshake timed out".to_owned())?
.map_err(|error| error.to_string())?;
Ok(Self {
sender,
driver: Some(tokio::spawn(connection)),
})
}
async fn request(&mut self, config: &ClientConfig) -> Result<(), String> {
self.sender
.ready()
.await
.map_err(|error| error.to_string())?;
let request = Request::builder()
.method("GET")
.uri(config.path.as_str())
.header(
HOST,
HeaderValue::from_str(&config.target.authority)
.map_err(|error| error.to_string())?,
)
.body(Empty::new())
.map_err(|error| error.to_string())?;
let response = self
.sender
.send_request(request)
.await
.map_err(|error| error.to_string())?;
validate_response(&config.expected, response).await
}
async fn close(mut self) {
if let Some(driver) = self.driver.take() {
driver.abort();
let _ = driver.await;
}
}
}
impl Drop for Http1Connection {
fn drop(&mut self) {
if let Some(driver) = self.driver.take() {
driver.abort();
}
}
}
#[derive(Default)]
struct Http2Connections {
connections: Vec<Http2Connection>,
}
impl Http2Connections {
async fn connect(config: &ClientConfig, shape: ConnectionShape) -> Result<Self, String> {
let mut connections = Vec::with_capacity(shape.physical_connections);
for _ in 0..shape.physical_connections {
match Http2Connection::connect(config).await {
Ok(connection) => connections.push(connection),
Err(error) => {
close_http2_connections(connections).await;
return Err(error);
}
}
}
Ok(Self { connections })
}
fn sender(&self, connection: usize) -> http2::SendRequest<Empty<Bytes>> {
self.connections[connection].sender.clone()
}
fn successful_handshakes(&self) -> u64 {
self.connections.len() as u64
}
async fn close(&mut self) {
close_http2_connections(std::mem::take(&mut self.connections)).await;
}
}
struct Http2Connection {
sender: http2::SendRequest<Empty<Bytes>>,
driver: Option<JoinHandle<Result<(), hyper::Error>>>,
}
impl Http2Connection {
async fn connect(config: &ClientConfig) -> Result<Self, String> {
let limits = config.http2.ok_or("HTTP/2 client has no HTTP/2 limits")?;
let stream = tls::connect(
config.target.port,
config.timeout,
config.tls.as_ref(),
HttpVersion::H2,
)
.await?;
let mut builder = http2::Builder::new(TokioExecutor::new());
builder
.timer(TokioTimer::new())
.adaptive_window(false)
.initial_stream_window_size(limits.stream_window_bytes)
.initial_connection_window_size(limits.connection_window_bytes)
.initial_max_send_streams(limits.streams)
.max_concurrent_streams(0)
.max_header_list_size(HEADER_BYTES as u32)
.max_frame_size(HTTP2_FRAME_BYTES)
.max_send_buf_size(HTTP2_FRAME_BYTES as usize);
let (sender, connection) =
tokio::time::timeout(config.timeout, builder.handshake(TokioIo::new(stream)))
.await
.map_err(|_| "HTTP/2 handshake timed out".to_owned())?
.map_err(|error| error.to_string())?;
Ok(Self {
sender,
driver: Some(tokio::spawn(connection)),
})
}
async fn close(mut self) {
if let Some(driver) = self.driver.take() {
driver.abort();
let _ = driver.await;
}
}
}
impl Drop for Http2Connection {
fn drop(&mut self) {
if let Some(driver) = self.driver.take() {
driver.abort();
}
}
}
async fn close_http2_connections(connections: Vec<Http2Connection>) {
for connection in connections {
connection.close().await;
}
}
async fn http2_request(
sender: &mut http2::SendRequest<Empty<Bytes>>,
config: &ClientConfig,
) -> Result<(), String> {
sender.ready().await.map_err(|error| error.to_string())?;
let scheme = if config.target.tls { "https" } else { "http" };
let uri = format!("{scheme}://{}{}", config.target.authority, config.path);
let request = Request::builder()
.method("GET")
.version(Version::HTTP_2)
.uri(uri)
.body(Empty::new())
.map_err(|error| error.to_string())?;
let response = sender
.send_request(request)
.await
.map_err(|error| error.to_string())?;
validate_response(&config.expected, response).await
}
async fn validate_response(
expected: &response::ExpectedResponse,
response: hyper::Response<Incoming>,
) -> Result<(), String> {
if response.status() != StatusCode::OK {
return Err(format!("unexpected response status {}", response.status()));
}
let content_type = response
.headers()
.get(CONTENT_TYPE)
.ok_or("response has no Content-Type")?;
if content_type.as_bytes() != expected.content_type.as_bytes() {
return Err("response Content-Type does not match the benchmark contract".into());
}
let mut body = response.into_body();
let mut offset: usize = 0;
while let Some(frame) = body.frame().await {
let frame = frame.map_err(|error| error.to_string())?;
let data = frame
.into_data()
.map_err(|_| "response contains unexpected trailers")?;
let end = offset.saturating_add(data.len());
if end > expected.body.len() || data.as_ref() != &expected.body[offset..end] {
return Err("response body does not match the benchmark contract".into());
}
offset = end;
}
if offset != expected.body.len() {
return Err("response body is shorter than the benchmark contract".into());
}
Ok(())
}
struct Results {
http_version: HttpVersion,
shape: ConnectionShape,
successful_handshakes: u64,
scheduled: u64,
sent: u64,
completed: u64,
succeeded: u64,
errors: u64,
dropped: u64,
dropped_capacity: u64,
dispatch_expired: u64,
elapsed: Duration,
latency: Histogram,
dispatch_lag: Histogram,
request_latency: Histogram,
}
impl Results {
fn new(
scheduled: u64,
http_version: HttpVersion,
shape: ConnectionShape,
successful_handshakes: u64,
) -> Self {
Self {
http_version,
shape,
successful_handshakes,
scheduled,
sent: 0,
completed: 0,
succeeded: 0,
errors: 0,
dropped: 0,
dropped_capacity: 0,
dispatch_expired: 0,
elapsed: Duration::ZERO,
latency: Histogram::new(),
dispatch_lag: Histogram::new(),
request_latency: Histogram::new(),
}
}
fn observe(&mut self, event: Event, ready: &mut VecDeque<usize>) {
match event {
Event::Ready(id) => ready.push_back(id),
Event::Complete(completion) => {
self.completed += 1;
self.successful_handshakes += completion.successful_handshakes;
self.dispatch_lag.record(completion.dispatch_lag_us);
if completion.succeeded {
self.succeeded += 1;
self.latency.record(completion.latency_us);
self.request_latency.record(completion.request_latency_us);
} else {
self.errors += 1;
}
}
}
}
fn json(&self) -> String {
let elapsed = self.elapsed.as_secs_f64();
let qps = if elapsed == 0.0 {
0.0
} else {
self.succeeded as f64 / elapsed
};
let latency = percentiles_json(&self.latency);
let dispatch_lag = percentiles_json(&self.dispatch_lag);
let request_latency = percentiles_json(&self.request_latency);
format!(
concat!(
"{{\"http_version\":\"{}\",\"physical_connections\":{},",
"\"streams_per_connection\":{},\"request_slots\":{},",
"\"successful_handshakes\":{},",
"\"scheduled\":{},\"sent\":{},\"completed\":{},",
"\"succeeded\":{},\"errors\":{},\"dropped\":{},",
"\"dropped_capacity\":{},\"dispatch_expired\":{},",
"\"latency_samples\":{},\"dispatch_lag_samples\":{},",
"\"request_latency_samples\":{},",
"\"elapsed_seconds\":{:.6},\"actual_success_qps\":{:.3},",
"\"latency_ms\":{},\"dispatch_lag_ms\":{},",
"\"request_latency_ms\":{}}}"
),
self.http_version.as_str(),
self.shape.physical_connections,
self.shape.streams_per_connection,
self.shape.request_slots,
self.successful_handshakes,
self.scheduled,
self.sent,
self.completed,
self.succeeded,
self.errors,
self.dropped,
self.dropped_capacity,
self.dispatch_expired,
self.latency.count(),
self.dispatch_lag.count(),
self.request_latency.count(),
elapsed,
qps,
latency,
dispatch_lag,
request_latency,
)
}
}
fn percentiles_json(histogram: &Histogram) -> String {
if histogram.count() == 0 {
return "null".into();
}
format!(
"{{\"sample_count\":{},\"p50\":{:.3},\"p95\":{:.3},\"p99\":{:.3}}}",
histogram.count(),
histogram.percentile_ms(50),
histogram.percentile_ms(95),
histogram.percentile_ms(99),
)
}
fn micros(duration: Duration) -> u64 {
duration.as_micros().min(u128::from(u64::MAX)) as u64
}
fn number(value: &str, name: &str, minimum: u64, maximum: u64) -> Result<u64, String> {
let parsed = value
.parse::<u64>()
.map_err(|_| format!("{name} must be an integer"))?;
if !(minimum..=maximum).contains(&parsed) {
return Err(format!("{name} must be between {minimum} and {maximum}"));
}
Ok(parsed)
}
fn decimal_duration(value: &str, name: &str, maximum: f64) -> Result<Duration, String> {
let seconds = value
.parse::<f64>()
.map_err(|_| format!("{name} must be a number"))?;
if !seconds.is_finite() || seconds <= 0.0 || seconds > maximum {
return Err(format!(
"{name} must be greater than 0 and at most {maximum}"
));
}
Ok(Duration::from_secs_f64(seconds))
}
@@ -0,0 +1,67 @@
const EXACT_BUCKETS: usize = 1_024;
const SUB_BUCKETS: usize = 64;
const BUCKETS: usize = 4_096;
pub struct Histogram {
buckets: Box<[u64; BUCKETS]>,
count: u64,
}
impl Histogram {
pub fn new() -> Self {
Self {
buckets: Box::new([0; BUCKETS]),
count: 0,
}
}
pub fn record(&mut self, microseconds: u64) {
self.buckets[index(microseconds)] += 1;
self.count += 1;
}
pub fn count(&self) -> u64 {
self.count
}
pub fn percentile_ms(&self, percentile: u64) -> f64 {
if self.count == 0 {
return 0.0;
}
let target = self.count.saturating_mul(percentile).div_ceil(100);
let mut observed = 0;
for (index, count) in self.buckets.iter().enumerate() {
observed += count;
if observed >= target {
return upper_bound(index) as f64 / 1_000.0;
}
}
upper_bound(BUCKETS - 1) as f64 / 1_000.0
}
}
fn index(microseconds: u64) -> usize {
if microseconds < EXACT_BUCKETS as u64 {
return microseconds as usize;
}
let exponent = 63 - microseconds.leading_zeros() as usize;
let lower = 1_u64 << exponent;
let step = (lower / SUB_BUCKETS as u64).max(1);
let sub = ((microseconds - lower) / step).min((SUB_BUCKETS - 1) as u64) as usize;
(EXACT_BUCKETS + (exponent - 10) * SUB_BUCKETS + sub).min(BUCKETS - 1)
}
fn upper_bound(index: usize) -> u64 {
if index < EXACT_BUCKETS {
return index as u64;
}
let offset = index - EXACT_BUCKETS;
let exponent = 10 + offset / SUB_BUCKETS;
if exponent >= 63 {
return u64::MAX;
}
let sub = offset % SUB_BUCKETS;
let lower = 1_u64 << exponent;
let step = lower / SUB_BUCKETS as u64;
lower.saturating_add(step.saturating_mul(sub as u64 + 1))
}
@@ -0,0 +1,117 @@
use bytes::Bytes;
use http_body_util::Full;
use hyper::header::{CONTENT_TYPE, HeaderValue};
use hyper::{Response, StatusCode};
pub const PLAIN_PATH: &str = "/plain";
pub const JSON_PATH: &str = "/json";
pub const BYTES_PATH: &str = "/bytes";
pub const ORM_COUNT_PATH: &str = "/benchmark/item/count";
const PLAIN_BODY: &[u8] = b"hello";
const JSON_BODY: &[u8] = br#"{"ok":true}"#;
#[derive(Clone, Debug)]
pub struct ExpectedResponse {
pub body: Bytes,
pub content_type: &'static str,
}
impl ExpectedResponse {
pub fn for_path(path: &str, orm_count: Option<usize>) -> Result<Self, String> {
match path {
PLAIN_PATH => Ok(Self {
body: Bytes::from_static(PLAIN_BODY),
content_type: "text/plain",
}),
JSON_PATH => Ok(Self {
body: Bytes::from_static(JSON_BODY),
content_type: "application/json",
}),
BYTES_PATH => Ok(Self {
body: Bytes::from(vec![b'x'; 65_536]),
content_type: "application/octet-stream",
}),
ORM_COUNT_PATH => Ok(Self {
body: Bytes::from(format!(
"{{\"code\":0,\"message\":\"ok\",\"data\":{}}}",
orm_count.ok_or("benchmark.orm_count is required for ORM count requests")?
)),
content_type: "application/json; charset=utf-8",
}),
_ => Err(format!("unsupported benchmark path '{path}'")),
}
}
}
pub struct HyperResponses {
bytes: Bytes,
}
impl HyperResponses {
pub fn new() -> Self {
Self {
bytes: Bytes::from(vec![b'x'; 65_536]),
}
}
pub fn response(&self, path: &str) -> Response<Full<Bytes>> {
let (body, content_type) = match path {
PLAIN_PATH => (Bytes::from_static(PLAIN_BODY), "text/plain"),
JSON_PATH => (Bytes::from_static(JSON_BODY), "application/json"),
BYTES_PATH => (self.bytes.clone(), "application/octet-stream"),
_ => {
let mut response = Response::new(Full::new(Bytes::new()));
*response.status_mut() = StatusCode::NOT_FOUND;
return response;
}
};
let mut response = Response::new(Full::new(body));
response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
response
}
}
#[derive(Clone)]
pub struct RuntimeResponses {
bytes: dever_runtime::bytes::Bytes,
}
impl RuntimeResponses {
pub fn new() -> Self {
Self {
bytes: dever_runtime::bytes::Bytes::new(vec![b'x'; 65_536]),
}
}
pub fn response(&self, path: &str) -> dever_runtime::http::Response {
let (body, content_type) = match path {
PLAIN_PATH => (
dever_runtime::bytes::Bytes::from_text("hello"),
"text/plain",
),
JSON_PATH => (
dever_runtime::bytes::Bytes::from_text(r#"{"ok":true}"#),
"application/json",
),
BYTES_PATH => (self.bytes.clone(), "application/octet-stream"),
_ => {
return dever_runtime::http::Response {
status: 404,
headers: vec![],
body: dever_runtime::bytes::Bytes::new(vec![]),
};
}
};
dever_runtime::http::Response {
status: 200,
headers: vec![dever_runtime::http::Header {
name: "content-type".into(),
value: dever_runtime::bytes::Bytes::from_text(content_type),
}],
body,
}
}
}
@@ -0,0 +1,369 @@
use std::future::Future;
use std::io::{self, Write};
use std::pin::Pin;
use std::sync::Arc;
use bytes::{Bytes, BytesMut};
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use hyper::header::{CONNECTION, HOST, UPGRADE};
use hyper::server::conn::{http1, http2};
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode, Version};
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
use tokio::net::TcpListener;
use tokio::runtime::Builder;
use tokio::sync::{Semaphore, watch};
use tokio::task::JoinSet;
use tokio::time::{Instant, timeout, timeout_at};
use crate::config::{
BODY_BYTES, HEADER_BYTES, HTTP2_FRAME_BYTES, Http2Config, HttpVersion, ServiceConfig,
ServiceKind,
};
use crate::{response, tls};
pub fn run(kind: ServiceKind) -> Result<(), String> {
let settings = crate::settings::Settings::load()?;
let config = ServiceConfig::from_settings(&settings, kind)?;
if kind.uses_runtime() {
run_runtime(config, kind)
} else {
run_hyper(config, kind)
}
}
fn run_runtime(config: ServiceConfig, kind: ServiceKind) -> Result<(), String> {
let server_tls = config
.cert_path
.as_deref()
.zip(config.key_path.as_deref())
.map(|(cert, key)| tls::runtime_server(cert, key))
.transpose()?;
let runtime = dever_runtime::task::RuntimeConfig {
worker_threads: config.workers,
max_blocking_threads: 2,
task_capacity: config.task_capacity,
};
dever_runtime::task::run_entry_with(runtime, async move {
let responses = response::RuntimeResponses::new();
let listener = dever_runtime::net::listen("127.0.0.1", 0).await?;
let port = dever_runtime::net::port(&listener)?;
ready(u16::try_from(port).map_err(|_| "listener returned an invalid port")?)?;
let limits = dever_runtime::http::Limits {
header_bytes: HEADER_BYTES as i64,
body_bytes: BODY_BYTES as i64,
timeout_ms: config.timeout.as_millis() as i64,
connections: config.connections as i64,
http2: config.http2.map(|limits| dever_runtime::http::Http2Limits {
streams: limits.streams as i64,
stream_window_bytes: i64::from(limits.stream_window_bytes),
connection_window_bytes: i64::from(limits.connection_window_bytes),
}),
};
let owned = listener.clone();
let route = move |request: dever_runtime::http::Request| {
let response = responses.response(&request.target);
async move { Ok(response) }
};
let server: Pin<Box<dyn Future<Output = Result<(), String>> + Send>> = match server_tls {
Some(tls) if kind.uses_tls() => {
Box::pin(dever_runtime::http::serve_tls(route, owned, limits, tls))
}
_ => Box::pin(dever_runtime::http::serve(route, owned, limits)),
};
tokio::pin!(server);
tokio::select! {
result = server.as_mut() => result,
_ = tokio::time::sleep(config.lifetime) => {
listener.close()?;
timeout(config.timeout, server.as_mut())
.await
.map_err(|_| "runtime server shutdown timed out".to_owned())?
}
}
})
}
fn run_hyper(config: ServiceConfig, kind: ServiceKind) -> Result<(), String> {
let server_tls = config
.cert_path
.as_deref()
.zip(config.key_path.as_deref())
.map(|(cert, key)| tls::server(cert, key, kind.http_version()))
.transpose()?;
let runtime = Builder::new_multi_thread()
.worker_threads(config.workers)
.max_blocking_threads(1)
.enable_io()
.enable_time()
.build()
.map_err(|error| format!("cannot start benchmark runtime: {error}"))?;
runtime.block_on(hyper_server(
config,
kind.http_version(),
kind.uses_tls().then_some(server_tls).flatten(),
))
}
async fn hyper_server(
config: ServiceConfig,
http_version: HttpVersion,
server_tls: Option<Arc<rustls::ServerConfig>>,
) -> Result<(), String> {
let responses = Arc::new(response::HyperResponses::new());
let listener = TcpListener::bind(("127.0.0.1", 0))
.await
.map_err(|error| error.to_string())?;
ready(
listener
.local_addr()
.map_err(|error| error.to_string())?
.port(),
)?;
let connections = Arc::new(Semaphore::new(config.connections));
let (shutdown, shutdown_receiver) = watch::channel(false);
let mut tasks = JoinSet::new();
let deadline = Instant::now() + config.lifetime;
loop {
while let Some(result) = tasks.try_join_next() {
result.map_err(|error| format!("connection task failed: {error}"))??;
}
let connections = connections.clone();
let accepted = timeout_at(deadline, async {
let permit = connections
.acquire_owned()
.await
.map_err(|_| "connection limit closed".to_owned())?;
let (stream, _) = listener.accept().await.map_err(|error| error.to_string())?;
stream
.set_nodelay(true)
.map_err(|error| error.to_string())?;
Ok::<_, String>((permit, stream))
})
.await;
let Ok(accepted) = accepted else {
break;
};
let (permit, stream) = accepted?;
let connection_config = config.clone();
let connection_tls = server_tls.clone();
let connection_shutdown = shutdown_receiver.clone();
let connection_responses = responses.clone();
tasks.spawn(async move {
let _permit = permit;
let stream = match tls::accept(
stream,
connection_config.timeout,
connection_tls.as_ref(),
http_version,
)
.await
{
Ok(stream) => stream,
Err(_) => return Ok(()),
};
serve_connection(
stream,
connection_config.timeout,
connection_shutdown,
connection_responses,
http_version,
connection_config.http2,
)
.await
});
}
let _ = shutdown.send(true);
if timeout(config.timeout, drain_connections(&mut tasks))
.await
.is_err()
{
tasks.abort_all();
while tasks.join_next().await.is_some() {}
return Err("hyper server shutdown timed out".into());
}
Ok(())
}
async fn serve_connection(
stream: tls::BoxedStream,
operation_timeout: std::time::Duration,
shutdown: watch::Receiver<bool>,
responses: Arc<response::HyperResponses>,
http_version: HttpVersion,
http2_config: Option<Http2Config>,
) -> Result<(), String> {
match http_version {
HttpVersion::H1 => serve_http1(stream, operation_timeout, shutdown, responses).await,
HttpVersion::H2 => {
let limits = http2_config.ok_or("HTTP/2 service has no HTTP/2 limits")?;
serve_http2(stream, operation_timeout, shutdown, responses, limits).await
}
}
}
async fn serve_http1(
stream: tls::BoxedStream,
operation_timeout: std::time::Duration,
mut shutdown: watch::Receiver<bool>,
responses: Arc<response::HyperResponses>,
) -> Result<(), String> {
let service = service_fn(move |request| {
dispatch(
request,
operation_timeout,
responses.clone(),
HttpVersion::H1,
)
});
let mut builder = http1::Builder::new();
builder
.timer(TokioTimer::new())
.header_read_timeout(operation_timeout)
.max_buf_size(HEADER_BYTES);
let connection = builder.serve_connection(TokioIo::new(stream), service);
tokio::pin!(connection);
let result = tokio::select! {
result = connection.as_mut() => result,
_ = shutdown.changed() => {
connection.as_mut().graceful_shutdown();
timeout(operation_timeout, connection.as_mut())
.await
.map_err(|_| "connection shutdown timed out".to_owned())?
}
};
// Peer disconnects and malformed requests end only their own connection.
let _ = result;
Ok(())
}
async fn serve_http2(
stream: tls::BoxedStream,
operation_timeout: std::time::Duration,
mut shutdown: watch::Receiver<bool>,
responses: Arc<response::HyperResponses>,
limits: Http2Config,
) -> Result<(), String> {
let service = service_fn(move |request| {
dispatch(
request,
operation_timeout,
responses.clone(),
HttpVersion::H2,
)
});
let mut builder = http2::Builder::new(TokioExecutor::new());
builder
.timer(TokioTimer::new())
.adaptive_window(false)
.initial_stream_window_size(limits.stream_window_bytes)
.initial_connection_window_size(limits.connection_window_bytes)
.max_concurrent_streams(limits.streams as u32)
.max_header_list_size(HEADER_BYTES as u32)
.max_frame_size(HTTP2_FRAME_BYTES)
.max_send_buf_size(HTTP2_FRAME_BYTES as usize);
let connection = builder.serve_connection(TokioIo::new(stream), service);
tokio::pin!(connection);
let result = tokio::select! {
result = connection.as_mut() => result,
_ = shutdown.changed() => {
connection.as_mut().graceful_shutdown();
timeout(operation_timeout, connection.as_mut())
.await
.map_err(|_| "connection shutdown timed out".to_owned())?
}
};
// Peer disconnects and malformed requests end only their own connection.
let _ = result;
Ok(())
}
async fn drain_connections(tasks: &mut JoinSet<Result<(), String>>) -> Result<(), String> {
while let Some(result) = tasks.join_next().await {
result.map_err(|error| format!("connection task failed: {error}"))??;
}
Ok(())
}
async fn dispatch(
request: Request<Incoming>,
operation_timeout: std::time::Duration,
responses: Arc<response::HyperResponses>,
http_version: HttpVersion,
) -> Result<Response<Full<Bytes>>, std::convert::Infallible> {
let response = timeout(
operation_timeout,
read_and_route(request, responses, http_version),
)
.await
.unwrap_or_else(|_| reject(StatusCode::REQUEST_TIMEOUT, http_version));
Ok(response)
}
async fn read_and_route(
request: Request<Incoming>,
responses: Arc<response::HyperResponses>,
http_version: HttpVersion,
) -> Response<Full<Bytes>> {
if request.method() == Method::CONNECT || request.headers().contains_key(UPGRADE) {
return reject(StatusCode::NOT_IMPLEMENTED, http_version);
}
if request.version() == Version::HTTP_11 && request.headers().get_all(HOST).iter().count() != 1
{
return reject(StatusCode::BAD_REQUEST, http_version);
}
if header_size(&request) > HEADER_BYTES {
return reject(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, http_version);
}
let (head, mut body) = request.into_parts();
let mut received = BytesMut::new();
while let Some(frame) = body.frame().await {
let Ok(frame) = frame else {
return reject(StatusCode::BAD_REQUEST, http_version);
};
if frame.is_trailers() {
return reject(StatusCode::NOT_IMPLEMENTED, http_version);
}
if let Ok(data) = frame.into_data() {
if received.len().saturating_add(data.len()) > BODY_BYTES {
return reject(StatusCode::PAYLOAD_TOO_LARGE, http_version);
}
received.extend_from_slice(&data);
}
}
responses.response(head.uri.path())
}
fn header_size(request: &Request<Incoming>) -> usize {
let start = request.method().as_str().len() + request.uri().to_string().len() + 14;
request
.headers()
.iter()
.fold(start, |total, (name, value)| {
total
.saturating_add(name.as_str().len())
.saturating_add(value.as_bytes().len())
.saturating_add(4)
})
}
fn reject(status: StatusCode, http_version: HttpVersion) -> Response<Full<Bytes>> {
let mut response = Response::new(Full::new(Bytes::new()));
*response.status_mut() = status;
if http_version == HttpVersion::H1 {
response
.headers_mut()
.insert(CONNECTION, hyper::header::HeaderValue::from_static("close"));
}
response
}
fn ready(port: u16) -> Result<(), String> {
let mut output = io::stdout().lock();
writeln!(output, "READY|{port}").map_err(|error| error.to_string())?;
output.flush().map_err(|error| error.to_string())
}
@@ -0,0 +1,128 @@
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, pem::PemObject};
use rustls::{ClientConfig, RootCertStore, ServerConfig};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use tokio_rustls::{TlsAcceptor, TlsConnector};
use crate::config::HttpVersion;
pub trait Stream: AsyncRead + AsyncWrite {}
impl<T: AsyncRead + AsyncWrite> Stream for T {}
pub type BoxedStream = Box<dyn Stream + Unpin + Send>;
pub fn runtime_server(
cert_path: &str,
key_path: &str,
) -> Result<dever_runtime::tls::ServerTls, String> {
let certificate = std::fs::read(cert_path)
.map_err(|error| format!("cannot read certificate {cert_path}: {error}"))?;
let key = std::fs::read(key_path)
.map_err(|error| format!("cannot read private key {key_path}: {error}"))?;
dever_runtime::tls::server(
&dever_runtime::bytes::Bytes::new(certificate),
&dever_runtime::bytes::Bytes::new(key),
)
}
pub fn server(
cert_path: &str,
key_path: &str,
http_version: HttpVersion,
) -> Result<Arc<ServerConfig>, String> {
let certificates = certificates(cert_path)?;
let key_bytes = std::fs::read(key_path)
.map_err(|error| format!("cannot read private key {key_path}: {error}"))?;
let key = PrivateKeyDer::from_pem_slice(&key_bytes).map_err(|error| error.to_string())?;
let mut config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certificates, key)
.map_err(|error| error.to_string())?;
config.session_storage = rustls::server::ServerSessionMemoryCache::new(32);
config.send_tls13_tickets = 1;
config.alpn_protocols = vec![http_version.alpn().to_vec()];
Ok(Arc::new(config))
}
pub fn client(ca_path: &str, http_version: HttpVersion) -> Result<Arc<ClientConfig>, String> {
let mut roots = RootCertStore::empty();
for certificate in certificates(ca_path)? {
roots.add(certificate).map_err(|error| error.to_string())?;
}
let mut config = ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();
config.resumption = rustls::client::Resumption::in_memory_sessions(32);
config.alpn_protocols = vec![http_version.alpn().to_vec()];
Ok(Arc::new(config))
}
pub async fn connect(
port: u16,
timeout: Duration,
tls: Option<&Arc<ClientConfig>>,
http_version: HttpVersion,
) -> Result<BoxedStream, String> {
let tcp = tokio::time::timeout(timeout, TcpStream::connect(("127.0.0.1", port)))
.await
.map_err(|_| "TCP connection timed out".to_owned())?
.map_err(|error| error.to_string())?;
tcp.set_nodelay(true).map_err(|error| error.to_string())?;
let Some(config) = tls else {
return Ok(Box::new(tcp));
};
let name = ServerName::try_from("localhost").map_err(|error| error.to_string())?;
let stream = tokio::time::timeout(
timeout,
TlsConnector::from(config.clone()).connect(name, tcp),
)
.await
.map_err(|_| "TLS handshake timed out".to_owned())?
.map_err(|error| error.to_string())?;
verify_alpn(stream.get_ref().1.alpn_protocol(), http_version)?;
Ok(Box::new(stream))
}
pub async fn accept(
tcp: TcpStream,
timeout: Duration,
tls: Option<&Arc<ServerConfig>>,
http_version: HttpVersion,
) -> Result<BoxedStream, String> {
let Some(config) = tls else {
return Ok(Box::new(tcp));
};
let stream = tokio::time::timeout(timeout, TlsAcceptor::from(config.clone()).accept(tcp))
.await
.map_err(|_| "TLS handshake timed out".to_owned())?
.map_err(|error| error.to_string())?;
verify_alpn(stream.get_ref().1.alpn_protocol(), http_version)?;
Ok(Box::new(stream))
}
fn verify_alpn(negotiated: Option<&[u8]>, http_version: HttpVersion) -> Result<(), String> {
if http_version == HttpVersion::H2 && negotiated != Some(http_version.alpn()) {
return Err("TLS peer did not negotiate h2".into());
}
Ok(())
}
fn certificates(path: impl AsRef<Path>) -> Result<Vec<CertificateDer<'static>>, String> {
let path = path.as_ref();
let bytes = std::fs::read(path)
.map_err(|error| format!("cannot read certificate {}: {error}", path.display()))?;
let certificates = CertificateDer::pem_slice_iter(&bytes)
.collect::<Result<Vec<_>, _>>()
.map_err(|error| error.to_string())?;
if certificates.is_empty() {
return Err(format!(
"certificate {} contains no certificates",
path.display()
));
}
Ok(certificates)
}
@@ -0,0 +1,80 @@
use std::path::Path;
use serde_json::{Map, Value};
pub struct Settings(Map<String, Value>);
impl Settings {
pub fn load() -> Result<Self, String> {
let executable = std::env::current_exe()
.map_err(|error| format!("cannot locate benchmark executable: {error}"))?;
let root = executable
.parent()
.ok_or("benchmark executable has no parent")?;
Self::read(&root.join("config/setting.json"))
}
pub fn check() -> Result<(), String> {
println!("{{\"workers\":{}}}", Self::load()?.workers()?);
Ok(())
}
pub fn read(path: &Path) -> Result<Self, String> {
let bytes = std::fs::read(path)
.map_err(|error| format!("cannot read {}: {error}", path.display()))?;
let mut document: Map<String, Value> = serde_json::from_slice(&bytes)
.map_err(|error| format!("invalid {}: {error}", path.display()))?;
let Some(Value::Object(fields)) = document.remove("benchmark") else {
return Err("config/setting.json must contain a benchmark object".into());
};
for key in fields.keys() {
if !matches!(
key.as_str(),
"workers"
| "connections"
| "task_capacity"
| "timeout_ms"
| "lifetime_ms"
| "http2_streams"
| "http2_stream_window_bytes"
| "http2_connection_window_bytes"
| "cert_path"
| "key_path"
| "orm_count"
) {
return Err(format!("unknown benchmark setting '{key}'"));
}
}
Ok(Self(fields))
}
pub fn integer(
&self,
name: &str,
default: usize,
minimum: usize,
maximum: usize,
) -> Result<usize, String> {
let value = match self.value(name) {
None => default,
Some(value) => value
.as_u64()
.and_then(|value| usize::try_from(value).ok())
.ok_or_else(|| format!("benchmark.{name} must be an unsigned integer"))?,
};
if !(minimum..=maximum).contains(&value) {
return Err(format!(
"benchmark.{name} must be between {minimum} and {maximum}"
));
}
Ok(value)
}
pub fn workers(&self) -> Result<usize, String> {
self.integer("workers", 1, 1, 64)
}
pub fn value(&self, name: &str) -> Option<&Value> {
self.0.get(name)
}
}