This is an automated email from the ASF dual-hosted git repository.
paulo pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push:
new a9ee34b62d Add compaction strategy override via system properties
a9ee34b62d is described below
commit a9ee34b62d977893380b0b753c25b2b0aa68fa11
Author: Paulo Motta <[email protected]>
AuthorDate: Thu Feb 12 14:12:45 2026 -0500
Add compaction strategy override via system properties
Introduce the ability to override compaction strategy for specific keyspaces
and tables at startup via two new system properties:
- cassandra.override_compaction.entities: comma-separated list of keyspaces
and keyspace.table pairs (e.g. "ks1,ks2.tbl1,ks3.tbl2")
- cassandra.override_compaction.params: JSON compaction parameters to apply
Patch by Paulo Motta; Reviewed by Jaydeepkumar Chovatia for CASSANDRA-21169
---
CHANGES.txt | 1 +
conf/jvm-server.options | 6 +
.../config/CassandraRelevantProperties.java | 2 +
.../apache/cassandra/service/CassandraDaemon.java | 78 +++++++++++-
.../test/CompactionStrategyOverrideTest.java | 95 +++++++++++++++
.../cassandra/service/CassandraDaemonTest.java | 132 +++++++++++++++++++++
6 files changed, 310 insertions(+), 4 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 0f61587cd1..9d0aafacdb 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
5.1
+ * Allow overriding compaction strategy parameters during startup
(CASSANDRA-21169)
* Introduce created_at column to system_distributed.compression_dictionaries
(CASSANDRA-21178)
* Be able to detect and remove orphaned compression dictionaries
(CASSANDRA-21157)
* Fix BigTableVerifier to only read a data file during extended verification
(CASSANDRA-21150)
diff --git a/conf/jvm-server.options b/conf/jvm-server.options
index b977e710bd..4a397fbfb9 100644
--- a/conf/jvm-server.options
+++ b/conf/jvm-server.options
@@ -113,6 +113,12 @@
# Imposes an upper bound on hint lifetime below the normal min gc_grace_seconds
#-Dcassandra.maxHintTTL=max_hint_ttl_in_seconds
+# Override compaction strategy for specific keyspaces or tables at startup.
+# Entities is a comma-separated list of keyspaces and keyspace.table pairs.
+# Params is a JSON string with the compaction parameters to apply.
+#-Dcassandra.override_compaction.entities=ks1,ks2.tbl1
+#-Dcassandra.override_compaction.params={"class":"SizeTieredCompactionStrategy"}
+
########################
# GENERAL JVM SETTINGS #
########################
diff --git
a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
index cf514b7dfc..9184b78395 100644
--- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
+++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java
@@ -431,6 +431,8 @@ public enum CassandraRelevantProperties
OTCP_LARGE_MESSAGE_THRESHOLD("cassandra.otcp_large_message_threshold",
convertToString(1024 * 64)),
/** Enabled/disable TCP_NODELAY for intradc connections. Defaults is
enabled. */
OTC_INTRADC_TCP_NODELAY("cassandra.otc_intradc_tcp_nodelay", "true"),
+ OVERRIDE_COMPACTION_ENTITIES("cassandra.override_compaction.entities"),
+ OVERRIDE_COMPACTION_PARAMS("cassandra.override_compaction.params"),
OVERRIDE_DECOMMISSION("cassandra.override_decommission"),
PARENT_REPAIR_STATUS_CACHE_SIZE("cassandra.parent_repair_status_cache_size",
"100000"),
PARENT_REPAIR_STATUS_EXPIRY_SECONDS("cassandra.parent_repair_status_expiry_seconds",
convertToString(TimeUnit.SECONDS.convert(1, TimeUnit.DAYS))),
diff --git a/src/java/org/apache/cassandra/service/CassandraDaemon.java
b/src/java/org/apache/cassandra/service/CassandraDaemon.java
index 0144fcae07..4a5c2f1d9d 100644
--- a/src/java/org/apache/cassandra/service/CassandraDaemon.java
+++ b/src/java/org/apache/cassandra/service/CassandraDaemon.java
@@ -26,8 +26,11 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
@@ -44,6 +47,7 @@ import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
+import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -116,6 +120,8 @@ import static
org.apache.cassandra.config.CassandraRelevantProperties.JAVA_CLASS
import static
org.apache.cassandra.config.CassandraRelevantProperties.JAVA_RMI_SERVER_RANDOM_ID;
import static
org.apache.cassandra.config.CassandraRelevantProperties.JAVA_VERSION;
import static
org.apache.cassandra.config.CassandraRelevantProperties.JAVA_VM_NAME;
+import static
org.apache.cassandra.config.CassandraRelevantProperties.OVERRIDE_COMPACTION_ENTITIES;
+import static
org.apache.cassandra.config.CassandraRelevantProperties.OVERRIDE_COMPACTION_PARAMS;
import static
org.apache.cassandra.config.CassandraRelevantProperties.SIZE_RECORDER_INTERVAL;
import static
org.apache.cassandra.config.CassandraRelevantProperties.START_NATIVE_TRANSPORT;
import static
org.apache.cassandra.metrics.CassandraMetricsRegistry.createMetricsKeyspaceTables;
@@ -196,8 +202,8 @@ public class CassandraDaemon
}
@VisibleForTesting
- public static Runnable SPECULATION_THRESHOLD_UPDATER =
- () ->
+ public static Runnable SPECULATION_THRESHOLD_UPDATER =
+ () ->
{
try
{
@@ -209,7 +215,7 @@ public class CassandraDaemon
JVMStabilityInspector.inspectThrowable(t);
}
};
-
+
static final CassandraDaemon instance = new CassandraDaemon();
private volatile NativeTransportService nativeTransportService;
@@ -415,6 +421,8 @@ public class CassandraDaemon
ScheduledExecutors.optionalTasks.schedule(viewRebuild,
StorageService.RING_DELAY_MILLIS, TimeUnit.MILLISECONDS);
StorageService.instance.doAuthSetup();
+ // Apply overrides before re-enabling auto-compaction
+ setCompactionStrategyOverrides(Schema.instance.getKeyspaces());
// re-enable auto-compaction after replay, so correct disk boundaries
are used
enableAutoCompaction(Schema.instance.getKeyspaces());
@@ -427,7 +435,7 @@ public class CassandraDaemon
ScheduledExecutors.optionalTasks.scheduleWithFixedDelay(ColumnFamilyStore.getBackgroundCompactionTaskSubmitter(),
5, 1, TimeUnit.MINUTES);
// schedule periodic recomputation of speculative retry thresholds
-
ScheduledExecutors.optionalTasks.scheduleWithFixedDelay(SPECULATION_THRESHOLD_UPDATER,
+
ScheduledExecutors.optionalTasks.scheduleWithFixedDelay(SPECULATION_THRESHOLD_UPDATER,
DatabaseDescriptor.getReadRpcTimeout(NANOSECONDS),
DatabaseDescriptor.getReadRpcTimeout(NANOSECONDS),
NANOSECONDS);
@@ -564,6 +572,68 @@ public class CassandraDaemon
}
}
+ public static void setCompactionStrategyOverrides(Collection<String>
keyspaces)
+ {
+ if (StringUtils.isBlank(OVERRIDE_COMPACTION_ENTITIES.getString()) ||
StringUtils.isBlank(OVERRIDE_COMPACTION_PARAMS.getString()))
+ {
+ return;
+ }
+
+ Map<String, List<String>> entitiesToChangeCompaction =
parseEntititesToOverrideCompaction();
+ logger.info("Compaction strategy override is enabled via
'cassandra.override_compaction.params' for the following
'cassandra.override_compaction.entities': {}",
+ entitiesToChangeCompaction);
+ String overrideParams = OVERRIDE_COMPACTION_PARAMS.getString();
+
+ for (String ksNme : keyspaces)
+ {
+ Keyspace keyspace = Keyspace.open(ksNme);
+ for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores())
+ {
+ for (final ColumnFamilyStore store : cfs.concatWithIndexes())
+ {
+ List<String> tablesToOverrideCompaction =
entitiesToChangeCompaction.get(ksNme);
+ if (tablesToOverrideCompaction != null &&
(tablesToOverrideCompaction.isEmpty() ||
tablesToOverrideCompaction.contains(store.name)))
+ {
+ logger.info("Overriding compaction parameters for
{}.{} with {}", store.getKeyspaceName(), store.name, overrideParams);
+ cfs.setCompactionParametersJson(overrideParams);
+ }
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static Map<String, List<String>> parseEntititesToOverrideCompaction()
+ {
+ String entitiesCsv = OVERRIDE_COMPACTION_ENTITIES.getString();
+ if (StringUtils.isBlank(entitiesCsv))
+ return Collections.emptyMap();
+
+ // entititesCSV can be like "ks1,ks2,k3.tbl3,ks4.tbl1"
+ Map<String, List<String>> entitiesToChangeCompaction = new HashMap<>();
+ for (String entity : entitiesCsv.split(","))
+ {
+ String[] ksTable = entity.split("\\.");
+ String keyspace = ksTable[0].trim();
+ if (ksTable.length == 1)
+ {
+ entitiesToChangeCompaction.put(keyspace, new
java.util.ArrayList<>());
+ }
+ else if (ksTable.length == 2)
+ {
+ // Empty list for a keyspace means all tables in that keyspace
should be changed, so if we already have an entry for the keyspace with an
empty list,
+ // we can skip adding specific tables for that keyspace as
they are redundant.
+ List<String> existing =
entitiesToChangeCompaction.get(keyspace);
+ if (existing == null || !existing.isEmpty())
+ {
+ String table = ksTable[1].trim();
+ entitiesToChangeCompaction.computeIfAbsent(keyspace, k ->
new java.util.ArrayList<>()).add(table);
+ }
+ }
+ }
+ return entitiesToChangeCompaction;
+ }
+
public void setupVirtualKeyspaces()
{
VirtualKeyspaceRegistry.instance.register(VirtualSchemaKeyspace.instance);
diff --git
a/test/distributed/org/apache/cassandra/distributed/test/CompactionStrategyOverrideTest.java
b/test/distributed/org/apache/cassandra/distributed/test/CompactionStrategyOverrideTest.java
new file mode 100644
index 0000000000..ccf4bcd5e3
--- /dev/null
+++
b/test/distributed/org/apache/cassandra/distributed/test/CompactionStrategyOverrideTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.cassandra.distributed.test;
+
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.distributed.Cluster;
+import org.apache.cassandra.distributed.Constants;
+import org.apache.cassandra.distributed.api.ConsistencyLevel;
+import org.apache.cassandra.distributed.api.Feature;
+
+import static
org.apache.cassandra.config.CassandraRelevantProperties.OVERRIDE_COMPACTION_ENTITIES;
+import static
org.apache.cassandra.config.CassandraRelevantProperties.OVERRIDE_COMPACTION_PARAMS;
+
+public class CompactionStrategyOverrideTest extends TestBaseImpl
+{
+ private static final String OVERRIDE_PARAMS =
"{\"class\":\"org.apache.cassandra.db.compaction.LeveledCompactionStrategy\",\"sstable_size_in_mb\":\"512\"}";
+
+ @Test
+ public void testCompactionStrategyOverrideOnRestart() throws Exception
+ {
+ try (Cluster cluster = init(builder().withNodes(1)
+ .withConfig(config ->
config.with(Feature.NETWORK, Feature.GOSSIP)
+
.set(Constants.KEY_DTEST_FULL_STARTUP, true))
+ .start()))
+ {
+ cluster.coordinator(1).execute("CREATE KEYSPACE ks1 WITH
replication = {'class': 'SimpleStrategy', 'replication_factor': 1}",
ConsistencyLevel.ALL);
+ cluster.coordinator(1).execute("CREATE KEYSPACE ks2 WITH
replication = {'class': 'SimpleStrategy', 'replication_factor': 1}",
ConsistencyLevel.ALL);
+ cluster.coordinator(1).execute("CREATE TABLE ks1.tbl1 (id int
PRIMARY KEY, value text)", ConsistencyLevel.ALL);
+ cluster.coordinator(1).execute("CREATE TABLE ks1.tbl2 (id int
PRIMARY KEY, value text)", ConsistencyLevel.ALL);
+ cluster.coordinator(1).execute("CREATE TABLE ks2.tbl1 (id int
PRIMARY KEY, value text)", ConsistencyLevel.ALL);
+ cluster.coordinator(1).execute("CREATE TABLE ks2.tbl2 (id int
PRIMARY KEY, value text)", ConsistencyLevel.ALL);
+
+ // Verify all tables start with the default
SizeTieredCompactionStrategy
+ cluster.get(1).runOnInstance(() -> {
+ for (String ks : new String[]{ "ks1", "ks2" })
+ for (String tbl : new String[]{ "tbl1", "tbl2" })
+
Assert.assertEquals("org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy",
+
Keyspace.open(ks).getColumnFamilyStore(tbl).getCompactionParameters().get("class"));
+ });
+
+ // Shut down, set override properties, and restart so
CassandraDaemon.setup() applies them
+ cluster.get(1).shutdown().get();
+
+ OVERRIDE_COMPACTION_ENTITIES.setString("ks1.tbl1,ks1,ks2.tbl2");
+ OVERRIDE_COMPACTION_PARAMS.setString(OVERRIDE_PARAMS);
+
+ cluster.get(1).startup();
+
+ cluster.get(1).runOnInstance(() -> {
+ // ks1 was listed as a whole keyspace (ks1.tbl1,ks1 -> ks1
overrides), so both tables should be overridden
+ Map<String, String> ks1tbl1 =
Keyspace.open("ks1").getColumnFamilyStore("tbl1").getCompactionParameters();
+
Assert.assertEquals("org.apache.cassandra.db.compaction.LeveledCompactionStrategy",
ks1tbl1.get("class"));
+ Assert.assertEquals("512", ks1tbl1.get("sstable_size_in_mb"));
+
+ Map<String, String> ks1tbl2 =
Keyspace.open("ks1").getColumnFamilyStore("tbl2").getCompactionParameters();
+
Assert.assertEquals("org.apache.cassandra.db.compaction.LeveledCompactionStrategy",
ks1tbl2.get("class"));
+ Assert.assertEquals("512", ks1tbl2.get("sstable_size_in_mb"));
+
+ // ks2.tbl2 was explicitly listed, so it should be overridden
+ Map<String, String> ks2tbl2 =
Keyspace.open("ks2").getColumnFamilyStore("tbl2").getCompactionParameters();
+
Assert.assertEquals("org.apache.cassandra.db.compaction.LeveledCompactionStrategy",
ks2tbl2.get("class"));
+ Assert.assertEquals("512", ks2tbl2.get("sstable_size_in_mb"));
+
+ // ks2.tbl1 was not listed, so it should retain the default
SizeTieredCompactionStrategy
+ Map<String, String> ks2tbl1 =
Keyspace.open("ks2").getColumnFamilyStore("tbl1").getCompactionParameters();
+
Assert.assertEquals("org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy",
ks2tbl1.get("class"));
+ Assert.assertNull(ks2tbl1.get("sstable_size_in_mb"));
+ });
+
+ System.clearProperty(OVERRIDE_COMPACTION_ENTITIES.getKey());
+ System.clearProperty(OVERRIDE_COMPACTION_PARAMS.getKey());
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/service/CassandraDaemonTest.java
b/test/unit/org/apache/cassandra/service/CassandraDaemonTest.java
new file mode 100644
index 0000000000..965e00b047
--- /dev/null
+++ b/test/unit/org/apache/cassandra/service/CassandraDaemonTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.cassandra.service;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.Test;
+
+import static
org.apache.cassandra.config.CassandraRelevantProperties.OVERRIDE_COMPACTION_ENTITIES;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class CassandraDaemonTest
+{
+ @After
+ public void tearDown()
+ {
+ System.clearProperty(OVERRIDE_COMPACTION_ENTITIES.getKey());
+ }
+
+ @Test
+ public void testParseEntitiesBlankReturnsEmptyMap()
+ {
+ for (String blank : Arrays.asList(null, "", " "))
+ {
+ if (blank == null)
+ System.clearProperty(OVERRIDE_COMPACTION_ENTITIES.getKey());
+ else
+ OVERRIDE_COMPACTION_ENTITIES.setString(blank);
+
+ Map<String, List<String>> result =
CassandraDaemon.parseEntititesToOverrideCompaction();
+ assertTrue(result.isEmpty());
+ }
+ }
+
+ @Test
+ public void testParseEntitiesSingleKeyspace()
+ {
+ OVERRIDE_COMPACTION_ENTITIES.setString("ks1");
+ Map<String, List<String>> result =
CassandraDaemon.parseEntititesToOverrideCompaction();
+ assertEquals(1, result.size());
+ assertTrue(result.get("ks1").isEmpty());
+ }
+
+ @Test
+ public void testParseEntitiesMultipleKeyspaces()
+ {
+ OVERRIDE_COMPACTION_ENTITIES.setString("ks1,ks2,ks3");
+ Map<String, List<String>> result =
CassandraDaemon.parseEntititesToOverrideCompaction();
+ assertEquals(3, result.size());
+ assertTrue(result.get("ks1").isEmpty());
+ assertTrue(result.get("ks2").isEmpty());
+ assertTrue(result.get("ks3").isEmpty());
+ }
+
+ @Test
+ public void testParseEntitiesSpecificTables()
+ {
+ OVERRIDE_COMPACTION_ENTITIES.setString("ks1.tbl1,ks1.tbl2");
+ Map<String, List<String>> result =
CassandraDaemon.parseEntititesToOverrideCompaction();
+ assertEquals(1, result.size());
+ assertEquals(List.of("tbl1", "tbl2"), result.get("ks1"));
+ }
+
+ @Test
+ public void testParseEntitiesMixedKeyspacesAndTables()
+ {
+ OVERRIDE_COMPACTION_ENTITIES.setString("ks1,ks2.tbl1,ks2.tbl2");
+ Map<String, List<String>> result =
CassandraDaemon.parseEntititesToOverrideCompaction();
+ assertEquals(2, result.size());
+ assertTrue(result.get("ks1").isEmpty());
+ assertEquals(List.of("tbl1", "tbl2"), result.get("ks2"));
+ }
+
+ @Test
+ public void testParseEntitiesKeyspaceAfterTableOverrides()
+ {
+ // keyspace-only entry after table-specific entries overrides to all
tables
+ OVERRIDE_COMPACTION_ENTITIES.setString("ks1.tbl1,ks1.tbl2,ks1");
+ Map<String, List<String>> result =
CassandraDaemon.parseEntititesToOverrideCompaction();
+ assertEquals(1, result.size());
+ assertTrue(result.get("ks1").isEmpty());
+ }
+
+ @Test
+ public void testParseEntitiesTableAfterKeyspaceIsIgnored()
+ {
+ // keyspace-only entry selects all tables, subsequent table entries
are ignored
+ OVERRIDE_COMPACTION_ENTITIES.setString("ks1,ks1.tbl1");
+ Map<String, List<String>> result =
CassandraDaemon.parseEntititesToOverrideCompaction();
+ assertEquals(1, result.size());
+ assertTrue(result.get("ks1").isEmpty());
+ }
+
+ @Test
+ public void testParseEntitiesTableAfterKeyspaceOverrideIsIgnored()
+ {
+ OVERRIDE_COMPACTION_ENTITIES.setString("ks1.tbl1,ks1,ks1.tbl2");
+ Map<String, List<String>> result =
CassandraDaemon.parseEntititesToOverrideCompaction();
+ assertEquals(1, result.size());
+ assertTrue(result.get("ks1").isEmpty());
+ }
+
+ @Test
+ public void testParseEntitiesWhitespaceTrimmed()
+ {
+ OVERRIDE_COMPACTION_ENTITIES.setString(" ks1 , ks2 . tbl1 ");
+ Map<String, List<String>> result =
CassandraDaemon.parseEntititesToOverrideCompaction();
+ assertEquals(2, result.size());
+ assertTrue(result.get("ks1").isEmpty());
+ assertEquals(List.of("tbl1"), result.get("ks2"));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]