diff --git a/servicetalk-http-netty/gradle/spotbugs/test-exclusions.xml b/servicetalk-http-netty/gradle/spotbugs/test-exclusions.xml
index e3a8d42e02..cf24eaa958 100644
--- a/servicetalk-http-netty/gradle/spotbugs/test-exclusions.xml
+++ b/servicetalk-http-netty/gradle/spotbugs/test-exclusions.xml
@@ -79,4 +79,8 @@
+
+
+
+
diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpMessageDiscardWatchdogClientFilter.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpMessageDiscardWatchdogClientFilter.java
index 9d631d826e..059c15c777 100644
--- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpMessageDiscardWatchdogClientFilter.java
+++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpMessageDiscardWatchdogClientFilter.java
@@ -35,67 +35,39 @@
import java.util.concurrent.atomic.AtomicReference;
-import static io.servicetalk.http.netty.HttpMessageDiscardWatchdogServiceFilter.generifyAtomicReference;
-
/**
* Filter which tracks message bodies and warns if they are not discarded properly.
*/
-final class HttpMessageDiscardWatchdogClientFilter implements StreamingHttpConnectionFilterFactory {
+final class HttpMessageDiscardWatchdogClientFilter {
private static final ContextMap.Key>> MESSAGE_PUBLISHER_KEY = ContextMap.Key
.newKey(HttpMessageDiscardWatchdogClientFilter.class.getName() + ".messagePublisher",
- generifyAtomicReference());
+ WatchdogLeakDetector.generifyAtomicReference());
private static final Logger LOGGER = LoggerFactory.getLogger(HttpMessageDiscardWatchdogClientFilter.class);
/**
* Instance of {@link HttpMessageDiscardWatchdogClientFilter}.
*/
- static final HttpMessageDiscardWatchdogClientFilter INSTANCE = new HttpMessageDiscardWatchdogClientFilter();
+ static final StreamingHttpConnectionFilterFactory INSTANCE;
/**
* Instance of {@link StreamingHttpClientFilterFactory} with the cleaner implementation.
*/
- static final StreamingHttpClientFilterFactory CLIENT_CLEANER = new CleanerStreamingHttpClientFilterFactory();
-
- private HttpMessageDiscardWatchdogClientFilter() {
- // Singleton
- }
+ static final StreamingHttpClientFilterFactory CLIENT_CLEANER;
- @Override
- public StreamingHttpConnectionFilter create(final FilterableStreamingHttpConnection connection) {
- return new StreamingHttpConnectionFilter(connection) {
- @Override
- public Single request(final StreamingHttpRequest request) {
- return delegate().request(request).map(response -> {
- // always write the buffer publisher into the request context. When a downstream subscriber
- // arrives, mark the message as subscribed explicitly (having a message present and no
- // subscription is an indicator that it must be freed later on).
- final AtomicReference> reference = request.context()
- .computeIfAbsent(MESSAGE_PUBLISHER_KEY, key -> new AtomicReference<>());
- assert reference != null;
- if (reference.getAndSet(response.messageBody()) != null) {
- // If a previous message exists, the Single got resubscribed to
- // (i.e. during a retry) and so previous message body needs to be cleaned up by the
- // user.
- LOGGER.warn("Discovered un-drained HTTP response message body which has " +
- "been dropped by user code - this is a strong indication of a bug " +
- "in a user-defined filter. Response payload (message) body must " +
- "be fully consumed before retrying. connectionInfo={}", connectionContext());
- }
-
- return response.transformMessageBody(msgPublisher -> msgPublisher.beforeSubscriber(() -> {
- reference.set(null);
- return HttpMessageDiscardWatchdogServiceFilter.NoopSubscriber.INSTANCE;
- }));
- });
- }
- };
+ static {
+ if (WatchdogLeakDetector.strictDetection()) {
+ INSTANCE = new GcHttpMessageDiscardWatchdogClientFilter();
+ CLIENT_CLEANER = new NoopCleaner();
+ } else {
+ INSTANCE = new ContextHttpMessageDiscardWatchdogClientFilter();
+ CLIENT_CLEANER = new CleanerStreamingHttpClientFilterFactory();
+ }
}
- @Override
- public HttpExecutionStrategy requiredOffloads() {
- return HttpExecutionStrategies.offloadNone();
+ private HttpMessageDiscardWatchdogClientFilter() {
+ // No instances
}
private static final class CleanerStreamingHttpClientFilterFactory implements StreamingHttpClientFilterFactory {
@@ -128,4 +100,92 @@ public HttpExecutionStrategy requiredOffloads() {
return HttpExecutionStrategies.offloadNone();
}
}
+
+ private static final class ContextHttpMessageDiscardWatchdogClientFilter
+ implements StreamingHttpConnectionFilterFactory {
+
+ @Override
+ public StreamingHttpConnectionFilter create(final FilterableStreamingHttpConnection connection) {
+ return new StreamingHttpConnectionFilter(connection) {
+ @Override
+ public Single request(final StreamingHttpRequest request) {
+ return delegate().request(request).map(response -> {
+ // always write the buffer publisher into the request context. When a downstream subscriber
+ // arrives, mark the message as subscribed explicitly (having a message present and no
+ // subscription is an indicator that it must be freed later on).
+ final AtomicReference> reference = request.context()
+ .computeIfAbsent(MESSAGE_PUBLISHER_KEY, key -> new AtomicReference<>());
+ assert reference != null;
+ if (reference.getAndSet(response.messageBody()) != null) {
+ // If a previous message exists, the Single got resubscribed to
+ // (i.e. during a retry) and so previous message body needs to be cleaned up by the
+ // user.
+ LOGGER.warn("Discovered un-drained HTTP response message body which has " +
+ "been dropped by user code - this is a strong indication of a bug " +
+ "in a user-defined filter. Response payload (message) body must " +
+ "be fully consumed before retrying. connectionInfo={}", connectionContext());
+ }
+
+ return response.transformMessageBody(msgPublisher -> msgPublisher.beforeSubscriber(() -> {
+ reference.set(null);
+ return HttpMessageDiscardWatchdogServiceFilter.NoopSubscriber.INSTANCE;
+ }));
+ });
+ }
+ };
+ }
+
+ @Override
+ public HttpExecutionStrategy requiredOffloads() {
+ return HttpExecutionStrategies.offloadNone();
+ }
+ }
+
+ private static final class GcHttpMessageDiscardWatchdogClientFilter
+ implements StreamingHttpConnectionFilterFactory {
+
+ @Override
+ public StreamingHttpConnectionFilter create(FilterableStreamingHttpConnection connection) {
+ return new StreamingHttpConnectionFilter(connection) {
+ @Override
+ public Single request(final StreamingHttpRequest request) {
+ return delegate().request(request.transformMessageBody(publisher ->
+ WatchdogLeakDetector.gcLeakDetection(publisher, this::onRequestLeak)))
+ .map(response -> response.transformMessageBody(publisher ->
+ WatchdogLeakDetector.gcLeakDetection(publisher, this::onResponseLeak)));
+ }
+
+ private void onRequestLeak() {
+ LOGGER.warn("Discovered un-drained HTTP request message body which has " +
+ "been dropped by user code - this is a strong indication of a bug " +
+ "in a user-defined filter. The request payload (message) body must " +
+ "be fully consumed. connectionInfo={}", connectionContext());
+ }
+
+ private void onResponseLeak() {
+ LOGGER.warn("Discovered un-drained HTTP response message body which has " +
+ "been dropped by user code - this is a strong indication of a bug " +
+ "in a user-defined filter. Response payload (message) body must " +
+ "be fully consumed before retrying. connectionInfo={}", connectionContext());
+ }
+ };
+ }
+
+ @Override
+ public HttpExecutionStrategy requiredOffloads() {
+ return HttpExecutionStrategies.offloadNone();
+ }
+ }
+
+ private static final class NoopCleaner implements StreamingHttpClientFilterFactory {
+ @Override
+ public StreamingHttpClientFilter create(FilterableStreamingHttpClient client) {
+ return new StreamingHttpClientFilter(client) { };
+ }
+
+ @Override
+ public HttpExecutionStrategy requiredOffloads() {
+ return HttpExecutionStrategies.offloadNone();
+ }
+ }
}
diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpMessageDiscardWatchdogServiceFilter.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpMessageDiscardWatchdogServiceFilter.java
index 875acf0291..c2f70567c2 100644
--- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpMessageDiscardWatchdogServiceFilter.java
+++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/HttpMessageDiscardWatchdogServiceFilter.java
@@ -39,76 +39,53 @@
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
+import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone;
+
/**
* Filter which tracks message bodies and warns if they are not discarded properly.
*/
-final class HttpMessageDiscardWatchdogServiceFilter implements StreamingHttpServiceFilterFactory {
+final class HttpMessageDiscardWatchdogServiceFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(HttpMessageDiscardWatchdogServiceFilter.class);
+ private static final String REQUEST_LEAK_MESSAGE =
+ "Discovered un-drained HTTP service request message body which has " +
+ "been dropped by user code - this is a strong indication of a bug " +
+ "in a user-defined filter. Requests (or their message body) must " +
+ "be fully consumed before retrying.";
+
+ private static final String RESPONSE_LEAK_MESSAGE =
+ "Discovered un-drained HTTP service response message body which has " +
+ "been dropped by user code - this is a strong indication of a bug " +
+ "in a user-defined filter. Responses (or their message body) must " +
+ "be fully consumed before retrying.";
+
+ private static final ContextMap.Key>> MESSAGE_PUBLISHER_KEY = ContextMap.Key
+ .newKey(HttpMessageDiscardWatchdogServiceFilter.class.getName() + ".messagePublisher",
+ WatchdogLeakDetector.generifyAtomicReference());
+
/**
* Instance of {@link HttpMessageDiscardWatchdogServiceFilter}.
*/
- static final StreamingHttpServiceFilterFactory INSTANCE = new HttpMessageDiscardWatchdogServiceFilter();
+ static final StreamingHttpServiceFilterFactory INSTANCE;
/**
* Instance of {@link HttpLifecycleObserverServiceFilter} with the cleaner implementation.
*/
- static final StreamingHttpServiceFilterFactory CLEANER =
- new HttpLifecycleObserverServiceFilter(new CleanerHttpLifecycleObserver());
-
- private static final ContextMap.Key>> MESSAGE_PUBLISHER_KEY = ContextMap.Key
- .newKey(HttpMessageDiscardWatchdogServiceFilter.class.getName() + ".messagePublisher",
- generifyAtomicReference());
-
- private HttpMessageDiscardWatchdogServiceFilter() {
- // Singleton
- }
-
- @Override
- public StreamingHttpServiceFilter create(final StreamingHttpService service) {
-
- return new StreamingHttpServiceFilter(service) {
- @Override
- public Single handle(final HttpServiceContext ctx,
- final StreamingHttpRequest request,
- final StreamingHttpResponseFactory responseFactory) {
- return delegate()
- .handle(ctx, request, responseFactory)
- .map(response -> {
- // always write the buffer publisher into the request context. When a downstream subscriber
- // arrives, mark the message as subscribed explicitly (having a message present and no
- // subscription is an indicator that it must be freed later on).
- final AtomicReference> reference = request.context()
- .computeIfAbsent(MESSAGE_PUBLISHER_KEY, key -> new AtomicReference<>());
- assert reference != null;
- if (reference.getAndSet(response.messageBody()) != null) {
- // If a previous message exists, the Single got resubscribed to
- // (i.e. during a retry) and so previous message body needs to be cleaned up by the
- // user.
- LOGGER.warn("Discovered un-drained HTTP response message body which has " +
- "been dropped by user code - this is a strong indication of a bug " +
- "in a user-defined filter. Responses (or their message body) must " +
- "be fully consumed before retrying.");
- }
-
- return response.transformMessageBody(msgPublisher -> msgPublisher.beforeSubscriber(() -> {
- reference.set(null);
- return NoopSubscriber.INSTANCE;
- }));
- });
- }
- };
- }
-
- @Override
- public HttpExecutionStrategy requiredOffloads() {
- return HttpExecutionStrategies.offloadNone();
+ static final StreamingHttpServiceFilterFactory CLEANER;
+
+ static {
+ if (WatchdogLeakDetector.strictDetection()) {
+ INSTANCE = new GcHttpMessageWatchdogServiceFilter();
+ CLEANER = new NoopFilterFactory();
+ } else {
+ INSTANCE = new ContextHttpMessageDiscardWatchdogServiceFilter();
+ CLEANER = new HttpLifecycleObserverServiceFilter(new CleanerHttpLifecycleObserver());
+ }
}
- @SuppressWarnings("unchecked")
- static Class generifyAtomicReference() {
- return (Class) AtomicReference.class;
+ private HttpMessageDiscardWatchdogServiceFilter() {
+ // no instances
}
static final class NoopSubscriber implements PublisherSource.Subscriber