Fix ColumnFilter behaviour to prevent digest mitmatches during upgrades

patch by Jacek Lewandowski; reviewed by Andrés de la Peña and Yifan Cai for CASSANDRA-16415
This commit is contained in:
jacek-lewandowski 2021-03-01 12:24:28 +00:00 committed by Andrés de la Peña
parent 8c43f510a3
commit 865b67b21d
4 changed files with 462 additions and 94 deletions

View File

@ -1,4 +1,5 @@
3.0.25:
* Fix ColumnFilter behaviour to prevent digest mitmatches during upgrades (CASSANDRA-16415)
* Update debian packaging for python3 (CASSANDRA-16396)
* Avoid pushing schema mutations when setting up distributed system keyspaces locally (CASSANDRA-16387)
Merged from 2.2:

View File

@ -26,7 +26,6 @@ import com.google.common.collect.TreeMultimap;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.io.util.DataInputPlus;
@ -141,44 +140,6 @@ public class ColumnFilter
return false;
}
/**
* Whether the provided cell of a complex column is selected by this selection.
*/
public boolean includes(Cell cell)
{
if (isFetchAll || subSelections == null || !cell.column().isComplex())
return true;
SortedSet<ColumnSubselection> s = subSelections.get(cell.column().name);
if (s.isEmpty())
return true;
for (ColumnSubselection subSel : s)
if (subSel.compareInclusionOf(cell.path()) == 0)
return true;
return false;
}
/**
* Whether we can skip the value of the cell of a complex column.
*/
public boolean canSkipValue(ColumnDefinition column, CellPath path)
{
if (!isFetchAll || subSelections == null || !column.isComplex())
return false;
SortedSet<ColumnSubselection> s = subSelections.get(column.name);
if (s.isEmpty())
return false;
for (ColumnSubselection subSel : s)
if (subSel.compareInclusionOf(path) == 0)
return false;
return true;
}
/**
* Creates a new {@code Tester} to efficiently test the inclusion of cells of complex column
* {@code column}.
@ -340,44 +301,31 @@ public class ColumnFilter
Objects.equals(otherCf.subSelections, this.subSelections);
}
@Override
public String toString()
{
String prefix = "";
if (isFetchAll)
return "*";
return "*/*";
if (queried.isEmpty())
return "";
return prefix + "[]";
Iterator<ColumnDefinition> defs = queried.selectOrderIterator();
if (!defs.hasNext())
return "<none>";
StringBuilder sb = new StringBuilder();
appendColumnDef(sb, defs.next());
while (defs.hasNext())
appendColumnDef(sb.append(", "), defs.next());
return sb.toString();
}
private void appendColumnDef(StringBuilder sb, ColumnDefinition column)
{
if (subSelections == null)
StringJoiner joiner = new StringJoiner(", ", "[", "]");
Iterator<ColumnDefinition> it = queried.selectOrderIterator();
while (it.hasNext())
{
sb.append(column.name);
return;
}
ColumnDefinition column = it.next();
SortedSet<ColumnSubselection> s = subSelections != null ? subSelections.get(column.name) : Collections.emptySortedSet();
SortedSet<ColumnSubselection> s = subSelections.get(column.name);
if (s.isEmpty())
{
sb.append(column.name);
return;
if (s.isEmpty())
joiner.add(String.valueOf(column.name));
else
s.forEach(subSel -> joiner.add(String.format("%s%s", column.name, subSel)));
}
int i = 0;
for (ColumnSubselection subSel : s)
sb.append(i++ == 0 ? "" : ", ").append(column.name).append(subSel);
return prefix + joiner.toString();
}
public static class Serializer

View File

