Skip to content

Commit c230f27

Browse files
committed
Revert "Switch webserver implementation from single-thread nio to blocking IO using a virtual-thread per connection"
This reverts commit b832612.
1 parent 3183723 commit c230f27

14 files changed

+570
-435
lines changed

common/src/main/java/de/bluecolored/bluemap/common/web/BlueMapResponseModifier.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ public HttpResponse handle(HttpRequest request) {
4949
HttpResponse response = delegate.handle(request);
5050

5151
HttpStatusCode status = response.getStatusCode();
52-
if (status.getCode() >= 400 && response.getBody() != null){
53-
response.setBody(status.getCode() + " - " + status.getMessage() + "\n" + this.serverName);
52+
if (status.getCode() >= 400 && !response.hasData()){
53+
response.setData(status.getCode() + " - " + status.getMessage() + "\n" + this.serverName);
5454
}
5555

5656
response.addHeader("Server", this.serverName);

common/src/main/java/de/bluecolored/bluemap/common/web/FileRequestHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ private HttpResponse generateResponse(HttpRequest request) throws IOException {
8585
// redirect to have correct relative paths
8686
if (Files.isDirectory(filePath) && !request.getPath().endsWith("/")) {
8787
HttpResponse response = new HttpResponse(HttpStatusCode.SEE_OTHER);
88-
response.addHeader("Location", "/" + path + "/" + (request.getRawQueryString().isEmpty() ? "" : "?" + request.getRawQueryString()));
88+
response.addHeader("Location", "/" + path + "/" + (request.getGETParamString().isEmpty() ? "" : "?" + request.getGETParamString()));
8989
return response;
9090
}
9191

@@ -151,7 +151,7 @@ private HttpResponse generateResponse(HttpRequest request) throws IOException {
151151

152152
//send response
153153
try {
154-
response.setBody(Files.newInputStream(filePath));
154+
response.setData(Files.newInputStream(filePath));
155155
return response;
156156
} catch (FileNotFoundException e) {
157157
return new HttpResponse(HttpStatusCode.NOT_FOUND);

common/src/main/java/de/bluecolored/bluemap/common/web/JsonDataRequestHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public HttpResponse handle(HttpRequest request) {
4848
HttpResponse response = new HttpResponse(HttpStatusCode.OK);
4949
response.addHeader("Cache-Control", "no-cache");
5050
response.addHeader("Content-Type", "application/json");
51-
response.setBody(dataSupplier.get());
51+
response.setData(dataSupplier.get());
5252
return response;
5353
}
5454

common/src/main/java/de/bluecolored/bluemap/common/web/LoggingRequestHandler.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import lombok.NonNull;
3232
import lombok.Setter;
3333

34+
import java.net.URI;
35+
3436
@Getter @Setter
3537
@AllArgsConstructor
3638
public class LoggingRequestHandler implements HttpRequestHandler {
@@ -63,9 +65,7 @@ public HttpResponse handle(HttpRequest request) {
6365
}
6466

6567
String method = request.getMethod();
66-
String path = request.getPath();
67-
String queryString = request.getRawQueryString();
68-
String address = queryString == null ? path : path + "?" + queryString;
68+
URI address = request.getAddress();
6969
String version = request.getVersion();
7070

7171
// run request
@@ -81,7 +81,7 @@ public HttpResponse handle(HttpRequest request) {
8181
source,
8282
xffSource,
8383
method,
84-
address,
84+
address.toString(),
8585
version,
8686
statusCode,
8787
statusMessage

common/src/main/java/de/bluecolored/bluemap/common/web/MapStorageRequestHandler.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private void writeToResponse(CompressedInputStream data, HttpResponse response,
122122
request.hasHeaderValue("Accept-Encoding", compression.getId())
123123
) {
124124
response.addHeader("Content-Encoding", compression.getId());
125-
response.setBody(data);
125+
response.setData(data);
126126
} else if (
127127
compression != Compression.GZIP &&
128128
!response.hasHeaderValue("Content-Type", "image/png") &&
@@ -134,9 +134,9 @@ private void writeToResponse(CompressedInputStream data, HttpResponse response,
134134
data.decompress().transferTo(os);
135135
}
136136
byte[] compressedData = byteOut.toByteArray();
137-
response.setBody(new ByteArrayInputStream(compressedData));
137+
response.setData(new ByteArrayInputStream(compressedData));
138138
} else {
139-
response.setBody(data.decompress());
139+
response.setData(data.decompress());
140140
}
141141
}
142142

common/src/main/java/de/bluecolored/bluemap/common/web/http/HttpConnection.java

Lines changed: 113 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,57 +25,134 @@
2525
package de.bluecolored.bluemap.common.web.http;
2626

2727
import de.bluecolored.bluemap.core.logger.Logger;
28-
import lombok.RequiredArgsConstructor;
2928

30-
import java.io.BufferedInputStream;
31-
import java.io.BufferedOutputStream;
32-
import java.io.EOFException;
3329
import java.io.IOException;
34-
import java.net.Socket;
35-
import java.net.SocketTimeoutException;
30+
import java.net.InetAddress;
31+
import java.net.InetSocketAddress;
32+
import java.net.SocketAddress;
33+
import java.nio.channels.Channel;
34+
import java.nio.channels.SelectableChannel;
35+
import java.nio.channels.SelectionKey;
36+
import java.nio.channels.SocketChannel;
37+
import java.util.concurrent.CompletableFuture;
38+
import java.util.concurrent.Executor;
3639

37-
public class HttpConnection implements Runnable {
40+
public class HttpConnection implements SelectionConsumer {
3841

39-
private final Socket socket;
40-
private final HttpRequestInputStream requestIn;
41-
private final HttpResponseOutputStream responseOut;
4242
private final HttpRequestHandler requestHandler;
43+
private final Executor responseHandlerExecutor;
44+
private HttpRequest request;
45+
private CompletableFuture<HttpResponse> futureResponse;
46+
private HttpResponse response;
4347

44-
public HttpConnection(Socket socket, HttpRequestHandler requestHandler) throws IOException {
45-
this.socket = socket;
46-
this.requestHandler = requestHandler;
48+
public HttpConnection(HttpRequestHandler requestHandler) {
49+
this(requestHandler, Runnable::run); //run synchronously
50+
}
4751

48-
this.requestIn = new HttpRequestInputStream(new BufferedInputStream(socket.getInputStream()), socket.getInetAddress());
49-
this.responseOut = new HttpResponseOutputStream(new BufferedOutputStream(socket.getOutputStream()));
52+
public HttpConnection(HttpRequestHandler requestHandler, Executor responseHandlerExecutor) {
53+
this.requestHandler = requestHandler;
54+
this.responseHandlerExecutor = responseHandlerExecutor;
5055
}
5156

52-
public void run() {
57+
@Override
58+
public void accept(SelectionKey selectionKey) {
59+
if (!selectionKey.isValid()) return;
60+
61+
SelectableChannel selChannel = selectionKey.channel();
62+
63+
if (!(selChannel instanceof SocketChannel)) return;
64+
SocketChannel channel = (SocketChannel) selChannel;
65+
5366
try {
54-
while (socket.isConnected() && !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown()) {
55-
HttpRequest request = requestIn.read();
56-
if (request == null) continue;
5767

58-
try (HttpResponse response = requestHandler.handle(request)) {
59-
responseOut.write(response);
60-
}
68+
if (request == null) {
69+
SocketAddress remote = channel.getRemoteAddress();
70+
InetAddress remoteInet = null;
71+
if (remote instanceof InetSocketAddress)
72+
remoteInet = ((InetSocketAddress) remote).getAddress();
73+
74+
request = new HttpRequest(remoteInet);
6175
}
62-
} catch (EOFException | SocketTimeoutException ignore) {
63-
// ignore known exceptions that happen when browsers or us close the connection
64-
} catch (IOException e) {
65-
if ( // ignore known exceptions that happen when browsers close the connection
66-
e.getMessage() == null ||
67-
!e.getMessage().equals("Broken pipe")
68-
) {
69-
Logger.global.logDebug("Exception in HttpConnection: " + e);
76+
77+
// receive request
78+
if (!request.write(channel)) {
79+
if (!selectionKey.isValid()) return;
80+
selectionKey.interestOps(SelectionKey.OP_READ);
81+
return;
82+
}
83+
84+
// process request
85+
if (futureResponse == null) {
86+
futureResponse = CompletableFuture.supplyAsync(
87+
() -> requestHandler.handle(request),
88+
responseHandlerExecutor
89+
);
90+
futureResponse.handle((response, error) -> {
91+
if (error != null) {
92+
Logger.global.logError("Unexpected error handling request", error);
93+
response = new HttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR);
94+
}
95+
96+
try {
97+
response.read(channel); // do an initial read to trigger response sending intent
98+
this.response = response;
99+
} catch (IOException e) {
100+
handleIOException(channel, e);
101+
}
102+
103+
return null;
104+
});
105+
}
106+
107+
if (response == null) return;
108+
if (!selectionKey.isValid()) return;
109+
110+
// send response
111+
if (!response.read(channel)){
112+
selectionKey.interestOps(SelectionKey.OP_WRITE);
113+
return;
70114
}
71-
} catch (Exception e) {
72-
Logger.global.logDebug("Exception in HttpConnection: " + e);
73-
} finally {
115+
116+
// reset to accept new request
117+
request.clear();
118+
response.close();
119+
futureResponse = null;
120+
response = null;
121+
selectionKey.interestOps(SelectionKey.OP_READ);
122+
123+
} catch (IOException e) {
124+
handleIOException(channel, e);
125+
}
126+
}
127+
128+
private void handleIOException(Channel channel, IOException e) {
129+
request.clear();
130+
131+
if (response != null) {
74132
try {
75-
socket.close();
76-
} catch (IOException e) {
77-
Logger.global.logDebug("Exception closing HttpConnection: " + e);
133+
response.close();
134+
} catch (IOException e2) {
135+
Logger.global.logWarning("Failed to close response: " + e2);
78136
}
137+
response = null;
138+
}
139+
140+
if (futureResponse != null) {
141+
futureResponse.thenAccept(response -> {
142+
try {
143+
response.close();
144+
} catch (IOException e2) {
145+
Logger.global.logWarning("Failed to close response: " + e2);
146+
}
147+
});
148+
futureResponse = null;
149+
}
150+
151+
Logger.global.logDebug("Failed to process selection: " + e);
152+
try {
153+
channel.close();
154+
} catch (IOException e2) {
155+
Logger.global.logWarning("Failed to close channel" + e2);
79156
}
80157
}
81158

common/src/main/java/de/bluecolored/bluemap/common/web/http/HttpHeader.java

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -24,52 +24,40 @@
2424
*/
2525
package de.bluecolored.bluemap.common.web.http;
2626

27-
import lombok.Getter;
28-
2927
import java.util.*;
3028

3129
public class HttpHeader {
3230

33-
@Getter private final String key;
34-
@Getter private String value;
31+
private final String key;
32+
private final String value;
3533
private List<String> values;
3634
private Set<String> valuesLC;
3735

38-
public HttpHeader(String key, String... values) {
36+
public HttpHeader(String key, String value) {
3937
this.key = key;
40-
this.value = String.join(",", values);
38+
this.value = value;
4139
}
4240

43-
public synchronized void add(String... values) {
44-
if (value.isEmpty()) {
45-
set(values);
46-
return;
47-
}
48-
49-
this.value = value + "," + String.join(",", values);
50-
this.values = null;
51-
this.valuesLC = null;
41+
public String getKey() {
42+
return key;
5243
}
5344

54-
public synchronized void set(String... values) {
55-
this.value = String.join(",", values);
56-
this.values = null;
57-
this.valuesLC = null;
45+
public String getValue() {
46+
return value;
5847
}
5948

60-
public synchronized List<String> getValues() {
49+
public List<String> getValues() {
6150
if (values == null) {
62-
List<String> vs = new ArrayList<>();
51+
values = new ArrayList<>();
6352
for (String v : value.split(",")) {
64-
vs.add(v.trim());
53+
values.add(v.trim());
6554
}
66-
values = Collections.unmodifiableList(vs);
6755
}
6856

6957
return values;
7058
}
7159

72-
public synchronized boolean contains(String value) {
60+
public boolean contains(String value) {
7361
if (valuesLC == null) {
7462
valuesLC = new HashSet<>();
7563
for (String v : getValues()) {
@@ -80,21 +68,4 @@ public synchronized boolean contains(String value) {
8068
return valuesLC.contains(value);
8169
}
8270

83-
@Override
84-
public boolean equals(Object o) {
85-
if (o == null || getClass() != o.getClass()) return false;
86-
HttpHeader that = (HttpHeader) o;
87-
return Objects.equals(key, that.key) && Objects.equals(value, that.value);
88-
}
89-
90-
@Override
91-
public int hashCode() {
92-
return Objects.hash(key, value);
93-
}
94-
95-
@Override
96-
public String toString() {
97-
return key + ": " + value;
98-
}
99-
10071
}

0 commit comments

Comments
 (0)