Change Liquibase to use session lock 09/142709/2
authordanielhanrahan <daniel.hanrahan@est.tech>
Thu, 4 Dec 2025 10:59:36 +0000 (10:59 +0000)
committerdanielhanrahan <daniel.hanrahan@est.tech>
Tue, 9 Dec 2025 00:31:36 +0000 (00:31 +0000)
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 <daniel.hanrahan@est.tech>
runtime-acm/pom.xml
runtime-acm/src/test/java/org/onap/policy/clamp/acm/runtime/liquibase/LiquibaseSessionLockTest.java [new file with mode: 0644]

index a0d4d85..f981202 100644 (file)
             <groupId>org.liquibase</groupId>
             <artifactId>liquibase-core</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.github.blagerweij</groupId>
+            <artifactId>liquibase-sessionlock</artifactId>
+            <version>1.6.9</version>
+        </dependency>
         <dependency>
             <groupId>info.picocli</groupId>
             <artifactId>picocli</artifactId>
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 (file)
index 0000000..87b74ee
--- /dev/null
@@ -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());
+    }
+}