This is an automated email from the ASF dual-hosted git repository.
gian 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 ee614dc3439 Add /status/ready endpoint for service health. (#19148)
ee614dc3439 is described below
commit ee614dc3439835e05cd1670bd5f145e675b98f8a
Author: Gian Merlino <[email protected]>
AuthorDate: Fri Mar 13 01:05:21 2026 -0700
Add /status/ready endpoint for service health. (#19148)
Currently the most natural endpoint to use for service health (e.g. if
adding Druid services to a load balancer) is /status/health. However,
this does not play nicely with graceful shutdown mechanisms.
When druid.server.http.unannouncePropagationDelay is used, there is a
delay between unannounce and server shutdown, which allows Druid's
internal service discovery to stop sending traffic to a service before
it shuts down its server. However, /status/health continues to return OK
until the server is shut down, so external load balancers cannot take
advantage of this.
This patch adds /status/ready, an endpoint that is tied to announcement.
It allows external load balancers to take advantage of this graceful
shutdown mechanism.
---
.../druid/curator/discovery/DiscoveryModule.java | 3 +
.../druid/server/ServiceAnnouncementState.java | 44 +++++++++++++++
.../org/apache/druid/server/StatusResource.java | 23 +++++++-
.../druid/server/ServiceAnnouncementStateTest.java | 64 ++++++++++++++++++++++
.../apache/druid/server/StatusResourceTest.java | 22 ++++++++
.../java/org/apache/druid/cli/CliOverlord.java | 3 +-
.../cli/CoordinatorJettyServerInitializer.java | 1 +
.../cli/MiddleManagerJettyServerInitializer.java | 3 +-
.../druid/cli/QueryJettyServerInitializer.java | 1 +
.../druid/cli/RouterJettyServerInitializer.java | 1 +
.../java/org/apache/druid/cli/ServerRunnable.java | 8 +++
11 files changed, 169 insertions(+), 4 deletions(-)
diff --git
a/server/src/main/java/org/apache/druid/curator/discovery/DiscoveryModule.java
b/server/src/main/java/org/apache/druid/curator/discovery/DiscoveryModule.java
index fec67cb3412..bd1ad64aaca 100644
---
a/server/src/main/java/org/apache/druid/curator/discovery/DiscoveryModule.java
+++
b/server/src/main/java/org/apache/druid/curator/discovery/DiscoveryModule.java
@@ -59,6 +59,7 @@ import org.apache.druid.guice.PolyBind;
import org.apache.druid.guice.annotations.Self;
import org.apache.druid.java.util.common.lifecycle.Lifecycle;
import org.apache.druid.server.DruidNode;
+import org.apache.druid.server.ServiceAnnouncementState;
import org.apache.druid.server.initialization.CuratorDiscoveryConfig;
import org.apache.druid.server.initialization.ZkPathsConfig;
@@ -163,6 +164,8 @@ public class DiscoveryModule implements Module
binder.bind(CuratorServiceAnnouncer.class).in(LazySingleton.class);
+ binder.bind(ServiceAnnouncementState.class).in(LazySingleton.class);
+
// Build the binder so that it will at a minimum inject an empty set.
DruidBinders.discoveryAnnouncementBinder(binder);
diff --git
a/server/src/main/java/org/apache/druid/server/ServiceAnnouncementState.java
b/server/src/main/java/org/apache/druid/server/ServiceAnnouncementState.java
new file mode 100644
index 00000000000..3e8d804583e
--- /dev/null
+++ b/server/src/main/java/org/apache/druid/server/ServiceAnnouncementState.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+/**
+ * Tracks whether this service has announced itself to service discovery and
is ready to receive traffic.
+ * Used by the {@code /status/ready} endpoint to report readiness to load
balancers.
+ */
+public class ServiceAnnouncementState
+{
+ private volatile boolean ready = false;
+
+ public void markReady()
+ {
+ ready = true;
+ }
+
+ public void markNotReady()
+ {
+ ready = false;
+ }
+
+ public boolean isReady()
+ {
+ return ready;
+ }
+}
diff --git a/server/src/main/java/org/apache/druid/server/StatusResource.java
b/server/src/main/java/org/apache/druid/server/StatusResource.java
index 29aaae98c25..4c971eae6b7 100644
--- a/server/src/main/java/org/apache/druid/server/StatusResource.java
+++ b/server/src/main/java/org/apache/druid/server/StatusResource.java
@@ -40,6 +40,7 @@ import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -59,19 +60,22 @@ public class StatusResource
private final DruidServerConfig druidServerConfig;
private final ExtensionsLoader extnLoader;
private final RuntimeInfo runtimeInfo;
+ private final ServiceAnnouncementState serviceAnnouncementState;
@Inject
public StatusResource(
final Properties properties,
final DruidServerConfig druidServerConfig,
final ExtensionsLoader extnLoader,
- final RuntimeInfo runtimeInfo
+ final RuntimeInfo runtimeInfo,
+ final ServiceAnnouncementState serviceAnnouncementState
)
{
this.properties = properties;
this.druidServerConfig = druidServerConfig;
this.extnLoader = extnLoader;
this.runtimeInfo = runtimeInfo;
+ this.serviceAnnouncementState = serviceAnnouncementState;
}
@GET
@@ -135,6 +139,23 @@ public class StatusResource
return true;
}
+ /**
+ * This is an unsecured endpoint, defined as such in UNSECURED_PATHS in the
service initialization files.
+ * Returns 200 when the service has announced itself and is ready to receive
traffic,
+ * or 503 when the service has not yet announced or has begun unannouncing
(e.g. during shutdown).
+ */
+ @GET
+ @Path("/ready")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getReady()
+ {
+ final boolean ready = serviceAnnouncementState.isReady();
+ if (ready) {
+ return Response.ok(true).build();
+ }
+ return
Response.status(Response.Status.SERVICE_UNAVAILABLE).entity(false).build();
+ }
+
public static class Status
{
final String version;
diff --git
a/server/src/test/java/org/apache/druid/server/ServiceAnnouncementStateTest.java
b/server/src/test/java/org/apache/druid/server/ServiceAnnouncementStateTest.java
new file mode 100644
index 00000000000..cf60882b5be
--- /dev/null
+++
b/server/src/test/java/org/apache/druid/server/ServiceAnnouncementStateTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ServiceAnnouncementStateTest
+{
+ @Test
+ public void testInitialStateIsNotReady()
+ {
+ final ServiceAnnouncementState state = new ServiceAnnouncementState();
+ Assert.assertFalse(state.isReady());
+ }
+
+ @Test
+ public void testMarkReady()
+ {
+ final ServiceAnnouncementState state = new ServiceAnnouncementState();
+ state.markReady();
+ Assert.assertTrue(state.isReady());
+ }
+
+ @Test
+ public void testMarkNotReady()
+ {
+ final ServiceAnnouncementState state = new ServiceAnnouncementState();
+ state.markReady();
+ Assert.assertTrue(state.isReady());
+ state.markNotReady();
+ Assert.assertFalse(state.isReady());
+ }
+
+ @Test
+ public void testMultipleTransitions()
+ {
+ final ServiceAnnouncementState state = new ServiceAnnouncementState();
+ state.markReady();
+ state.markReady();
+ Assert.assertTrue(state.isReady());
+ state.markNotReady();
+ Assert.assertFalse(state.isReady());
+ state.markNotReady();
+ Assert.assertFalse(state.isReady());
+ }
+}
diff --git
a/server/src/test/java/org/apache/druid/server/StatusResourceTest.java
b/server/src/test/java/org/apache/druid/server/StatusResourceTest.java
index eb96b769f73..54dab17a40a 100644
--- a/server/src/test/java/org/apache/druid/server/StatusResourceTest.java
+++ b/server/src/test/java/org/apache/druid/server/StatusResourceTest.java
@@ -32,6 +32,7 @@ import org.apache.druid.utils.JvmUtils;
import org.junit.Assert;
import org.junit.Test;
+import javax.ws.rs.core.Response;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -78,6 +79,27 @@ public class StatusResourceTest
testHiddenPropertiesWithPropertyFileName("status.resource.test.runtime.hpc.properties");
}
+ @Test
+ public void testGetReadyReturns200WhenReady()
+ {
+ final ServiceAnnouncementState state = new ServiceAnnouncementState();
+ state.markReady();
+ final StatusResource resource = new StatusResource(new Properties(), null,
null, null, state);
+ final Response response = resource.getReady();
+ Assert.assertEquals(200, response.getStatus());
+ Assert.assertEquals(true, response.getEntity());
+ }
+
+ @Test
+ public void testGetReadyReturns503WhenNotReady()
+ {
+ final ServiceAnnouncementState state = new ServiceAnnouncementState();
+ final StatusResource resource = new StatusResource(new Properties(), null,
null, null, state);
+ final Response response = resource.getReady();
+ Assert.assertEquals(503, response.getStatus());
+ Assert.assertEquals(false, response.getEntity());
+ }
+
private void testHiddenPropertiesWithPropertyFileName(String fileName)
throws Exception
{
Injector injector = new StartupInjectorBuilder()
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 6b82029055e..1ae9b827f9e 100644
--- a/services/src/main/java/org/apache/druid/cli/CliOverlord.java
+++ b/services/src/main/java/org/apache/druid/cli/CliOverlord.java
@@ -174,7 +174,8 @@ public class CliOverlord extends ServerRunnable
protected static final List<String> UNSECURED_PATHS = ImmutableList.of(
"/druid/indexer/v1/isLeader",
- "/status/health"
+ "/status/health",
+ "/status/ready"
);
private Properties properties;
diff --git
a/services/src/main/java/org/apache/druid/cli/CoordinatorJettyServerInitializer.java
b/services/src/main/java/org/apache/druid/cli/CoordinatorJettyServerInitializer.java
index 3fd6b41031b..ba32c3f666f 100644
---
a/services/src/main/java/org/apache/druid/cli/CoordinatorJettyServerInitializer.java
+++
b/services/src/main/java/org/apache/druid/cli/CoordinatorJettyServerInitializer.java
@@ -53,6 +53,7 @@ class CoordinatorJettyServerInitializer implements
JettyServerInitializer
"/coordinator/false",
"/overlord/false",
"/status/health",
+ "/status/ready",
"/druid/coordinator/v1/isLeader"
);
diff --git
a/services/src/main/java/org/apache/druid/cli/MiddleManagerJettyServerInitializer.java
b/services/src/main/java/org/apache/druid/cli/MiddleManagerJettyServerInitializer.java
index b338aa805fd..caa971c4efa 100644
---
a/services/src/main/java/org/apache/druid/cli/MiddleManagerJettyServerInitializer.java
+++
b/services/src/main/java/org/apache/druid/cli/MiddleManagerJettyServerInitializer.java
@@ -40,7 +40,6 @@ import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.DefaultHandler;
-import java.util.Collections;
import java.util.List;
/**
@@ -59,7 +58,7 @@ class MiddleManagerJettyServerInitializer implements
JettyServerInitializer
this.authConfig = authConfig;
}
- private static List<String> UNSECURED_PATHS =
Collections.singletonList("/status/health");
+ private static final List<String> UNSECURED_PATHS =
List.of("/status/health", "/status/ready");
@Override
public void initialize(Server server, Injector injector)
diff --git
a/services/src/main/java/org/apache/druid/cli/QueryJettyServerInitializer.java
b/services/src/main/java/org/apache/druid/cli/QueryJettyServerInitializer.java
index a236e887e57..565b0dcf278 100644
---
a/services/src/main/java/org/apache/druid/cli/QueryJettyServerInitializer.java
+++
b/services/src/main/java/org/apache/druid/cli/QueryJettyServerInitializer.java
@@ -59,6 +59,7 @@ public class QueryJettyServerInitializer implements
JettyServerInitializer
private static final Logger log = new
Logger(QueryJettyServerInitializer.class);
private static List<String> UNSECURED_PATHS = Lists.newArrayList(
"/status/health",
+ "/status/ready",
"/druid/historical/v1/readiness",
"/druid/broker/v1/readiness"
);
diff --git
a/services/src/main/java/org/apache/druid/cli/RouterJettyServerInitializer.java
b/services/src/main/java/org/apache/druid/cli/RouterJettyServerInitializer.java
index 6ec4a38548c..3e657d49460 100644
---
a/services/src/main/java/org/apache/druid/cli/RouterJettyServerInitializer.java
+++
b/services/src/main/java/org/apache/druid/cli/RouterJettyServerInitializer.java
@@ -55,6 +55,7 @@ public class RouterJettyServerInitializer implements
JettyServerInitializer
{
private static final List<String> UNSECURED_PATHS = ImmutableList.of(
"/status/health",
+ "/status/ready",
// JDBC authentication uses the JDBC connection context instead of HTTP
headers, skip the normal auth checks.
// The router will keep the connection context in the forwarded message,
and the broker is responsible for
// performing the auth checks.
diff --git a/services/src/main/java/org/apache/druid/cli/ServerRunnable.java
b/services/src/main/java/org/apache/druid/cli/ServerRunnable.java
index 3dc1711a50e..0c4143c48ee 100644
--- a/services/src/main/java/org/apache/druid/cli/ServerRunnable.java
+++ b/services/src/main/java/org/apache/druid/cli/ServerRunnable.java
@@ -41,6 +41,7 @@ import org.apache.druid.java.util.common.lifecycle.Lifecycle;
import org.apache.druid.java.util.common.logger.Logger;
import org.apache.druid.java.util.emitter.EmittingLogger;
import org.apache.druid.server.DruidNode;
+import org.apache.druid.server.ServiceAnnouncementState;
import java.lang.annotation.Annotation;
import java.util.Collections;
@@ -135,6 +136,9 @@ public abstract class ServerRunnable extends GuiceRunnable
@Inject
private Map<NodeRole, Set<Class<? extends DruidService>>> serviceClasses;
+ @Inject
+ private ServiceAnnouncementState serviceAnnouncementState;
+
private final boolean useLegacyAnnouncer;
public static DiscoverySideEffectsProvider create()
@@ -181,11 +185,15 @@ public abstract class ServerRunnable extends GuiceRunnable
if (useLegacyAnnouncer) {
legacyAnnouncer.announce(discoveryDruidNode.getDruidNode());
}
+
+ serviceAnnouncementState.markReady();
}
@Override
public void stop()
{
+ serviceAnnouncementState.markNotReady();
+
// Reverse order vs. start().
if (useLegacyAnnouncer) {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]