Graceful shutdown 33/10233/1
authorPatrikBuhr <patrik.buhr@est.tech>
Tue, 20 Dec 2022 15:59:14 +0000 (16:59 +0100)
committerPatrikBuhr <patrik.buhr@est.tech>
Thu, 5 Jan 2023 11:01:19 +0000 (12:01 +0100)
Support for granceful shutdown via signal "SIGTERM"
and via calling REST POST /actuator/shutdown

Signed-off-by: PatrikBuhr <patrik.buhr@est.tech>
Issue-ID: NONRTRIC-830
Change-Id: I2f58ab8ec50f0f08e45b3c4f9da19c7f5c1c2723

api/ics-api.json
api/ics-api.yaml
config/application.yaml
src/main/java/org/oransc/ics/Application.java
src/main/java/org/oransc/ics/BeanFactory.java
src/main/java/org/oransc/ics/repository/InfoProducers.java
src/test/java/org/oransc/ics/ApplicationTest.java

index 199c284..722097b 100644 (file)
             }},
             "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",
index 2f0fec3..dad91b5 100644 (file)
@@ -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:
index 4e4ca3e..345b3fd 100644 (file)
@@ -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)
index 46caac1..694bfe3 100644 (file)
 
 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);
+            }
+        });
     }
-
 }
index 233be90..f0f43ab 100644 (file)
@@ -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;
index aab4608..17ad366 100644 (file)
@@ -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<String, InfoProducer> allInfoProducers = new HashMap<>();
index d5f78fa..d3814b5 100644 (file)
@@ -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";
     }