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
13 changes: 13 additions & 0 deletions frameworks/robaho-httpserver/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -q
COPY src ./src
RUN mvn package -DskipTests -q

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/robaho-httpserver-1.0.0.jar app.jar
COPY --from=build /app/target/libs ./libs
EXPOSE 8080
ENTRYPOINT ["java", "-Drobaho.net.httpserver.nodelay=true", "-jar", "app.jar"]
23 changes: 23 additions & 0 deletions frameworks/robaho-httpserver/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"display_name": "robaho-httpserver",
"language": "Java",
"type": "production",
"engine": "robaho-httpserver",
"description": "High-performance Java HTTP server using robaho/httpserver, a drop-in replacement for com.sun.net.httpserver with improved throughput.",
"repo": "https://github.com/robaho/httpserver",
"enabled": true,
"tests": [
"baseline",
"pipelined",
"limited-conn",
"json",
"json-comp",
"upload",
"static",
"async-db",
"fortunes",
"api-4",
"api-16"
],
"maintainers": []
}
74 changes: 74 additions & 0 deletions frameworks/robaho-httpserver/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.httparena</groupId>
<artifactId>robaho-httpserver</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<properties>
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>io.github.robaho</groupId>
<artifactId>httpserver</artifactId>
<version>1.0.25</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>io.pebbletemplates</groupId>
<artifactId>pebble</artifactId>
<version>3.2.4</version>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-pg-client</artifactId>
<version>5.0.0.CR7</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>com.httparena.Main</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.7.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals><goal>copy-dependencies</goal></goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
249 changes: 249 additions & 0 deletions frameworks/robaho-httpserver/src/main/java/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package com.httparena;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.pebbletemplates.pebble.PebbleEngine;
import io.pebbletemplates.pebble.template.PebbleTemplate;
import io.vertx.pgclient.PgBuilder;
import io.vertx.pgclient.PgConnectOptions;
import io.vertx.sqlclient.*;
import robaho.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.zip.GZIPOutputStream;

public class Main {

static final ObjectMapper mapper = new ObjectMapper();
static final PebbleEngine pebble = new PebbleEngine.Builder().autoEscaping(true).build();
static final PebbleTemplate fortunesTemplate = pebble.getTemplate("templates/fortunes.html");
static List<Item> dataset = List.of();
static Pool pgPool;

public static void main(String[] args) throws Exception {
loadDataset();
initPostgres();

HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());

server.createContext("/baseline11", new BaselineHandler());
server.createContext("/baseline2", new BaselineHandler());
server.createContext("/pipeline", new PipelineHandler());
server.createContext("/json/", new JsonHandler());
server.createContext("/upload", new UploadHandler());
server.createContext("/static/", new StaticHandler());
if (pgPool != null) {
server.createContext("/async-db", new AsyncDbHandler());
server.createContext("/fortunes", new FortunesHandler());
}

server.start();
}

static void loadDataset() {
String path = System.getenv().getOrDefault("DATASET_PATH", "/data/dataset.json");
try {
dataset = mapper.readValue(new File(path), new TypeReference<>() {});
} catch (Exception ignored) {}
}

static void initPostgres() {
String url = System.getenv("POSTGRES_URL");
if (url == null || url.isEmpty()) return;
try {
URI uri = new URI(url);
String[] userInfo = uri.getUserInfo().split(":");
PgConnectOptions connectOptions = new PgConnectOptions()
.setHost(uri.getHost())
.setPort(uri.getPort())
.setDatabase(uri.getPath().substring(1))
.setUser(userInfo[0])
.setPassword(userInfo[1]);
PoolOptions poolOptions = new PoolOptions().setMaxSize(64);
pgPool = PgBuilder.pool()
.with(poolOptions)
.connectingTo(connectOptions)
.build();
} catch (Exception ignored) {}
}

static Map<String, String> parseQuery(String query) {
Map<String, String> params = new HashMap<>();
if (query == null) return params;
for (String pair : query.split("&")) {
int eq = pair.indexOf('=');
if (eq > 0) params.put(pair.substring(0, eq), pair.substring(eq + 1));
}
return params;
}

static void sendText(HttpExchange ex, String text) throws IOException {
byte[] bytes = text.getBytes();
ex.getResponseHeaders().set("Content-Type", "text/plain");
ex.sendResponseHeaders(200, bytes.length);
ex.getResponseBody().write(bytes);
ex.close();
}

static void sendJson(HttpExchange ex, Object obj) throws IOException {
byte[] bytes = mapper.writeValueAsBytes(obj);
String accept = ex.getRequestHeaders().getFirst("Accept-Encoding");
if (accept != null && accept.contains("gzip")) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPOutputStream gz = new GZIPOutputStream(baos)) { gz.write(bytes); }
bytes = baos.toByteArray();
ex.getResponseHeaders().set("Content-Encoding", "gzip");
}
ex.getResponseHeaders().set("Content-Type", "application/json");
ex.sendResponseHeaders(200, bytes.length);
ex.getResponseBody().write(bytes);
ex.close();
}

// --- Handlers ---

