Skip to content

Commit 33ec556

Browse files
authored
fix: bound HTTPServer request resources (#2333)
## Summary - use a fixed-size default executor with a bounded queue and non-blocking rejection - close rejected authenticated request bodies instead of draining an unbounded body - close rejected exchanges after sending HTTP 403 This is the focused replacement for the #2284 portion of #2297. Fixes #2284 ## Ongoing discussion None currently. Earlier review feedback about preserving default concurrency and closing rejected exchanges is incorporated here. ## Validation - `mise run lint:fix` - `mise run build` - `./mvnw test -pl prometheus-metrics-exporter-httpserver -Dcoverage.skip=true -Dcheckstyle.skip=true` --------- Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent 6b2f478 commit 33ec556

3 files changed

Lines changed: 35 additions & 101 deletions

File tree

prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/BlockingRejectedExecutionHandler.java

Lines changed: 0 additions & 18 deletions
This file was deleted.

prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.java

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
import io.prometheus.metrics.model.registry.PrometheusRegistry;
1313
import java.io.Closeable;
1414
import java.io.IOException;
15-
import java.io.InputStream;
1615
import java.net.InetAddress;
1716
import java.net.InetSocketAddress;
1817
import java.security.PrivilegedActionException;
1918
import java.security.PrivilegedExceptionAction;
19+
import java.util.concurrent.ArrayBlockingQueue;
2020
import java.util.concurrent.ExecutionException;
2121
import java.util.concurrent.ExecutorService;
22-
import java.util.concurrent.SynchronousQueue;
2322
import java.util.concurrent.ThreadPoolExecutor;
2423
import java.util.concurrent.TimeUnit;
2524
import javax.annotation.Nullable;
@@ -39,6 +38,10 @@
3938
@StableApi
4039
public class HTTPServer implements Closeable {
4140

41+
private static final int DEFAULT_MIN_THREADS = 10;
42+
private static final int DEFAULT_MAX_THREADS = 10;
43+
private static final int DEFAULT_QUEUE_SIZE = 100;
44+
4245
static {
4346
if (!System.getProperties().containsKey("sun.net.httpserver.maxReqTime")) {
4447
System.setProperty("sun.net.httpserver.maxReqTime", "60");
@@ -154,22 +157,14 @@ public void handle(HttpExchange exchange) throws IOException {
154157
}
155158
}
156159
} else {
157-
drainInputAndClose(exchange);
160+
exchange.getRequestBody().close();
158161
exchange.sendResponseHeaders(403, -1);
162+
exchange.close();
159163
}
160164
}
161165
};
162166
}
163167

164-
private void drainInputAndClose(HttpExchange httpExchange) throws IOException {
165-
InputStream inputStream = httpExchange.getRequestBody();
166-
byte[] b = new byte[4096];
167-
while (inputStream.read(b) != -1) {
168-
// nop
169-
}
170-
inputStream.close();
171-
}
172-
173168
/** Stop the HTTP server. Same as {@link #close()}. */
174169
public void stop() {
175170
close();
@@ -354,13 +349,12 @@ private ExecutorService makeExecutorService() {
354349
return executorService;
355350
} else {
356351
return new ThreadPoolExecutor(
357-
1,
358-
10,
352+
DEFAULT_MIN_THREADS,
353+
DEFAULT_MAX_THREADS,
359354
120,
360355
TimeUnit.SECONDS,
361-
new SynchronousQueue<>(true),
362-
NamedDaemonThreadFactory.defaultThreadFactory(true),
363-
new BlockingRejectedExecutionHandler());
356+
new ArrayBlockingQueue<>(DEFAULT_QUEUE_SIZE),
357+
NamedDaemonThreadFactory.defaultThreadFactory(true));
364358
}
365359
}
366360

prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HTTPServerTest.java

Lines changed: 24 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import java.security.Principal;
2626
import java.util.List;
2727
import java.util.concurrent.Executors;
28-
import java.util.concurrent.atomic.AtomicReference;
28+
import java.util.concurrent.ThreadPoolExecutor;
2929
import javax.net.ssl.SSLContext;
3030
import javax.security.auth.Subject;
3131
import org.junit.jupiter.api.BeforeEach;
@@ -46,7 +46,7 @@ void setUp() {
4646
}
4747

