From 84eb5571d6723c6f0f1c7933170642f58707feee Mon Sep 17 00:00:00 2001 From: ruhan Date: Tue, 17 Mar 2026 15:20:14 +0800 Subject: [PATCH 1/7] Upgrade to Java 17 and replace cassandra-maven-plugin with testcontainers Migrated the project from Java 11 to Java 17 to take advantage of newer language features and long-term support. Replaced cassandra-maven-plugin with testcontainers for Cassandra integration tests, as the maven plugin does not support Java 17. Changes: - Updated Java version from 11 to 17 in all configurations (pom.xml, toolchains.xml, CI workflows, Dockerfile) - Updated GitHub Actions to use ubuntu-latest instead of ubuntu-22.04 - Removed explicit Maven setup from CI workflows (uses default Maven from ubuntu-latest) - Removed cassandra-maven-plugin and added testcontainers:cassandra dependency - Created CassandraTestResource to manage Cassandra container lifecycle for tests - Updated integration tests to use @QuarkusTestResource with CassandraTestResource - Simplified test configuration by removing hardcoded Cassandra connection details Benefits: - Java 17 LTS support with improved performance and features - Better test isolation with containerized Cassandra instances - Simplified local development workflow - tests can run directly in IDE without manual Cassandra setup - Cleaner CI configuration using default tooling Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/merge-build.yml | 9 +-- .github/workflows/pr-build.yml | 9 +-- README.md | 2 +- pom.xml | 36 ++-------- src/main/image/Dockerfile.jvm | 2 +- .../service/storage/S3StorageIT.java | 2 + .../commonjava/service/storage/StorageIT.java | 22 +++--- .../storage/util/CassandraTestResource.java | 72 +++++++++++++++++++ src/test/resources/application.yaml | 5 +- toolchains.xml | 4 +- 10 files changed, 97 insertions(+), 66 deletions(-) create mode 100644 src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java diff --git a/.github/workflows/merge-build.yml b/.github/workflows/merge-build.yml index 84168a5..7f869cf 100644 --- a/.github/workflows/merge-build.yml +++ b/.github/workflows/merge-build.yml @@ -25,7 +25,7 @@ jobs: publish-snapshot: name: publish to oss sonatype & push image - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest permissions: contents: read @@ -42,12 +42,7 @@ jobs: with: distribution: 'temurin' architecture: x64 - java-version: 11 - - - name: Set up Maven - uses: stCarolas/setup-maven@v4.5 - with: - maven-version: 3.8.8 + java-version: 17 - name: maven-settings-xml-action uses: whelk-io/maven-settings-xml-action@v14 diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index bbc3277..317862b 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -24,7 +24,7 @@ on: [pull_request] jobs: build: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -32,14 +32,9 @@ jobs: - name: Set up JDK uses: actions/setup-java@v2 with: - java-version: '11' + java-version: '17' distribution: 'adopt' - - name: Set up Maven - uses: stCarolas/setup-maven@v4.5 - with: - maven-version: 3.8.8 - - name: maven-settings-xml-action uses: whelk-io/maven-settings-xml-action@v14 with: diff --git a/README.md b/README.md index 50c459c..3e31fe1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Users r/w the files via REST api. Multiple users can r/w same file without affec The concurrent r/w is no surprise when you run a standalone service which only concerns one node. The most significant fact lies on the 'cluster' mode. You can deploy it on a Cloud platform, such as k8s or Openshift, and scale up to as many nodes as you want. The concurrent r/w promise still hold. On cluster mode, all nodes share the same persistent volume and connect to the same Cassandra as the backend DB. ## Prerequisite -1. jdk11 +1. jdk17 2. mvn 3.6.2+ ## Prerequisite for debugging in local diff --git a/pom.xml b/pom.xml index 87733e1..e32b0cf 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,6 @@ UTF-8 3.0 3.0.0 - 3.8 uber-jar 1.19.7 false @@ -196,6 +195,11 @@ localstack test + + org.testcontainers + cassandra + test + @@ -254,7 +258,7 @@ - 11 + 17 OpenJDK @@ -279,34 +283,6 @@ - - org.codehaus.mojo - cassandra-maven-plugin - ${cassandra-maven-plugin.version} - - true - 9942 - true - - ${skipTests} - - - - net.java.dev.jna - jna - 5.8.0 - - - - - cassandra - - start - stop - - - - org.apache.maven.plugins maven-release-plugin diff --git a/src/main/image/Dockerfile.jvm b/src/main/image/Dockerfile.jvm index 24ca0fd..ecf968e 100644 --- a/src/main/image/Dockerfile.jvm +++ b/src/main/image/Dockerfile.jvm @@ -23,7 +23,7 @@ ### FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1 -ARG JAVA_PACKAGE=java-11-openjdk-headless +ARG JAVA_PACKAGE=java-17-openjdk-headless ARG RUN_JAVA_VERSION=1.3.8 ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' diff --git a/src/test/java/org/commonjava/service/storage/S3StorageIT.java b/src/test/java/org/commonjava/service/storage/S3StorageIT.java index 127cb1d..a17cb49 100644 --- a/src/test/java/org/commonjava/service/storage/S3StorageIT.java +++ b/src/test/java/org/commonjava/service/storage/S3StorageIT.java @@ -18,6 +18,7 @@ import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; +import org.commonjava.service.storage.util.CassandraTestResource; import org.commonjava.service.storage.util.LocalStackTestResource; import org.junit.jupiter.api.Test; import software.amazon.awssdk.services.s3.S3Client; @@ -32,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @QuarkusTest +@QuarkusTestResource(CassandraTestResource.class) @QuarkusTestResource(LocalStackTestResource.class) public class S3StorageIT extends StorageIT diff --git a/src/test/java/org/commonjava/service/storage/StorageIT.java b/src/test/java/org/commonjava/service/storage/StorageIT.java index 79b9ea5..e45bf58 100644 --- a/src/test/java/org/commonjava/service/storage/StorageIT.java +++ b/src/test/java/org/commonjava/service/storage/StorageIT.java @@ -15,8 +15,10 @@ */ package org.commonjava.service.storage; +import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.common.http.TestHTTPResource; import org.apache.commons.io.IOUtils; +import org.commonjava.service.storage.util.CassandraTestResource; import org.commonjava.storage.pathmapped.core.PathMappedFileManager; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -27,6 +29,7 @@ import java.io.OutputStream; import java.net.URL; +@QuarkusTestResource(CassandraTestResource.class) public abstract class StorageIT { protected final String PATH = "io/quarkus/quarkus-junit5/quarkus-junit5-1.12.0.Final.jar"; @@ -45,27 +48,18 @@ public abstract class StorageIT public static void init() throws Exception { /* - * The reason I dropped embedded cassandra: - * Previously I use 'cassandra-unit' which is great because we can run the unit tests both - * by mvn command and in IDEA. Unfortunately, after upgrading to Quarkus 3.x, there is a - * dependence change, and it breaks the embedded cassandra. + * Now using testcontainers for Cassandra which supports Java 17. + * The CassandraTestResource starts/stops the container automatically. + * Tests can now be run both from mvn and directly in IDEA. * - * Because of that, I moved to cassandra-maven-plugin in pom.xml. It works well with Quarkus 3 - * to start/stop cassandra. But I have to move Junit tests to integration tests because - * this plugin works for integration phase only. The test classes are refactored as '*IT.java'. - * The downside is that we can not run the IT tests in IDEA by simply clicking the 'Run'. - * We need to do from command line as 'mvn verify'. Or we run 'mvn cassandra:start' beforehand - * then run IT tests in IDEA. - * - * ruhan Feb 9, 2024 + * Updated for Java 17 migration, March 2026 */ - //EmbeddedCassandraServerHelper.startEmbeddedCassandra(); } @AfterAll public static void stop() throws Exception { - //EmbeddedCassandraServerHelper.cleanEmbeddedCassandra(); + // Cleanup handled by CassandraTestResource } @BeforeEach diff --git a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java new file mode 100644 index 0000000..5e80db7 --- /dev/null +++ b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2021 Red Hat, Inc. (https://github.com/Commonjava/service-parent) + * + * 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. + */ +package org.commonjava.service.storage.util; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.testcontainers.containers.CassandraContainer; +import org.testcontainers.utility.DockerImageName; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +public class CassandraTestResource implements QuarkusTestResourceLifecycleManager { + + static DockerImageName dockerImageName = DockerImageName.parse("cassandra:3.11.10"); + static CassandraContainer cassandraContainer = new CassandraContainer<>(dockerImageName) + .withExposedPorts(9042); + + @Override + public Map start() { + cassandraContainer.start(); + + // Load CQL script if it exists + try { + String scriptPath = "src/test/resources/cql/load.cql"; + if (Files.exists(Paths.get(scriptPath))) { + String cqlScript = Files.readString(Paths.get(scriptPath)).trim(); + if (!cqlScript.isEmpty()) { + // Split by semicolon and execute each statement + String[] statements = cqlScript.split(";"); + for (String statement : statements) { + String trimmed = statement.trim(); + if (!trimmed.isEmpty() && !trimmed.startsWith("--") && !trimmed.startsWith("//")) { + cassandraContainer.getSession().execute(trimmed); + } + } + } + } + } catch (IOException e) { + // Script loading is optional, continue without it + } + + HashMap map = new HashMap<>(); + map.put("cassandra.host", cassandraContainer.getHost()); + map.put("cassandra.port", String.valueOf(cassandraContainer.getMappedPort(9042))); + map.put("cassandra.user", cassandraContainer.getUsername()); + map.put("cassandra.pass", cassandraContainer.getPassword()); + return map; + } + + @Override + public void stop() { + if (cassandraContainer != null) { + cassandraContainer.stop(); + } + } +} diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml index b9141cc..3ebae89 100644 --- a/src/test/resources/application.yaml +++ b/src/test/resources/application.yaml @@ -32,11 +32,8 @@ quarkus: swagger-ui: always-include: true +# Cassandra configuration is provided by CassandraTestResource at runtime cassandra: - host: localhost - port: 9942 - user: cassandra - pass: cassandra keyspace: indystorage storage: diff --git a/toolchains.xml b/toolchains.xml index 0314f4a..de1ef43 100644 --- a/toolchains.xml +++ b/toolchains.xml @@ -30,11 +30,11 @@ jdk - 11 + 17 OpenJDK - /usr/lib/jvm/java-11-openjdk + /usr/lib/jvm/java-17-openjdk From cf4be04e13033ee87aab7457653b49576a5f68a3 Mon Sep 17 00:00:00 2001 From: ruhan Date: Tue, 17 Mar 2026 15:43:29 +0800 Subject: [PATCH 2/7] Fix CassandraTestResource compilation error Use withInitScript() instead of getSession() which doesn't exist in CassandraContainer API. This is the proper way to load init scripts with testcontainers. Co-Authored-By: Claude Sonnet 4.5 --- .../storage/util/CassandraTestResource.java | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java index 5e80db7..7cbce8c 100644 --- a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java +++ b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java @@ -19,9 +19,6 @@ import org.testcontainers.containers.CassandraContainer; import org.testcontainers.utility.DockerImageName; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @@ -29,32 +26,13 @@ public class CassandraTestResource implements QuarkusTestResourceLifecycleManage static DockerImageName dockerImageName = DockerImageName.parse("cassandra:3.11.10"); static CassandraContainer cassandraContainer = new CassandraContainer<>(dockerImageName) - .withExposedPorts(9042); + .withExposedPorts(9042) + .withInitScript("cql/load.cql"); @Override public Map start() { cassandraContainer.start(); - // Load CQL script if it exists - try { - String scriptPath = "src/test/resources/cql/load.cql"; - if (Files.exists(Paths.get(scriptPath))) { - String cqlScript = Files.readString(Paths.get(scriptPath)).trim(); - if (!cqlScript.isEmpty()) { - // Split by semicolon and execute each statement - String[] statements = cqlScript.split(";"); - for (String statement : statements) { - String trimmed = statement.trim(); - if (!trimmed.isEmpty() && !trimmed.startsWith("--") && !trimmed.startsWith("//")) { - cassandraContainer.getSession().execute(trimmed); - } - } - } - } - } catch (IOException e) { - // Script loading is optional, continue without it - } - HashMap map = new HashMap<>(); map.put("cassandra.host", cassandraContainer.getHost()); map.put("cassandra.port", String.valueOf(cassandraContainer.getMappedPort(9042))); From ad4f80c75b7ab8fc64f35b5bf6826f92d0ccbd14 Mon Sep 17 00:00:00 2001 From: ruhan Date: Tue, 17 Mar 2026 15:46:36 +0800 Subject: [PATCH 3/7] Remove withInitScript from CassandraTestResource The load.cql file is empty and causing test failures. The application handles its own schema initialization, so no init script is needed. Co-Authored-By: Claude Sonnet 4.5 --- .../commonjava/service/storage/util/CassandraTestResource.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java index 7cbce8c..0a49542 100644 --- a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java +++ b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java @@ -26,8 +26,7 @@ public class CassandraTestResource implements QuarkusTestResourceLifecycleManage static DockerImageName dockerImageName = DockerImageName.parse("cassandra:3.11.10"); static CassandraContainer cassandraContainer = new CassandraContainer<>(dockerImageName) - .withExposedPorts(9042) - .withInitScript("cql/load.cql"); + .withExposedPorts(9042); @Override public Map start() { From f29e31418bbe665e85bfb3ef6b64cff36131895c Mon Sep 17 00:00:00 2001 From: ruhan Date: Tue, 17 Mar 2026 15:52:02 +0800 Subject: [PATCH 4/7] Use getContactPoint() API for Cassandra connection details Use the proper CassandraContainer API to get connection details. getContactPoint() returns the correct host and port for the running container. TestContainers already waits for the container to be ready. Co-Authored-By: Claude Sonnet 4.5 --- .../service/storage/util/CassandraTestResource.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java index 0a49542..075df29 100644 --- a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java +++ b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java @@ -25,16 +25,15 @@ public class CassandraTestResource implements QuarkusTestResourceLifecycleManager { static DockerImageName dockerImageName = DockerImageName.parse("cassandra:3.11.10"); - static CassandraContainer cassandraContainer = new CassandraContainer<>(dockerImageName) - .withExposedPorts(9042); + static CassandraContainer cassandraContainer = new CassandraContainer<>(dockerImageName); @Override public Map start() { cassandraContainer.start(); HashMap map = new HashMap<>(); - map.put("cassandra.host", cassandraContainer.getHost()); - map.put("cassandra.port", String.valueOf(cassandraContainer.getMappedPort(9042))); + map.put("cassandra.host", cassandraContainer.getContactPoint().getHostName()); + map.put("cassandra.port", String.valueOf(cassandraContainer.getContactPoint().getPort())); map.put("cassandra.user", cassandraContainer.getUsername()); map.put("cassandra.pass", cassandraContainer.getPassword()); return map; From de974c60f9e2012174e5efd57580035d1c64005d Mon Sep 17 00:00:00 2001 From: ruhan Date: Tue, 17 Mar 2026 15:58:31 +0800 Subject: [PATCH 5/7] Force IPv4 address for Cassandra connection Use 127.0.0.1 explicitly instead of localhost to avoid IPv6 resolution issues in GitHub Actions. Use getFirstMappedPort() for cleaner API. Co-Authored-By: Claude Sonnet 4.5 --- .../service/storage/util/CassandraTestResource.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java index 075df29..69e66c6 100644 --- a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java +++ b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java @@ -32,8 +32,9 @@ public Map start() { cassandraContainer.start(); HashMap map = new HashMap<>(); - map.put("cassandra.host", cassandraContainer.getContactPoint().getHostName()); - map.put("cassandra.port", String.valueOf(cassandraContainer.getContactPoint().getPort())); + // Use 127.0.0.1 instead of localhost to avoid IPv6 issues + map.put("cassandra.host", "127.0.0.1"); + map.put("cassandra.port", String.valueOf(cassandraContainer.getFirstMappedPort())); map.put("cassandra.user", cassandraContainer.getUsername()); map.put("cassandra.pass", cassandraContainer.getPassword()); return map; From 6621c2e9d1c236ddd65361399c856b6853c02655 Mon Sep 17 00:00:00 2001 From: ruhan Date: Tue, 17 Mar 2026 16:05:27 +0800 Subject: [PATCH 6/7] Use standard TestContainers API for Cassandra connection Use getHost() and getMappedPort(9042) instead of custom approaches. This is the standard pattern used by TestContainers users. Co-Authored-By: Claude Sonnet 4.5 --- .../service/storage/util/CassandraTestResource.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java index 69e66c6..5100e8f 100644 --- a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java +++ b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java @@ -32,9 +32,8 @@ public Map start() { cassandraContainer.start(); HashMap map = new HashMap<>(); - // Use 127.0.0.1 instead of localhost to avoid IPv6 issues - map.put("cassandra.host", "127.0.0.1"); - map.put("cassandra.port", String.valueOf(cassandraContainer.getFirstMappedPort())); + map.put("cassandra.host", cassandraContainer.getHost()); + map.put("cassandra.port", String.valueOf(cassandraContainer.getMappedPort(9042))); map.put("cassandra.user", cassandraContainer.getUsername()); map.put("cassandra.pass", cassandraContainer.getPassword()); return map; From 6fd3d5c118b038cca7f881a525027a80d6031eb4 Mon Sep 17 00:00:00 2001 From: ruhan Date: Tue, 17 Mar 2026 16:11:49 +0800 Subject: [PATCH 7/7] Add readiness check for Cassandra before starting tests Wait for Cassandra to be fully ready by attempting to connect, rather than just checking if the port is open. Cassandra takes time to initialize even after the port is listening. Co-Authored-By: Claude Sonnet 4.5 --- .../storage/util/CassandraTestResource.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java index 5100e8f..0653862 100644 --- a/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java +++ b/src/test/java/org/commonjava/service/storage/util/CassandraTestResource.java @@ -15,6 +15,8 @@ */ package org.commonjava.service.storage.util; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.Session; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import org.testcontainers.containers.CassandraContainer; import org.testcontainers.utility.DockerImageName; @@ -31,6 +33,9 @@ public class CassandraTestResource implements QuarkusTestResourceLifecycleManage public Map start() { cassandraContainer.start(); + // Wait for Cassandra to be truly ready by attempting to connect + waitForCassandraToBeReady(); + HashMap map = new HashMap<>(); map.put("cassandra.host", cassandraContainer.getHost()); map.put("cassandra.port", String.valueOf(cassandraContainer.getMappedPort(9042))); @@ -39,6 +44,27 @@ public Map start() { return map; } + private void waitForCassandraToBeReady() { + int maxAttempts = 30; + for (int i = 0; i < maxAttempts; i++) { + try (Cluster cluster = cassandraContainer.getCluster(); + Session session = cluster.connect()) { + // Successfully connected + return; + } catch (Exception e) { + if (i == maxAttempts - 1) { + throw new RuntimeException("Cassandra did not become ready in time", e); + } + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for Cassandra", ie); + } + } + } + } + @Override public void stop() { if (cassandraContainer != null) {