@ -0,0 +1,113 @@
/*
* 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.Arrays;
import java.util.UUID;
import org.junit.Assert;
import org.junit.Test;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.distributed.api.ConsistencyLevel;
import org.apache.cassandra.distributed.api.ICoordinator;
public class ReadDigestConsistencyTest extends TestBaseImpl
{
public static final String TABLE_NAME = "tbl";
public static final String CREATE_TABLE = String.format("CREATE TABLE %s.%s (" +
" k int, " +
" c int, " +
" s1 int static, " +
" s2 set<int> static, " +
" v1 int, " +
" v2 set<int>, " +
" PRIMARY KEY (k, c))", KEYSPACE, TABLE_NAME);
public static final String INSERT = String.format("INSERT INTO %s.%s (k, c, s1, s2, v1, v2) VALUES (?, ?, ?, ?, ?, ?)", KEYSPACE, TABLE_NAME);
public static final String SELECT_TRACE = "SELECT activity FROM system_traces.events where session_id = ? and source = ? ALLOW FILTERING;";
@Test
public void testDigestConsistency() throws Exception
{
try (Cluster cluster = init(builder().withNodes(2).start()))
{
cluster.schemaChange(CREATE_TABLE);
insertData(cluster.coordinator(1));
testDigestConsistency(cluster.coordinator(1));
testDigestConsistency(cluster.coordinator(2));
}
}
public static void checkTraceForDigestMismatch(ICoordinator coordinator, String query, Object... boundValues)
{
UUID sessionId = UUID.randomUUID();
coordinator.executeWithTracing(sessionId, query, ConsistencyLevel.ALL, boundValues);
Object[][] results = coordinator.execute(SELECT_TRACE,
ConsistencyLevel.ALL,
sessionId,
coordinator.instance().broadcastAddress().getAddress());
for (Object[] result : results)
{
String activity = (String) result[0];
Assert.assertFalse(String.format("Found Digest Mismatch while executing query: %s with bound values %s on %s/%s",
query,
Arrays.toString(boundValues),
coordinator.instance().broadcastAddress(),
coordinator.instance().getReleaseVersionString()),
activity.toLowerCase().contains("mismatch for key"));
}
}
public static void insertData(ICoordinator coordinator)
{
coordinator.execute(String.format("INSERT INTO %s.%s (k, c, s1, s2, v1, v2) VALUES (1, 1, 2, {1, 2, 3, 4, 5}, 3, {6, 7, 8, 9, 10})", KEYSPACE, TABLE_NAME), ConsistencyLevel.ALL);
coordinator.execute(String.format("INSERT INTO %s.%s (k, c, s1, s2, v1, v2) VALUES (1, 2, 3, {2, 3, 4, 5, 6}, 4, {7, 8, 9, 10, 11})", KEYSPACE, TABLE_NAME), ConsistencyLevel.ALL);
}
public static void testDigestConsistency(ICoordinator coordinator)
{
String queryPattern = "SELECT %s FROM %s.%s WHERE %s";
String[] columnss1 = {
"k, c",
"*",
"v1",
"v2",
"v1, s1",
"v1, s2"
};
String[] columnss2 = {
"s1",
"s2"
};
for (String columns : columnss1)
{
checkTraceForDigestMismatch(coordinator, String.format(queryPattern, columns, KEYSPACE, TABLE_NAME, "k = 1"));
checkTraceForDigestMismatch(coordinator, String.format(queryPattern, columns, KEYSPACE, TABLE_NAME, "k = 1 AND c = 2"));
}
for (String columns : columnss2)
{
checkTraceForDigestMismatch(coordinator, String.format(queryPattern, columns, KEYSPACE, TABLE_NAME, "k = 1"));
}
}
}

View File

@ -18,12 +18,19 @@
package org.apache.cassandra.db.filter;
import java.io.IOException;
import java.util.function.Consumer;
import com.google.common.base.Throwables;
import org.junit.Assert;
import org.junit.Test;
import junit.framework.Assert;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.db.PartitionColumns;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.SetType;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.dht.Murmur3Partitioner;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.io.util.DataInputPlus;
@ -31,40 +38,339 @@ import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.utils.ByteBufferUtil;
import static org.junit.Assert.assertEquals;
public class ColumnFilterTest
{
final static ColumnFilter.Serializer serializer = new ColumnFilter.Serializer();
private static final ColumnFilter.Serializer serializer = new ColumnFilter.Serializer();
private final CFMetaData metadata = CFMetaData.Builder.create("ks", "table")
.withPartitioner(Murmur3Partitioner.instance)
.addPartitionKey("pk", Int32Type.instance)
.addClusteringColumn("ck", Int32Type.instance)
.addStaticColumn("s1", Int32Type.instance)
.addStaticColumn("s2", SetType.getInstance(Int32Type.instance, true))
.addRegularColumn("v1", Int32Type.instance)
.addRegularColumn("v2", SetType.getInstance(Int32Type.instance, true))
.build();
private final ColumnDefinition s1 = metadata.getColumnDefinition(ByteBufferUtil.bytes("s1"));
private final ColumnDefinition s2 = metadata.getColumnDefinition(ByteBufferUtil.bytes("s2"));
private final ColumnDefinition v1 = metadata.getColumnDefinition(ByteBufferUtil.bytes("v1"));
private final ColumnDefinition v2 = metadata.getColumnDefinition(ByteBufferUtil.bytes("v2"));
private final CellPath path0 = CellPath.create(ByteBufferUtil.bytes(0));
private final CellPath path1 = CellPath.create(ByteBufferUtil.bytes(1));
private final CellPath path2 = CellPath.create(ByteBufferUtil.bytes(2));
private final CellPath path3 = CellPath.create(ByteBufferUtil.bytes(3));
private final CellPath path4 = CellPath.create(ByteBufferUtil.bytes(4));
// Select all
@Test
public void columnFilterSerialisationRoundTrip() throws Exception
public void testSelectAll()
{
CFMetaData metadata = CFMetaData.Builder.create("ks", "table")
.withPartitioner(Murmur3Partitioner.instance)
.addPartitionKey("pk", Int32Type.instance)
.addClusteringColumn("ck", Int32Type.instance)
.addRegularColumn("v1", Int32Type.instance)
.addRegularColumn("v2", Int32Type.instance)
.addRegularColumn("v3", Int32Type.instance)
.build();
Consumer<ColumnFilter> check = filter -> {
testRoundTrips(filter);
assertEquals("*/*", filter.toString());
assertFetchedQueried(true, true, filter, v1, v2, s1, s2);
assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
};
ColumnDefinition v1 = metadata.getColumnDefinition(ByteBufferUtil.bytes("v1"));
testRoundTrip(ColumnFilter.all(metadata), metadata, MessagingService.VERSION_30);
testRoundTrip(ColumnFilter.all(metadata), metadata, MessagingService.VERSION_3014);
testRoundTrip(ColumnFilter.selection(metadata.partitionColumns().without(v1)), metadata, MessagingService.VERSION_30);
testRoundTrip(ColumnFilter.selection(metadata.partitionColumns().without(v1)), metadata, MessagingService.VERSION_3014);
testRoundTrip(ColumnFilter.selection(metadata, metadata.partitionColumns().without(v1)), metadata, MessagingService.VERSION_30);
testRoundTrip(ColumnFilter.selection(metadata, metadata.partitionColumns().without(v1)), metadata, MessagingService.VERSION_3014);
check.accept(ColumnFilter.all(metadata));
check.accept(ColumnFilter.allColumnsBuilder(metadata).build());
}
static void testRoundTrip(ColumnFilter columnFilter, CFMetaData metadata, int version) throws Exception
// Selections
@Test
public void testSelectNothing()
{
DataOutputBuffer output = new DataOutputBuffer();
serializer.serialize(columnFilter, output, version);
Assert.assertEquals(serializer.serializedSize(columnFilter, version), output.position());
DataInputPlus input = new DataInputBuffer(output.buffer(), false);
Assert.assertEquals(serializer.deserialize(input, version, metadata), columnFilter);
Consumer<ColumnFilter> check = filter -> {
testRoundTrips(filter);
assertEquals("[]", filter.toString());
assertFetchedQueried(false, false, filter, v1, v2, s1, s2);
assertCellFetchedQueried(false, false, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(false, false, filter, s2, path0, path1, path2, path3, path4);
};
check.accept(ColumnFilter.selection(PartitionColumns.NONE));
check.accept(ColumnFilter.selectionBuilder().build());
}
@Test
public void testSelectSimpleColumn()
{
Consumer<ColumnFilter> check = filter -> {
testRoundTrips(filter);
assertEquals("[v1]", filter.toString());
assertFetchedQueried(true, true, filter, v1);
assertFetchedQueried(false, false, filter, v2, s1, s2);
assertCellFetchedQueried(false, false, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(false, false, filter, s2, path0, path1, path2, path3, path4);
};
check.accept(ColumnFilter.selection(PartitionColumns.builder().add(v1).build()));
check.accept(ColumnFilter.selectionBuilder().add(v1).build());
}
@Test
public void testSelectComplexColumn()
{
Consumer<ColumnFilter> check = filter -> {
testRoundTrips(filter);
assertEquals("[v2]", filter.toString());
assertFetchedQueried(true, true, filter, v2);
assertFetchedQueried(false, false, filter, v1, s1, s2);
assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(false, false, filter, s2, path0, path1, path2, path3, path4);
};
check.accept(ColumnFilter.selection(PartitionColumns.builder().add(v2).build()));
check.accept(ColumnFilter.selectionBuilder().add(v2).build());
}
@Test
public void testSelectStaticColumn()
{
Consumer<ColumnFilter> check = filter -> {
testRoundTrips(filter);
assertEquals("[s1]", filter.toString());
assertFetchedQueried(true, true, filter, s1);
assertFetchedQueried(false, false, filter, v1, v2, s2);
assertCellFetchedQueried(false, false, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(false, false, filter, s2, path0, path1, path2, path3, path4);
};
check.accept(ColumnFilter.selection(PartitionColumns.builder().add(s1).build()));
check.accept(ColumnFilter.selectionBuilder().add(s1).build());
}
@Test
public void testSelectStaticComplexColumn()
{
Consumer<ColumnFilter> check = filter -> {
testRoundTrips(filter);
assertEquals("[s2]", filter.toString());
assertFetchedQueried(true, true, filter, s2);
assertFetchedQueried(false, false, filter, v1, v2, s1);
assertCellFetchedQueried(false, false, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
};
check.accept(ColumnFilter.selection(PartitionColumns.builder().add(s2).build()));
check.accept(ColumnFilter.selectionBuilder().add(s2).build());
}
@Test
public void testSelectColumns()
{
Consumer<ColumnFilter> check = filter -> {
testRoundTrips(filter);
assertEquals("[s1, s2, v1, v2]", filter.toString());
assertFetchedQueried(true, true, filter, v1, v2, s1, s2);
assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
};
check.accept(ColumnFilter.selection(PartitionColumns.builder().add(v1).add(v2).add(s1).add(s2).build()));
check.accept(ColumnFilter.selectionBuilder().add(v1).add(v2).add(s1).add(s2).build());
}
@Test
public void testSelectIndividualCells()
{
ColumnFilter filter = ColumnFilter.selectionBuilder().select(v2, path1).select(v2, path3).build();
testRoundTrips(filter);
assertEquals("[v2[1], v2[3]]", filter.toString());
assertFetchedQueried(true, true, filter, v2);
assertFetchedQueried(false, false, filter, v1, s1, s2);
assertCellFetchedQueried(true, true, filter, v2, path1, path3);
assertCellFetchedQueried(false, false, filter, v2, path0, path2, path4);
assertCellFetchedQueried(false, false, filter, s2, path0, path1, path2, path3, path4);
}
@Test
public void testSelectIndividualCellsFromStatic()
{
ColumnFilter filter = ColumnFilter.selectionBuilder().select(s2, path1).select(s2, path3).build();
testRoundTrips(filter);
assertEquals("[s2[1], s2[3]]", filter.toString());
assertFetchedQueried(true, true, filter, s2);
assertFetchedQueried(false, false, filter, v1, v2, s1);
assertCellFetchedQueried(false, false, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(true, true, filter, s2, path1, path3);
assertCellFetchedQueried(false, false, filter, s2, path0, path2, path4);
}
@Test
public void testSelectCellSlice()
{
ColumnFilter filter = ColumnFilter.selectionBuilder().slice(v2, path1, path3).build();
testRoundTrips(filter);
assertEquals("[v2[1:3]]", filter.toString());
assertFetchedQueried(true, true, filter, v2);
assertFetchedQueried(false, false, filter, v1, s1, s2);
assertCellFetchedQueried(true, true, filter, v2, path1, path2, path3);
assertCellFetchedQueried(false, false, filter, v2, path0, path4);
assertCellFetchedQueried(false, false, filter, s2, path0, path1, path2, path3, path4);
}
@Test
public void testSelectCellSliceFromStatic()
{
ColumnFilter filter = ColumnFilter.selectionBuilder().slice(s2, path1, path3).build();
testRoundTrips(filter);
assertEquals("[s2[1:3]]", filter.toString());
assertFetchedQueried(true, true, filter, s2);
assertFetchedQueried(false, false, filter, v1, v2, s1);
assertCellFetchedQueried(false, false, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(true, true, filter, s2, path1, path2, path3);
assertCellFetchedQueried(false, false, filter, s2, path0, path4);
}
@Test
public void testSelectColumnsWithCellsAndSlices()
{
ColumnFilter filter = ColumnFilter.selectionBuilder()
.add(v1)
.add(s1)
.slice(v2, path0, path2)
.select(v2, path4)
.select(s2, path0)
.slice(s2, path2, path4)
.build();
testRoundTrips(filter);
assertEquals("[s1, s2[0], s2[2:4], v1, v2[0:2], v2[4]]", filter.toString());
assertFetchedQueried(true, true, filter, v1, v2, s1, s2);
assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path4);
assertCellFetchedQueried(false, false, filter, v2, path3);
assertCellFetchedQueried(true, true, filter, s2, path0, path2, path3, path4);
assertCellFetchedQueried(false, false, filter, s2, path1);
}
// select with metadata
@Test
public void testSelectSimpleColumnWithMetadata()
{
Consumer<ColumnFilter> check = filter -> {
testRoundTrips(filter);
assertFetchedQueried(true, true, filter, v1);
assertEquals("*/*", filter.toString());
assertFetchedQueried(true, true, filter, s1, s2, v2);
assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
};
check.accept(ColumnFilter.selection(metadata, PartitionColumns.builder().add(v1).build()));
check.accept(ColumnFilter.allColumnsBuilder(metadata).add(v1).build());
}
@Test
public void testSelectStaticColumnWithMetadata()
{
Consumer<ColumnFilter> check = filter -> {
testRoundTrips(filter);
assertFetchedQueried(true, true, filter, s1);
assertEquals("*/*", filter.toString());
assertFetchedQueried(true, true, filter, v1, v2, s2);
assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
};
check.accept(ColumnFilter.selection(metadata, PartitionColumns.builder().add(s1).build()));
check.accept(ColumnFilter.allColumnsBuilder(metadata).add(s1).build());
}
@Test
public void testSelectCellWithMetadata()
{
ColumnFilter filter = ColumnFilter.allColumnsBuilder(metadata).select(v2, path1).build();
testRoundTrips(filter);
assertFetchedQueried(true, true, filter, v2);
assertEquals("*/*", filter.toString());
assertFetchedQueried(true, true, filter, s1, s2, v1);
assertCellFetchedQueried(true, true, filter, v2, path1);
assertCellFetchedQueried(true, false, filter, v2, path0, path2, path3, path4);
assertCellFetchedQueried(true, true, filter, s2, path0, path1, path2, path3, path4);
}
@Test
public void testSelectStaticColumnCellWithMetadata()
{
ColumnFilter filter = ColumnFilter.allColumnsBuilder(metadata).select(s2, path1).build();
testRoundTrips(filter);
assertFetchedQueried(true, true, filter, s2);
assertEquals("*/*", filter.toString());
assertFetchedQueried(true, true, filter, v1, v2, s1);
assertCellFetchedQueried(true, true, filter, v2, path0, path1, path2, path3, path4);
assertCellFetchedQueried(true, true, filter, s2, path1);
assertCellFetchedQueried(true, false, filter, s2, path0, path2, path3, path4);
}
private void testRoundTrips(ColumnFilter cf)
{
testRoundTrip(cf, MessagingService.VERSION_30);
testRoundTrip(cf, MessagingService.VERSION_3014);
}
private void testRoundTrip(ColumnFilter columnFilter, int version)
{
try
{
DataOutputBuffer output = new DataOutputBuffer();
serializer.serialize(columnFilter, output, version);
Assert.assertEquals(serializer.serializedSize(columnFilter, version), output.position());
DataInputPlus input = new DataInputBuffer(output.buffer(), false);
ColumnFilter deserialized = serializer.deserialize(input, version, metadata);
Assert.assertEquals(deserialized, columnFilter);
}
catch (IOException e)
{
throw Throwables.propagate(e);
}
}
private static void assertFetchedQueried(boolean expectedIncluded,
boolean expectedNotSkipped,
ColumnFilter filter,
ColumnDefinition... columns)
{
for (ColumnDefinition column : columns)
{
assertEquals(String.format("Expected includes(%s) to be %s", column.name, expectedIncluded),
expectedIncluded, filter.includes(column));
if (expectedIncluded)
assertEquals(String.format("Expected canSkipValue(%s) to be %s", column.name, !expectedNotSkipped),
!expectedNotSkipped, filter.canSkipValue(column));
}
}
private static void assertCellFetchedQueried(boolean expectedIncluded,
boolean expectedNotSkipped,
ColumnFilter filter,
ColumnDefinition column,
CellPath... paths)
{
ColumnFilter.Tester tester = filter.newTester(column);
for (CellPath path : paths)
{
int p = ByteBufferUtil.toInt(path.get(0));
if (tester != null)
{
assertEquals(String.format("Expected tester.includes(%s:%s) to be %s", column.name, p, expectedIncluded),
expectedIncluded, tester.includes(path));
if (expectedIncluded)
assertEquals(String.format("Expected tester.canSkipValue(%s:%s) to be %s", column.name, p, !expectedNotSkipped),
!expectedNotSkipped, tester.canSkipValue(path));
}
}
}
}