Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,136 @@
|
||||
type Content {
|
||||
body: Bytes
|
||||
kind: Text
|
||||
}
|
||||
|
||||
listening(result: dever.net.ListenResult.Listening(listener)) (
|
||||
value: dever.system.Listener
|
||||
) {
|
||||
value = listener
|
||||
}
|
||||
|
||||
listening(result: dever.net.ListenResult.Failed(message)) (value: dever.system.Listener) {
|
||||
fail(app.WorkerFailure.Failed(message))
|
||||
}
|
||||
|
||||
bound(result: dever.net.PortResult.Bound(port)) (value: Int) {
|
||||
value = port
|
||||
}
|
||||
|
||||
bound(result: dever.net.PortResult.Failed(message)) (value: Int) {
|
||||
fail(app.WorkerFailure.Failed(message))
|
||||
}
|
||||
|
||||
paused(result: dever.time.SleepResult.Done) () {}
|
||||
|
||||
paused(result: dever.time.SleepResult.Failed(message)) () {
|
||||
fail(app.WorkerFailure.Failed(message))
|
||||
}
|
||||
|
||||
closed(result: dever.io.CloseResult.Closed) () {}
|
||||
|
||||
closed(result: dever.io.CloseResult.Failed(message)) () {
|
||||
fail(app.WorkerFailure.Failed(message))
|
||||
}
|
||||
|
||||
serverTls(result: dever.tls.ServerResult.Ready(tls)) (value: dever.system.ServerTls) {
|
||||
value = tls
|
||||
}
|
||||
|
||||
serverTls(result: dever.tls.ServerResult.Failed(message)) (
|
||||
value: dever.system.ServerTls
|
||||
) {
|
||||
fail(app.WorkerFailure.Failed(message))
|
||||
}
|
||||
|
||||
responseValue(content: Content) (result: dever.http.Response) {
|
||||
result = dever.http.Response {
|
||||
status = 200
|
||||
headers = [dever.http.header("Content-Type", content.kind)]
|
||||
body = content.body
|
||||
}
|
||||
}
|
||||
|
||||
route(request: dever.http.Request, large: Bytes) (response: dever.http.Response) {
|
||||
response = responseValue(select(request.target, large))
|
||||
}
|
||||
|
||||
select(path: "/plain", large: Bytes) (result: Content) {
|
||||
result = Content {
|
||||
body = dever.bytes.fromText("hello")
|
||||
kind = "text/plain"
|
||||
}
|
||||
}
|
||||
|
||||
select(path: "/json", large: Bytes) (result: Content) {
|
||||
result = Content {
|
||||
body = dever.bytes.fromText("{\"ok\":true}")
|
||||
kind = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
select(path: "/bytes", large: Bytes) (result: Content) {
|
||||
result = Content {
|
||||
body = large
|
||||
kind = "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
select(path: other, large: Bytes) (result: Content) {
|
||||
result = Content {
|
||||
body = dever.bytes.fromText("unknown benchmark path")
|
||||
kind = "text/plain"
|
||||
}
|
||||
}
|
||||
|
||||
serve(
|
||||
listener: dever.system.Listener,
|
||||
tls: null,
|
||||
limits: dever.http.Limits,
|
||||
large: Bytes
|
||||
) () {
|
||||
dever.system.httpServe(route, listener, limits, large)
|
||||
}
|
||||
|
||||
serve(
|
||||
listener: dever.system.Listener,
|
||||
tls: dever.system.ServerTls,
|
||||
limits: dever.http.Limits,
|
||||
large: Bytes
|
||||
) () {
|
||||
dever.system.httpServeTls(route, listener, limits, tls, large)
|
||||
}
|
||||
|
||||
worker(
|
||||
tls: dever.system.ServerTls?,
|
||||
limits: dever.http.Limits,
|
||||
large: Bytes,
|
||||
lifetime: Int
|
||||
) () {
|
||||
listener = listening(result(dever.net.listen("127.0.0.1", 0)))
|
||||
port = bound(result(dever.net.port(listener)))
|
||||
server = run(serve(listener, tls, limits, large))
|
||||
blocking(dever.io.println("READY|" + int.toText(port)))
|
||||
paused(result(dever.task.sleep(lifetime)))
|
||||
closed(result(dever.net.closeListener(listener)))
|
||||
stop(server)
|
||||
}
|
||||
|
||||
port.plain(limits: dever.http.Limits, large: Bytes, lifetime: Int) () {
|
||||
worker(null, limits, large, lifetime)
|
||||
}
|
||||
|
||||
port.secure(
|
||||
limits: dever.http.Limits,
|
||||
large: Bytes,
|
||||
lifetime: Int,
|
||||
certificate: Bytes,
|
||||
key: Bytes
|
||||
) () {
|
||||
worker(
|
||||
serverTls(result(dever.tls.server(certificate, key))),
|
||||
limits,
|
||||
large,
|
||||
lifetime
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
cmd http = app.http
|
||||
|
||||
cmd https = app.https
|
||||
|
||||
cmd http2 = app.http2
|
||||
|
||||
cmd https2 = app.https2
|
||||
@@ -0,0 +1,35 @@
|
||||
type WorkerFailure {
|
||||
error Failed(message: Text)
|
||||
}
|
||||
|
||||
http() (result: Bool) {
|
||||
port.plain(config.limits(), config.largeBody(), config.lifetime())
|
||||
result = true
|
||||
}
|
||||
|
||||
https() (result: Bool) {
|
||||
port.secure(
|
||||
config.limits(),
|
||||
config.largeBody(),
|
||||
config.lifetime(),
|
||||
config.certificate(),
|
||||
config.key()
|
||||
)
|
||||
result = true
|
||||
}
|
||||
|
||||
http2() (result: Bool) {
|
||||
port.plain(config.http2Limits(), config.largeBody(), config.lifetime())
|
||||
result = true
|
||||
}
|
||||
|
||||
https2() (result: Bool) {
|
||||
port.secure(
|
||||
config.http2Limits(),
|
||||
config.largeBody(),
|
||||
config.lifetime(),
|
||||
config.certificate(),
|
||||
config.key()
|
||||
)
|
||||
result = true
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! 独立 Axum 路由;用官方 Hyper 入口统一连接、报文和超时上限。
|
||||
use axum::{
|
||||
Router,
|
||||
body::Bytes,
|
||||
extract::DefaultBodyLimit,
|
||||
http::header,
|
||||
routing::{get, post},
|
||||
};
|
||||
use hyper_util::{
|
||||
rt::{TokioIo, TokioTimer},
|
||||
service::TowerToHyperService,
|
||||
};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::{net::TcpListener, sync::Semaphore};
|
||||
|
||||
async fn serve(capacity: usize) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let router = Router::new()
|
||||
.route(
|
||||
"/json",
|
||||
get(async |_: Bytes| {
|
||||
(
|
||||
[(header::CONTENT_TYPE, "application/json")],
|
||||
"{\"ok\":true}",
|
||||
)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/echo",
|
||||
post(async |body: Bytes| ([(header::CONTENT_TYPE, "application/octet-stream")], body)),
|
||||
)
|
||||
.layer(DefaultBodyLimit::max(65536));
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let slots = Arc::new(Semaphore::new(capacity));
|
||||
println!("READY|{}", listener.local_addr()?.port());
|
||||
loop {
|
||||
let permit = slots.clone().acquire_owned().await?;
|
||||
let (stream, _) = listener.accept().await?;
|
||||
stream.set_nodelay(true)?;
|
||||
let service = TowerToHyperService::new(router.clone());
|
||||
tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
let mut builder = hyper::server::conn::http1::Builder::new();
|
||||
builder
|
||||
.timer(TokioTimer::new())
|
||||
.header_read_timeout(Duration::from_secs(5))
|
||||
.max_buf_size(8192);
|
||||
if let Err(error) = builder
|
||||
.serve_connection(TokioIo::new(stream), service)
|
||||
.await
|
||||
{
|
||||
if !error.is_incomplete_message() && !error.is_closed() {
|
||||
eprintln!("connection: {error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let capacity = std::env::args().nth(1).unwrap().parse()?;
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(serve(capacity))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
route(request: dever.http.Request) (response: dever.http.Response) {
|
||||
response = selected(request.target, request.body)
|
||||
}
|
||||
|
||||
selected(path: "/json", body: Bytes) (response: dever.http.Response) {
|
||||
response = dever.http.Response { status = 200, headers = [dever.http.header("content-type", "application/json")], body = dever.bytes.fromText("{\"ok\":true}") }
|
||||
}
|
||||
selected(path: "/echo", body: Bytes) (response: dever.http.Response) {
|
||||
response = dever.http.Response { status = 200, headers = [dever.http.header("content-type", "application/octet-stream")], body = body }
|
||||
}
|
||||
selected(path: other, body: Bytes) (response: dever.http.Response) {
|
||||
response = dever.http.Response { status = 404, headers = [], body = dever.bytes.fromText("") }
|
||||
}
|
||||
|
||||
serve(connections: Int) () {
|
||||
listener = dever.net.listen("127.0.0.1", 0)
|
||||
port = dever.net.port(listener)
|
||||
limits = dever.http.defaultLimits()
|
||||
limits.connections = connections
|
||||
limits.body_bytes = 65536
|
||||
blocking(dever.io.println("READY|" + int.toText(port)))
|
||||
dever.http.serve(route, listener, limits)
|
||||
}
|
||||
|
||||
main() () {
|
||||
serve(2048)
|
||||
}
|
||||
|
||||
overload() () {
|
||||
serve(8192)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type listener struct {
|
||||
net.Listener
|
||||
slots chan struct{}
|
||||
}
|
||||
|
||||
type connection struct {
|
||||
net.Conn
|
||||
slots chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (value *connection) Close() error {
|
||||
err := value.Conn.Close()
|
||||
value.once.Do(func() { <-value.slots })
|
||||
return err
|
||||
}
|
||||
|
||||
func (value *listener) Accept() (net.Conn, error) {
|
||||
value.slots <- struct{}{}
|
||||
stream, err := value.Listener.Accept()
|
||||
if err != nil {
|
||||
<-value.slots
|
||||
return nil, err
|
||||
}
|
||||
return &connection{Conn: stream, slots: value.slots}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
capacity, err := strconv.Atoi(os.Args[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
runtime.GOMAXPROCS(2)
|
||||
bound, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
server := http.Server{
|
||||
ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second,
|
||||
IdleTimeout: 5 * time.Second, MaxHeaderBytes: 8192,
|
||||
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
defer request.Body.Close()
|
||||
body, err := io.ReadAll(http.MaxBytesReader(writer, request.Body, 65536))
|
||||
if err != nil {
|
||||
http.Error(writer, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
switch request.URL.RequestURI() {
|
||||
case "/json":
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.Write([]byte(`{"ok":true}`))
|
||||
case "/echo":
|
||||
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||
writer.Write(body)
|
||||
default:
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}),
|
||||
}
|
||||
fmt.Printf("READY|%d\n", bound.Addr().(*net.TCPAddr).Port)
|
||||
if err := server.Serve(&listener{Listener: bound, slots: make(chan struct{}, capacity)}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! 独立 Hyper 服务及三种语言共用的 HTTP/1.1 客户端,不链接 Dever 运行库。
|
||||
use bytes::Bytes;
|
||||
use http_body_util::{BodyExt, Full, Limited};
|
||||
use hyper::{Request, Response, body::Incoming, service::service_fn};
|
||||
use hyper_util::rt::{TokioIo, TokioTimer};
|
||||
use std::{convert::Infallible, sync::Arc, time::Duration};
|
||||
use tokio::{net::TcpListener, sync::{Barrier, Semaphore}, time::Instant};
|
||||
|
||||
async fn route(request: Request<Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
|
||||
let path = request.uri().path().to_owned();
|
||||
let body = match Limited::new(request.into_body(), 65536).collect().await {
|
||||
Ok(body) => body.to_bytes(),
|
||||
Err(_) => return Ok(Response::builder().status(400).body(Full::new(Bytes::new())).unwrap()),
|
||||
};
|
||||
let (status, kind, body) = match path.as_str() {
|
||||
"/json" => (200, "application/json", Bytes::from_static(b"{\"ok\":true}")),
|
||||
"/echo" => (200, "application/octet-stream", body),
|
||||
_ => (404, "text/plain", Bytes::new()),
|
||||
};
|
||||
Ok(Response::builder().status(status).header("content-type", kind).body(Full::new(body)).unwrap())
|
||||
}
|
||||
|
||||
async fn serve(capacity: usize) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let slots = Arc::new(Semaphore::new(capacity));
|
||||
println!("READY|{}", listener.local_addr()?.port());
|
||||
loop {
|
||||
let permit = slots.clone().acquire_owned().await?;
|
||||
let (stream, _) = listener.accept().await?;
|
||||
stream.set_nodelay(true)?;
|
||||
tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
let mut builder = hyper::server::conn::http1::Builder::new();
|
||||
builder.timer(TokioTimer::new()).header_read_timeout(Duration::from_secs(5)).max_buf_size(8192);
|
||||
if let Err(error) = builder.serve_connection(TokioIo::new(stream), service_fn(route)).await {
|
||||
if !error.is_incomplete_message() && !error.is_closed() {
|
||||
eprintln!("connection: {error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn load(port: u16, path: String, concurrency: usize, seconds: f64) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let payload = if path == "/echo" { Bytes::from(vec![b'x'; 65536]) } else { Bytes::new() };
|
||||
let expected = if path == "/echo" { payload.clone() } else { Bytes::from_static(b"{\"ok\":true}") };
|
||||
let kind = if path == "/echo" { "application/octet-stream" } else { "application/json" };
|
||||
let barrier = Arc::new(Barrier::new(concurrency + 1));
|
||||
let mut workers = Vec::new();
|
||||
for _ in 0..concurrency {
|
||||
let stream = tokio::net::TcpStream::connect(("127.0.0.1", port)).await?;
|
||||
stream.set_nodelay(true)?;
|
||||
let (mut sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await?;
|
||||
let driver = tokio::spawn(connection);
|
||||
let barrier = barrier.clone();
|
||||
let payload = payload.clone();
|
||||
let expected = expected.clone();
|
||||
let path = path.clone();
|
||||
workers.push(tokio::spawn(async move {
|
||||
barrier.wait().await;
|
||||
let deadline = Instant::now() + Duration::from_secs_f64(seconds);
|
||||
let mut samples = Vec::new();
|
||||
let mut errors = 0_u64;
|
||||
while Instant::now() < deadline {
|
||||
let request = Request::builder().method(if path == "/echo" { "POST" } else { "GET" })
|
||||
.uri(&path).header("host", "localhost").body(Full::new(payload.clone())).unwrap();
|
||||
let started = Instant::now();
|
||||
let exchange = async {
|
||||
let response = sender.send_request(request).await.map_err(|error| error.to_string())?;
|
||||
if response.status() != 200 || response.headers().get("content-type").and_then(|value| value.to_str().ok()) != Some(kind) {
|
||||
return Err("status or content type differs".to_owned());
|
||||
}
|
||||
let body = response.into_body().collect().await.map_err(|error| error.to_string())?.to_bytes();
|
||||
if body != expected { return Err("response body differs".to_owned()); }
|
||||
Ok(())
|
||||
};
|
||||
match tokio::time::timeout(Duration::from_secs(5), exchange).await {
|
||||
Ok(Ok(())) => samples.push(started.elapsed().as_micros() as u64),
|
||||
result => {
|
||||
eprintln!("request: {result:?}");
|
||||
errors += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(sender);
|
||||
driver.abort();
|
||||
let _ = driver.await;
|
||||
(samples, errors)
|
||||
}));
|
||||
}
|
||||
let started = Instant::now();
|
||||
barrier.wait().await;
|
||||
let mut samples = Vec::new();
|
||||
let mut errors = 0;
|
||||
for worker in workers {
|
||||
let (mut values, failed) = worker.await?;
|
||||
samples.append(&mut values);
|
||||
errors += failed;
|
||||
}
|
||||
let elapsed = started.elapsed().as_secs_f64();
|
||||
samples.sort_unstable();
|
||||
let percentile = |percent: usize| -> Option<f64> {
|
||||
if samples.is_empty() { None } else { Some(samples[(samples.len() * percent).div_ceil(100) - 1] as f64 / 1000.0) }
|
||||
};
|
||||
println!("{}", serde_json::json!({"concurrency":concurrency,"seconds":seconds,"elapsed_seconds":elapsed,
|
||||
"succeeded":samples.len(),"errors":errors,"qps":samples.len() as f64 / elapsed,
|
||||
"p50_ms":percentile(50),"p95_ms":percentile(95),"p99_ms":percentile(99)}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let arguments = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
let server = arguments[0] == "serve";
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(if server { 2 } else { 4 }).enable_all().build()?;
|
||||
if server {
|
||||
runtime.block_on(serve(arguments[1].parse()?))
|
||||
} else {
|
||||
runtime.block_on(load(arguments[1].parse()?, arguments[2].clone(), arguments[3].parse()?, arguments[4].parse()?))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
capacity, err := strconv.Atoi(os.Args[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
runtime.GOMAXPROCS(2)
|
||||
app := fiber.New(fiber.Config{
|
||||
Concurrency: capacity,
|
||||
BodyLimit: 65536,
|
||||
ReadBufferSize: 8192,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 5 * time.Second,
|
||||
IdleTimeout: 5 * time.Second,
|
||||
CaseSensitive: true,
|
||||
StrictRouting: true,
|
||||
})
|
||||
app.Get("/json", func(context fiber.Ctx) error {
|
||||
context.Set("Content-Type", "application/json")
|
||||
return context.SendString(`{"ok":true}`)
|
||||
})
|
||||
app.Post("/echo", func(context fiber.Ctx) error {
|
||||
context.Set("Content-Type", "application/octet-stream")
|
||||
return context.Send(context.BodyRaw())
|
||||
})
|
||||
bound, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Printf("READY|%d\n", bound.Addr().(*net.TCPAddr).Port)
|
||||
if err := app.Listener(bound, fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type settings struct {
|
||||
Workers int `json:"workers"`
|
||||
Connections int `json:"connections"`
|
||||
TimeoutMS int `json:"timeout_ms"`
|
||||
LifetimeMS int `json:"lifetime_ms"`
|
||||
Certificate string `json:"cert_path"`
|
||||
Key string `json:"key_path"`
|
||||
}
|
||||
|
||||
type limitedListener struct {
|
||||
net.Listener
|
||||
slots chan struct{}
|
||||
closed chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
type limitedConnection struct {
|
||||
net.Conn
|
||||
slots chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (connection *limitedConnection) Close() error {
|
||||
err := connection.Conn.Close()
|
||||
connection.once.Do(func() { <-connection.slots })
|
||||
return err
|
||||
}
|
||||
|
||||
func (listener *limitedListener) Accept() (net.Conn, error) {
|
||||
connection, err := listener.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case listener.slots <- struct{}{}:
|
||||
return &limitedConnection{Conn: connection, slots: listener.slots}, nil
|
||||
case <-listener.closed:
|
||||
connection.Close()
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (listener *limitedListener) Close() error {
|
||||
listener.once.Do(func() { close(listener.closed) })
|
||||
return listener.Listener.Close()
|
||||
}
|
||||
|
||||
func run() error {
|
||||
if len(os.Args) != 2 || (os.Args[1] != "http" && os.Args[1] != "https") {
|
||||
return errors.New("usage: go_http http|https")
|
||||
}
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := os.ReadFile(filepath.Join(filepath.Dir(executable), "config", "setting.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var document struct {
|
||||
Benchmark settings `json:"benchmark"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &document); err != nil {
|
||||
return err
|
||||
}
|
||||
config := document.Benchmark
|
||||
if config.Workers < 1 || config.Connections < 1 || config.TimeoutMS < 1 || config.LifetimeMS < 1 {
|
||||
return errors.New("benchmark requires positive workers, connections, timeout_ms and lifetime_ms")
|
||||
}
|
||||
runtime.GOMAXPROCS(config.Workers)
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bounded := &limitedListener{Listener: listener, slots: make(chan struct{}, config.Connections), closed: make(chan struct{})}
|
||||
defer bounded.Close()
|
||||
large := bytes.Repeat([]byte{'x'}, 65536)
|
||||
deadline := time.Duration(config.TimeoutMS) * time.Millisecond
|
||||
server := &http.Server{
|
||||
ReadTimeout: deadline, ReadHeaderTimeout: deadline, WriteTimeout: deadline,
|
||||
IdleTimeout: deadline, MaxHeaderBytes: 8192,
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
|
||||
Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
body := http.MaxBytesReader(writer, request.Body, 65536)
|
||||
defer body.Close()
|
||||
if _, err := io.Copy(io.Discard, body); err != nil {
|
||||
http.Error(writer, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
switch request.URL.RequestURI() {
|
||||
case "/plain":
|
||||
writer.Header().Set("Content-Type", "text/plain")
|
||||
io.WriteString(writer, "hello")
|
||||
case "/json":
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
io.WriteString(writer, `{"ok":true}`)
|
||||
case "/bytes":
|
||||
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||
writer.Write(large)
|
||||
default:
|
||||
http.NotFound(writer, request)
|
||||
}
|
||||
}),
|
||||
}
|
||||
timer := time.AfterFunc(time.Duration(config.LifetimeMS)*time.Millisecond, func() { server.Close() })
|
||||
defer timer.Stop()
|
||||
fmt.Printf("READY|%d\n", listener.Addr().(*net.TCPAddr).Port)
|
||||
if os.Args[1] == "https" {
|
||||
err = server.ServeTLS(bounded, config.Certificate, config.Key)
|
||||
} else {
|
||||
err = server.Serve(bounded)
|
||||
}
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
plain(limits: dever.http.Limits, large: Bytes, lifetime: Int) () fails app.WorkerFailure
|
||||
|
||||
secure(
|
||||
limits: dever.http.Limits,
|
||||
large: Bytes,
|
||||
lifetime: Int,
|
||||
certificate: Bytes,
|
||||
key: Bytes
|
||||
) () fails app.WorkerFailure
|
||||
Reference in new issue
Block a user