Skip to content

degroff/virtual threads update #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 50 commits into from
Jun 7, 2025
Merged
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
43e9767
update some logging, add comments
robotdan May 13, 2025
55a2aba
working
robotdan May 13, 2025
5a2c059
working
robotdan May 13, 2025
981f84e
working
robotdan May 13, 2025
22dc35c
working
robotdan May 14, 2025
f128fbc
Add logging, and tests
robotdan May 15, 2025
dfdc104
Tests
robotdan May 15, 2025
8c8c3ec
Tests
robotdan May 15, 2025
6836604
Tests
robotdan May 15, 2025
c709bbb
Tests
robotdan May 15, 2025
309e8c3
Working
robotdan May 15, 2025
32746a9
Tests
robotdan May 16, 2025
8356101
Tests
robotdan May 16, 2025
af92fe8
Tests
robotdan May 16, 2025
8ae5cf1
copyright
robotdan May 16, 2025
2f7c476
Working
robotdan May 24, 2025
2376537
Working
robotdan May 24, 2025
b23099d
Working
robotdan May 28, 2025
d425944
Working
robotdan May 28, 2025
78e4c2e
Working
robotdan May 28, 2025
daaa59c
Working
robotdan May 28, 2025
2e0aa12
Working
robotdan May 28, 2025
409b40f
Working
robotdan May 28, 2025
55f14b3
Working
robotdan May 28, 2025
3d322ac
Working
robotdan May 28, 2025
9dc897f
Working
robotdan May 28, 2025
f7148a0
Working
robotdan May 30, 2025
a468f6b
Working
robotdan May 30, 2025
713ccb9
Working
robotdan May 30, 2025
91db717
Tests
robotdan May 30, 2025
e659c0e
Working
robotdan May 30, 2025
a0691f0
Working
robotdan May 30, 2025
797ed9c
Get Tomcat setup
robotdan May 30, 2025
598729b
README
robotdan May 30, 2025
31a8e6f
review edits
robotdan May 30, 2025
993eb99
build updates
robotdan May 30, 2025
cd8c7b7
Working
robotdan Jun 3, 2025
3c45a5c
Working
robotdan Jun 3, 2025
130b48c
Working
robotdan Jun 3, 2025
8a22c0c
Working
robotdan Jun 3, 2025
d69682c
Working
robotdan Jun 4, 2025
251c938
Working
robotdan Jun 4, 2025
d2950be
Working
robotdan Jun 4, 2025
5d312dd
Working
robotdan Jun 4, 2025
a56fce4
Working
robotdan Jun 4, 2025
2ec79ed
Working
robotdan Jun 4, 2025
4f0adc2
Working
robotdan Jun 5, 2025
6a73fa4
Add option to keep additional attributes on cookies
robotdan Jun 5, 2025
cff1c68
Working
robotdan Jun 6, 2025
8519136
Working
robotdan Jun 7, 2025
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
72 changes: 58 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
## FusionAuth HTTP client and server ![semver 2.0.0 compliant](http://img.shields.io/badge/semver-2.0.0-brightgreen.svg?style=flat-square) [![test](https://github.com/FusionAuth/java-http/actions/workflows/test.yml/badge.svg)](https://github.com/FusionAuth/java-http/actions/workflows/test.yml)

**NOTE:** This project is in progress.
**NOTE:** This project is in progress. Version `0.3.0` is production ready, version `0.4.0` which will likely become `1.0.0` is still in development.

The goal of this project is to build a full-featured HTTP server and client in plain Java without the use of any libraries. The client and server will use Project Loom virtual threads and blocking I/O so that the Java VM will handle all the context switching between virtual threads as they block on I/O.

For more information about Project Loom and virtual threads, here is a good article to read: https://blogs.oracle.com/javamagazine/post/java-virtual-threads

## Project Goals

- Very fast
- Easy to make a simple web server like you can in Node.js
- No dependencies
- To not boil the ocean. This is a purpose built HTTP server that probably won't do everything.

## Installation

To add this library to your project, you can include this dependency in your Maven POM:
@@ -14,20 +21,20 @@ To add this library to your project, you can include this dependency in your Mav
<dependency>
<groupId>io.fusionauth</groupId>
<artifactId>java-http</artifactId>
<version>0.4.0-RC.4</version>
<version>0.4.0-RC.7</version>
</dependency>
```

If you are using Gradle, you can add this to your build file:

```groovy
implementation 'io.fusionauth:java-http:0.4.0-RC.4'
implementation 'io.fusionauth:java-http:0.4.0-RC.7'
```

If you are using Savant, you can add this to your build file:

```groovy
dependency(id: "io.fusionauth:java-http:0.4.0-RC.4")
dependency(id: "io.fusionauth:java-http:0.4.0-RC.7")
```

## Examples Usages:
@@ -158,17 +165,54 @@ Then you can open `https://example.org` in a browser or call it using an HTTP cl

## Performance

A key component for this project is to have awesome performance. Here are some basic metrics using the FusionAuth load test suite against a simple application using the Prime Framework MVC. The controller does nothing except return a simple 200. Here are some simple comparisons between `Tomcat`, `Netty`, and `java-http`.
A key purpose for this project is obtain screaming performance. Here are some basic metrics using the FusionAuth load test suite against a boilerplate request handler. The request handler simply returns a `200`. Here are some simple comparisons between `apache-tomcat`, `Netty`, and `java-http`.

The load test configuration is set to `100` clients with `100,000` requests each per worker. This means the entire test will execute `10,000,000` requests. The HTTP client is [Restify](https://github.com/inversoft/restify) which is a FusionAuth library that uses `HttpURLConnection` under the hoods. This REST client is used because it is considerably faster than the native Java REST client. In a real life example, depending upon your application, this performance may not matter. For the purposes of a load test, we have attempted to remove as many limitations to pushing the server as hard as we can.

The load test configuration is set to 10 clients with 500,000 requests each. The client is Restify which is a FusionAuth library that uses `HttpURLConnection` under the hoods. All the servers were HTTP so that TLS would not introduce any additional latency.
All the servers were HTTP so that TLS would not introduce any additional latency.

Here are the current test results:

| Server | RPS | Failures per second |
|-------------|--------|---------------------|
| `java-http` | 63,216 | 0 |
| `Tomcat` | 51,351 | 0.103 |
| `Netty` | 540 | 1.818 |
| Server | Avg requests per second | Failures per second | Avg latency in ms | Normalized Performance (%) |
|----------------|---------------------------|-----------------------|-------------------------|----------------------------|
| java-http | 101,317 | 0 | 0.350 | 100% |
| Apache Tomcat | 83,463 | 0 | 0.702 | 82.3% |
| Netty | ? | ? | ? | |
| OkHttp | ? | ? | ? | |
| JDK HttpServer | ? | ? | ? | |

Note the JDK HTTP Server is `com.sun.net.httpserver.HttpServer`. I don't know that anyone would use this in production, the JDK has not yet made a version of this using a public API. It is included here for reference only.

Load test last performed May 30, 2025. Using the [fusionauth-load-test](https://github.com/fusionauth/fusionauth-load-tests) library.

### Running load tests

Start the HTTP server to test.

#### java-http

Start the HTTP server. Run the following commands from the `java-http` repo.

```bash
cd load-tests/self
sb clean start
```

#### Apache Tomcat

Start the HTTP server. Run the following commands from the `java-http` repo.

```bash
cd load-tests/tomcat
sb clean start
```

Once you have the server started you wish to test, start the load test. Run the following commands from the `fusionauth-load-tests` repo.

```bash
sb clean int
./load-test.sh HTTP.json
```

Netty and Tomcat both seem to suffer from buffering and connection issues at very high scale. Regardless of the configuration, both servers always begins to fail with connection timeout problems at scale. `java-http` does not have these issues because of the way it handles connections via the selector. Connections don't back up and client connection pools can always be re-used with Keep-Alive.

@@ -222,8 +266,8 @@ We are looking for Java developers that are interested in helping us build the c
```bash
$ mkdir ~/savant
$ cd ~/savant
$ wget http://savant.inversoft.org/org/savantbuild/savant-core/2.0.0-RC.6/savant-2.0.0-RC.7.tar.gz
$ tar xvfz savant-2.0.0-RC.7.tar.gz
$ ln -s ./savant-2.0.0-RC.7 current
$ wget http://savant.inversoft.org/org/savantbuild/savant-core/2.0.0/savant-2.0.0.tar.gz
$ tar xvfz savant-2.0.0.tar.gz
$ ln -s ./savant-2.0.0 current
$ export PATH=$PATH:~/savant/current/bin/
```
34 changes: 23 additions & 11 deletions build.savant
Original file line number Diff line number Diff line change
@@ -15,9 +15,10 @@
*/
jackson5Version = "3.0.1"
restifyVersion = "4.2.1"
testngVersion = "7.10.2"
slf4jVersion = "2.0.17"
testngVersion = "7.11.0"

project(group: "io.fusionauth", name: "java-http", version: "0.4.0-RC.4", licenses: ["ApacheV2_0"]) {
project(group: "io.fusionauth", name: "java-http", version: "0.4.0-RC.7", licenses: ["ApacheV2_0"]) {
workflow {
fetch {
// Dependency resolution order:
@@ -41,10 +42,18 @@ project(group: "io.fusionauth", name: "java-http", version: "0.4.0-RC.4", licens
}

dependencies {
group(name: "compile") {
// Ha! Just kidding. This is pure Java - no deps!
}
group(name: "test-compile", export: false) {
dependency(id: "com.inversoft:jackson5:${jackson5Version}")
dependency(id: "com.inversoft:restify:${restifyVersion}")
dependency(id: "org.testng:testng:${testngVersion}")
// Gets rid of SLF warnings on test run
// SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
// SLF4J: Defaulting to no-operation (NOP) logger implementation
// SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
dependency(id: "org.slf4j:slf4j-nop:${slf4jVersion}")
}
}

@@ -54,25 +63,28 @@ project(group: "io.fusionauth", name: "java-http", version: "0.4.0-RC.4", licens
}

// Plugins
dependency = loadPlugin(id: "org.savantbuild.plugin:dependency:2.0.0-RC.7")
java = loadPlugin(id: "org.savantbuild.plugin:java:2.0.0-RC.6")
javaTestNG = loadPlugin(id: "org.savantbuild.plugin:java-testng:2.0.0-RC.6")
idea = loadPlugin(id: "org.savantbuild.plugin:idea:2.0.0-RC.7")
release = loadPlugin(id: "org.savantbuild.plugin:release-git:2.0.0-RC.6")
pom = loadPlugin(id: "org.savantbuild.plugin:pom:2.0.0-RC.6")
dependency = loadPlugin(id: "org.savantbuild.plugin:dependency:2.0.0")
java = loadPlugin(id: "org.savantbuild.plugin:java:2.0.0")
javaTestNG = loadPlugin(id: "org.savantbuild.plugin:java-testng:2.1.0")
idea = loadPlugin(id: "org.savantbuild.plugin:idea:2.0.0")
release = loadPlugin(id: "org.savantbuild.plugin:release-git:2.0.0")
pom = loadPlugin(id: "org.savantbuild.plugin:pom:2.0.0")

java.settings.javaVersion = "21"
java.settings.compilerArguments = "--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.util=ALL-UNNAMED -XDignore.symbol.file"
javaTestNG.settings.javaVersion = "21"
javaTestNG.settings.jvmArguments = "--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.util=ALL-UNNAMED"
javaTestNG.settings.jvmArguments = "--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.util=ALL-UNNAMED "
javaTestNG.settings.testngArguments = "-listener io.fusionauth.http.BaseTest\$TestListener"

target(name: "clean", description: "Cleans the build directory") {
java.clean()
}

target(name: "compile", description: "Compiles the Java source files") {
java.compile()
// We want to file compile on prod code if we need any -add-exports, so separate the prod and test compiles
java.settings.compilerArguments = ""
java.compileMain()
java.settings.compilerArguments = "--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.util=ALL-UNNAMED -XDignore.symbol.file"
java.compileTest()
}

target(name: "jar", description: "Builds the project JARs", dependsOn: ["compile"]) {
27 changes: 19 additions & 8 deletions java-http.iml
Original file line number Diff line number Diff line change
@@ -72,44 +72,55 @@
<orderEntry type="module-library" scope="TEST">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.10.2/testng-7.10.2.jar!/" />
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.11.0/testng-7.11.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.10.2/testng-7.10.2-src.jar!/" />
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.11.0/testng-7.11.0-src.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="module-library" scope="TEST">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/.savant/cache/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar!/" />
<root url="jar://$MODULE_DIR$/.savant/cache/org/jcommander/jcommander/1.83/jcommander-1.83.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MODULE_DIR$/.savant/cache/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36-src.jar!/" />
<root url="jar://$MODULE_DIR$/.savant/cache/org/jcommander/jcommander/1.83.0/jcommander-1.83.0-src.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="module-library" scope="TEST">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/.savant/cache/com/beust/jcommander/1.82.0/jcommander-1.82.0.jar!/" />
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.7.1/jquery-3.7.1.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MODULE_DIR$/.savant/cache/com/beust/jcommander/1.82.0/jcommander-1.82.0-src.jar!/" />
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.7.1/jquery-3.7.1-src.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="module-library" scope="TEST">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.7.1/jquery-3.7.1.jar!/" />
<root url="jar://$MODULE_DIR$/.savant/cache/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.7.1/jquery-3.7.1-src.jar!/" />
<root url="jar://$MODULE_DIR$/.savant/cache/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17-src.jar!/" />
</SOURCES>
</library>
</orderEntry>
<orderEntry type="module-library" scope="TEST">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/.savant/cache/org/slf4j/slf4j-nop/2.0.17/slf4j-nop-2.0.17.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$MODULE_DIR$/.savant/cache/org/slf4j/slf4j-nop/2.0.17/slf4j-nop-2.0.17-src.jar!/" />
</SOURCES>
</library>
</orderEntry>
41 changes: 41 additions & 0 deletions java-http.ipr
Original file line number Diff line number Diff line change
@@ -358,12 +358,14 @@
</HTMLCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" />
<option name="BLANK_LINES_AROUND_FIELD_WITH_ANNOTATIONS" value="1" />
<option name="CLASS_NAMES_IN_JAVADOC" value="3" />
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="9999" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="9999" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="javax" withSubpackages="true" static="false" />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
@@ -687,6 +689,7 @@
<codeStyleSettings language="JAVA">
<option name="BLANK_LINES_AROUND_FIELD" value="1" />
<option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" />
<option name="METHOD_PARAMETERS_WRAP" value="1" />
<option name="THROWS_KEYWORD_WRAP" value="1" />
<option name="WRAP_COMMENTS" value="true" />
@@ -1406,7 +1409,45 @@
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="Java 21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="ProjectRunConfigurationManager">
<configuration default="true" type="TestNG">
<shortenClasspath name="NONE" />
<useClassPathOnly />
<option name="SUITE_NAME" value="" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="GROUP_NAME" value="" />
<option name="TEST_OBJECT" value="CLASS" />
<option name="VM_PARAMETERS" value="-ea --add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.util=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED" />
<option name="PARAMETERS" value="" />
<option name="OUTPUT_DIRECTORY" value="" />
<option name="TEST_SEARCH_SCOPE">
<value defaultName="moduleWithDependencies" />
</option>
<option name="PROPERTIES_FILE" value="" />
<properties />
<listeners>
<listener class="io.fusionauth.http.BaseTest$TestListener" />
</listeners>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
<component name="libraryTable">
<library name=".kts definition dependencies">
<CLASSES>
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-reflect.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-script-runtime.jar!/" />
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$KOTLIN_BUNDLED$/lib/kotlin-stdlib-sources.jar!/" />
</SOURCES>
</library>
</component>
</project>
Loading