Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ The reply states which baseline it used, and profiles the other framework does n
| Gateway | `gateway-64`, `gateway-h3` | Reverse proxy + server stack over HTTP/2 and HTTP/3 with mixed workload |
| Production Stack | `production-stack` | Four-service architecture: edge + Redis + JWT auth sidecar + server, 10K-item cache-aside, concurrent reads + writes |
| WebSocket | `echo-ws`, `echo-ws-pipeline`, `echo-ws-limited` | Echo throughput across connection counts; 16x batched echo; echo with each connection closed after 10 messages (upgrade-handshake cost) |
| Interaction | `rendezvous` | `GET /rendezvous/{key}?v={value}`: two connections meet on a key, neither gets an answer until both have arrived, and each is answered with the other's value. Measures what crossing between workers costs, on its own board |

## Run Locally

Expand Down Expand Up @@ -105,6 +106,7 @@ Swap `actix` for your entry and `h1` for the family you want:
| Gateway | `gw` | reverse-proxy and production-stack profiles |
| gRPC | `grpc` | unary and streaming, plaintext and TLS |
| WebSocket | `ws` | the echo profiles |
| Interaction | `ix` | the Rendezvous profile |

`<framework>` is your entry's name on the board, lowercased, with anything
outside `A-Z a-z 0-9 . _ -` turned into `-`, the same name as your file in
Expand Down
1 change: 1 addition & 0 deletions frameworks/actix/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frameworks/actix/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ num_cpus = "1"
tokio-postgres = { version = "0.7", features = ["with-serde_json-1"] }
deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] }
futures-util = "0.3"
tokio = { version = "1", features = ["sync"] }