4848
@Test
49-
public void testSubjectDoAs() throws Exception {
49+
void testSubjectDoAs() throws Exception {
5050
final String user = "joe";
5151
final Subject subject = new Subject();
5252
subject.getPrincipals().add(() -> user);
@@ -160,55 +160,35 @@ void metricsCustomRootPath() throws Exception {
160160
}
161161

162162
@Test
163-
void registryThrows() throws Exception {
164-
HTTPServer server = HTTPServer.builder().port(0).registry(throwingRegistry()).buildAndStart();
165-
run(
166-
server,
167-
"/metrics",
168-
500,
169-
"Configure an HTTP error reporter for details.",
170-
"IllegalStateException",
171-
"test");
172-
}
173-
174-
@Test
175-
void registryExceptionIsPassedToConfiguredReporter() throws Exception {
176-
AtomicReference<Throwable> reportedError = new AtomicReference<>();
177-
HTTPServer server =
178-
HTTPServer.builder()
179-
.port(0)
180-
.registry(throwingRegistry())
181-
.errorHandlingPolicy(
182-
HttpErrorHandlingPolicy.builder().errorReporter(reportedError::set).build())
183-
.buildAndStart();
184-
185-
run(
186-
server,
187-
"/metrics",
188-
500,
189-
"Configure an HTTP error reporter for details.",
190-
"IllegalStateException",
191-
"test");
192-
193-
assertThat(reportedError.get()).isInstanceOf(IllegalStateException.class).hasMessage("test");
163+
void defaultExecutorHasBoundedQueueAndNonBlockingRejection() throws Exception {
164+
HTTPServer server = HTTPServer.builder().port(0).buildAndStart();
165+
try {
166+
assertThat(server.executorService).isInstanceOf(ThreadPoolExecutor.class);
167+
ThreadPoolExecutor executor = (ThreadPoolExecutor) server.executorService;
168+
assertThat(executor.getCorePoolSize()).isEqualTo(10);
169+
assertThat(executor.getMaximumPoolSize()).isEqualTo(10);
170+
assertThat(executor.getQueue().remainingCapacity()).isEqualTo(100);
171+
assertThat(executor.getRejectedExecutionHandler())
172+
.isInstanceOf(ThreadPoolExecutor.AbortPolicy.class);
173+
} finally {
174+
server.stop();
175+
}
194176
}
195177

196178
@Test
197-
void registryExceptionCanUseUnsafeDebugResponse() throws Exception {
179+
void registryThrows() throws Exception {
198180
HTTPServer server =
199181
HTTPServer.builder()
200182
.port(0)
201-
.registry(throwingRegistry())
202-
.errorHandlingPolicy(
203-
HttpErrorHandlingPolicy.builder().unsafeDebugResponse(true).build())
183+
.registry(
184+
new PrometheusRegistry() {
185+
@Override
186+
public MetricSnapshots scrape(PrometheusScrapeRequest scrapeRequest) {
187+
throw new IllegalStateException("test");
188+
}
189+
})
204190
.buildAndStart();
205-
206-
run(
207-
server,
208-
"/metrics",
209-
500,
210-
"IllegalStateException: test",
211-
"Configure an HTTP error reporter for details.");
191+
run(server, "/metrics", 500, "An internal error occurred while scraping metrics");
212192
}
213193

214194
@Test
@@ -274,25 +254,6 @@ void healthDisabled() throws Exception {
274254
private static void run(
275255
HTTPServer server, String path, int expectedStatusCode, String expectedBody)
276256
throws Exception {
277-
run(server, path, expectedStatusCode, expectedBody, new String[0]);
278-
}
279-
280-
private static PrometheusRegistry throwingRegistry() {
281-
return new PrometheusRegistry() {
282-
@Override
283-
public MetricSnapshots scrape(PrometheusScrapeRequest scrapeRequest) {
284-
throw new IllegalStateException("test");
285-
}
286-
};
287-
}
288-
289-
private static void run(
290-
HTTPServer server,
291-
String path,
292-
int expectedStatusCode,
293-
String expectedBody,
294-
String... unexpectedBody)
295-
throws Exception {
296257
// we cannot use try-with-resources or even client.close(), or the test will fail with Java 17
297258
@SuppressWarnings("resource")
298259
final HttpClient client = HttpClient.newBuilder().build();
@@ -304,9 +265,6 @@ private static void run(
304265
client.send(request, HttpResponse.BodyHandlers.ofString());
305266
assertThat(response.statusCode()).isEqualTo(expectedStatusCode);
306267
assertThat(response.body()).contains(expectedBody);
307-
if (unexpectedBody.length > 0) {
308-
assertThat(response.body()).doesNotContain(unexpectedBody);
309-
}
310268
} finally {
311269
server.stop();
312270
}

0 commit comments

Comments
 (0)