Dusty - new HTTP server

As if Zig didn’t already have enough HTTP servers, I’m creating another one. This is very early, but it’s one of the reasons for me working on zio in the first place. I have a server application, where I want to run custom TCP protocol, HTTP server and NATS servers, all in a single event loop, all single-threaded for the networking part.

So let me introduce Dusty, a simple HTTP server, based on my zio runtime for networking and llhttp for request parsing. Feel free to experiment with it. Later on, I’ll probably make the code use std.Io instead of zio directly, but for now zio is a hard dependency.

4 Likes

Hey! So I cloned your repo and been doing some basic testing, curious, what the bottleneck is here, both servers are running in debug mode, personally I can’t ZIO to work on my laptop, but dusty works fine? Is llhttp the bottleneck here, cause the metrics seems to be extremely off, and I also have built my own custom event loop, which is single threaded?

Its a very simple wrk request just to see the event loops ability
wrk -t1 -c50 -d10s http://127.0.0.1:8080/ping

dusty results

Running 10s test @ http://127.0.0.1:8080/ping
  1 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   421.05us  100.35us  11.41ms   96.89%
    Req/Sec   111.50k     2.28k  113.55k    96.04%
  1120759 requests in 10.10s, 49.17MB read
Requests/sec: 110958.17
Transfer/sec:      4.87MB

other server results

Running 10s test @ http://127.0.0.1:8080/ping
  1 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   185.82us   90.20us   2.57ms   89.17%
    Req/Sec   167.04k    25.10k  188.65k    86.14%
  1678688 requests in 10.10s, 158.49MB read
Requests/sec: 166170.75
Transfer/sec:     15.69MB

For dusty I’m just using the basic example setup but with the following handle root

fn handleRoot(ctx: *AppContext, req: *http.Request, res: *http.Response) !void {
    _ = ctx;
    _ = req;
    res.body = "SUCCESS\n";
}
pub fn runServer(allocator: std.mem.Allocator, rt: *zio.Runtime) !void {
    var ctx: AppContext = .{ .rt = rt };
    const AppServer = http.Server(AppContext);

    const config: http.ServerConfig = .{
        .timeout = .{
            .request = 60 * std.time.ms_per_s,
            .keepalive = 300 * std.time.ms_per_s,
        },
    };

    var server = AppServer.init(allocator, config, &ctx);
    defer server.deinit();

    // Register routes
    server.router.get("/ping", handleRoot);
    var signal = try zio.Signal.init(.interrupt);
    defer signal.deinit();

    const addr = try zio.net.IpAddress.parseIp("127.0.0.1", 8080);

    var task = try rt.spawn(AppServer.listen, .{ &server, rt, addr }, .{});
    defer task.cancel(rt);

    while (true) {
        const result = try zio.select(rt, .{ .task = &task, .signal = &signal });
        switch (result) {
            .task => |r| {
                return r;
            },
            .signal => {
                server.stop();
            },
        }
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var rt = try zio.Runtime.init(allocator, .{});
    defer rt.deinit();

    try rt.runUntilComplete(runServer, .{ allocator, rt }, .{});
}

Love the work so far, but not sure if there is some configuration or something I need to do to get Dusty up in performance, or if you are mainly working on ergonomics, but I assume zio is great, since you mentioned it outperfs go and tokio.

Thanks for all the help!

This might be the problem, try running them in ReleaseSafe, there is a lot of code, specially in the C parser, that would really benefit from optimizations. I haven’t spend much time benchmarking, but in my tests, it was slightly faster than http.zig.

When i build in either release safe or fast, i get this error each time I make one request

➜  dusty git:(main) ✗ zig build --release=safe run
info(dusty): Listening on 127.0.0.1:8080
run
└─ run exe basic-example failure
error: the following command terminated unexpectedly:
./.zig-cache/o/b1572f98f6241b9f71778f855c097645/basic-example

Build Summary: 1/3 steps succeeded; 1 failed
run transitive failure
└─ run exe basic-example failure

error: the following build command failed with exit code 1:
.zig-cache/o/57c6eee1fbbe8e0c6c5790dd27972638/build /Users/vic-augustrokx-nellemann/.zvm/0.15.1/zig /Users/vic-augustrokx-nellemann/.zvm/0.15.1/lib /Users/vic-augustrokx-nellemann/Desktop/Zig/dusty .zig-cache /Users/vic-augustrokx-nellemann/.cache/zig --seed 0x9f1320fe -Z0a44256127c94359 --release=safe run

the request

curl -v http://127.0.0.1:8080/ping

*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET /ping HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 8
<
SUCCESS
* Connection #0 to host 127.0.0.1 left intact

But your right best to not benchmark beforehand, was just curious!

I’ll need to investigate this, but I have a hunch it might be the same bug I’m just trying to solve. Can I ping you tomorrow to try a new version?

Yes that’s perfect

So yes, the crash you were seeing was almost certainly the zio.Timeout problem I was fixing yesterday. It was just manifesting differently on macOS. Now I have an integration test on all platforms that runs the server and checks the output.

1 Like