From b224464450433734a892240bc795787894fbaa13 Mon Sep 17 00:00:00 2001 From: sourabh_sourabh Date: Tue, 8 Apr 2025 11:13:08 +0100 Subject: [PATCH] Introduced a startup delay mechanism based on the container hostname - 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 --- .../src/main/java/org/onap/cps/Application.java | 19 +++++- .../cps/startup/InstanceStartupDelayManager.java | 68 +++++++++++++++++++ .../startup/InstanceStartupDelayManagerSpec.groovy | 78 ++++++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 cps-application/src/main/java/org/onap/cps/startup/InstanceStartupDelayManager.java create mode 100644 cps-application/src/test/groovy/org/onap/cps/startup/InstanceStartupDelayManagerSpec.groovy diff --git a/cps-application/src/main/java/org/onap/cps/Application.java b/cps-application/src/main/java/org/onap/cps/Application.java index 62103bf368..d1c99bcc31 100644 --- a/cps-application/src/main/java/org/onap/cps/Application.java +++ b/cps-application/src/main/java/org/onap/cps/Application.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2020 Nordix Foundation. + * Copyright (C) 2020-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. @@ -20,12 +20,29 @@ package org.onap.cps; +import lombok.extern.slf4j.Slf4j; +import org.onap.cps.startup.InstanceStartupDelayManager; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication +@Slf4j public class Application { + + static InstanceStartupDelayManager instanceStartupDelayManager = new InstanceStartupDelayManager(); + + /** + * The main method which serves as the entry point to the Spring Boot application. + * It first applies a hostname-based startup delay to avoid potential race conditions + * during schema migration in distributed environments. After applying the delay, + * it initializes the Spring Application context and logs the application startup status. + * + * @param args Command-line arguments passed to the application (not used in this implementation). + */ public static void main(final String[] args) { + instanceStartupDelayManager.applyHostnameBasedStartupDelay(); + log.info("Initializing Spring Application context..."); SpringApplication.run(Application.class, args); + log.info("🚀 APPLICATION STARTED"); } } 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 index 0000000000..b221bdc03b --- /dev/null +++ b/cps-application/src/main/java/org/onap/cps/startup/InstanceStartupDelayManager.java @@ -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 index 0000000000..68ef6cce34 --- /dev/null +++ b/cps-application/src/test/groovy/org/onap/cps/startup/InstanceStartupDelayManagerSpec.groovy @@ -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() + } + +} -- 2.16.6