[profile.release]
opt-level = 3
Expand Down
1 change: 1 addition & 0 deletions frameworks/actix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Actix-web 4 HTTP server with rustls for TLS/HTTP/2 support, compiled with thin L
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/pipeline` | GET | Returns `ok` (plain text) |
| `/rendezvous/{key}` | GET | Waits for a second request on the same key, answers each with the other's `v` |
| `/baseline11` | GET | Sums query parameter values |
| `/baseline11` | POST | Sums query parameters + request body |
| `/baseline2` | GET | Sums query parameter values (HTTP/2 variant) |
Expand Down
3 changes: 2 additions & 1 deletion frameworks/actix/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"static-tls",
"async-db",
"baseline-h2",
"static-h2"
"static-h2",
"rendezvous"
],
"maintainers": []
}
42 changes: 42 additions & 0 deletions frameworks/actix/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use actix_web::http::header;
use actix_web::http::header::{ContentType, HeaderValue, SERVER};
use actix_web::middleware::Compress;
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
use dashmap::mapref::entry::Entry;
use dashmap::DashMap;
use deadpool_postgres::{Manager, ManagerConfig, Pool as PgPool, RecyclingMethod};
use futures_util::StreamExt;
Expand Down Expand Up @@ -156,6 +157,46 @@ fn load_dataset() -> Vec<DatasetItem> {
// actix-web re-exports its runtime, so the sleep comes from there rather than from a direct
// tokio dependency the entry does not have. It parks the task rather than the thread, so the
// waits in flight are bounded by memory rather than by the worker count.
#[derive(Deserialize)]
struct RendezvousQuery {
v: String,
}

// Actix runs one worker per core and a handler stays on the worker that accepted its
// connection, so the two sides of a key usually sit on different workers. The first
// parks on a oneshot, and the second wakes it across workers.
static RENDEZVOUS: std::sync::LazyLock<DashMap<String, (String, tokio::sync::oneshot::Sender<String>)>> =
std::sync::LazyLock::new(DashMap::new);

async fn rendezvous(path: web::Path<String>, query: web::Query<RendezvousQuery>) -> HttpResponse {
let key = path.into_inner();
let mut value = query.into_inner().v;
let other = loop {
match RENDEZVOUS.entry(key.clone()) {
Entry::Occupied(waiting) => {
let (peer_value, reply) = waiting.remove();
match reply.send(value) {
Ok(()) => break peer_value,
// That client left while it waited: nobody to meet, so wait instead.
Err(returned) => value = returned,
}
}
Entry::Vacant(free) => {
let (reply, wait) = tokio::sync::oneshot::channel();
free.insert((value, reply));
match wait.await {
Ok(peer_value) => break peer_value,
Err(_) => return HttpResponse::InternalServerError().finish(),
}
}
}
};
HttpResponse::Ok()
.insert_header((SERVER, SERVER_HDR.clone()))
.content_type(ContentType::plaintext())
.body(other)
}

async fn delay(path: web::Path<u64>) -> HttpResponse {
let ms = path.into_inner();
if ms > 0 {
Expand Down Expand Up @@ -591,6 +632,7 @@ async fn main() -> io::Result<()> {
.app_data(crud_cache.clone())
.route("/pipeline", web::get().to(pipeline))
.route("/delay/{ms}", web::get().to(delay))
.route("/rendezvous/{key}", web::get().to(rendezvous))
.route("/baseline11", web::get().to(baseline11_get))
.route("/baseline11", web::post().to(baseline11_post))
.route("/baseline2", web::get().to(baseline2))
Expand Down
30 changes: 30 additions & 0 deletions frameworks/aspnet-minimal/Handlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using HttpArena.Services;
using HttpArena.Types;
using System.Buffers;
using System.Collections.Concurrent;

static class Handlers
{
Expand All @@ -16,6 +17,35 @@ public static async ValueTask<string> SumBody(int a, int b, HttpRequest req)

public static string Text() => "ok";

// GET /rendezvous/{key}?v= - the first request for a key waits on a completion
// source; the second takes it out of the map, completes it with its own value and
// answers with the one the first left behind.
sealed class RendezvousWaiter(string value)
{
public readonly string Value = value;
public readonly TaskCompletionSource<string> Reply = new(TaskCreationOptions.RunContinuationsAsynchronously);
}

static readonly ConcurrentDictionary<string, RendezvousWaiter> RendezvousWaiting = new();

public static ValueTask<string> Rendezvous(string key, string v)
{
RendezvousWaiter? mine = null;
while (true)
{
if (RendezvousWaiting.TryRemove(key, out var peer))
{
peer.Reply.TrySetResult(v);
return new ValueTask<string>(peer.Value);
}
mine ??= new RendezvousWaiter(v);
if (RendezvousWaiting.TryAdd(key, mine))
{
return new ValueTask<string>(mine.Reply.Task);
}
}
}

// GET /delay/{ms} - answer after ms milliseconds. Task.Delay registers a
// timer and yields; the thread goes back to the pool instead of sitting on
// the request, so the number of waits in flight is bounded by memory rather
Expand Down
1 change: 1 addition & 0 deletions frameworks/aspnet-minimal/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@

app.MapPost("/echo", Handlers.EchoBody);
app.MapGet("/delay/{ms:int}", Handlers.Delay);
app.MapGet("/rendezvous/{key}", Handlers.Rendezvous);
app.MapGet("/json/{count}", Handlers.Json);
app.MapGet("/async-db", Handlers.AsyncDatabase);

Expand Down
1 change: 1 addition & 0 deletions frameworks/aspnet-minimal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Minimal ASP.NET Core HTTP server using .NET 10 with Kestrel and minimal API rout
| Endpoint | Method | Description |
|---|---|---|
| `/pipeline` | GET | Returns `ok` (plain text) |
| `/rendezvous/{key}` | GET | Waits for a second request on the same key, answers each with the other's `v` |
| `/baseline11` | GET | Sums query parameter values |
| `/baseline11` | POST | Sums query parameters + request body |
| `/baseline2` | GET | Sums query parameter values (HTTP/2 variant) |
Expand Down
3 changes: 2 additions & 1 deletion frameworks/aspnet-minimal/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"baseline-h2c",
"json-h2c",
"baseline-h3",
"static-h3"
"static-h3",
"rendezvous"
],
"maintainers": []
}
1 change: 1 addition & 0 deletions frameworks/chi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ chi router on the Go `net/http` server, default configuration.
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/pipeline` | GET | Returns `ok` (plain text) |
| `/rendezvous/{key}` | GET | Waits for a second request on the same key, answers each with the other's `v` |
| `/baseline11` | GET | Sums query parameter values |
| `/baseline11` | POST | Sums query parameters + request body |
| `/json/{count}?m=N` | GET | First `count` dataset items with `total = price * quantity * m` |
Expand Down
41 changes: 41 additions & 0 deletions frameworks/chi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"time"

