diff --git a/versions/scylla/3.11.5.18/ignore.yaml b/versions/scylla/3.11.5.18/ignore.yaml new file mode 100644 index 0000000..c071e53 --- /dev/null +++ b/versions/scylla/3.11.5.18/ignore.yaml @@ -0,0 +1,23 @@ +tests: + # test count on tracing which its content is different in scylla, should be skipped on scylla + # (should_create_tombstone_when_null_value_on_bound_statement) + - PreparedStatementTest + + # disable because now CCM uses different ip address and port for the JMX + - CCMBridgeTest + + # using 2 node cluster, and stopping one, isn't supported by scylla since raft + # (should_receive_changes_made_while_control_connection_is_down_on_reconnect) + - SchemaChangesCCTest + + # as ScyllaSkip mark doesn't seem to function correctly (skipping, but then failing the test again anyway) + # the class is disabled due to unsupported options used for Scylla (should_keep_reconnecting_on_authentication_error) + - ReconnectionTest + + # scylla-ccm no longer supports --sni-proxy option + - ScyllaSniProxyTest + + # node stop/start sequence causes Scylla to fail to open its binary port within the 5-minute + # CCM timeout, likely due to timing differences vs Cassandra in this driver version + # (should_call_onAdd_with_bootstrap_stop_start) + - NodeRefreshDebouncerTest diff --git a/versions/scylla/3.11.5.18/patch b/versions/scylla/3.11.5.18/patch new file mode 100644 index 0000000..514fe82 --- /dev/null +++ b/versions/scylla/3.11.5.18/patch @@ -0,0 +1,92 @@ +diff --git a/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java b/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java +index 1012033..abf82ce 100644 +--- a/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java ++++ b/driver-core/src/test/java/com/datastax/driver/core/CCMBridge.java +@@ -207,11 +207,14 @@ public class CCMBridge implements CCMAccess { + installArgs.add("-v git:" + branch.trim().replaceAll("\"", "")); + } else if (inputScyllaVersion != null && !inputScyllaVersion.trim().isEmpty()) { + installArgs.add(" --scylla "); +- if (isVersionNumber(inputScyllaVersion)) { +- installArgs.add("-v release:" + inputScyllaVersion); +- } else { +- installArgs.add("-v " + inputScyllaVersion); +- } ++ // Use release: prefix in GitHub Actions (no local tarball); bare version in Jenkins ++ // where SCYLLA_UNIFIED_PACKAGE points CCM to a local tarball via packages_from_env(). ++ String scyllaCcmVersion = ++ System.getenv("SCYLLA_UNIFIED_PACKAGE") != null ++ ? inputScyllaVersion ++ : "release:" + inputScyllaVersion; ++ installArgs.add("-v " + scyllaCcmVersion); ++ + // Detect Scylla Enterprise - it should start with + // a 4-digit year. + if (inputScyllaVersion.matches("\\d{4}\\..*")) { +@@ -480,10 +483,14 @@ public class CCMBridge implements CCMAccess { + + @Override + public InetSocketAddress jmxAddressOfNode(int n) { ++ // For dynamically added nodes (via add()), jmxPorts[] may not have an entry since it is ++ // sized only for nodes declared at cluster-creation time. Fall back to the same deterministic ++ // formula used in add(). ++ int port = (n - 1 < jmxPorts.length) ? jmxPorts[n - 1] : (7000 + n * 100); + if (GLOBAL_SCYLLA_VERSION_NUMBER != null) { +- return new InetSocketAddress(ipOfNode(n), jmxPorts[n - 1]); ++ return new InetSocketAddress(ipOfNode(n), port); + } else { +- return new InetSocketAddress("localhost", jmxPorts[n - 1]); ++ return new InetSocketAddress("localhost", port); + } + } + +@@ -737,6 +744,10 @@ public class CCMBridge implements CCMAccess { + String binaryItf = ipOfNode(n) + ":" + binaryPort; + String remoteLogItf = ipOfNode(n) + ":" + TestUtils.findAvailablePort(); + if (isScylla) { ++ // Use a deterministic JMX port formula to avoid TOCTOU races from findAvailablePort() ++ // and to match the fallback in jmxAddressOfNode() for dynamically added nodes (jmxPorts[] ++ // is sized only for nodes declared at cluster-creation time). ++ int jmxPort = 7000 + n * 100; + // scylla-ccm's `add` command has no thrift option: Scylla never had a Thrift interface. + execute( + CCM_COMMAND +@@ -747,7 +758,7 @@ public class CCMBridge implements CCMAccess { + n, + storageItf, + binaryItf, +- TestUtils.findAvailablePort(), ++ jmxPort, + remoteLogItf); + } else { + String thriftItf = ipOfNode(n) + ":" + thriftPort; +diff --git a/driver-core/src/test/java/com/datastax/driver/core/CCMTestsSupport.java b/driver-core/src/test/java/com/datastax/driver/core/CCMTestsSupport.java +index c8627f1..514ddb8 100644 +--- a/driver-core/src/test/java/com/datastax/driver/core/CCMTestsSupport.java ++++ b/driver-core/src/test/java/com/datastax/driver/core/CCMTestsSupport.java +@@ -1023,7 +1023,10 @@ public class CCMTestsSupport { + try { + keyspace = TestUtils.generateIdentifier("ks_"); + LOGGER.debug("Using keyspace " + keyspace); +- session.execute(String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, keyspace, 1)); ++ boolean isScylla = CCMBridge.getGlobalScyllaVersion() != null; ++ session.execute( ++ String.format(CREATE_KEYSPACE_SIMPLE_FORMAT, keyspace, 1) ++ + (isScylla ? " AND tablets = {'enabled': false}" : "")); + useKeyspace(keyspace); + } catch (Exception e) { + errorOut(); +diff --git a/driver-core/src/test/java/com/datastax/driver/core/SessionStressTest.java b/driver-core/src/test/java/com/datastax/driver/core/SessionStressTest.java +index ea75f84..ea70e90 100644 +--- a/driver-core/src/test/java/com/datastax/driver/core/SessionStressTest.java ++++ b/driver-core/src/test/java/com/datastax/driver/core/SessionStressTest.java +@@ -38,7 +38,9 @@ import org.slf4j.LoggerFactory; + import org.testng.annotations.AfterMethod; + import org.testng.annotations.Test; + +-@CCMConfig(dirtiesContext = true) ++@CCMConfig( ++ dirtiesContext = true, ++ jvmArgs = {"--smp", "1", "--max-networking-io-control-blocks", "15000"}) + public class SessionStressTest extends CCMTestsSupport { + + private static final Logger logger = LoggerFactory.getLogger(SessionStressTest.class); diff --git a/versions/scylla/4.19.2.1/ignore.yaml b/versions/scylla/4.19.2.1/ignore.yaml new file mode 100644 index 0000000..2dc36ca --- /dev/null +++ b/versions/scylla/4.19.2.1/ignore.yaml @@ -0,0 +1,14 @@ +tests: + # awaitScyllaAuth times out waiting for Scylla auth to become available (30s exceeded) + - PlainTextAuthProviderIT + # ccm start fails intermittently when starting clusters inside test methods + # (both TLS and non-TLS variants); ignore the whole class to avoid flakiness + - ClientRoutesIT + # Event-driven config reload is flaky in the matrix runner. + - DriverExecutionProfileReloadIT + # PeersV2NodeRefreshIT fails with BindNodeException on hardcoded port 49152 when + # the port is still occupied from a prior test run in the same CI job. + # Root cause: NodePerPortResolver missing release() override + shared static singleton. + # Fix: https://github.com/scylladb/java-simulacron/pull/5 (issue #4) + # TODO: remove this entry once the simulacron fix is released and picked up in the driver pom.xml + - PeersV2NodeRefreshIT diff --git a/versions/scylla/4.19.2.1/patch b/versions/scylla/4.19.2.1/patch new file mode 100644 index 0000000..e56f5c8 --- /dev/null +++ b/versions/scylla/4.19.2.1/patch @@ -0,0 +1,348 @@ +diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/PeersV2NodeRefreshIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/PeersV2NodeRefreshIT.java +index 9feb7bb..73cba7e 100644 +--- a/integration-tests/src/test/java/com/datastax/oss/driver/core/PeersV2NodeRefreshIT.java ++++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/PeersV2NodeRefreshIT.java +@@ -29,6 +29,7 @@ import com.datastax.oss.simulacron.common.cluster.QueryLog; + import com.datastax.oss.simulacron.server.BoundCluster; + import com.datastax.oss.simulacron.server.Server; + import java.util.concurrent.ExecutionException; ++import org.junit.After; + import org.junit.AfterClass; + import org.junit.BeforeClass; + import org.junit.Test; +@@ -38,6 +39,7 @@ public class PeersV2NodeRefreshIT { + + private static Server peersV2Server; + private static BoundCluster cluster; ++ private static CqlSession session; + + @BeforeClass + public static void setup() { +@@ -55,6 +57,13 @@ public class PeersV2NodeRefreshIT { + } + } + ++ @After ++ public void closeSession() { ++ if (session != null) { ++ session.close(); ++ } ++ } ++ + @Test + public void should_successfully_send_peers_v2_node_refresh_query() + throws InterruptedException, ExecutionException { +diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/clientroutes/ClientRoutesIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/clientroutes/ClientRoutesIT.java +index 9a23f36..872f122 100644 +--- a/integration-tests/src/test/java/com/datastax/oss/driver/core/clientroutes/ClientRoutesIT.java ++++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/clientroutes/ClientRoutesIT.java +@@ -91,6 +91,13 @@ public class ClientRoutesIT { + private static final Logger LOG = LoggerFactory.getLogger(ClientRoutesIT.class); + + private static final int NLB_BASE_PORT = 29042; ++ // Separate base port for should_work_with_mixed_proxy_and_direct_nodes to avoid port collision ++ // with should_survive_full_node_replacement_through_nlb, which also uses NLB_BASE_PORT. ++ // Both tests run sequentially with no delay between them; using distinct ranges eliminates the ++ // risk of SO_REUSEADDR being insufficient to rebind ports still in TIME_WAIT. ++ // NLB_BASE_PORT range: 29042 (discovery), 29043-29046 (nodes 1-4) ++ // NLB_BASE_PORT_2 range: 29100 (discovery), 29101 (node 1) ++ private static final int NLB_BASE_PORT_2 = 29100; + private static final String NLB_ADDRESS = "127.254.254.254"; + private static final String CONNECTION_ID = "11111111-1111-1111-1111-111111111111"; + private static final String DC_NAME = "dc1"; +@@ -829,7 +836,7 @@ public class ClientRoutesIT { + ccm.create(); + ccm.start(); + +- NlbSimulator nlb = new NlbSimulator(ccm, NLB_ADDRESS, NLB_BASE_PORT); ++ NlbSimulator nlb = new NlbSimulator(ccm, NLB_ADDRESS, NLB_BASE_PORT_2); + try { + nlb.addNode(1); + +diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/pool/AdvancedShardAwarenessIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/pool/AdvancedShardAwarenessIT.java +index aeda4a0..782aa2d 100644 +--- a/integration-tests/src/test/java/com/datastax/oss/driver/core/pool/AdvancedShardAwarenessIT.java ++++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/pool/AdvancedShardAwarenessIT.java +@@ -234,7 +234,7 @@ public class AdvancedShardAwarenessIT { + CqlSession session4 = CompletableFutures.getUninterruptibly(stage4); ) { + List allSessions = Arrays.asList(session1, session2, session3, session4); + Awaitility.await() +- .atMost(20, TimeUnit.SECONDS) ++ .atMost(60, TimeUnit.SECONDS) + .pollInterval(500, TimeUnit.MILLISECONDS) + .until(() -> areAllPoolsFullyInitialized(allSessions, expectedChannelsPerNode)); + int tolerance = 2; // Sometimes socket ends up already in use +diff --git a/integration-tests/src/test/java/com/datastax/oss/driver/core/resolver/MockResolverIT.java b/integration-tests/src/test/java/com/datastax/oss/driver/core/resolver/MockResolverIT.java +index 4e9eefe..ab1a5bc 100644 +--- a/integration-tests/src/test/java/com/datastax/oss/driver/core/resolver/MockResolverIT.java ++++ b/integration-tests/src/test/java/com/datastax/oss/driver/core/resolver/MockResolverIT.java +@@ -248,8 +248,10 @@ public class MockResolverIT { + int counter = 0; + while (filteredNodes.size() == 1) { + counter++; +- if (counter == 255) { +- LOG.error("Completed 254 runs. Breaking."); ++ // Capping to 99 in the patch because that's what ccm create --help says is the max id ++ // allowed ++ if (counter == 99) { ++ LOG.error("Completed 99 runs. Breaking."); + break; + } + LOG.warn( +diff --git a/pom.xml b/pom.xml +index 13757e9..ecb01cc 100644 +--- a/pom.xml ++++ b/pom.xml +@@ -100,7 +100,7 @@ + ${skipTests} + false + false +- ++ + false + + +diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java +index b50d568..eb12ba2 100644 +--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java ++++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java +@@ -23,9 +23,14 @@ + */ + package com.datastax.oss.driver.api.testinfra.ccm; + ++import static org.awaitility.Awaitility.await; ++ ++import com.datastax.oss.driver.api.core.AllNodesFailedException; ++import com.datastax.oss.driver.api.core.CqlSession; + import com.datastax.oss.driver.api.core.DefaultProtocolVersion; + import com.datastax.oss.driver.api.core.ProtocolVersion; + import com.datastax.oss.driver.api.core.Version; ++import com.datastax.oss.driver.api.core.auth.ProgrammaticPlainTextAuthProvider; + import com.datastax.oss.driver.api.core.metadata.EndPoint; + import com.datastax.oss.driver.api.testinfra.CassandraResourceRule; + import com.datastax.oss.driver.api.testinfra.ScyllaOnly; +@@ -36,12 +41,17 @@ import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; + import java.net.InetSocketAddress; + import java.util.Collections; + import java.util.Set; ++import java.util.concurrent.TimeUnit; + import org.junit.AssumptionViolatedException; + import org.junit.runner.Description; + import org.junit.runners.model.Statement; ++import org.slf4j.Logger; ++import org.slf4j.LoggerFactory; + + public abstract class BaseCcmRule extends CassandraResourceRule { + ++ private static final Logger LOG = LoggerFactory.getLogger(BaseCcmRule.class); ++ + protected final CcmBridge ccmBridge; + + BaseCcmRule(CcmBridge ccmBridge) { +@@ -62,6 +72,37 @@ public abstract class BaseCcmRule extends CassandraResourceRule { + protected void before() { + ccmBridge.create(); + ccmBridge.start(); ++ if (CcmBridge.isDistributionOf(BackendType.SCYLLA) && ccmBridge.isAuthEnabled()) { ++ awaitScyllaAuth(); ++ } ++ } ++ ++ private void awaitScyllaAuth() { ++ InetSocketAddress contact = new InetSocketAddress(ccmBridge.getNodeIpAddress(1), 9042); ++ LOG.info("Waiting for ScyllaDB superuser to become available at {}", contact); ++ await() ++ .atMost(30, TimeUnit.SECONDS) ++ .pollInterval(1, TimeUnit.SECONDS) ++ .until( ++ () -> { ++ try (CqlSession session = ++ CqlSession.builder() ++ .addContactPoint(contact) ++ .withLocalDatacenter("dc1") ++ .withAuthProvider( ++ new ProgrammaticPlainTextAuthProvider("cassandra", "cassandra")) ++ .build()) { ++ return session.execute("SELECT key FROM system.local WHERE key='local'").one() ++ != null; ++ } catch (AllNodesFailedException e) { ++ LOG.debug("ScyllaDB superuser not ready yet: {}", e.getMessage()); ++ return false; ++ } catch (Exception e) { ++ LOG.debug("ScyllaDB auth check failed unexpectedly: {}", e.getMessage()); ++ return false; ++ } ++ }); ++ LOG.info("ScyllaDB superuser is ready"); + } + + @Override +diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java +index e35c801..a941785 100644 +--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java ++++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java +@@ -185,6 +185,7 @@ public class CcmBridge implements AutoCloseable { + private final List createOptions; + private final List dseWorkloads; + private final String jvmArgs; ++ private final boolean authEnabled; + + private CcmBridge( + Path configDirectory, +@@ -195,7 +196,8 @@ public class CcmBridge implements AutoCloseable { + List dseConfigurationRawYaml, + List createOptions, + Collection jvmArgs, +- List dseWorkloads) { ++ List dseWorkloads, ++ boolean authEnabled) { + this.configDirectory = configDirectory; + if (nodes.length == 1) { + // Hack to ensure that the default DC is always called 'dc1': pass a list ('-nX:0') even if +@@ -237,6 +239,7 @@ public class CcmBridge implements AutoCloseable { + } + this.jvmArgs = allJvmArgs.toString(); + this.dseWorkloads = dseWorkloads; ++ this.authEnabled = authEnabled; + } + + // Copied from Netty's PlatformDependent to avoid the dependency on Netty +@@ -344,7 +347,17 @@ public class CcmBridge implements AutoCloseable { + if (shouldReplace) { + versionString = versionString.replace(".0-", "."); + } +- return "release:" + versionString; ++ // When SCYLLA_UNIFIED_PACKAGE is set, a local ScyllaDB tarball is available (e.g. ++ // Jenkins pre-downloads dev/unreleased builds that are not published to S3). Pass the ++ // bare version string so CCM resolves via packages_from_env() and uses the local file. ++ // When not set (e.g. GH Actions CI), prepend "release:" only for release-repository ++ // versions so CCM downloads them from S3 and dev labels stay unprefixed. ++ if (System.getenv("SCYLLA_UNIFIED_PACKAGE") != null) { ++ return versionString; ++ } ++ return versionString.matches("\\d+(\\.\\d+)+([-.]?(rc|alpha|beta)\\d+)?") ++ ? "release:" + versionString ++ : versionString; + } + // for 4.0 or 5.0 pre-releases, the CCM version string needs to be "4.0-alpha1", "4.0-alpha2" or + // "5.0-beta1" Version.toString() always adds a patch value, even if it's not specified when +@@ -504,7 +517,18 @@ public class CcmBridge implements AutoCloseable { + } + + public void addWithoutStart(int n, String dc) { +- String[] initialArgs = new String[] {"add", "-i", ipPrefix + n, "-d", dc, "node" + n}; ++ // Pass the JMX port explicitly so CCM cannot auto-assign a port that collides with ++ // another node that was added in the same test run. CCM's auto-assign formula is ++ // 7000 + nodeid * 100 + cluster.id, where cluster.id is always 0 for CCM clusters ++ // created by CcmBridge (they are created fresh in a temp config directory). Using ++ // the same formula here ensures the port is predictable and avoids the "JMX port is ++ // already in use" error that occurs when CCM resolves the nodeid from the current ++ // nodelist length rather than from the requested node number. ++ int jmxPort = 7000 + n * 100; ++ String[] initialArgs = ++ new String[] { ++ "add", "-i", ipPrefix + n, "-j", String.valueOf(jmxPort), "-d", dc, "node" + n ++ }; + ArrayList args = new ArrayList<>(Arrays.asList(initialArgs)); + args.addAll(Arrays.asList(DISTRIBUTION.getCcmOptions())); + execute(args.toArray(new String[] {})); +@@ -644,6 +668,15 @@ public class CcmBridge implements AutoCloseable { + return ipPrefix + nodeId; + } + ++ /** ++ * Returns {@code true} if a non-AllowAll authenticator was configured for this cluster. Used by ++ * {@link BaseCcmRule} to determine whether a post-start auth readiness wait is needed on ++ * ScyllaDB, which provisions the default superuser asynchronously. ++ */ ++ boolean isAuthEnabled() { ++ return authEnabled; ++ } ++ + private static final String IN_MS_STR = "_in_ms"; + private static final int IN_MS_STR_LENGTH = IN_MS_STR.length(); + private static final String ENABLE_STR = "enable_"; +@@ -691,6 +724,7 @@ public class CcmBridge implements AutoCloseable { + private String ipPrefix; + private final List createOptions = new ArrayList<>(); + private final List dseWorkloads = new ArrayList<>(); ++ private boolean authEnabled = false; + + private final Path configDirectory; + +@@ -709,6 +743,9 @@ public class CcmBridge implements AutoCloseable { + + public Builder withCassandraConfiguration(String key, Object value) { + cassandraConfiguration.put(key, value); ++ if ("authenticator".equals(key) && !"AllowAllAuthenticator".equals(value)) { ++ authEnabled = true; ++ } + return this; + } + +@@ -737,6 +774,12 @@ public class CcmBridge implements AutoCloseable { + return this; + } + ++ /** Sets a numeric cluster ID prefix used by {@link CustomCcmRule} to avoid IP collisions. */ ++ public Builder withIdPrefix(String idPrefix) { ++ this.ipPrefix = "127.0." + idPrefix + "."; ++ return this; ++ } ++ + /** Adds an option to the {@code ccm create} command. */ + public Builder withCreateOption(String option) { + this.createOptions.add(option); +@@ -808,7 +851,8 @@ public class CcmBridge implements AutoCloseable { + dseRawYaml, + createOptions, + jvmArgs, +- dseWorkloads); ++ dseWorkloads, ++ authEnabled); + } + } + +diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java +index 5ea1bf7..3f7f5be 100644 +--- a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java ++++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java +@@ -17,6 +17,7 @@ + */ + package com.datastax.oss.driver.api.testinfra.ccm; + ++import java.util.concurrent.atomic.AtomicInteger; + import java.util.concurrent.atomic.AtomicReference; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; +@@ -33,6 +34,7 @@ import org.slf4j.LoggerFactory; + public class CustomCcmRule extends BaseCcmRule { + + private static final Logger LOG = LoggerFactory.getLogger(CustomCcmRule.class); ++ private static final AtomicInteger CLUSTER_ID = new AtomicInteger(1); + private static final AtomicReference CURRENT = new AtomicReference<>(); + + CustomCcmRule(CcmBridge ccmBridge) { +@@ -84,6 +86,10 @@ public class CustomCcmRule extends BaseCcmRule { + + private final CcmBridge.Builder bridgeBuilder = CcmBridge.builder(); + ++ public Builder() { ++ this.withIdPrefix(Integer.toString(CLUSTER_ID.incrementAndGet())); ++ } ++ + public Builder withNodes(int... nodes) { + bridgeBuilder.withNodes(nodes); + return this; +@@ -134,6 +140,11 @@ public class CustomCcmRule extends BaseCcmRule { + return this; + } + ++ public Builder withIdPrefix(String idPrefix) { ++ bridgeBuilder.withIdPrefix(idPrefix); ++ return this; ++ } ++ + public CustomCcmRule build() { + return new CustomCcmRule(bridgeBuilder.build()); + }