From 88d81a5b7d15e68bbb2e0ea7c05203bfc1afd11f Mon Sep 17 00:00:00 2001 From: ychacon Date: Fri, 10 Feb 2023 13:09:06 +0100 Subject: [PATCH] Graceful shutdown Support for granceful shutdown via signal "SIGTERM" and via calling REST POST /actuator/shutdown Signed-off-by: PatrikBuhr Issue-ID: NONRTRIC-830 Change-Id: I2f58ab8ec50f0f08e45b3c4f9da19c7f5c1c2723 Signed-off-by: ychacon --- api/ics-api.json | 9 ++++++++ api/ics-api.yaml | 13 ++++++++++++ config/application.yaml | 9 ++++++-- src/main/java/org/oransc/ics/Application.java | 17 +++++++++++++-- src/main/java/org/oransc/ics/BeanFactory.java | 8 ++++++++ .../org/oransc/ics/repository/InfoProducers.java | 2 -- src/test/java/org/oransc/ics/ApplicationTest.java | 24 ++++++++++++++++++++++ 7 files changed, 76 insertions(+), 6 deletions(-) diff --git a/api/ics-api.json b/api/ics-api.json index 199c284..722097b 100644 --- a/api/ics-api.json +++ b/api/ics-api.json @@ -627,6 +627,15 @@ }}, "tags": ["Data consumer (callbacks)"] }}, + "/actuator/shutdown": {"post": { + "summary": "Actuator web endpoint 'shutdown'", + "operationId": "shutdown", + "responses": {"200": { + "description": "OK", + "content": {"*/*": {"schema": {"type": "object"}}} + }}, + "tags": ["Actuator"] + }}, "/actuator/metrics/{requiredMetricName}": {"get": { "summary": "Actuator web endpoint 'metrics-requiredMetricName'", "operationId": "metrics-requiredMetricName", diff --git a/api/ics-api.yaml b/api/ics-api.yaml index 2f0fec3..dad91b5 100644 --- a/api/ics-api.yaml +++ b/api/ics-api.yaml @@ -383,6 +383,19 @@ paths: application/json: schema: $ref: '#/components/schemas/Void' + /actuator/shutdown: + post: + tags: + - Actuator + summary: Actuator web endpoint 'shutdown' + operationId: shutdown + responses: + 200: + description: OK + content: + '*/*': + schema: + type: object /actuator/metrics/{requiredMetricName}: get: tags: diff --git a/config/application.yaml b/config/application.yaml index 4e4ca3e..345b3fd 100644 --- a/config/application.yaml +++ b/config/application.yaml @@ -29,8 +29,12 @@ management: web: exposure: # Enabling of springboot actuator features. See springboot documentation. - include: "loggers,logfile,health,info,metrics,threaddump,heapdump" - + include: "loggers,logfile,health,info,metrics,threaddump,heapdump,shutdown" + endpoint: + shutdown: + enabled: true +lifecycle: + timeout-per-shutdown-phase: "20s" logging: # Configuration of logging level: @@ -52,6 +56,7 @@ server: key-store: /opt/app/information-coordinator-service/etc/cert/keystore.jks key-password: policy_agent key-alias: policy_agent + shutdown: "graceful" app: webclient: # Configuration of the trust store used for the HTTP client (outgoing requests) diff --git a/src/main/java/org/oransc/ics/Application.java b/src/main/java/org/oransc/ics/Application.java index 46caac1..694bfe3 100644 --- a/src/main/java/org/oransc/ics/Application.java +++ b/src/main/java/org/oransc/ics/Application.java @@ -20,14 +20,27 @@ package org.oransc.ics; +import java.lang.invoke.MethodHandles; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication public class Application { + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + public static void main(String[] args) { - SpringApplication.run(Application.class); + ConfigurableApplicationContext context = SpringApplication.run(Application.class); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + logger.warn("Shutting down, received signal SIGTERM"); + SpringApplication.exit(context); + } + }); } - } diff --git a/src/main/java/org/oransc/ics/BeanFactory.java b/src/main/java/org/oransc/ics/BeanFactory.java index 233be90..f0f43ab 100644 --- a/src/main/java/org/oransc/ics/BeanFactory.java +++ b/src/main/java/org/oransc/ics/BeanFactory.java @@ -27,8 +27,10 @@ import java.lang.invoke.MethodHandles; import org.apache.catalina.connector.Connector; import org.oransc.ics.clients.SecurityContext; import org.oransc.ics.configuration.ApplicationConfig; +import org.oransc.ics.controllers.a1e.A1eCallbacks; import org.oransc.ics.controllers.r1producer.ProducerCallbacks; import org.oransc.ics.repository.InfoJobs; +import org.oransc.ics.repository.InfoProducers; import org.oransc.ics.repository.InfoTypeSubscriptions; import org.oransc.ics.repository.InfoTypes; import org.slf4j.Logger; @@ -103,6 +105,12 @@ class BeanFactory { return this.producerCallbacks; } + @Bean + public InfoProducers getInfoProducers(ProducerCallbacks producerCallbacks, A1eCallbacks consumerCallbacks, + InfoJobs infoJobs, InfoTypes infoTypes) { + return new InfoProducers(producerCallbacks, consumerCallbacks, infoJobs, infoTypes); + } + @Bean public ApplicationConfig getApplicationConfig() { return this.applicationConfig; diff --git a/src/main/java/org/oransc/ics/repository/InfoProducers.java b/src/main/java/org/oransc/ics/repository/InfoProducers.java index aab4608..17ad366 100644 --- a/src/main/java/org/oransc/ics/repository/InfoProducers.java +++ b/src/main/java/org/oransc/ics/repository/InfoProducers.java @@ -33,13 +33,11 @@ import org.oransc.ics.exceptions.ServiceException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; /** * Dynamic representation of all EiProducers. */ @SuppressWarnings("squid:S2629") // Invoke method(s) only conditionally -@Component public class InfoProducers { private final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final Map allInfoProducers = new HashMap<>(); diff --git a/src/test/java/org/oransc/ics/ApplicationTest.java b/src/test/java/org/oransc/ics/ApplicationTest.java index d5f78fa..d3814b5 100644 --- a/src/test/java/org/oransc/ics/ApplicationTest.java +++ b/src/test/java/org/oransc/ics/ApplicationTest.java @@ -43,7 +43,9 @@ import java.util.Map; import org.json.JSONObject; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; import org.oransc.ics.clients.AsyncRestClient; import org.oransc.ics.clients.AsyncRestClientFactory; import org.oransc.ics.clients.SecurityContext; @@ -97,11 +99,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.context.TestPropertySource; +import org.springframework.web.reactive.function.client.WebClientRequestException; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +@TestMethodOrder(MethodOrderer.MethodName.class) @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @TestPropertySource( properties = { // @@ -1277,6 +1281,26 @@ class ApplicationTest { } + @Test + @SuppressWarnings("squid:S2925") // "Thread.sleep" should not be used in tests. + void testZZActuator() throws Exception { + // The test must be run last, hence the "ZZ" in the name. All succeeding tests + // will fail. + AsyncRestClient client = restClient(); + client.post("/actuator/loggers/org.oransc.ics", "{\"configuredLevel\":\"trace\"}").block(); + String resp = client.get("/actuator/loggers/org.oransc.ics").block(); + assertThat(resp).contains("TRACE"); + client.post("/actuator/loggers/org.springframework.boot.actuate", "{\"configuredLevel\":\"trace\"}").block(); + // This will stop the web server and all coming tests will fail. + client.post("/actuator/shutdown", "").block(); + Thread.sleep(1000); + + StepVerifier.create(restClient().get(ConsumerConsts.API_ROOT + "/info-jobs")) // Any call + .expectSubscription() // + .expectErrorMatches(t -> t instanceof WebClientRequestException) // + .verify(); + } + private String typeSubscriptionUrl() { return ConsumerConsts.API_ROOT + "/info-type-subscription"; } -- 2.16.6