- 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>
/*\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
--- /dev/null
+/*
+ * ============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
--- /dev/null
+/*
+ * ============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()
+ }
+
+}