mirror of https://github.com/apache/cassandra
Avoid schema mismatch problems on memtable API misconfiguration
patch by Branimir Lambov; reviewed by Caleb Rackliffe for CASSANDRA-18040
This commit is contained in:
parent
36e16ee3c9
commit
e08c7a6295
|
|
@ -91,9 +91,10 @@ ALTER TABLE ... WITH memtable = 'default';
|
|||
```
|
||||
|
||||
The memtable configuration selection is per table, i.e. it will be propagated to all nodes in the cluster. If some nodes
|
||||
do not have a definition for that configuration, cannot instantiate the class, or are still on a version of Cassandra
|
||||
before 4.1, they will reject the schema change. We therefore recommend using a separate `ALTER` statement to change a
|
||||
table's memtable implementation; upgrading all nodes to 4.1 or later is required to use the API.
|
||||
do not have a definition for that configuration or cannot instantiate the class, they will log an error and fall
|
||||
back to the default memtable configuration to avoid schema disagreements. However, if some nodes are still on a version
|
||||
of Cassandra before 4.1, they will reject the schema change. We therefore recommend using a separate `ALTER` statement
|
||||
to change a table's memtable implementation; upgrading all nodes to 4.1 or later is required to use the API.
|
||||
|
||||
As additional safety when first deploying an alternative implementation to a production cluster, one may consider
|
||||
first deploying a remapped `default` configuration to all nodes in the cluster, switching the schema to reference
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import com.google.common.annotations.VisibleForTesting;
|
|||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.InheritingClass;
|
||||
import org.apache.cassandra.config.ParameterizedClass;
|
||||
|
|
@ -110,6 +112,22 @@ public final class MemtableParams
|
|||
}
|
||||
}
|
||||
|
||||
public static MemtableParams getWithFallback(String key)
|
||||
{
|
||||
try
|
||||
{
|
||||
return get(key);
|
||||
}
|
||||
catch (ConfigurationException e)
|
||||
{
|
||||
LoggerFactory.getLogger(MemtableParams.class).error("Invalid memtable configuration \"" + key + "\" in schema. " +
|
||||
"Falling back to default to avoid schema mismatch.\n" +
|
||||
"Please ensure the correct definition is given in cassandra.yaml.",
|
||||
e);
|
||||
return new MemtableParams(DEFAULT.factory(), key);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static Map<String, ParameterizedClass> expandDefinitions(Map<String, InheritingClass> memtableConfigurations)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -967,7 +967,9 @@ public final class SchemaKeyspace
|
|||
.comment(row.getString("comment"))
|
||||
.compaction(CompactionParams.fromMap(row.getFrozenTextMap("compaction")))
|
||||
.compression(CompressionParams.fromMap(row.getFrozenTextMap("compression")))
|
||||
.memtable(MemtableParams.get(row.has("memtable") ? row.getString("memtable") : null)) // memtable column was introduced in 4.1
|
||||
.memtable(MemtableParams.getWithFallback(row.has("memtable")
|
||||
? row.getString("memtable")
|
||||
: null)) // memtable column was introduced in 4.1
|
||||
.defaultTimeToLive(row.getInt("default_time_to_live"))
|
||||
.extensions(row.getFrozenMap("extensions", UTF8Type.instance, BytesType.instance))
|
||||
.gcGraceSeconds(row.getInt("gc_grace_seconds"))
|
||||
|
|
|
|||
|
|
@ -18,17 +18,28 @@
|
|||
|
||||
package org.apache.cassandra.distributed.test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.cql3.Lists;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.distributed.Cluster;
|
||||
import org.apache.cassandra.distributed.api.Feature;
|
||||
import org.apache.cassandra.distributed.api.ICluster;
|
||||
import org.apache.cassandra.distributed.api.IInstanceConfig;
|
||||
import org.apache.cassandra.distributed.api.IInvokableInstance;
|
||||
import org.apache.cassandra.distributed.api.IIsolatedExecutor;
|
||||
import org.apache.cassandra.distributed.api.SimpleQueryResult;
|
||||
import org.apache.cassandra.distributed.api.TokenSupplier;
|
||||
import org.apache.cassandra.distributed.shared.ClusterUtils;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.utils.Throwables;
|
||||
|
||||
import static org.apache.cassandra.distributed.action.GossipHelper.withProperty;
|
||||
import static org.apache.cassandra.distributed.api.ConsistencyLevel.ONE;
|
||||
|
|
@ -39,6 +50,7 @@ import static org.apache.cassandra.distributed.api.TokenSupplier.evenlyDistribut
|
|||
import static org.apache.cassandra.distributed.shared.NetworkTopology.singleDcNetworkTopology;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class AlterTest extends TestBaseImpl
|
||||
{
|
||||
|
|
@ -107,4 +119,67 @@ public class AlterTest extends TestBaseImpl
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void unknownMemtableConfigurationTest() throws Throwable
|
||||
{
|
||||
Logger logger = LoggerFactory.getLogger(getClass());
|
||||
try (Cluster cluster = Cluster.build(1)
|
||||
.withTokenSupplier(TokenSupplier.evenlyDistributedTokens(3, 1))
|
||||
.withConfig(c -> c.with(Feature.values())
|
||||
.set("memtable", ImmutableMap.of(
|
||||
"configurations", ImmutableMap.of(
|
||||
"testconfig", ImmutableMap.of(
|
||||
"class_name", "SkipListMemtable")))))
|
||||
.start())
|
||||
{
|
||||
init(cluster);
|
||||
cluster.schemaChange("CREATE TABLE " + KEYSPACE + ".tbl (pk int PRIMARY KEY)");
|
||||
|
||||
// Start Node 2 without the memtable configuration definition.
|
||||
IInvokableInstance node1 = cluster.get(1);
|
||||
IInvokableInstance node2 = ClusterUtils.addInstance(cluster, node1.config(), c -> c.set("memtable", ImmutableMap.of()));
|
||||
node2.startup(cluster);
|
||||
|
||||
try
|
||||
{
|
||||
cluster.schemaChange("ALTER TABLE " + KEYSPACE + ".tbl WITH memtable = 'testconfig'", false, node2);
|
||||
fail("Expected ALTER to fail with unknown memtable configuration.");
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
// expected
|
||||
logger.info("Expected: {}", t.getMessage());
|
||||
Assert.assertTrue(Throwables.isCausedBy(t, x -> x.getMessage().matches("Memtable configuration.*not found.*")));
|
||||
}
|
||||
long mark = node2.logs().mark();
|
||||
|
||||
cluster.schemaChange("ALTER TABLE " + KEYSPACE + ".tbl WITH memtable = 'testconfig'", false, node1);
|
||||
// the above should succeed, the configuration is acceptable to node1
|
||||
|
||||
ClusterUtils.awaitGossipSchemaMatch(cluster);
|
||||
List<String> errorInLog = node2.logs().grep(mark,"ERROR.*Invalid memtable configuration.*").getResult();
|
||||
Assert.assertTrue(errorInLog.size() > 0);
|
||||
logger.info(Lists.listToString(errorInLog));
|
||||
|
||||
// Add a new node that has an invalid definition but should accept the already defined table schema.
|
||||
IInvokableInstance node3 = ClusterUtils.addInstance(cluster,
|
||||
node2.config(),
|
||||
c -> c.set("memtable", ImmutableMap.of(
|
||||
"configurations", ImmutableMap.of(
|
||||
"testconfig", ImmutableMap.of(
|
||||
"class_name", "NotExistingMemtable")))));
|
||||
node3.startup(cluster);
|
||||
ClusterUtils.awaitGossipSchemaMatch(cluster);
|
||||
|
||||
errorInLog = node3.logs().grep("ERROR.*Invalid memtable configuration.*").getResult();
|
||||
Assert.assertTrue(errorInLog.size() > 0);
|
||||
logger.info(Lists.listToString(errorInLog));
|
||||
|
||||
// verify that all nodes can write to the table
|
||||
node1.executeInternalWithResult("INSERT INTO " + KEYSPACE + ".tbl (pk) VALUES (?)", 1);
|
||||
node2.executeInternalWithResult("INSERT INTO " + KEYSPACE + ".tbl (pk) VALUES (?)", 2);
|
||||
node3.executeInternalWithResult("INSERT INTO " + KEYSPACE + ".tbl (pk) VALUES (?)", 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue