Introduced a startup delay mechanism based on the container hostname 59/140659/8
authorsourabh_sourabh <sourabh.sourabh@est.tech>
Tue, 8 Apr 2025 10:13:08 +0000 (11:13 +0100)
committersourabh_sourabh <sourabh.sourabh@est.tech>
Thu, 10 Apr 2025 16:22:40 +0000 (17:22 +0100)
- Extracted hostname using InetAddress and Calculated delay or fallback hash-based delay.
- This helps prevent the 'relation "databasechangelog" already exists' error seen in concurrent startups.

Issue-ID:CPS-2752
Change-Id: I051a8edd5ddab5a9fb012183b0526c113d90304e
Signed-off-by: sourabh_sourabh <sourabh.sourabh@est.tech>
cps-application/src/main/java/org/onap/cps/Application.java
cps-application/src/main/java/org/onap/cps/startup/InstanceStartupDelayManager.java [new file with mode: 0644]
cps-application/src/test/groovy/org/onap/cps/startup/InstanceStartupDelayManagerSpec.groovy [new file with mode: 0644]

index 62103bf..d1c99bc 100644 (file)
@@ -1,6 +1,6 @@
 /*\r
  * ============LICENSE_START=======================================================\r
- * Copyright (C) 2020 Nordix Foundation.\r
+ * Copyright (C) 2020-2025 OpenInfra Foundation Europe. All rights reserved.\r
  * ================================================================================\r
  * Licensed under the Apache License, Version 2.0 (the "License");\r
  * you may not use this file except in compliance with the License.\r
 \r
 package org.onap.cps;\r
 \r
+import lombok.extern.slf4j.Slf4j;\r
+import org.onap.cps.startup.InstanceStartupDelayManager;\r
 import org.springframework.boot.SpringApplication;\r
 import org.springframework.boot.autoconfigure.SpringBootApplication;\r
 \r
 @SpringBootApplication\r
+@Slf4j\r
 public class Application {\r
+\r
+    static InstanceStartupDelayManager instanceStartupDelayManager = new InstanceStartupDelayManager();\r
+\r
+    /**\r
+     * The main method which serves as the entry point to the Spring Boot application.\r
+     * It first applies a hostname-based startup delay to avoid potential race conditions\r
+     * during schema migration in distributed environments. After applying the delay,\r
+     * it initializes the Spring Application context and logs the application startup status.\r
+     *\r
+     * @param args Command-line arguments passed to the application (not used in this implementation).\r
+     */\r
     public static void main(final String[] args) {\r
+        instanceStartupDelayManager.applyHostnameBasedStartupDelay();\r
+        log.info("Initializing Spring Application context...");\r
         SpringApplication.run(Application.class, args);\r
+        log.info("🚀 APPLICATION STARTED");\r
     }\r
 }\r
