Skip to content

Commit

Permalink
Adding more instrumentation for thread counts and fixing byte count t…
Browse files Browse the repository at this point in the history
…hat was ignoring the preamble. Load testing to ensure the server can handle when tons of clients are junk and leave connections open.
  • Loading branch information
voidmain committed Sep 7, 2024
1 parent c8102ed commit 8cb06b6
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 32 deletions.
33 changes: 15 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

**NOTE:** This project is in progress.

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 non-blocking NIO in order to provide the highest performance possible.
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

## Installation

Expand All @@ -12,20 +14,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.3.4</version>
<version>0.4.0-RC.2</version>
</dependency>
```

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

```groovy
implementation 'io.fusionauth:java-http:0.3.4'
implementation 'io.fusionauth:java-http:0.4.0-RC.2'
```

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

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

## Examples Usages:
Expand All @@ -43,7 +45,8 @@ public class Example {
// Handler code goes here
};

HTTPServer server = new HTTPServer().withHandler(handler).withListener(new HTTPListenerConfiguration(4242));
HTTPServer server = new HTTPServer().withHandler(handler)
.withListener(new HTTPListenerConfiguration(4242));
server.start();
// Use server
server.close();
Expand All @@ -64,7 +67,8 @@ public class Example {
// Handler code goes here
};

try (HTTPServer server = new HTTPServer().withHandler(handler).withListener(new HTTPListenerConfiguration(4242))) {
try (HTTPServer server = new HTTPServer().withHandler(handler)
.withListener(new HTTPListenerConfiguration(4242))) {
server.start();
// When this block exits, the server will be shutdown
}
Expand All @@ -88,7 +92,6 @@ public class Example {
};

HTTPServer server = new HTTPServer().withHandler(handler)
.withNumberOfWorkerThreads(42)
.withShutdownDuration(Duration.ofSeconds(10L))
.withListener(new HTTPListenerConfiguration(4242));
server.start();
Expand Down Expand Up @@ -204,15 +207,9 @@ The general requirements and roadmap are as follows:

## FAQ

### Why no Loom?

Project Loom is an exciting development which brings a lot of great new features to Java, such as fibers, continuations and more.

Loom is currently available in Java 19 as a preview feature. Therefore, you can't use it without compiled code that is difficult to use in future Java releases.

This project is anchored to the Java LTS releases to ensure compatibility. Loom will be evaluated once it is out of preview, and available in an LTS version of Java.
### Why virtual threads and not NIO?

The next scheduled LTS release will be Java 21 set to release in September 2023. We are looking forward to that release and to see if we can leverage the Loom features in this project.
Let's face it, NIO is insanely complex to write and maintain. The first 3 versions of `java-http` used NIO with non-blocking selectors, and we encountered numerous bugs, performance issues, etc. If you compare the `0.3-maintenance` branch with `main` of this project, you'll quickly see that switching to virtual threads and standard blocking I/O made our code **MUCH** simpler.

## Helping out

Expand All @@ -225,8 +222,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.6.tar.gz
$ tar xvfz savant-2.0.0-RC.6.tar.gz
$ ln -s ./savant-2.0.0-RC.6 current
$ 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
$ export PATH=$PATH:~/savant/current/bin/
```
2 changes: 1 addition & 1 deletion build.savant
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jackson5Version = "3.0.1"
restifyVersion = "4.2.1"
testngVersion = "7.10.2"

