Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Health endpoints #111

Merged
merged 11 commits into from
Aug 16, 2024
18 changes: 8 additions & 10 deletions charts/hedera-block-node/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,14 @@ spec:
name: {{ include "hedera-block-node.fullname" . }}-config
- secretRef:
name: {{ include "hedera-block-node.fullname" . }}-secret

# TODO: Uncomment them once we have the health and readiness probes in our application
#livenessProbe:
# httpGet:
# path: /health # we need to define this in our application
# port: 8080 # will be the same as server port?
#readinessProbe:
# httpGet:
# path: /ready # we need to define this in our application
# port: 8080 # will be the same as server port?
livenessProbe:
httpGet:
path: {{ .Values.blockNode.health.liveness.endpoint }}
port: {{ .Values.service.port }}
readinessProbe:
httpGet:
path: {{ .Values.blockNode.health.readiness.endpoint }}
port: {{ .Values.service.port }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
Expand Down
5 changes: 5 additions & 0 deletions charts/hedera-block-node/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,8 @@ blockNode:
secret:
# if blank will use same as AppVersion of chart.
PRIVATE_KEY: "fake_private_key"
health:
readiness:
endpoint: "/healthz/readiness"
liveness:
endpoint: "/healthz/liveness"
22 changes: 21 additions & 1 deletion server/src/main/java/com/hedera/block/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import com.hedera.block.server.config.BlockNodeContext;
import com.hedera.block.server.config.BlockNodeContextFactory;
import com.hedera.block.server.data.ObjectEvent;
import com.hedera.block.server.health.HealthService;
import com.hedera.block.server.health.HealthServiceImpl;
import com.hedera.block.server.mediator.LiveStreamMediatorBuilder;
import com.hedera.block.server.mediator.StreamMediator;
import com.hedera.block.server.persistence.storage.PersistenceStorageConfig;
Expand All @@ -32,6 +34,7 @@
import edu.umd.cs.findbugs.annotations.NonNull;
import io.helidon.webserver.WebServer;
import io.helidon.webserver.grpc.GrpcRouting;
import io.helidon.webserver.http.HttpRouting;
import java.io.IOException;

/** Main class for the block node server */
Expand Down Expand Up @@ -82,16 +85,33 @@ public static void main(final String[] args) {
final GrpcRouting.Builder grpcRouting =
GrpcRouting.builder().service(blockStreamService);

@NonNull final HealthService healthService = new HealthServiceImpl(serviceStatus);

@NonNull
final HttpRouting.Builder httpRouting =
HttpRouting.builder()
.register(healthService.getHealthRootPath(), healthService);

// Build the web server
// TODO: make port server a configurable value.
@NonNull
final WebServer webServer =
WebServer.builder().port(8080).addRouting(grpcRouting).build();
WebServer.builder()
.port(8080)
.addRouting(grpcRouting)
.addRouting(httpRouting)
.build();

// Update the serviceStatus with the web server
serviceStatus.setWebServer(webServer);

// Start the web server
webServer.start();

// Log the server status
LOGGER.log(
System.Logger.Level.INFO,
"Block Node Server started at port: " + webServer.port());
mattp-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed 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 com.hedera.block.server.health;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.helidon.webserver.http.HttpService;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;

/** Defines the contract for health http service, needed for implementing an standard */
public interface HealthService extends HttpService {
/**
* The path for the health group endpoints. Root path for all health endpoints.
*
* @return the root path for the health group endpoints
*/
mattp-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved
@NonNull
String getHealthRootPath();

/**
* Handles the request for liveness endpoint, that it most be defined on routing implementation.
*
* @param req the server request
* @param res the server response
*/
mattp-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved
void handleLiveness(@NonNull final ServerRequest req, @NonNull final ServerResponse res);

/**
* Handles the request for readiness endpoint, that it most be defined on routing
* implementation.
*
* @param req the server request
* @param res the server response
*/
mattp-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved
void handleReadiness(@NonNull final ServerRequest req, @NonNull final ServerResponse res);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed 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 com.hedera.block.server.health;

import com.hedera.block.server.ServiceStatus;
import edu.umd.cs.findbugs.annotations.NonNull;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;

/** Provides implementation for the health endpoints of the server. */
public class HealthServiceImpl implements HealthService {

private static final String LIVENESS_PATH = "/liveness";
private static final String READINESS_PATH = "/readiness";

private final ServiceStatus serviceStatus;

/**
* It initializes the HealthService with needed dependencies.
*
* @param serviceStatus is used to check the status of the service
*/
public HealthServiceImpl(@NonNull ServiceStatus serviceStatus) {
this.serviceStatus = serviceStatus;
}

@Override
@NonNull
public String getHealthRootPath() {
return "/healthz";
}

/**
* Configures the health routes for the server.
*
* @param httpRules is used to configure the health endpoints routes
*/
@Override
mattp-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved
public void routing(@NonNull final HttpRules httpRules) {
httpRules
.get(LIVENESS_PATH, this::handleLiveness)
.get(READINESS_PATH, this::handleReadiness);
}

mattp-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved
/**
* Handles the request for liveness endpoint, that it most be defined on routing implementation.
*
* @param req the server request
* @param res the server response
*/
@Override
mattp-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved
public final void handleLiveness(
@NonNull final ServerRequest req, @NonNull final ServerResponse res) {
if (serviceStatus.isRunning()) {
res.status(200).send("OK");
} else {
res.status(503).send("Service is not running");
}
}

/**
* Handles the request for readiness endpoint, that it most be defined on routing
* implementation.
*
* @param req the server request
* @param res the server response
*/
@Override
mattp-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved
public final void handleReadiness(
@NonNull final ServerRequest req, @NonNull final ServerResponse res) {
if (serviceStatus.isRunning()) {
res.status(200).send("OK");
} else {
res.status(503).send("Service is not running");
}
}
}
6 changes: 2 additions & 4 deletions server/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

/** Runtime module of the server. */
module com.hedera.block.server {
exports com.hedera.block.server.consumer to
com.swirlds.config.impl;
exports com.hedera.block.server.persistence.storage to
com.swirlds.config.impl;
exports com.hedera.block.server.consumer;
exports com.hedera.block.server.persistence.storage;

requires com.hedera.block.protos;
requires com.google.protobuf;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed 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 com.hedera.block.server.health;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

import com.hedera.block.server.ServiceStatus;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class HealthServiceTest {
mattp-swirldslabs marked this conversation as resolved.
Show resolved Hide resolved

private static final String READINESS_PATH = "/readiness";
private static final String LIVENESS_PATH = "/liveness";
private static final String HEALTH_PATH = "/healthz";

@Mock private ServiceStatus serviceStatus;

@Mock ServerRequest serverRequest;

@Mock ServerResponse serverResponse;

@Test
public void testHandleLiveness() {
// given
when(serviceStatus.isRunning()).thenReturn(true);
when(serverResponse.status(200)).thenReturn(serverResponse);
doNothing().when(serverResponse).send("OK");
HealthService healthService = new HealthServiceImpl(serviceStatus);

// when
healthService.handleLiveness(serverRequest, serverResponse);

// then
verify(serverResponse, times(1)).status(200);
verify(serverResponse, times(1)).send("OK");
}

@Test
public void testHandleLiveness_notRunning() {
// given
when(serviceStatus.isRunning()).thenReturn(false);
when(serverResponse.status(503)).thenReturn(serverResponse);
doNothing().when(serverResponse).send("Service is not running");
HealthService healthService = new HealthServiceImpl(serviceStatus);

// when
healthService.handleLiveness(serverRequest, serverResponse);

// then
verify(serverResponse, times(1)).status(503);
verify(serverResponse, times(1)).send("Service is not running");
}

@Test
public void testHandleReadiness() {
// given
when(serviceStatus.isRunning()).thenReturn(true);
when(serverResponse.status(200)).thenReturn(serverResponse);
doNothing().when(serverResponse).send("OK");
HealthService healthService = new HealthServiceImpl(serviceStatus);

// when
healthService.handleReadiness(serverRequest, serverResponse);

// then
verify(serverResponse, times(1)).status(200);
verify(serverResponse, times(1)).send("OK");
}

@Test
public void testHandleReadiness_notRunning() {
// given
when(serviceStatus.isRunning()).thenReturn(false);
when(serverResponse.status(503)).thenReturn(serverResponse);
doNothing().when(serverResponse).send("Service is not running");
HealthService healthService = new HealthServiceImpl(serviceStatus);

// when
healthService.handleReadiness(serverRequest, serverResponse);

// then
verify(serverResponse, times(1)).status(503);
verify(serverResponse, times(1)).send("Service is not running");
}

@Test
public void testRouting() {
// given
HealthService healthService = new HealthServiceImpl(serviceStatus);
HttpRules httpRules = mock(HttpRules.class);
when(httpRules.get(anyString(), any())).thenReturn(httpRules);

// when
healthService.routing(httpRules);

// then
verify(httpRules, times(1)).get(eq(LIVENESS_PATH), any());
verify(httpRules, times(1)).get(eq(READINESS_PATH), any());
assertEquals(HEALTH_PATH, healthService.getHealthRootPath());
}
}
Loading