This is an automated email from the ASF dual-hosted git repository.
cryptoe pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 4a4c8838da5 minor: Add QOS filtering for coordinator endpoints (#19271)
4a4c8838da5 is described below
commit 4a4c8838da5b07fdf4563d65db02f9464dc17e23
Author: Karan Kumar <[email protected]>
AuthorDate: Wed Jul 22 23:50:39 2026 +0530
minor: Add QOS filtering for coordinator endpoints (#19271)
---
docs/configuration/index.md | 2 +-
.../supervisor/SeekableStreamSupervisor.java | 12 +-
.../druid/server/initialization/ServerConfig.java | 12 ++
.../jetty/CliIndexerServerModule.java | 8 +-
.../server/initialization/jetty/JettyBindings.java | 41 +++++-
.../jetty/PathExcludingQoSFilter.java | 82 +++++++++++
.../jetty/PathExcludingQoSFilterTest.java | 154 +++++++++++++++++++++
.../java/org/apache/druid/cli/CliCoordinator.java | 20 +++
.../java/org/apache/druid/cli/CliOverlord.java | 32 ++---
.../org/apache/druid/cli/CliCoordinatorTest.java | 121 ++++++++++++++++
.../java/org/apache/druid/cli/CliOverlordTest.java | 23 +--
website/.spelling | 1 +
12 files changed, 453 insertions(+), 55 deletions(-)
diff --git a/docs/configuration/index.md b/docs/configuration/index.md
index 308b523a0da..f4c40ba15b7 100644
--- a/docs/configuration/index.md
+++ b/docs/configuration/index.md
@@ -739,7 +739,7 @@ These Coordinator static configurations can be defined in
the `coordinator/runti
|`druid.coordinator.kill.bufferPeriod`|The amount of time that a segment must
be unused before it is able to be permanently removed from metadata and deep
storage. This can serve as a buffer period to prevent data loss if data ends up
being needed after being marked unused.|`P30D`|
|`druid.coordinator.kill.maxSegments`|The number of unused segments to kill
per kill task. This number must be greater than 0. This only applies when
`druid.coordinator.kill.on=true`.|100|
|`druid.coordinator.kill.maxInterval`|The largest interval, as an [ISO 8601
duration](https://en.wikipedia.org/wiki/ISO_8601#Durations), of segments to
delete per kill task. Set to zero, e.g. `PT0S`, for unlimited. This only
applies when `druid.coordinator.kill.on=true`.|`P30D`|
-
+|`druid.coordinator.server.maxConcurrentRequests`|Maximum number of requests
to non-exempt Coordinator API paths processed concurrently. Requests beyond
this are queued briefly and rejected with HTTP 503 if no slot frees. Leadership
endpoints (`/leader`, `/isLeader`) and non-matching paths such as `/status/*`
are never throttled. This bounds Jetty thread-pool exhaustion from heavy or
excessive calls (for example large `/loadstatus`, `/metadata/segments`, or
`/intervals` scans). Note: thi [...]
##### Metadata management
|Property|Description|Required|Default|
diff --git
a/indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/supervisor/SeekableStreamSupervisor.java
b/indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/supervisor/SeekableStreamSupervisor.java
index 5b6220fb246..e2a5a4b1ccd 100644
---
a/indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/supervisor/SeekableStreamSupervisor.java
+++
b/indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/supervisor/SeekableStreamSupervisor.java
@@ -1456,12 +1456,12 @@ public abstract class
SeekableStreamSupervisor<PartitionIdType, SequenceOffsetTy
// from actively reading to pending completion, etc.
// This is a potential silent-loss window where data will not be
queryable until handoff.
log.info(
- "Could not find any task matching taskAllocatorId[%s] in
supervisor[%s] for upgraded pending segment[%s]"
- + " (upgradedFrom[%s]); it will not be re-announced until handoff.",
- taskAllocatorId,
- supervisorId,
- pendingSegmentRecord.getId(),
- pendingSegmentRecord.getUpgradedFromSegmentId()
+ "Could not find any task matching taskAllocatorId[%s] in
supervisor[%s] for upgraded pending segment[%s]"
+ + " (upgradedFrom[%s]); it will not be re-announced
until handoff.",
+ taskAllocatorId,
+ supervisorId,
+ pendingSegmentRecord.getId(),
+ pendingSegmentRecord.getUpgradedFromSegmentId()
);
emitter.emit(
IndexTaskUtils.setPendingSegmentDimensions(getMetricBuilder(),
pendingSegmentRecord)
diff --git
a/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
b/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
index a7206ca9cf7..2eff2f54a32 100644
---
a/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
+++
b/server/src/main/java/org/apache/druid/server/initialization/ServerConfig.java
@@ -47,6 +47,7 @@ import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
+import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.zip.Deflater;
@@ -441,6 +442,17 @@ public class ServerConfig
return Math.max(10, (JvmUtils.getRuntimeInfo().getAvailableProcessors() *
17) / 16 + 2) + 30;
}
+ public static int getNumThreadsFromProperties(Properties properties)
+ {
+ final String value =
properties.getProperty("druid.server.http.numThreads");
+ return value == null ? getDefaultNumThreads() : Integer.parseInt(value);
+ }
+
+ public static int getDefaultMaxConcurrentRequests(int numThreads)
+ {
+ return Math.max(1, Math.max(numThreads - 4, (int) (numThreads * 0.8)));
+ }
+
public static class UriComplianceDeserializer extends
JsonDeserializer<UriCompliance>
{
@Override
diff --git
a/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
b/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
index c1c10547d88..f8768f3a7f0 100644
---
a/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
+++
b/server/src/main/java/org/apache/druid/server/initialization/jetty/CliIndexerServerModule.java
@@ -47,7 +47,6 @@ import java.util.Properties;
*/
public class CliIndexerServerModule implements Module
{
- public static final String SERVER_HTTP_NUM_THREADS_PROPERTY =
"druid.server.http.numThreads";
private final Properties properties;
public CliIndexerServerModule(Properties properties)
@@ -62,12 +61,7 @@ public class CliIndexerServerModule implements Module
LifecycleModule.register(binder, ChatHandlerResource.class);
// Use an equal number of threads for chat handler and non-chat handler
requests.
- int serverHttpNumThreads;
- if (properties.getProperty(SERVER_HTTP_NUM_THREADS_PROPERTY) == null) {
- serverHttpNumThreads = ServerConfig.getDefaultNumThreads();
- } else {
- serverHttpNumThreads =
Integer.parseInt(properties.getProperty(SERVER_HTTP_NUM_THREADS_PROPERTY));
- }
+ int serverHttpNumThreads =
ServerConfig.getNumThreadsFromProperties(properties);
JettyBindings.addQosFilter(
binder,
diff --git
a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyBindings.java
b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyBindings.java
index 3934e33277b..2c0ed518e0a 100644
---
a/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyBindings.java
+++
b/server/src/main/java/org/apache/druid/server/initialization/jetty/JettyBindings.java
@@ -22,7 +22,6 @@ package org.apache.druid.server.initialization.jetty;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Binder;
import com.google.inject.multibindings.Multibinder;
-import org.eclipse.jetty.ee8.servlets.QoSFilter;
import org.eclipse.jetty.server.Handler;
import javax.servlet.DispatcherType;
@@ -43,6 +42,18 @@ public class JettyBindings
}
public static void addQosFilter(Binder binder, String[] paths, int
maxRequests)
+ {
+ addQosFilter(binder, paths, maxRequests, null);
+ }
+
+ /**
+ * Registers a QoS filter for the given {@code paths}, exempting any request
+ * that matches one of {@code excludedPaths} (servlet path-specs) from QoS
+ * throttling. Exclusions are needed because servlet filter mappings cannot
+ * express a "match this prefix except this sub-path" rule; see
+ * {@link PathExcludingQoSFilter}.
+ */
+ public static void addQosFilter(Binder binder, String[] paths, int
maxRequests, String[] excludedPaths)
{
if (maxRequests <= 0) {
return;
@@ -50,7 +61,7 @@ public class JettyBindings
Multibinder.newSetBinder(binder, QosFilterHolder.class)
.addBinding()
- .toInstance(new QosFilterHolder(paths, maxRequests));
+ .toInstance(new QosFilterHolder(paths, maxRequests,
excludedPaths));
}
public static void addHandler(Binder binder, Class<? extends Handler>
handlerClass)
@@ -67,28 +78,46 @@ public class JettyBindings
private final long timeoutMs;
- public QosFilterHolder(String[] paths, int maxRequests, long timeoutMs)
+ private final String[] excludedPaths;
+
+ public QosFilterHolder(String[] paths, int maxRequests, long timeoutMs,
String[] excludedPaths)
{
this.paths = paths;
this.maxRequests = maxRequests;
this.timeoutMs = timeoutMs;
+ this.excludedPaths = excludedPaths == null ? new String[0] :
excludedPaths;
+ }
+
+ public QosFilterHolder(String[] paths, int maxRequests, long timeoutMs)
+ {
+ this(paths, maxRequests, timeoutMs, null);
+ }
+
+ public QosFilterHolder(String[] paths, int maxRequests, String[]
excludedPaths)
+ {
+ this(paths, maxRequests, -1, excludedPaths);
}
public QosFilterHolder(String[] paths, int maxRequests)
{
- this(paths, maxRequests, -1);
+ this(paths, maxRequests, -1, null);
}
@Override
public Filter getFilter()
{
- return new QoSFilter();
+ return new PathExcludingQoSFilter(excludedPaths);
}
@Override
public Class<? extends Filter> getFilterClass()
{
- return QoSFilter.class;
+ return PathExcludingQoSFilter.class;
+ }
+
+ public String[] getExcludedPaths()
+ {
+ return excludedPaths;
}
@Override
diff --git
a/server/src/main/java/org/apache/druid/server/initialization/jetty/PathExcludingQoSFilter.java
b/server/src/main/java/org/apache/druid/server/initialization/jetty/PathExcludingQoSFilter.java
new file mode 100644
index 00000000000..e5b5066dd0f
--- /dev/null
+++
b/server/src/main/java/org/apache/druid/server/initialization/jetty/PathExcludingQoSFilter.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.druid.server.initialization.jetty;
+
+import org.eclipse.jetty.ee8.servlets.QoSFilter;
+import org.eclipse.jetty.http.pathmap.PathSpecSet;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * A {@link QoSFilter} that allows a set of request paths to bypass QoS
+ * throttling entirely. This is useful for lightweight leadership /
health-check
+ * endpoints (such as {@code /druid/coordinator/v1/isLeader}) that must remain
+ * responsive even when heavier APIs matched by the same broad filter path-spec
+ * have saturated the QoS semaphore. Servlet filter mappings cannot express
path
+ * exclusions, so the exclusion is applied here at request time.
+ *
+ * <p>Excluded requests are passed straight down the filter chain without ever
+ * acquiring the QoS semaphore; all other requests are handled by the standard
+ * {@link QoSFilter} behavior.
+ */
+public class PathExcludingQoSFilter extends QoSFilter
+{
+ private final PathSpecSet excludedPaths;
+
+ public PathExcludingQoSFilter(String[] excludedPaths)
+ {
+ this.excludedPaths = new PathSpecSet();
+ if (excludedPaths != null) {
+ for (String path : excludedPaths) {
+ this.excludedPaths.add(path);
+ }
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
+ throws IOException, ServletException
+ {
+ if (isExcluded(request)) {
+ chain.doFilter(request, response);
+ } else {
+ super.doFilter(request, response, chain);
+ }
+ }
+
+ private boolean isExcluded(ServletRequest request)
+ {
+ if (!(request instanceof HttpServletRequest)) {
+ return false;
+ }
+ final HttpServletRequest httpRequest = (HttpServletRequest) request;
+ final String contextPath = httpRequest.getContextPath();
+ String path = httpRequest.getRequestURI();
+ if (contextPath != null && !contextPath.isEmpty() &&
path.startsWith(contextPath)) {
+ path = path.substring(contextPath.length());
+ }
+ return excludedPaths.test(path);
+ }
+}
diff --git
a/server/src/test/java/org/apache/druid/server/initialization/jetty/PathExcludingQoSFilterTest.java
b/server/src/test/java/org/apache/druid/server/initialization/jetty/PathExcludingQoSFilterTest.java
new file mode 100644
index 00000000000..3fe56d5053a
--- /dev/null
+++
b/server/src/test/java/org/apache/druid/server/initialization/jetty/PathExcludingQoSFilterTest.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.druid.server.initialization.jetty;
+
+import org.apache.druid.server.mocks.MockHttpServletRequest;
+import org.apache.druid.server.mocks.MockHttpServletResponse;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Map;
+
+public class PathExcludingQoSFilterTest
+{
+ // The concrete request URIs those specs are expected to exempt.
+ private static final String[] EXCLUDED_PATHS = new String[]{
+ "/druid/coordinator/v1/isLeader",
+ "/druid/coordinator/v1/leader"
+ };
+
+ @Test
+ public void testExcludedPathsBypassQosFilter() throws Exception
+ {
+ // The filter is intentionally NOT initialized: excluded requests must
never touch the QoS semaphore,
+ // so if super.doFilter() were invoked it would fail on the uninitialized
semaphore.
+ final PathExcludingQoSFilter filter = new
PathExcludingQoSFilter(EXCLUDED_PATHS);
+
+ for (String excludedPath : EXCLUDED_PATHS) {
+ final MockHttpServletRequest request = request(excludedPath);
+ final MockHttpServletResponse response = new MockHttpServletResponse();
+ final RecordingFilterChain chain = new RecordingFilterChain();
+
+ filter.doFilter(request, response, chain);
+
+ Assert.assertEquals("Excluded request should be passed straight down the
chain", 1, chain.invocations);
+ Assert.assertSame(request, chain.lastRequest);
+ Assert.assertEquals("Excluded request should not be rejected", 0,
response.getStatus());
+ }
+ }
+
+ @Test
+ public void testNonExcludedPathIsHandledByQosFilter() throws Exception
+ {
+ final PathExcludingQoSFilter filter = new
PathExcludingQoSFilter(EXCLUDED_PATHS);
+ filter.init(new DummyFilterConfig(Map.of("maxRequests", "1")));
+
+ final MockHttpServletRequest request =
request("/druid/coordinator/v1/loadstatus");
+ final MockHttpServletResponse response = new MockHttpServletResponse();
+ final RecordingFilterChain chain = new RecordingFilterChain();
+
+ // A single request with a free semaphore is accepted and passed down the
chain by the standard QoSFilter.
+ filter.doFilter(request, response, chain);
+
+ Assert.assertEquals("Non-excluded request should be handled by the QoS
filter", 1, chain.invocations);
+ Assert.assertNotEquals(
+ "Accepted request must not be rejected with 503",
+ HttpServletResponse.SC_SERVICE_UNAVAILABLE,
+ response.getStatus()
+ );
+ }
+
+ @Test
+ public void testNullExcludedPathsHandlesAllRequests()
+ {
+ // A null exclusion list must not blow up; nothing should be treated as
excluded.
+ final PathExcludingQoSFilter filter = new PathExcludingQoSFilter(null);
+ Assert.assertNotNull(filter);
+ }
+
+ private static MockHttpServletRequest request(String requestUri)
+ {
+ final MockHttpServletRequest request = new MockHttpServletRequest()
+ {
+ @Override
+ public String getContextPath()
+ {
+ return "";
+ }
+ };
+ request.requestUri = requestUri;
+ return request;
+ }
+
+ private static class RecordingFilterChain implements FilterChain
+ {
+ private int invocations = 0;
+ private ServletRequest lastRequest;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response)
+ {
+ invocations++;
+ lastRequest = request;
+ }
+ }
+
+ private static class DummyFilterConfig implements FilterConfig
+ {
+ private final Map<String, String> initParameters;
+
+ DummyFilterConfig(Map<String, String> initParameters)
+ {
+ this.initParameters = initParameters;
+ }
+
+ @Override
+ public String getFilterName()
+ {
+ return "qos";
+ }
+
+ @Override
+ public ServletContext getServletContext()
+ {
+ return null;
+ }
+
+ @Override
+ public String getInitParameter(String name)
+ {
+ return initParameters.get(name);
+ }
+
+ @Override
+ public Enumeration<String> getInitParameterNames()
+ {
+ return Collections.enumeration(initParameters.keySet());
+ }
+ }
+}
diff --git a/services/src/main/java/org/apache/druid/cli/CliCoordinator.java
b/services/src/main/java/org/apache/druid/cli/CliCoordinator.java
index f8b52fafbdf..7fc344d781d 100644
--- a/services/src/main/java/org/apache/druid/cli/CliCoordinator.java
+++ b/services/src/main/java/org/apache/druid/cli/CliCoordinator.java
@@ -105,6 +105,8 @@ import org.apache.druid.server.http.RulesResource;
import org.apache.druid.server.http.SelfDiscoveryResource;
import org.apache.druid.server.http.ServersResource;
import org.apache.druid.server.http.TiersResource;
+import org.apache.druid.server.initialization.ServerConfig;
+import org.apache.druid.server.initialization.jetty.JettyBindings;
import org.apache.druid.server.initialization.jetty.JettyServerInitializer;
import org.apache.druid.server.lookup.cache.LookupCoordinatorManager;
import org.apache.druid.server.lookup.cache.LookupCoordinatorManagerConfig;
@@ -231,6 +233,24 @@ public class CliCoordinator extends ServerRunnable
binder.bind(JettyServerInitializer.class)
.to(CoordinatorJettyServerInitializer.class);
+ // QoS filtering to prevent heavy coordinator API requests from
starving health check endpoints.
+ // Set druid.coordinator.server.maxConcurrentRequests=-1 to
disable.
+ final int serverHttpNumThreads =
ServerConfig.getNumThreadsFromProperties(properties);
+ final int maxConcurrentRequests =
properties.containsKey("druid.coordinator.server.maxConcurrentRequests")
+ ?
Integer.parseInt(properties.getProperty("druid.coordinator.server.maxConcurrentRequests"))
+ :
ServerConfig.getDefaultMaxConcurrentRequests(serverHttpNumThreads);
+ if (maxConcurrentRequests > 0) {
+ log.info("Coordinator QoS filtering enabled. Max concurrent
requests: [%d]", maxConcurrentRequests);
+ JettyBindings.addQosFilter(
+ binder,
+ new String[]{"/druid/coordinator/v1/*", "/druid-internal/*"},
+ maxConcurrentRequests,
+ new String[]{"/druid/coordinator/v1/isLeader",
"/druid/coordinator/v1/leader"}
+ );
+ } else {
+ log.info("Coordinator QoS filtering disabled.");
+ }
+
Jerseys.addResource(binder, CoordinatorResource.class);
Jerseys.addResource(binder, CoordinatorCompactionResource.class);
Jerseys.addResource(binder,
CoordinatorDynamicConfigsResource.class);
diff --git a/services/src/main/java/org/apache/druid/cli/CliOverlord.java
b/services/src/main/java/org/apache/druid/cli/CliOverlord.java
index 67a8be584df..50116cb4968 100644
--- a/services/src/main/java/org/apache/druid/cli/CliOverlord.java
+++ b/services/src/main/java/org/apache/druid/cli/CliOverlord.java
@@ -129,7 +129,6 @@ import org.apache.druid.server.http.RedirectFilter;
import org.apache.druid.server.http.RedirectInfo;
import org.apache.druid.server.http.SelfDiscoveryResource;
import org.apache.druid.server.initialization.ServerConfig;
-import org.apache.druid.server.initialization.jetty.CliIndexerServerModule;
import org.apache.druid.server.initialization.jetty.JettyBindings;
import org.apache.druid.server.initialization.jetty.JettyServerInitUtils;
import org.apache.druid.server.initialization.jetty.JettyServerInitializer;
@@ -483,27 +482,17 @@ public class CliOverlord extends ServerRunnable
Jerseys.addResource(binder, OverlordDataSourcesResource.class);
- final int serverHttpNumThreads =
properties.containsKey(CliIndexerServerModule.SERVER_HTTP_NUM_THREADS_PROPERTY)
- ?
Integer.parseInt(properties.getProperty(CliIndexerServerModule.SERVER_HTTP_NUM_THREADS_PROPERTY))
- :
ServerConfig.getDefaultNumThreads();
-
- final int maxConcurrentActions;
- if
(properties.containsKey("druid.indexer.server.maxConcurrentActions")) {
- maxConcurrentActions =
Integer.parseInt(properties.getProperty("druid.indexer.server.maxConcurrentActions"));
- } else {
- maxConcurrentActions =
getDefaultMaxConcurrentActions(serverHttpNumThreads);
- }
-
+ // QoS filtering to prevent action requests from starving health
check endpoints.
+ // Set druid.indexer.server.maxConcurrentActions=-1 to disable.
+ final int serverHttpNumThreads =
ServerConfig.getNumThreadsFromProperties(properties);
+ final int maxConcurrentActions =
properties.containsKey("druid.indexer.server.maxConcurrentActions")
+ ?
Integer.parseInt(properties.getProperty("druid.indexer.server.maxConcurrentActions"))
+ :
ServerConfig.getDefaultMaxConcurrentRequests(serverHttpNumThreads);
if (maxConcurrentActions > 0) {
- // Add QoS filtering for action endpoints only
- final String[] actionPaths = {
- "/druid/indexer/v1/action",
- };
-
log.info("Overlord QoS filtering enabled for action endpoints.
Max concurrent actions: [%d]", maxConcurrentActions);
- JettyBindings.addQosFilter(binder, actionPaths,
maxConcurrentActions);
+ JettyBindings.addQosFilter(binder, "/druid/indexer/v1/action",
maxConcurrentActions);
} else {
- log.info("Overlord QoS filtering disabled for action endpoints.
Max concurrent actions: [%d]", serverHttpNumThreads);
+ log.info("Overlord QoS filtering disabled for action
endpoints.");
}
}
},
@@ -520,11 +509,6 @@ public class CliOverlord extends ServerRunnable
);
}
- public static int getDefaultMaxConcurrentActions(int serverHttpNumThreads)
- {
- return Math.max(1, Math.max(serverHttpNumThreads - 4, (int)
(serverHttpNumThreads * 0.8)));
- }
-
/**
*/
private static class OverlordJettyServerInitializer implements
JettyServerInitializer
diff --git
a/services/src/test/java/org/apache/druid/cli/CliCoordinatorTest.java
b/services/src/test/java/org/apache/druid/cli/CliCoordinatorTest.java
new file mode 100644
index 00000000000..8be519dbd1d
--- /dev/null
+++ b/services/src/test/java/org/apache/druid/cli/CliCoordinatorTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.druid.cli;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import org.apache.druid.discovery.NodeRole;
+import org.apache.druid.guice.LazySingleton;
+import org.apache.druid.guice.LifecycleModule;
+import org.apache.druid.jackson.JacksonModule;
+import org.apache.druid.server.initialization.jetty.JettyBindings;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Arrays;
+import java.util.Properties;
+import java.util.Set;
+
+public class CliCoordinatorTest
+{
+ private static final String COORDINATOR_QOS_PATH = "/druid/coordinator/v1/*";
+
+ @Test
+ public void testQosFilterIsBoundByDefault()
+ {
+ final Injector injector = makeCoordinatorInjector(new Properties());
+
+ final Set<JettyBindings.QosFilterHolder> qosFilters =
getQosFilterHolders(injector);
+ Assert.assertTrue(
+ "Coordinator QoS filter should be bound when maxConcurrentRequests
defaults to a positive value",
+ hasCoordinatorQosFilter(qosFilters)
+ );
+ }
+
+ @Test
+ public void testQosFilterIsNotBoundWhenDisabled()
+ {
+ final Properties properties = new Properties();
+ properties.setProperty("druid.coordinator.server.maxConcurrentRequests",
"-1");
+ final Injector injector = makeCoordinatorInjector(properties);
+
+ final Set<JettyBindings.QosFilterHolder> qosFilters =
getQosFilterHolders(injector);
+ Assert.assertFalse(
+ "Coordinator QoS filter should not be bound when maxConcurrentRequests
is set to a non-positive value",
+ hasCoordinatorQosFilter(qosFilters)
+ );
+ }
+
+ @Test
+ public void testLeaderEndpointsExcludedFromQos()
+ {
+ final Injector injector = makeCoordinatorInjector(new Properties());
+
+ final JettyBindings.QosFilterHolder coordinatorQosFilter =
+ getQosFilterHolders(injector).stream()
+ .filter(holder ->
Arrays.asList(holder.getPaths()).contains(COORDINATOR_QOS_PATH))
+ .findFirst()
+ .orElseThrow(() -> new
AssertionError("Coordinator QoS filter should be bound"));
+
+
+ final Set<String> excludedPaths =
Set.of(coordinatorQosFilter.getExcludedPaths());
+ Assert.assertTrue(
+ "isLeader should be exempt from QoS filtering",
+ excludedPaths.contains("/druid/coordinator/v1/isLeader")
+ );
+ Assert.assertTrue(
+ "leader should be exempt from QoS filtering",
+ excludedPaths.contains("/druid/coordinator/v1/leader")
+ );
+ }
+
+ private static boolean
hasCoordinatorQosFilter(Set<JettyBindings.QosFilterHolder> qosFilters)
+ {
+ return qosFilters.stream()
+ .anyMatch(holder ->
Arrays.asList(holder.getPaths()).contains(COORDINATOR_QOS_PATH));
+ }
+
+ private static Set<JettyBindings.QosFilterHolder>
getQosFilterHolders(Injector injector)
+ {
+ return injector.getInstance(Key.get(new
TypeLiteral<Set<JettyBindings.QosFilterHolder>>() {}));
+ }
+
+ private static Injector makeCoordinatorInjector(final Properties props)
+ {
+ final Injector baseInjector = Guice.createInjector(
+ new JacksonModule(),
+ new LifecycleModule(),
+ binder -> {
+
binder.bind(Validator.class).toInstance(Validation.buildDefaultValidatorFactory().getValidator());
+ binder.bindScope(LazySingleton.class, Scopes.SINGLETON);
+ binder.bind(Properties.class).toInstance(props);
+ }
+ );
+
+ final CliCoordinator coordinator = new CliCoordinator();
+ baseInjector.injectMembers(coordinator);
+ return coordinator.makeInjector(Set.of(NodeRole.COORDINATOR));
+ }
+}
diff --git a/services/src/test/java/org/apache/druid/cli/CliOverlordTest.java
b/services/src/test/java/org/apache/druid/cli/CliOverlordTest.java
index 031c38ff8b2..9fec28aeeab 100644
--- a/services/src/test/java/org/apache/druid/cli/CliOverlordTest.java
+++ b/services/src/test/java/org/apache/druid/cli/CliOverlordTest.java
@@ -26,6 +26,7 @@ import org.apache.druid.metadata.SegmentsMetadataManager;
import org.apache.druid.metadata.segment.SqlSegmentsMetadataManagerV2;
import org.apache.druid.metadata.segment.cache.HeapMemorySegmentMetadataCache;
import org.apache.druid.metadata.segment.cache.SegmentMetadataCache;
+import org.apache.druid.server.initialization.ServerConfig;
import org.junit.Assert;
import org.junit.Test;
@@ -53,27 +54,27 @@ public class CliOverlordTest
@Test
- public void testGetDefaultMaxConcurrentActions()
+ public void testGetDefaultMaxConcurrentRequests()
{
// Small thread count where
- Assert.assertEquals(8, CliOverlord.getDefaultMaxConcurrentActions(10));
+ Assert.assertEquals(8, ServerConfig.getDefaultMaxConcurrentRequests(10));
// Medium thread count where
- Assert.assertEquals(21, CliOverlord.getDefaultMaxConcurrentActions(25));
- Assert.assertEquals(26, CliOverlord.getDefaultMaxConcurrentActions(30));
+ Assert.assertEquals(21, ServerConfig.getDefaultMaxConcurrentRequests(25));
+ Assert.assertEquals(26, ServerConfig.getDefaultMaxConcurrentRequests(30));
// Large thread count
- Assert.assertEquals(46, CliOverlord.getDefaultMaxConcurrentActions(50));
- Assert.assertEquals(96, CliOverlord.getDefaultMaxConcurrentActions(100));
+ Assert.assertEquals(46, ServerConfig.getDefaultMaxConcurrentRequests(50));
+ Assert.assertEquals(96, ServerConfig.getDefaultMaxConcurrentRequests(100));
// Test edge cases - return atleast 1 thread
- Assert.assertEquals(1, CliOverlord.getDefaultMaxConcurrentActions(-1));
- Assert.assertEquals(1, CliOverlord.getDefaultMaxConcurrentActions(0));
+ Assert.assertEquals(1, ServerConfig.getDefaultMaxConcurrentRequests(-1));
+ Assert.assertEquals(1, ServerConfig.getDefaultMaxConcurrentRequests(0));
// Test small clustesr
- Assert.assertEquals(2, CliOverlord.getDefaultMaxConcurrentActions(3));
- Assert.assertEquals(3, CliOverlord.getDefaultMaxConcurrentActions(4));
- Assert.assertEquals(4, CliOverlord.getDefaultMaxConcurrentActions(5));
+ Assert.assertEquals(2, ServerConfig.getDefaultMaxConcurrentRequests(3));
+ Assert.assertEquals(3, ServerConfig.getDefaultMaxConcurrentRequests(4));
+ Assert.assertEquals(4, ServerConfig.getDefaultMaxConcurrentRequests(5));
}
}
diff --git a/website/.spelling b/website/.spelling
index e1723b254dc..c99c42107f3 100644
--- a/website/.spelling
+++ b/website/.spelling
@@ -1693,6 +1693,7 @@ un
G1GC
GroupBys
QoS-type
+QoS
DumpSegment
SegmentMetadata
__time
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]