static class BaselineHandler implements HttpHandler {
public void handle(HttpExchange ex) throws IOException {
Map<String, String> params = parseQuery(ex.getRequestURI().getQuery());
int a = Integer.parseInt(params.getOrDefault("a", "0"));
int b = Integer.parseInt(params.getOrDefault("b", "0"));
int sum = a + b;
if ("POST".equals(ex.getRequestMethod())) {
String body = new String(ex.getRequestBody().readAllBytes());
sum += Integer.parseInt(body.trim());
}
sendText(ex, String.valueOf(sum));
}
}

static class PipelineHandler implements HttpHandler {
public void handle(HttpExchange ex) throws IOException {
sendText(ex, "ok");
}
}

static class JsonHandler implements HttpHandler {
public void handle(HttpExchange ex) throws IOException {
String path = ex.getRequestURI().getPath();
int count = Integer.parseInt(path.substring(path.lastIndexOf('/') + 1));
Map<String, String> params = parseQuery(ex.getRequestURI().getQuery());
int m = Integer.parseInt(params.getOrDefault("m", "1"));
int n = Math.min(Math.max(count, 0), dataset.size());
List<Map<String, Object>> items = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
Item item = dataset.get(i);
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", item.id());
map.put("name", item.name());
map.put("category", item.category());
map.put("price", item.price());
map.put("quantity", item.quantity());
map.put("active", item.active());
map.put("tags", item.tags());
map.put("rating", item.rating());
map.put("total", (long) item.price() * item.quantity() * m);
items.add(map);
}
sendJson(ex, Map.of("items", items, "count", items.size()));
}
}

static class UploadHandler implements HttpHandler {
public void handle(HttpExchange ex) throws IOException {
long size = ex.getRequestBody().transferTo(OutputStream.nullOutputStream());
sendText(ex, String.valueOf(size));
}
}

static class StaticHandler implements HttpHandler {
public void handle(HttpExchange ex) throws IOException {
String path = ex.getRequestURI().getPath();
String file = path.substring("/static/".length());
Path filePath = Path.of("/data/static", file);
if (!Files.exists(filePath)) {
ex.sendResponseHeaders(404, -1);
ex.close();
return;
}
byte[] data = Files.readAllBytes(filePath);
String contentType = Files.probeContentType(filePath);
if (contentType == null) contentType = "application/octet-stream";
ex.getResponseHeaders().set("Content-Type", contentType);
ex.sendResponseHeaders(200, data.length);
ex.getResponseBody().write(data);
ex.close();
}
}

static class AsyncDbHandler implements HttpHandler {
public void handle(HttpExchange ex) throws IOException {
Map<String, String> params = parseQuery(ex.getRequestURI().getQuery());
int min = Integer.parseInt(params.getOrDefault("min", "10"));
int max = Integer.parseInt(params.getOrDefault("max", "50"));
int limit = Math.min(Math.max(Integer.parseInt(params.getOrDefault("limit", "50")), 1), 50);

RowSet<Row> rows;
try {
rows = pgPool.preparedQuery("SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN $1 AND $2 LIMIT $3")
.execute(Tuple.of(min, max, limit))
.toCompletionStage().toCompletableFuture().join();
} catch (Exception e) {
ex.sendResponseHeaders(500, -1);
ex.close();
return;
}

List<Item> items = new ArrayList<>();
for (Row row : rows) {
List<String> tags = mapper.readValue(row.getString("tags"), new TypeReference<>() {});
items.add(new Item(row.getInteger("id"), row.getString("name"), row.getString("category"),
row.getInteger("price"), row.getInteger("quantity"), row.getBoolean("active"),
tags, new Rating(row.getInteger("rating_score"), row.getInteger("rating_count"))));
}
sendJson(ex, Map.of("items", items, "count", items.size()));
}
}

static class FortunesHandler implements HttpHandler {
public void handle(HttpExchange ex) throws IOException {
RowSet<Row> rows;
try {
rows = pgPool.preparedQuery("SELECT id, message FROM fortune")
.execute()
.toCompletionStage().toCompletableFuture().join();
} catch (Exception e) {
ex.sendResponseHeaders(500, -1);
ex.close();
return;
}

List<Fortune> fortunes = new ArrayList<>();
for (Row row : rows) {
fortunes.add(new Fortune(row.getInteger("id"), row.getString("message")));
}
fortunes.add(new Fortune(0, "Additional fortune added at request time."));
fortunes.sort((a, b) -> a.message().compareTo(b.message()));

StringWriter writer = new StringWriter();
fortunesTemplate.evaluate(writer, Map.of("fortunes", fortunes));
byte[] bytes = writer.toString().getBytes();
ex.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
ex.sendResponseHeaders(200, bytes.length);
ex.getResponseBody().write(bytes);
ex.close();
}
}

record Fortune(int id, String message) {}
record Item(int id, String name, String category, int price, int quantity, boolean active, List<String> tags, Rating rating) {}
record Rating(int score, int count) {}
}
12 changes: 12 additions & 0 deletions frameworks/robaho-httpserver/src/main/resources/fortunes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head><title>Fortunes</title></head>
<body>
<table>
<tr><th>id</th><th>message</th></tr>
{% for f in fortunes %}
<tr><td>{{ f.id }}</td><td>{{ f.message }}</td></tr>
{% endfor %}
</table>
</body>
</html>
Loading