From 7d7c498b5d182895755b99813cc931fc5fce94a5 Mon Sep 17 00:00:00 2001 From: danielhanrahan Date: Thu, 4 Dec 2025 10:59:36 +0000 Subject: [PATCH] Change Liquibase to use session lock Add liquibase-sessionlock dependency so that locks will be automatically released on failure, e.g. DB disconnection while Liquibase is running. Issue-ID: POLICY-5526 Change-Id: I8f302b2fc3ef5d5c935e8bfd054faf7890f7deb9 Signed-off-by: danielhanrahan --- runtime-acm/pom.xml | 5 + .../liquibase/LiquibaseSessionLockTest.java | 103 +++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 runtime-acm/src/test/java/org/onap/policy/clamp/acm/runtime/liquibase/LiquibaseSessionLockTest.java diff --git a/runtime-acm/pom.xml b/runtime-acm/pom.xml index a0d4d852e..f981202af 100644 --- a/runtime-acm/pom.xml +++ b/runtime-acm/pom.xml @@ -234,6 +234,11 @@ org.liquibase liquibase-core + + com.github.blagerweij + liquibase-sessionlock + 1.6.9 + info.picocli picocli diff --git a/runtime-acm/src/test/java/org/onap/policy/clamp/acm/runtime/liquibase/LiquibaseSessionLockTest.java b/runtime-acm/src/test/java/org/onap/policy/clamp/acm/runtime/liquibase/LiquibaseSessionLockTest.java new file mode 100644 index 000000000..87b74ee72 --- /dev/null +++ b/runtime-acm/src/test/java/org/onap/policy/clamp/acm/runtime/liquibase/LiquibaseSessionLockTest.java @@ -0,0 +1,103 @@ +/*- + * ============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.policy.clamp.acm.runtime.liquibase; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; +import liquibase.Liquibase; +import liquibase.database.DatabaseFactory; +import liquibase.database.jvm.JdbcConnection; +import liquibase.exception.DatabaseException; +import liquibase.exception.LiquibaseException; +import liquibase.resource.ClassLoaderResourceAccessor; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +@Testcontainers +class LiquibaseSessionLockTest { + + @Container + private static final PostgreSQLContainer postgres = new PostgreSQLContainer<>( + DockerImageName.parse("registry.nordix.org/onaptest/postgres:14.1").asCompatibleSubstituteFor("postgres")); + + @Test + void shouldCleanupStaleLockWhenConnectionFails() throws Exception { + // Given: A Liquibase instance with an active connection + var mainConnection = initConnection(); + var mainLiquibase = initLiquibase(mainConnection); + + // And: A background thread that will kill the connection once a lock is acquired + var connectionKiller = createConnectionKiller(mainConnection); + + // When: We start the connection killer and attempt a Liquibase update + connectionKiller.start(); + assertThrows(LiquibaseException.class, mainLiquibase::update); + connectionKiller.join(5000); + + // Then: The database lock should be automatically cleared + verifyLockIsCleared(); + } + + private static Thread createConnectionKiller(Connection connectionToKill) { + return new Thread(() -> { + try (var lockMonitor = initLiquibase(initConnection())) { + await().atMost(5, TimeUnit.SECONDS) + .pollDelay(50, TimeUnit.MILLISECONDS) + .until(() -> getLockCount(lockMonitor) > 0); + connectionToKill.close(); + } catch (Exception e) { + fail("Failed to monitor locks or close connection: " + e.getMessage(), e); + } + }); + } + + private static void verifyLockIsCleared() throws SQLException, LiquibaseException { + try (var lockVerifier = initLiquibase(initConnection())) { + assertEquals(0, getLockCount(lockVerifier)); + } + } + + private static int getLockCount(Liquibase liquibase) throws LiquibaseException { + return liquibase.listLocks().length; + } + + private static Liquibase initLiquibase(Connection connection) throws DatabaseException { + var database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection)); + return new Liquibase("db/changelog/db.changelog-master.yaml", new ClassLoaderResourceAccessor(), database); + } + + private static Connection initConnection() throws SQLException { + return DriverManager.getConnection( + postgres.getJdbcUrl(), + postgres.getUsername(), + postgres.getPassword()); + } +} -- 2.16.6