diff --git a/cps-application/src/main/java/org/onap/cps/startup/InstanceStartupDelayManager.java b/cps-application/src/main/java/org/onap/cps/startup/InstanceStartupDelayManager.java
new file mode 100644 (file)
index 0000000..b221bdc
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
+ * ================================================================================
+ * 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.startup;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.TimeUnit;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class InstanceStartupDelayManager {
+
+    /**
+     * Applies a startup delay based on the host's name to avoid race conditions during schema migration.
+
+     * In environments with multiple instances (e.g., Docker Compose, Kubernetes),
+     * this delay helps avoid simultaneous Liquibase executions that may result in conflicts.
+
+     * Delay logic:
+     * - If the last character of the hostname is a digit, delay = digit * 1000 ms.
+     * - Otherwise, a hash-based fallback delay up to 3000 ms is applied.
+     */
+    public void applyHostnameBasedStartupDelay() {
+        try {
+            final String hostname = getHostName();
+            final char lastCharacterOfHostName = hostname.charAt(hostname.length() - 1);
+            final long startupDelayInMillis;
+            if (Character.isDigit(lastCharacterOfHostName)) {
+                startupDelayInMillis = Character.getNumericValue(lastCharacterOfHostName) * 1_000L;
+            } else {
+                startupDelayInMillis = Math.abs(hostname.hashCode() % 3_000L);
+            }
+            log.info("Startup delay applied for Hostname: {} | Delay: {} ms", hostname, startupDelayInMillis);
+            haveALittleSleepInMs(startupDelayInMillis);
+        } catch (final InterruptedException e) {
+            log.warn("Sleep interrupted, re-interrupting the thread");
+            Thread.currentThread().interrupt();
+        } catch (final Exception e) {
+            log.info("Exception during startup delay ignored. {}", e.getMessage());
+        }
+    }
+
+    protected String getHostName() throws UnknownHostException {
+        return InetAddress.getLocalHost().getHostName();
+    }
+
+    protected void haveALittleSleepInMs(final long timeInMs) throws InterruptedException {
+        TimeUnit.MILLISECONDS.sleep(timeInMs);
+    }
+}
\ No newline at end of file
diff --git a/cps-application/src/test/groovy/org/onap/cps/startup/InstanceStartupDelayManagerSpec.groovy b/cps-application/src/test/groovy/org/onap/cps/startup/InstanceStartupDelayManagerSpec.groovy
new file mode 100644 (file)
index 0000000..68ef6cc
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2025 OpenInfra Foundation Europe. All rights reserved.
+ * ================================================================================
+ * 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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.startup
+
+import spock.lang.Specification
+
+class InstanceStartupDelayManagerSpec extends Specification {
+
+    def objectUnderTest = Spy(InstanceStartupDelayManager)
+
+    def 'Startup delay with real hostname.'() {
+        when: 'start up delay is called'
+            objectUnderTest.applyHostnameBasedStartupDelay()
+        then: 'the system will sleep for some time'
+            1 * objectUnderTest.haveALittleSleepInMs(_ as Long) >> { /* don't really sleep */ }
+    }
+
+    def 'Startup delay for hostname that ends with digit.'() {
+        given: 'a hostname with a digit at the end'
+            objectUnderTest.getHostName() >> 'host' + lastDigit
+        and: 'the expected delay is based on last digit'
+            def expectedDelay = lastDigit * 1_000
+        when: 'startup delay is called'
+            objectUnderTest.applyHostnameBasedStartupDelay()
+        then: 'the system will sleep for expected time'
+            1 * objectUnderTest.haveALittleSleepInMs(expectedDelay)
+        where: 'following last digits are used'
+            lastDigit << [0, 1]
+    }
+
+    def 'Startup delay for hostname that does not end with digit.'() {
+        given: 'a hostname with a non-digit at the end'
+            objectUnderTest.getHostName() >> 'hostX'
+        and: 'the expected delay is based on hash code with max of 3,000 ms'
+            def expectedDelay =  Math.abs('hostX'.hashCode() % 3_000)
+        when: 'startup delay is called'
+            objectUnderTest.applyHostnameBasedStartupDelay()
+        then: 'the system will sleep for expected time'
+            1 * objectUnderTest.haveALittleSleepInMs(expectedDelay) >> { /* don't really sleep */ }
+    }
+
+    def 'Startup delay when hostname cannot be resolved.'() {
+        given: 'an exception is thrown while getting the hostname'
+            objectUnderTest.getHostName() >> { throw new Exception('some message') }
+        when: 'startup delay is called'
+            objectUnderTest.applyHostnameBasedStartupDelay()
+        then: 'system will not sleep'
+            0 * objectUnderTest.haveALittleSleepInMs(_)
+    }
+
+    def 'Startup delay when sleep is interrupted'() {
+        given: 'sleep method throws InterruptedException'
+            objectUnderTest.haveALittleSleepInMs(_) >> { throw new InterruptedException('some message') }
+        when: 'startup delay is called'
+            objectUnderTest.applyHostnameBasedStartupDelay()
+        then: 'interrupt exception is ignored'
+            noExceptionThrown()
+    }
+
+}