Skip to content

Commit 251c938

Browse files
committed
Working
1 parent d69682c commit 251c938

File tree

14 files changed

+146
-67
lines changed

14 files changed

+146
-67
lines changed

load-tests/self/src/main/script/start.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ for f in lib/*.jar; do
3636
done
3737

3838
suspend=""
39-
if [[ $# -gte 1 && $1 == "--suspend" ]]; then
39+
if [[ $# -ge 1 && $1 == "--suspend" ]]; then
4040
suspend="-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000"
4141
shift
4242
fi

src/main/java/io/fusionauth/http/io/BodyException.java renamed to src/main/java/io/fusionauth/http/BodyException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022-2024, FusionAuth, All Rights Reserved
2+
* Copyright (c) 2022-2025, FusionAuth, All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -13,7 +13,7 @@
1313
* either express or implied. See the License for the specific
1414
* language governing permissions and limitations under the License.
1515
*/
16-
package io.fusionauth.http.io;
16+
package io.fusionauth.http;
1717

1818
/**
1919
* Exception that is thrown if any HTTP body fails to be read and/or processed.

src/main/java/io/fusionauth/http/io/ChunkException.java renamed to src/main/java/io/fusionauth/http/ChunkException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022-2024, FusionAuth, All Rights Reserved
2+
* Copyright (c) 2022-2025, FusionAuth, All Rights Reserved
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -13,7 +13,7 @@
1313
* either express or implied. See the License for the specific
1414
* language governing permissions and limitations under the License.
1515
*/
16-
package io.fusionauth.http.io;
16+
package io.fusionauth.http;
1717