project(group: "io.fusionauth", name: "java-http", version: "0.4.0-RC.1", licenses: ["ApacheV2_0"]) {
project(group: "io.fusionauth", name: "java-http", version: "0.4.0-RC.2", licenses: ["ApacheV2_0"]) {
workflow {
fetch {
// Dependency resolution order:
Expand Down
2 changes: 1 addition & 1 deletion load-tests/self/build.savant
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ project(group: "io.fusionauth", name: "self", version: "0.1.0", licenses: ["Apac

dependencies {
group(name: "compile") {
dependency(id: "io.fusionauth:java-http:0.4.0-{integration}")
dependency(id: "io.fusionauth:java-http:0.4.0-RC.2.{integration}")
}
}

Expand Down
4 changes: 2 additions & 2 deletions load-tests/self/self.iml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$USER_HOME$/.savant/cache/io/fusionauth/java-http/0.4.0-{integration}/java-http-0.4.0-{integration}.jar!/" />
<root url="jar://$USER_HOME$/.savant/cache/io/fusionauth/java-http/0.4.0-RC.2.{integration}/java-http-0.4.0-RC.2.{integration}.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$USER_HOME$/.savant/cache/io/fusionauth/java-http/0.4.0-{integration}/java-http-0.4.0-{integration}-src.jar!/" />
<root url="jar://$USER_HOME$/.savant/cache/io/fusionauth/java-http/0.4.0-RC.2.{integration}/java-http-0.4.0-RC.2.{integration}-src.jar!/" />
</SOURCES>
</library>
</orderEntry>
Expand Down
12 changes: 5 additions & 7 deletions load-tests/self/src/main/java/io/fusionauth/http/load/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,19 @@
*/
package io.fusionauth.http.load;

import java.time.Duration;

import io.fusionauth.http.log.Level;
import io.fusionauth.http.log.SystemOutLoggerFactory;
import io.fusionauth.http.server.CountingInstrumenter;
import io.fusionauth.http.server.HTTPListenerConfiguration;
import io.fusionauth.http.server.HTTPServer;
import io.fusionauth.http.server.ThreadSafeCountingInstrumenter;

public class Main {
public static void main(String[] args) throws Exception {
SystemOutLoggerFactory.FACTORY.getLogger(Object.class).setLevel(Level.Debug);

System.out.println("Starting java-http server");
CountingInstrumenter instrumenter = new CountingInstrumenter();
var instrumenter = new ThreadSafeCountingInstrumenter();
try (HTTPServer ignore = new HTTPServer().withHandler(new LoadHandler())
.withClientTimeout(Duration.ofSeconds(100L))
.withCompressByDefault(false)
.withInstrumenter(instrumenter)
.withListener(new HTTPListenerConfiguration(8080))
Expand All @@ -39,9 +36,10 @@ public static void main(String[] args) throws Exception {

for (int i = 0; i < 1_000; i++) {
Thread.sleep(10_000);
System.out.printf("Current stats. Bad requests [%s]. Bytes read [%s]. Bytes written [%s]. Chunked requests [%s]. Chunked responses [%s]. Closed connections [%s]. Connections [%s]. Started [%s].\n",
System.out.printf("Current stats. Bad requests [%s]. Bytes read [%s]. Bytes written [%s]. Chunked requests [%s]. Chunked responses [%s]. Closed connections [%s]. Connections [%s]. Started [%s]. Virtual threads [%s].\n",
instrumenter.getBadRequests(), instrumenter.getBytesRead(), instrumenter.getBytesWritten(), instrumenter.getChunkedRequests(),
instrumenter.getChunkedResponses(), instrumenter.getClosedConnections(), instrumenter.getConnections(), instrumenter.getStartedCount());
instrumenter.getChunkedResponses(), instrumenter.getClosedConnections(), instrumenter.getConnections(), instrumenter.getStartedCount(),
instrumenter.getThreadCount());
}

System.out.println("Shutting down java-http server");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2024, FusionAuth, All Rights Reserved
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific
* language governing permissions and limitations under the License.
*/
package io.fusionauth.http.load;

import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

public class SocketHammer {
public static void main(String[] args) throws Exception {
List<Socket> sockets = new ArrayList<>();

for (int i = 0; i < 10_000; i++) {
try {
Socket socket = new Socket("localhost", 8080);
sockets.add(socket);
System.out.println(i);

OutputStream outputStream = socket.getOutputStream();
outputStream.write("GET".getBytes());
outputStream.flush();
} catch (Exception e) {
//Smother
// System.out.println("Failed");
}
}

Thread.sleep(10_000);
for (Socket socket : sockets) {
socket.close();
}

System.out.println("Done");
}
}
39 changes: 39 additions & 0 deletions load-tests/self/src/main/script/hammer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash

#
# Copyright (c) 2022, FusionAuth, All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
#

# Grab the path
if [[ ! -d lib ]]; then
echo "Unable to locate library files needed to run the load tests. [lib]"
exit 1
fi

CLASSPATH=.
for f in lib/*.jar; do
CLASSPATH=${CLASSPATH}:${f}
done

suspend=""
if [[ $# -gt 1 && $1 == "--suspend" ]]; then
suspend="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000"
shift
fi

for i in {1..5} ; do
echo "${i}"
~/dev/java/current21/bin/java ${suspend} -cp "${CLASSPATH}" io.fusionauth.http.load.SocketHammer &
done
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
package io.fusionauth.http.server;

/**
* A simple counting instrumenter for the HTTPServer.
* A simple counting instrumenter for the HTTPServer. This is not thread safe, so if you need accurate data, you'll want to use the
* {@link ThreadSafeCountingInstrumenter}.
*
* @author Brian Pontarelli
*/
@SuppressWarnings("unused")
public class CountingInstrumenter implements Instrumenter {
private long badRequests;

Expand All @@ -37,6 +39,8 @@ public class CountingInstrumenter implements Instrumenter {

private long startedCount;

private long threadCount;

@Override
public void acceptedConnection() {
connections++;
Expand Down Expand Up @@ -94,6 +98,10 @@ public long getStartedCount() {
return startedCount;
}

public long getThreadCount() {
return threadCount;
}

@Override
public void readFromClient(long bytes) {
bytesRead += bytes;
Expand All @@ -104,6 +112,16 @@ public void serverStarted() {
startedCount++;
}

@Override
public void threadExited() {
threadCount--;
}

@Override
public void threadStarted() {
threadCount++;
}

@Override
public void wroteToClient(long bytes) {
bytesWritten += bytes;
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/io/fusionauth/http/server/Instrumenter.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ public interface Instrumenter {
*/
void serverStarted();

/**
* Signals that a virtual thread has exited.
*/
void threadExited();

/**
* Signals that a virtual thread has started.
*/
void threadStarted();

/**
* Called when bytes are written to a client.
*
Expand Down
Loading

0 comments on commit 8cb06b6

Please sign in to comment.