"github.com/go-chi/chi/v5"
Expand Down Expand Up @@ -80,6 +81,45 @@ func delay(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(strconv.Itoa(ms)))
}

type rendezvousWaiter struct {
value string
reply chan string
}

var rendezvousWaiters sync.Map

// The first request for a key parks its goroutine on a channel; the second finds it,
// hands over its own value and answers with the one the first left behind.
func rendezvous(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
value := r.URL.Query().Get("v")
other := ""
var mine *rendezvousWaiter
for {
if found, ok := rendezvousWaiters.LoadAndDelete(key); ok {
peer := found.(*rendezvousWaiter)
peer.reply <- value
other = peer.value
break
}
if mine == nil {
// Buffered, so the side completing the pair never blocks on this one.
mine = &rendezvousWaiter{value: value, reply: make(chan string, 1)}
}
if _, loaded := rendezvousWaiters.LoadOrStore(key, mine); !loaded {
select {
case other = <-mine.reply:
case <-r.Context().Done():
rendezvousWaiters.CompareAndDelete(key, mine)
return
}
break
}
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(other))
}

func baseline11(w http.ResponseWriter, r *http.Request) {
sum := 0
for _, values := range r.URL.Query() {
Expand Down Expand Up @@ -463,6 +503,7 @@ func main() {

r.Get("/pipeline", pipeline)
r.Get("/delay/{ms}", delay)
r.Get("/rendezvous/{key}", rendezvous)
r.Get("/baseline11", baseline11)
r.Post("/baseline11", baseline11)
r.Get("/json/{count}", jsonItems)
Expand Down
3 changes: 2 additions & 1 deletion frameworks/chi/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"json-tls",
"8gbit",
"static-tls",
"async-db"
"async-db",
"rendezvous"
],
"maintainers": []
}
3 changes: 2 additions & 1 deletion frameworks/helidon-production/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"echo-ws-limited",
"unary-grpc",
"unary-grpc-tls",
"async-db"
"async-db",
"rendezvous"
],
"maintainers": [
"tomas-langer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public static void main(String[] args) throws Exception {
.get("/pipeline", new PipelineHandler())
.get("/baseline11", baselineHandler)
.get("/delay/{ms}", new DelayHandler())
.get("/rendezvous/{key}", new RendezvousHandler())
.get("/json/{count}", jsonHandler)
.get("/json", jsonHandler)
.post("/baseline11", new BaselinePostHandler())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.httparena;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;

import io.helidon.webserver.http.Handler;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;

import static io.helidon.http.HeaderValues.CONTENT_TYPE_TEXT_PLAIN;

/**
* GET /rendezvous/{key}?v= - the first request for a key waits, the second answers both.
* Each request has a virtual thread of its own, so waiting is just blocking on a future.
*/
final class RendezvousHandler implements Handler {
private record Waiter(String value, CompletableFuture<String> reply) {}

private final ConcurrentHashMap<String, Waiter> waiting = new ConcurrentHashMap<>();

@Override
public void handle(ServerRequest req, ServerResponse res) throws Exception {
String key = req.path().pathParameters().get("key");
String value = req.query().first("v").orElse("");

Waiter mine = null;
String other;
while (true) {
Waiter peer = waiting.remove(key);
if (peer != null) {
peer.reply().complete(value);
other = peer.value();
break;
}
if (mine == null) {
mine = new Waiter(value, new CompletableFuture<>());
}
if (waiting.putIfAbsent(key, mine) == null) {
other = mine.reply().get();
break;
}
}

res.header(CONTENT_TYPE_TEXT_PLAIN);
res.send(other.getBytes(StandardCharsets.ISO_8859_1));
}
}
1 change: 1 addition & 0 deletions frameworks/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Node's own HTTP server, `node:http`, with no framework on top and no dependencie
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/pipeline` | GET | Returns `ok` (plain text) |
| `/rendezvous/{key}` | GET | Waits for a second request on the same key, answers each with the other's `v` |
| `/baseline11` | GET/POST | Sums query parameter values, plus the body for POST |
| `/baseline2` | GET | Sums query parameter values |
| `/json/:count` | GET | Serializes a slice of the dataset, gzipped when the client accepts it |
Expand Down
3 changes: 2 additions & 1 deletion frameworks/node/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"8gbit",
"static-tls",
"async-db",
"fortunes"
"fortunes",
"rendezvous"
],
"maintainers": []
}
Loading
Loading