1818
/**
1919
* Exception that is thrown if a Chunked request or response is invalid.

src/main/java/io/fusionauth/http/ParseException.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ public class ParseException extends RuntimeException {
2525

2626
private Integer index;
2727

28-
public ParseException() {
29-
this.state = null;
30-
}
31-
3228
public ParseException(String message) {
3329
super(message);
3430
this.state = null;

src/main/java/io/fusionauth/http/io/ChunkedInputStream.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.io.IOException;
1919
import java.io.InputStream;
2020

21+
import io.fusionauth.http.ChunkException;
2122
import io.fusionauth.http.ParseException;
2223
import io.fusionauth.http.util.HTTPTools;
2324
import static io.fusionauth.http.util.HTTPTools.makeParseException;
@@ -75,6 +76,13 @@ private int processChunk(byte[] destination, int offset, int length) throws IOEx
7576
int leftOver = bufferLength - bufferIndex;
7677
if (leftOver > 0) {
7778
// TODO : Daniel : Review : This doesn't seem like a good idea. It will fail silently, but this is required.
79+
// Discuss with Brian.
80+
// .
81+
// Options:
82+
// - Leave as is
83+
// - Throw a OverReadException that is caught by the HTTPInputStream which has a typed ref to PushbackInputStream and handled.
84+
// - Keep a typed ref for PushbackInputStream, but this messes up the 'commit' path in HTTPInputStream that swaps the pointer.
85+
// So we could re-work how we handle a chunked request body - instead of swapping out pointers we do something else?
7886
if (delegate instanceof PushbackInputStream pis) {
7987
pis.push(buffer, bufferIndex, leftOver);
8088
}
@@ -110,6 +118,7 @@ private int processChunk(byte[] destination, int offset, int length) throws IOEx
110118
int leftOver = bufferLength - bufferIndex;
111119
if (leftOver > 0) {
112120
// TODO : Daniel : Review : This doesn't seem like a good idea. It will fail silently, but this is required.
121+
// Discuss with Brian.
113122
if (delegate instanceof PushbackInputStream pis) {
114123
pis.push(buffer, bufferIndex, leftOver);
115124
}
@@ -155,6 +164,10 @@ private int processChunk(byte[] destination, int offset, int length) throws IOEx
155164

156165
// TODO : If we wanted to handle more than one chunk at a time, I think we would potentially continue here
157166
// and see if we can get more than one chunk completed before copying back to the destination.
167+
// Discuss with Brian.
168+
// Should we try this right now, or is this ok? Load testing with a small body,
169+
// Chunked is slower than fixed length, I suppose this makes sense. In practice I don't think browsers
170+
// and such use chunked for small requests.
158171

159172
System.arraycopy(buffer, bufferIndex, destination, offset, lengthToCopy);
160173
bufferIndex += lengthToCopy;

src/main/java/io/fusionauth/http/server/Configurable.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,18 @@ default T withMaxPendingSocketConnections(int maxPendingSocketConnections) {
183183
return (T) this;
184184
}
185185

186+
/**
187+
* Sets the base directory for this server. This is passed to the HTTPContext, which is available from this class. This defaults to the
188+
* current working directory of the process. Defaults to 100,000.
189+
*
190+
* @param maxRequestsPerConnection The maximum number of requests that can be handled by a single persistent connection.
191+
* @return This.
192+
*/
193+
default T withMaxRequestsPerConnection(int maxRequestsPerConnection) {
194+
configuration().withMaxRequestsPerConnection(maxRequestsPerConnection);
195+
return (T) this;
196+
}
197+
186198
/**
187199
* This configures the maximum size of a chunk in the response when the server is using chunked response encoding. Defaults to 16k.
188200
*

src/main/java/io/fusionauth/http/server/HTTPRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.util.TreeSet;
3939
import java.util.stream.Collectors;
4040

41+
import io.fusionauth.http.BodyException;
4142
import io.fusionauth.http.Buildable;
4243
import io.fusionauth.http.Cookie;
4344
import io.fusionauth.http.FileInfo;
@@ -47,7 +48,6 @@
4748
import io.fusionauth.http.HTTPValues.Headers;
4849
import io.fusionauth.http.HTTPValues.Protocols;
4950
import io.fusionauth.http.HTTPValues.TransferEncodings;
50-
import io.fusionauth.http.io.BodyException;
5151
import io.fusionauth.http.io.MultipartStream;
5252
import io.fusionauth.http.util.HTTPTools;
5353
import io.fusionauth.http.util.HTTPTools.HeaderValue;

src/main/java/io/fusionauth/http/server/HTTPResponse.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ public void reset() {
285285
* Reset the OutputStream.
286286
*/
287287
// TODO : Daniel : Review : This is used by prime-mvc. Is there any other way to handle this?
288+
// Discuss with Brian. I think all prime-mvc needs is for the socket to be reset?
288289
public void resetOutputStream() {
289290
if (outputStream.isCommitted()) {
290291
throw new IllegalStateException("The HTTPResponse can't be reset after it has been committed, meaning at least one byte was written back to the client.");

src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,11 @@ public class HTTPServerConfiguration implements Configurable<HTTPServerConfigura
5252

5353
private LoggerFactory loggerFactory = SystemOutLoggerFactory.FACTORY;
5454

55-
private int maxBytesToDrain = 128 * 1024; // 128k bytes
55+
private int maxBytesToDrain = 256 * 1024; // 256k bytes
5656

57-
private int maxPendingSocketConnections = 200;
57+
private int maxPendingSocketConnections = 250;
58+
59+
private int maxRequestsPerConnection = 100_000; // 100,000
5860

5961
private int maxResponseChunkSize = 16 * 1024; // 16k bytes
6062

@@ -68,9 +70,9 @@ public class HTTPServerConfiguration implements Configurable<HTTPServerConfigura
6870

6971
private Duration readThroughputCalculationDelayDuration = Duration.ofSeconds(5);
7072

71-
private int requestBufferSize = 16 * 1024;
73+
private int requestBufferSize = 16 * 1024; // 16k bytes
7274

73-
private int responseBufferSize = 64 * 1024;
75+
private int responseBufferSize = 64 * 1024; // 16k bytes
7476

7577
private Duration shutdownDuration = Duration.ofSeconds(10);
7678

@@ -159,7 +161,7 @@ public LoggerFactory getLoggerFactory() {
159161
/**
160162
* @return The maximum number of bytes to drain from the InputStream when the request handler did not read all available bytes and the
161163
* connection is using a keep-alive which means the server must drain the InputStream in preparation for the next request. Defaults to
162-
* 128k.
164+
* 256k.
163165
*/
164166
public int getMaxBytesToDrain() {
165167
return maxBytesToDrain;
@@ -172,12 +174,22 @@ public int getMaxBytesToDrain() {
172174
* accepted by the server socket, a client socket is created and handed to an HTTP Worker. This queue length only needs to be large enough
173175
* to buffer the incoming requests as fast as we can accept them and hand them to a worker.
174176
*
175-
* @return The maximum number of pending socket connections per HTTP listener. Defaults to 200.
177+
* @return The maximum number of pending socket connections per HTTP listener. Defaults to 250.
176178
*/
177179
public int getMaxPendingSocketConnections() {
178180
return maxPendingSocketConnections;
179181
}
180182

183+
/**
184+
* This limit only applies when using a persistent connection. If this number is reached without hitting a Keep-Alive timeout the
185+
* connection will be closed just as it would be if the Keep-Alive timeout was reached.
186+
*
187+
* @return The maximum number of requests that can be handled by a single persistent connection. Defaults to 100,000.
188+
*/
189+
public int getMaxRequestsPerConnection() {
190+
return maxRequestsPerConnection;
191+
}
192+
181193
/**
182194
* @return The max chunk size in the response. Defaults to 16k bytes.
183195
*/
@@ -390,10 +402,27 @@ public HTTPServerConfiguration withLoggerFactory(LoggerFactory loggerFactory) {
390402
*/
391403
@Override
392404
public HTTPServerConfiguration withMaxPendingSocketConnections(int maxPendingSocketConnections) {
405+
if (maxPendingSocketConnections < 25) {
406+
throw new IllegalArgumentException("The minimum pending socket connections must be greater than or equal to 25");
407+
}
408+
393409
this.maxPendingSocketConnections = maxPendingSocketConnections;
394410
return this;
395411
}
396412

413+
/**
414+
* {@inheritDoc}
415+
*/
416+
@Override
417+
public HTTPServerConfiguration withMaxRequestsPerConnection(int maxRequestsPerConnection) {
418+
if (maxRequestsPerConnection < 10) {
419+
throw new IllegalArgumentException("The maximum number of requests per connection must be greater than or equal to 10");
420+
}
421+
422+
this.maxRequestsPerConnection = maxRequestsPerConnection;
423+
return this;
424+
}
425+
397426
/**
398427
* {@inheritDoc}
399428
*/

src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.net.SocketTimeoutException;
2323

2424
import io.fusionauth.http.ConnectionClosedException;
25+
import io.fusionauth.http.server.HTTPRequest;
26+
import io.fusionauth.http.server.HTTPResponse;
2527
import io.fusionauth.http.HTTPValues;
2628
import io.fusionauth.http.HTTPValues.Connections;
2729
import io.fusionauth.http.HTTPValues.Headers;
@@ -32,8 +34,6 @@
3234
import io.fusionauth.http.log.Logger;
3335
import io.fusionauth.http.server.HTTPHandler;
3436
import io.fusionauth.http.server.HTTPListenerConfiguration;
35-
import io.fusionauth.http.server.HTTPRequest;
36-
import io.fusionauth.http.server.HTTPResponse;
3737
import io.fusionauth.http.server.HTTPServerConfiguration;
3838
import io.fusionauth.http.server.Instrumenter;
3939
import io.fusionauth.http.server.io.HTTPInputStream;
@@ -184,11 +184,14 @@ public void run() {
184184
logger.trace("[{}] Set state [{}]. Call the request handler.", Thread.currentThread().threadId(), state);
185185
configuration.getHandler().handle(request, response);
186186
logger.trace("[{}] Handler completed successfully", Thread.currentThread().threadId());
187-
response.close();
188187

189-
// TODO : Daniel : Test
190-
// Send a request body -> Handler ignores - does not read InputStream
191-
// Handler, writes a large response. This should flush the OutputStream implicitly even w/out calling HTTPOutputStream.close()
188+
// Do this before we write the response preamble. The normal Keep-Alive check below will handle closing the socket.
189+
if (handledRequests >= configuration.getMaxRequestsPerConnection()) {
190+
logger.trace("[{}] Maximum requests per connection has been reached. Turn off Keep-Alive.", Thread.currentThread().threadId());
191+
response.setHeader(Headers.Connection, Connections.Close);
192+
}
193+
194+
response.close();
192195

193196
boolean keepSocketAlive = keepSocketAlive(request, response);
194197
// Close the socket.

0 commit comments

Comments
 (0)