Fix Accord compaction purger tombstone logic

Accord compaction purgers see random slices of Accord state during compaction (based on randomly selected compaction inputs).

For at least the `durability` column in the `commands` table the tombstone being created when truncating was deleting the latest value since we can get enough information to truncate without actuall yhaving the latest `durability` value.

To fix we can wait to emit a tombstone until we are erasing the entire command row when truncating or truncating with outcome and meanwhile we can drop the extra columns that are no longer needed instead of using a tombstone. We don't need to emit cell tombstones we can drop them from the purger when processing each row.

patch by Ariel Weisberg; reviewed by David Capwell for CASSANDRA-18795
This commit is contained in:
David Capwell 2023-08-16 15:36:01 -07:00 committed by David Capwell
parent ba02bd0bc7
commit 10e065ffe5
9 changed files with 483 additions and 153 deletions

@ -1 +1 @@
Subproject commit 8c7a3c9ef4209d635b186189e17a2d9e728e9871
Subproject commit 1d6028ca20553d1c1a6fe2809b204254955da3b3

View File

@ -25,9 +25,9 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.LongPredicate;
import java.util.function.Supplier;
import javax.annotation.Nonnull;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
@ -54,6 +54,8 @@ import org.apache.cassandra.db.filter.ColumnFilter;
import org.apache.cassandra.db.partitions.PurgeFunction;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterators;
import org.apache.cassandra.db.rows.BTreeRow;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.RangeTombstoneBoundMarker;
import org.apache.cassandra.db.rows.RangeTombstoneMarker;
import org.apache.cassandra.db.rows.Row;
@ -77,6 +79,7 @@ import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.accord.AccordKeyspace;
import org.apache.cassandra.service.accord.AccordKeyspace.CommandRows;
import org.apache.cassandra.service.accord.AccordKeyspace.CommandsColumns;
import org.apache.cassandra.service.accord.AccordKeyspace.CommandsForKeyRows;
import org.apache.cassandra.service.accord.AccordService;
import org.apache.cassandra.service.accord.IAccordService;
@ -86,14 +89,23 @@ import org.apache.cassandra.service.paxos.uncommitted.PaxosRows;
import org.apache.cassandra.utils.Pair;
import org.apache.cassandra.utils.TimeUUID;
import static accord.impl.CommandsForKey.NO_LAST_EXECUTED_HLC;
import static accord.local.Commands.Cleanup.TRUNCATE_WITH_OUTCOME;
import static accord.local.Status.Durability.Universal;
import static com.google.common.base.Preconditions.checkState;
import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static org.apache.cassandra.config.Config.PaxosStatePurging.legacy;
import static org.apache.cassandra.config.DatabaseDescriptor.paxosStatePurging;
import static org.apache.cassandra.service.accord.AccordKeyspace.CommandRows.maybeDropTruncatedCommandColumns;
import static org.apache.cassandra.service.accord.AccordKeyspace.CommandRows.truncatedApply;
import static org.apache.cassandra.service.accord.AccordKeyspace.CommandsForKeyColumns.last_executed_micros;
import static org.apache.cassandra.service.accord.AccordKeyspace.CommandsForKeyColumns.last_executed_timestamp;
import static org.apache.cassandra.service.accord.AccordKeyspace.CommandsForKeyColumns.last_write_timestamp;
import static org.apache.cassandra.service.accord.AccordKeyspace.CommandsForKeyColumns.max_timestamp;
import static org.apache.cassandra.service.accord.AccordKeyspace.CommandsForKeyRows.truncateStaticRow;
import static org.apache.cassandra.service.accord.AccordKeyspace.deserializeDurabilityOrNull;
import static org.apache.cassandra.service.accord.AccordKeyspace.deserializeRouteOrNull;
import static org.apache.cassandra.service.accord.AccordKeyspace.deserializeSaveStatusOrNull;
import static org.apache.cassandra.service.accord.AccordKeyspace.deserializeTimestampOrNull;
/**
* Merge multiple iterators over the content of sstable into a "compacted" iterator.
@ -785,26 +797,43 @@ public class CompactionIterator extends CompactionInfo.Holder implements Unfilte
if (redundantBefore == null)
return row;
Timestamp executeAt = CommandRows.getExecuteAt(row);
Durability durability = CommandRows.getDurability(row);
SaveStatus saveStatus = CommandRows.getStatus(row);
Route<?> route = CommandRows.getRoute(row);
// When commands end up being sliced by compaction we need this to discard tombstones and slices
// without enough information to run the rest of the cleanup logic
if (durableBefore.min(txnId) == Universal)
return null;
Commands.Cleanup cleanup = Commands.shouldCleanup(txnId, saveStatus.status, durability, executeAt, route, redundantBefore, durableBefore);
Cell durabilityCell = row.getCell(CommandsColumns.durability);
Durability durability = deserializeDurabilityOrNull(durabilityCell);
Cell executeAtCell = row.getCell(CommandsColumns.execute_at);
Timestamp executeAt = deserializeTimestampOrNull(executeAtCell);
Cell routeCell = row.getCell(CommandsColumns.route);
Route<?> route = deserializeRouteOrNull(routeCell);
Cell statusCell = row.getCell(CommandsColumns.status);
SaveStatus saveStatus = deserializeSaveStatusOrNull(statusCell);
// With a sliced row we might not have enough columns to determine what to do so output the
// the row unmodified and we will try again later once it merges with the rest of the command state
// or is dropped by `durableBefore.min(txnId) == Universal`
if (executeAt == null || durability == null || saveStatus == null || route == null)
return row;
Commands.Cleanup cleanup = Commands.shouldCleanup(txnId, saveStatus.status, durability, executeAt, route, redundantBefore, durableBefore, false);
switch (cleanup)
{
default: throw new AssertionError(String.format("Unexpected cleanup task: %s", cleanup));
case ERASE:
return null;
// Emit a tombstone so if this is slicing the command and making it not possible to determine if it
// can be truncated later it can still be dropped via the tombstone.
// Eventually the tombstone can be dropped by `durableBefore.min(txnId) == Universal`
// We can still encounter sliced command state just because compaction inputs are random
return BTreeRow.emptyDeletedRow(row.clustering(), new Row.Deletion(DeletionTime.build(row.primaryKeyLivenessInfo().timestamp(), nowInSec), false));
case TRUNCATE_WITH_OUTCOME:
if (saveStatus.compareTo(cleanup.appliesIfNot) >= 0)
return row;
case TRUNCATE:
if (saveStatus.compareTo(cleanup.appliesIfNot) >= 0)
return row;
return truncatedApply(cleanup.appliesIfNot, row, nowInSec, cleanup == TRUNCATE_WITH_OUTCOME);
return maybeDropTruncatedCommandColumns(row, cleanup == TRUNCATE_WITH_OUTCOME, durabilityCell, executeAtCell, routeCell, statusCell);
return truncatedApply(cleanup.appliesIfNot,
row, nowInSec, durability, durabilityCell, executeAtCell, routeCell, cleanup == TRUNCATE_WITH_OUTCOME);
case NO:
return row;
@ -853,45 +882,45 @@ public class CompactionIterator extends CompactionInfo.Holder implements Unfilte
TxnId redundantBeforeTxnId = redundantBeforeEntry.redundantBefore;
boolean updatedColumn = false;
Timestamp max_timestamp = CommandsForKeyRows.getMaxTimestamp(row);
if (max_timestamp.compareTo(redundantBeforeTxnId) < 0)
Cell lastExecuteMicrosCell = row.getCell(last_executed_micros);
Long last_execute_micros = null;
if (lastExecuteMicrosCell != null && !lastExecuteMicrosCell.accessor().isEmpty(lastExecuteMicrosCell.value()))
last_execute_micros = lastExecuteMicrosCell.accessor().getLong(lastExecuteMicrosCell.value(), 0);
if (last_execute_micros != null && last_execute_micros < redundantBeforeTxnId.hlc())
{
max_timestamp = Timestamp.NONE;
updatedColumn = true;
lastExecuteMicrosCell = null;
}
Timestamp last_execute = CommandsForKeyRows.getLastExecutedTimestamp(row);
if (last_execute.compareTo(redundantBeforeTxnId) < 0)
Cell lastExecuteCell = row.getCell(last_executed_timestamp);
Timestamp last_execute = deserializeTimestampOrNull(lastExecuteCell);
if (last_execute != null && last_execute.compareTo(redundantBeforeTxnId) < 0)
{
last_execute = Timestamp.NONE;
updatedColumn = true;
lastExecuteCell = null;
}
Timestamp last_write = CommandsForKeyRows.getLastWriteTimestamp(row);
if (last_write.compareTo(redundantBeforeTxnId) < 0)
Cell lastWriteCell = row.getCell(last_write_timestamp);
Timestamp last_write = deserializeTimestampOrNull(lastWriteCell);
if (last_write != null && last_write.compareTo(redundantBeforeTxnId) < 0)
{
last_write = Timestamp.NONE;
updatedColumn = true;
lastWriteCell = null;
}
long last_execute_micros = CommandsForKeyRows.getLastExecutedMicros(row);
if (last_execute_micros < redundantBeforeTxnId.hlc())
Cell<?> maxTimestampCell = row.getCell(max_timestamp);
Timestamp max_timestamp = deserializeTimestampOrNull(maxTimestampCell);
if (max_timestamp != null && max_timestamp.compareTo(redundantBeforeTxnId) < 0)
{
last_execute_micros = NO_LAST_EXECUTED_HLC;
updatedColumn = true;
maxTimestampCell = null;
}
if (max_timestamp == Timestamp.NONE &&
last_execute == Timestamp.NONE &&
last_write == Timestamp.NONE &&
last_execute_micros == NO_LAST_EXECUTED_HLC)
// No need to emit a tombstone as earlier versions of the row will also be nulled out
// when compacted later or loaded into a commands for key
if (lastExecuteMicrosCell == null &&
lastExecuteCell == null &&
lastWriteCell == null &&
maxTimestampCell == null)
return null;
if (updatedColumn)
return truncateStaticRow(nowInSec, row, last_execute_micros, last_execute, last_write, max_timestamp);
return row;
return truncateStaticRow(nowInSec, row, lastExecuteMicrosCell, lastExecuteCell, lastWriteCell, maxTimestampCell);
}
@Override
@ -910,7 +939,7 @@ public class CompactionIterator extends CompactionInfo.Holder implements Unfilte
TxnId redundantBeforeTxnId = redundantBeforeEntry.redundantBefore;
Timestamp timestamp = CommandsForKeyRows.getTimestamp(row);
if (timestamp.compareTo(redundantBeforeTxnId) < 0)
if (timestamp != null && timestamp.compareTo(redundantBeforeTxnId) < 0)
return null;
return row;
@ -966,9 +995,4 @@ public class CompactionIterator extends CompactionInfo.Holder implements Unfilte
{
return cfs.name.equals(AccordKeyspace.COMMANDS_FOR_KEY) && cfs.keyspace.getName().equals(SchemaConstants.ACCORD_KEYSPACE_NAME);
}
private static boolean isAccordCommandsOrAccordCommandsForKey(ColumnFamilyStore cfs)
{
return isAccordCommands(cfs) || isAccordCommandsForKey(cfs);
}
}

View File

@ -351,6 +351,11 @@ public class TupleType extends MultiElementType<ByteBuffer>
return "component";
}
public static <V> V pack(ValueAccessor<V> accessor, V... components)
{
return pack(accessor, Arrays.asList(components));
}
public static <V> V pack(ValueAccessor<V> accessor, Collection<V> components)
{
int totalLength = 0;

View File

@ -62,6 +62,7 @@ import accord.local.Node;
import accord.local.RedundantBefore;
import accord.local.SaveStatus;
import accord.local.Status;
import accord.local.Status.Durability;
import accord.primitives.Ballot;
import accord.primitives.Deps;
import accord.primitives.PartialDeps;
@ -119,6 +120,7 @@ import org.apache.cassandra.db.rows.BufferCell;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.CellPath;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.Row.Deletion;
import org.apache.cassandra.db.rows.RowIterator;
import org.apache.cassandra.db.transform.FilteredPartitions;
import org.apache.cassandra.dht.ByteOrderedPartitioner;
@ -158,6 +160,8 @@ import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.btree.BTree;
import org.apache.cassandra.utils.bytecomparable.ByteComparable;
import static accord.utils.Invariants.checkArgument;
import static accord.utils.Invariants.checkState;
import static java.lang.String.format;
import static org.apache.cassandra.cql3.QueryProcessor.executeInternal;
import static org.apache.cassandra.cql3.QueryProcessor.executeOnceInternal;
@ -286,17 +290,17 @@ public class AccordKeyspace
return column;
}
private static class CommandsColumns
public static class CommandsColumns
{
static final ClusteringComparator keyComparator = Commands.partitionKeyAsClusteringComparator();
static final CompositeType partitionKeyType = (CompositeType) Commands.partitionKeyType;
static final ColumnMetadata txn_id = getColumn(Commands, "txn_id");
static final ColumnMetadata store_id = getColumn(Commands, "store_id");
static final ColumnMetadata status = getColumn(Commands, "status");
static final ColumnMetadata route = getColumn(Commands, "route");
static final ColumnMetadata durability = getColumn(Commands, "durability");
public static final ColumnMetadata status = getColumn(Commands, "status");
public static final ColumnMetadata route = getColumn(Commands, "route");
public static final ColumnMetadata durability = getColumn(Commands, "durability");
static final ColumnMetadata txn = getColumn(Commands, "txn");
static final ColumnMetadata execute_at = getColumn(Commands, "execute_at");
public static final ColumnMetadata execute_at = getColumn(Commands, "execute_at");
static final ColumnMetadata promised_ballot = getColumn(Commands, "promised_ballot");
static final ColumnMetadata accepted_ballot = getColumn(Commands, "accepted_ballot");
static final ColumnMetadata dependencies = getColumn(Commands, "dependencies");
@ -305,7 +309,7 @@ public class AccordKeyspace
static final ColumnMetadata waiting_on = getColumn(Commands, "waiting_on");
static final ColumnMetadata listeners = getColumn(Commands, "listeners");
static ColumnMetadata[][] TRUNCATE_FIELDS = new ColumnMetadata[][] {
public static ColumnMetadata[][] TRUNCATE_FIELDS = new ColumnMetadata[][] {
new ColumnMetadata[] { durability, execute_at, route, status },
new ColumnMetadata[] { durability, execute_at, result, route, status, writes },
};
@ -315,10 +319,9 @@ public class AccordKeyspace
for (ColumnMetadata[] cds : TRUNCATE_FIELDS)
{
for (int i = 1 ; i < cds.length ; ++i)
Invariants.checkState(cds[i - 1].compareTo(cds[i]) < 0);
checkState(cds[i - 1].compareTo(cds[i]) < 0);
}
}
}
public static class CommandRows extends CommandsColumns
@ -338,29 +341,41 @@ public class AccordKeyspace
return deserializeTimestampOrNull(partitionKeyComponents[txn_id.position()], ByteBufferAccessor.instance, TxnId::fromBits);
}
@Nullable
public static Timestamp getExecuteAt(Row row)
{
Cell cell = row.getCell(execute_at);
if (cell == null)
return null;
return deserializeTimestampOrNull(cell.value(), cell.accessor(), Timestamp::fromBits);
}
@Nullable
public static SaveStatus getStatus(Row row)
{
Cell cell = row.getCell(status);
if (cell == null)
return null;
int ordinal = cell.accessor().getInt(cell.value(), 0);
return CommandSerializers.saveStatus.forOrdinal(ordinal);
}
@Nullable
public static Status.Durability getDurability(Row row)
{
Cell cell = row.getCell(durability);
if (cell == null)
return null;
int ordinal = cell.accessor().getInt(cell.value(), 0);
return CommandSerializers.durability.forOrdinal(ordinal);
}
@Nullable
public static Route<?> getRoute(Row row)
{
Cell cell = row.getCell(route);
if (cell == null)
return null;
try
{
return deserializeOrNull(cell.buffer(), LocalVersionedSerializers.route);
@ -371,22 +386,94 @@ public class AccordKeyspace
}
}
public static Row truncatedApply(SaveStatus newSaveStatus, Row row, long nowInSec, boolean withOutcome)
private static Object[] truncatedApplyLeaf(long newTimestamp, SaveStatus newSaveStatus, Cell durabilityCell, Cell executeAtCell, @Nullable Cell resultCell, Cell routeCell, @Nullable Cell writesCell, boolean updateTimestamps)
{
checkArgument(durabilityCell.column() == CommandsColumns.durability);
checkArgument(executeAtCell.column() == CommandsColumns.execute_at);
checkArgument(resultCell == null || resultCell.column() == CommandsColumns.result);
checkArgument(routeCell.column() == CommandsColumns.route);
checkArgument(writesCell == null || writesCell.column() == CommandsColumns.writes);
boolean includeOutcome = resultCell != null;
Object[] newLeaf = BTree.unsafeAllocateNonEmptyLeaf(TRUNCATE_FIELDS[includeOutcome ? 1 : 0].length);
int colIndex = 0;
newLeaf[colIndex++] = updateTimestamps ? durabilityCell.withUpdatedTimestamp(newTimestamp) : durabilityCell;
newLeaf[colIndex++] = updateTimestamps ? executeAtCell.withUpdatedTimestamp(newTimestamp) : executeAtCell;
if (includeOutcome)
newLeaf[colIndex++] = updateTimestamps ? resultCell.withUpdatedTimestamp(newTimestamp) : resultCell;
newLeaf[colIndex++] = updateTimestamps ? routeCell.withUpdatedTimestamp(newTimestamp) : routeCell;
// Status always needs to use the new timestamp since we are replacing the existing value
// All the other columns are being retained unmodified with at most updated timestamps to accomdate deletion
newLeaf[colIndex++] = BufferCell.live(status, newTimestamp, ByteBufferAccessor.instance.valueOf(newSaveStatus.ordinal()));
if (includeOutcome)
newLeaf[colIndex++] = updateTimestamps ? writesCell.withUpdatedTimestamp(newTimestamp) : writesCell;
return newLeaf;
}
public static Row truncatedApply(SaveStatus newSaveStatus, Row row, long nowInSec, Durability durability, Cell durabilityCell, Cell executeAtCell, Cell routeCell, boolean withOutcome)
{
checkArgument(durabilityCell.column() == CommandsColumns.durability);
checkArgument(executeAtCell.column() == CommandsColumns.execute_at);
checkArgument(routeCell.column() == CommandsColumns.route);
long oldTimestamp = row.primaryKeyLivenessInfo().timestamp();
long newTimestamp = oldTimestamp + 1;
Cell resultCell = withOutcome ? row.getCell(CommandsColumns.result) : null;
Cell writesCell = withOutcome ? row.getCell(CommandsColumns.writes) : null;
checkState((resultCell != null) == (writesCell != null), "result and writes should always be set together");
boolean doDeletion = true;
// If durability is not universal we don't want to delete older versions of the row that might have recorded
// a higher durability value. maybeDropTruncatedCommandColumns will take care of dropping things even if we don't drop via tombstones.
// durability should be the only column that could have an older value that is insufficient for propagating forward
if (durability != Durability.Universal)
doDeletion = false;
// We may not have what we need to generate a deletion and include the outcome in the truncated row
// so need to wait until we can have the outcome to issue the deletion otherwise it would be shadowed and lost
if (withOutcome && resultCell == null)
doDeletion = false;
ColumnMetadata[] fields = TRUNCATE_FIELDS[withOutcome ? 1 : 0];
Object[] newLeaf = BTree.unsafeAllocateNonEmptyLeaf(fields.length);
for (int i = 0 ; i < fields.length ; ++i)
{
if (fields[i] == status) newLeaf[i] = BufferCell.live(status, newTimestamp, ByteBufferAccessor.instance.valueOf(newSaveStatus.ordinal()));
else newLeaf[i] = row.getCell(fields[i]).withUpdatedTimestamp(newTimestamp);
}
Object[] newLeaf = truncatedApplyLeaf(newTimestamp, newSaveStatus, durabilityCell, executeAtCell, resultCell, routeCell, writesCell, doDeletion);
// Including a deletion allows future compactions to drop data before it gets to the purger
// but it is pretty optional because maybeDropTruncatedCommandColumns will drop the extra columns
// regardless
Row.Deletion deletion = doDeletion ? new Row.Deletion(DeletionTime.build(oldTimestamp, nowInSec), false) : Deletion.LIVE;
return BTreeRow.create(row.clustering(), LivenessInfo.create(newTimestamp, nowInSec),
new Row.Deletion(DeletionTime.build(oldTimestamp, nowInSec), false),
newLeaf);
deletion, newLeaf);
}
public static Row maybeDropTruncatedCommandColumns(Row row, boolean withOutcome, Cell durabilityCell, Cell executeAtCell, Cell routeCell, Cell statusCell)
{
checkArgument(durabilityCell.column() == CommandsColumns.durability);
checkArgument(executeAtCell.column() == CommandsColumns.execute_at);
checkArgument(routeCell.column() == CommandsColumns.route);
checkArgument(statusCell.column() == CommandsColumns.status);
int colCount = row.columnCount();
// If it's the exact length of the post truncate column count without outcome fields
// then it is exactly the columns needed for getting this far and withOutcome doesn't matter since
// nothing additional is available to include anyways
if (colCount == TRUNCATE_FIELDS[0].length)
return row;
Cell resultCell = row.getCell(CommandsColumns.result);
Cell writesCell = row.getCell(CommandsColumns.writes);
checkState((resultCell != null) == (writesCell != null), "result and writes should always be set together");
boolean includeOutcome = withOutcome && resultCell != null;
// This has just the columns needed for truncation with outcome so return it unmodified
if (colCount == TRUNCATE_FIELDS[1].length && includeOutcome)
return row;
// Construct a replacement with just the available columns that are still needed
Object[] newLeaf = BTree.unsafeAllocateNonEmptyLeaf(TRUNCATE_FIELDS[includeOutcome ? 1 : 0].length);
int colIndex = 0;
newLeaf[colIndex++] = durabilityCell;
newLeaf[colIndex++] = executeAtCell;
if (includeOutcome)
newLeaf[colIndex++] = resultCell;
newLeaf[colIndex++] = routeCell;
newLeaf[colIndex++] = statusCell;
if (includeOutcome)
newLeaf[colIndex++] = writesCell;
return BTreeRow.create(row.clustering(), row.primaryKeyLivenessInfo(), row.deletion(), newLeaf);
}
public static Result getResult(Row row) throws IOException
@ -419,7 +506,7 @@ public class AccordKeyspace
.partitioner(new LocalPartitioner(CompositeType.getInstance(Int32Type.instance, BytesType.instance, KEY_TYPE)))
.build();
private static class CommandsForKeyColumns
public static class CommandsForKeyColumns
{
static final ClusteringComparator keyComparator = CommandsForKeys.partitionKeyAsClusteringComparator();
static final CompositeType partitionKeyType = (CompositeType) CommandsForKeys.partitionKeyType;
@ -428,10 +515,10 @@ public class AccordKeyspace
static final ColumnMetadata key_token = getColumn(CommandsForKeys, "key_token");
static final ColumnMetadata key = getColumn(CommandsForKeys, "key");
static final ColumnMetadata timestamp = getColumn(CommandsForKeys, "timestamp");
static final ColumnMetadata max_timestamp = getColumn(CommandsForKeys, "max_timestamp");
static final ColumnMetadata last_executed_timestamp = getColumn(CommandsForKeys, "last_executed_timestamp");
static final ColumnMetadata last_executed_micros = getColumn(CommandsForKeys, "last_executed_micros");
static final ColumnMetadata last_write_timestamp = getColumn(CommandsForKeys, "last_write_timestamp");
public static final ColumnMetadata max_timestamp = getColumn(CommandsForKeys, "max_timestamp");
public static final ColumnMetadata last_executed_timestamp = getColumn(CommandsForKeys, "last_executed_timestamp");
public static final ColumnMetadata last_executed_micros = getColumn(CommandsForKeys, "last_executed_micros");
public static final ColumnMetadata last_write_timestamp = getColumn(CommandsForKeys, "last_write_timestamp");
static final ColumnMetadata data = getColumn(CommandsForKeys, "data");
@ -486,6 +573,7 @@ public class AccordKeyspace
return Int32Type.instance.compose(partitionKeyComponents[store_id.position()]);
}
@Nullable
public static Timestamp getMaxTimestamp(Row row)
{
Cell cell = row.getCell(max_timestamp);
@ -494,6 +582,7 @@ public class AccordKeyspace
return deserializeTimestampOrNull(cell.value(), cell.accessor(), Timestamp::fromBits);
}
@Nullable
public static Timestamp getLastExecutedTimestamp(Row row)
{
Cell cell = row.getCell(last_executed_timestamp);
@ -515,11 +604,13 @@ public class AccordKeyspace
return deserializeKey(partitionKeyComponents[key.position()]);
}
@Nullable
public static Timestamp getTimestamp(Row row)
{
return deserializeTimestampOrNull(row.clustering().bufferAt(CommandsForKeyColumns.timestamp.position()), Timestamp::fromBits);
}
@Nullable
public static Timestamp getLastWriteTimestamp(Row row)
{
Cell cell = row.getCell(last_write_timestamp);
@ -528,21 +619,41 @@ public class AccordKeyspace
return deserializeTimestampOrNull(cell.value(), cell.accessor(), Timestamp::fromBits);
}
public static Row truncateStaticRow(long nowInSec, Row row, long last_execute_micros, Timestamp last_execute, Timestamp last_write, Timestamp max_timestamp)
public static Row truncateStaticRow(long nowInSec, Row row, Cell lastExecuteMicrosCell, Cell lastExecuteCell, Cell lastWriteCell, Cell maxTimestampCell)
{
long oldTimestamp = row.primaryKeyLivenessInfo().timestamp();
long newTimestamp = oldTimestamp + 1;
checkArgument(lastExecuteMicrosCell == null || lastExecuteMicrosCell.column() == CommandsForKeyColumns.last_executed_micros);
checkArgument(lastExecuteCell == null || lastExecuteCell.column() == CommandsForKeyColumns.last_executed_timestamp);
checkArgument(lastWriteCell == null || lastWriteCell.column() == CommandsForKeyColumns.last_write_timestamp);
checkArgument(maxTimestampCell == null || maxTimestampCell.column() == CommandsForKeyColumns.max_timestamp);
long timestamp = row.primaryKeyLivenessInfo().timestamp();
int colCount = 0;
if (lastExecuteMicrosCell != null)
colCount++;
if (lastExecuteCell != null)
colCount++;
if (lastWriteCell != null)
colCount++;
if (maxTimestampCell != null)
colCount++;
ColumnMetadata[] fields = CommandsForKeyColumns.static_columns_metadata;
Object[] newLeaf = BTree.unsafeAllocateNonEmptyLeaf(fields.length);
newLeaf[0] = BufferCell.live(fields[0], newTimestamp, ByteBufferAccessor.instance.valueOf(last_execute_micros));
newLeaf[1] = BufferCell.live(fields[1], newTimestamp, serializeTimestamp(last_execute));
newLeaf[2] = BufferCell.live(fields[2], newTimestamp, serializeTimestamp(last_write));
newLeaf[3] = BufferCell.live(fields[3], newTimestamp, serializeTimestamp(max_timestamp));
checkState(fields.length >= colCount, "CommandsForKeyColumns.static_columns_metadata should include all the columns");
Object[] newLeaf = BTree.unsafeAllocateNonEmptyLeaf(colCount);
int colIndex = 0;
return BTreeRow.create(row.clustering(), LivenessInfo.create(newTimestamp, nowInSec),
new Row.Deletion(DeletionTime.build(oldTimestamp, nowInSec), false),
newLeaf);
if (lastExecuteMicrosCell != null)
newLeaf[colIndex++] = lastExecuteMicrosCell;
if (lastExecuteCell != null)
newLeaf[colIndex++] = lastExecuteCell;
if (lastWriteCell != null)
newLeaf[colIndex++] = lastWriteCell;
if (maxTimestampCell != null)
newLeaf[colIndex++] = maxTimestampCell;
return BTreeRow.create(row.clustering(), LivenessInfo.create(timestamp, nowInSec),
Deletion.LIVE, newLeaf);
}
}
@ -791,6 +902,7 @@ public class AccordKeyspace
Row.Builder builder = BTreeRow.unsortedBuilder();
builder.newRow(Clustering.EMPTY);
int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(timestampMicros);
builder.addPrimaryKeyLivenessInfo(LivenessInfo.create(timestampMicros, nowInSeconds));
addEnumCellIfModified(CommandsColumns.status, Command::saveStatus, builder, timestampMicros, nowInSeconds, original, command);
addCellIfModified(CommandsColumns.route, Command::route, LocalVersionedSerializers.route, builder, timestampMicros, nowInSeconds, original, command);
@ -864,6 +976,7 @@ public class AccordKeyspace
T create(long msb, long lsb, Node.Id node);
}
@Nullable
public static <T extends Timestamp> T deserializeTimestampOrNull(ByteBuffer bytes, TimestampFactory<T> factory)
{
if (bytes == null || ByteBufferAccessor.instance.isEmpty(bytes))
@ -872,6 +985,18 @@ public class AccordKeyspace
return factory.create(split.get(0).getLong(), split.get(1).getLong(), new Node.Id(split.get(2).getInt()));
}
public static <V> Timestamp deserializeTimestampOrNull(Cell<V> cell)
{
if (cell == null)
return null;
ValueAccessor<V> accessor = cell.accessor();
V value = cell.value();
if (accessor.isEmpty(value))
return null;
List<V> split = TIMESTAMP_TYPE.unpack(value, accessor);
return Timestamp.fromBits(accessor.getLong(split.get(0), 0), accessor.getLong(split.get(1), 0), new Node.Id(accessor.getInt(split.get(2), 0)));
}
public static <V, T extends Timestamp> T deserializeTimestampOrNull(V value, ValueAccessor<V> accessor, TimestampFactory<T> factory)
{
if (value == null || accessor.isEmpty(value))
@ -902,6 +1027,30 @@ public class AccordKeyspace
return result;
}
public static Durability deserializeDurabilityOrNull(Cell cell)
{
return cell == null ? null : CommandSerializers.durability.forOrdinal(cell.accessor().getInt(cell.value(), 0));
}
public static SaveStatus deserializeSaveStatusOrNull(Cell cell)
{
return cell == null ? null : CommandSerializers.saveStatus.forOrdinal(cell.accessor().getInt(cell.value(), 0));
}
public static Route<?> deserializeRouteOrNull(Cell cell)
{
if (cell == null)
return null;
try
{
return deserializeOrNull(cell.buffer(), LocalVersionedSerializers.route);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
private static <T> T deserializeWithVersionOr(UntypedResultSet.Row row, String dataColumn, LocalVersionedSerializer<T> serializer, Supplier<T> defaultSupplier) throws IOException
{
if (!row.has(dataColumn))
@ -1149,7 +1298,7 @@ public class AccordKeyspace
try
{
UntypedResultSet.Row row = rows.one();
Invariants.checkState(deserializeTxnId(row).equals(txnId));
checkState(deserializeTxnId(row).equals(txnId));
SaveStatus status = deserializeStatus(row);
CommonAttributes.Mutable attributes = new CommonAttributes.Mutable(txnId);
// TODO: something less brittle than ordinal, more efficient than values()
@ -1250,12 +1399,13 @@ public class AccordKeyspace
SeriesKind kind,
PartitionUpdate.Builder partitionBuilder,
Row.Builder rowBuilder,
long timestampMicros,
LivenessInfo livenessInfo,
int nowInSeconds)
{
if (prev == value)
return;
long timestampMicros = livenessInfo.timestamp();
Set<Timestamp> deletions = Sets.difference(prev.keySet(), value.keySet());
Row.Deletion deletion = !deletions.isEmpty() ?
@ -1267,6 +1417,7 @@ public class AccordKeyspace
return;
rowBuilder.newRow(Clustering.make(ordinalBytes, serializeTimestamp(timestamp)));
rowBuilder.addCell(live(CommandsForKeyColumns.data, timestampMicros, bytes));
rowBuilder.addPrimaryKeyLivenessInfo(livenessInfo);
partitionBuilder.add(rowBuilder.build());
});
deletions.forEach(timestamp -> {
@ -1281,10 +1432,10 @@ public class AccordKeyspace
SeriesKind kind,
PartitionUpdate.Builder partitionBuilder,
Row.Builder rowBuilder,
long timestampMicros,
LivenessInfo livenessInfo,
int nowInSeconds)
{
addSeriesMutations(kind.getValues(original), kind.getValues(cfk), kind, partitionBuilder, rowBuilder, timestampMicros, nowInSeconds);
addSeriesMutations(kind.getValues(original), kind.getValues(cfk), kind, partitionBuilder, rowBuilder, livenessInfo, nowInSeconds);
}
private static DecoratedKey makeKey(int storeId, PartitionKey key)
@ -1315,6 +1466,7 @@ public class AccordKeyspace
ValueAccessor<ByteBuffer> accessor = ByteBufferAccessor.instance;
int nowInSeconds = (int) TimeUnit.MICROSECONDS.toSeconds(timestampMicros);
LivenessInfo livenessInfo = LivenessInfo.create(timestampMicros, nowInSeconds);
boolean hasStaticChanges = CommandsForKeyColumns.hasStaticChanges(original, cfk);
int expectedRows = (hasStaticChanges ? 1 : 0)
@ -1335,13 +1487,14 @@ public class AccordKeyspace
addCellIfModified(CommandsForKeyColumns.last_executed_timestamp, CommandsForKey::lastExecutedTimestamp, AccordKeyspace::serializeTimestamp, rowBuilder, timestampMicros, nowInSeconds, original, cfk);
addCellIfModified(CommandsForKeyColumns.last_executed_micros, CommandsForKey::rawLastExecutedHlc, accessor::valueOf, rowBuilder, timestampMicros, nowInSeconds, original, cfk);
addCellIfModified(CommandsForKeyColumns.last_write_timestamp, CommandsForKey::lastWriteTimestamp, AccordKeyspace::serializeTimestamp, rowBuilder, timestampMicros, nowInSeconds, original, cfk);
rowBuilder.addPrimaryKeyLivenessInfo(livenessInfo);
Row row = rowBuilder.build();
if (!row.isEmpty())
partitionBuilder.add(row);
}
addSeriesMutations(original, cfk, SeriesKind.BY_ID, partitionBuilder, rowBuilder, timestampMicros, nowInSeconds);
addSeriesMutations(original, cfk, SeriesKind.BY_EXECUTE_AT, partitionBuilder, rowBuilder, timestampMicros, nowInSeconds);
addSeriesMutations(original, cfk, SeriesKind.BY_ID, partitionBuilder, rowBuilder, livenessInfo, nowInSeconds);
addSeriesMutations(original, cfk, SeriesKind.BY_EXECUTE_AT, partitionBuilder, rowBuilder, livenessInfo, nowInSeconds);
PartitionUpdate update = partitionBuilder.build();
if (update.isEmpty())
@ -1431,7 +1584,7 @@ public class AccordKeyspace
seriesMaps.get(SeriesKind.values()[ordinal]).put(timestamp, data);
}
}
Invariants.checkState(!partitions.hasNext());
checkState(!partitions.hasNext());
return CommandsForKey.SerializerSupport.create(key, max, lastExecutedTimestamp, lastExecutedMicros, lastWriteTimestamp,
CommandsForKeySerializer.loader,
@ -1657,7 +1810,7 @@ public class AccordKeyspace
String cql = format("SELECT * FROM %s.%s WHERE epoch=?", ACCORD_KEYSPACE_NAME, TOPOLOGIES);
UntypedResultSet result = executeInternal(cql, epoch);
Invariants.checkState(!result.isEmpty(), "Nothing found for epoch %d", epoch);
checkState(!result.isEmpty(), "Nothing found for epoch %d", epoch);
UntypedResultSet.Row row = result.one();
Topology topology = row.has("topology")
? deserialize(row.getBytes("topology"), LocalVersionedSerializers.topology)

View File

@ -55,12 +55,6 @@ import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper;
import org.apache.cassandra.gms.ApplicationState;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.gms.VersionedValue;
import org.apache.cassandra.io.util.File;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assume;
import org.slf4j.Logger;
@ -91,12 +85,6 @@ import org.apache.cassandra.db.compaction.CompactionManager;
import org.apache.cassandra.db.compaction.CompactionTasks;
import org.apache.cassandra.db.compaction.OperationType;
import org.apache.cassandra.db.lifecycle.LifecycleTransaction;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.ReplicaCollection;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.AsciiType;
import org.apache.cassandra.db.marshal.Int32Type;
@ -123,6 +111,10 @@ import org.apache.cassandra.dht.RandomPartitioner.BigIntegerToken;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.index.internal.CassandraIndex;
import org.apache.cassandra.distributed.test.log.ClusterMetadataTestHelper;
import org.apache.cassandra.gms.ApplicationState;
import org.apache.cassandra.gms.Gossiper;
import org.apache.cassandra.gms.VersionedValue;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.SSTableId;
import org.apache.cassandra.io.sstable.SSTableLoader;
@ -131,8 +123,15 @@ import org.apache.cassandra.io.sstable.UUIDBasedSSTableId;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.sstable.format.SSTableReaderWithFilter;
import org.apache.cassandra.io.util.DataInputPlus;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.locator.Replica;
import org.apache.cassandra.locator.ReplicaCollection;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.Schema;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.TableMetadataRef;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.pager.PagingState;
@ -735,6 +734,20 @@ public class Util
.untilAsserted(() -> assertThat(message, actualSupplier.get(), matcher));
}
public static void spinAssertEquals(Object expected, int timeoutInSeconds, Callable<Object> call)
{
spinAssertEquals(null, expected, timeoutInSeconds, TimeUnit.SECONDS, call);
}
public static <T> void spinAssertEquals(String message, T expected, long timeout, TimeUnit timeUnit, Callable<? extends T> call)
{
Awaitility.await()
.pollInterval(Duration.ofMillis(100))
.pollDelay(0, TimeUnit.MILLISECONDS)
.atMost(timeout, timeUnit)
.untilAsserted(() -> assertThat(message, call.call(), equalTo(expected)));
}
public static void joinThread(Thread thread) throws InterruptedException
{
thread.join(10000);

View File

@ -24,24 +24,27 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import accord.api.Result;
import accord.api.RoutingKey;
import accord.local.CheckedCommands;
import accord.local.CommandStore;
import accord.local.DurableBefore;
import accord.local.RedundantBefore;
import accord.local.SaveStatus;
import accord.local.Status;
import accord.local.Status.Durability;
import accord.primitives.Ballot;
import accord.primitives.Deps;
@ -51,7 +54,6 @@ import accord.primitives.PartialRoute;
import accord.primitives.PartialTxn;
import accord.primitives.Ranges;
import accord.primitives.Seekable;
import accord.primitives.Timestamp;
import accord.primitives.Txn;
import accord.primitives.Txn.Kind;
import accord.primitives.TxnId;
@ -64,18 +66,22 @@ import org.apache.cassandra.db.Clustering;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ColumnFamilyStore.FlushReason;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.compaction.CompactionIteratorTest.Scanner;
import org.apache.cassandra.db.partitions.ImmutableBTreePartition;
import org.apache.cassandra.db.partitions.Partition;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.io.sstable.ISSTableScanner;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.SchemaConstants;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.accord.AccordCommandStore;
import org.apache.cassandra.service.accord.AccordKeyspace;
import org.apache.cassandra.service.accord.AccordKeyspace.CommandRows;
import org.apache.cassandra.service.accord.AccordKeyspace.CommandsColumns;
import org.apache.cassandra.service.accord.AccordKeyspace.CommandsForKeyRows;
import org.apache.cassandra.service.accord.AccordTestUtils;
import org.apache.cassandra.service.accord.IAccordService;
@ -86,6 +92,7 @@ import org.apache.cassandra.utils.Pair;
import static accord.impl.CommandsForKey.NO_LAST_EXECUTED_HLC;
import static accord.local.PreLoadContext.contextFor;
import static accord.utils.async.AsyncChains.getUninterruptibly;
import static org.apache.cassandra.Util.spinAssertEquals;
import static org.apache.cassandra.cql3.statements.schema.CreateTableStatement.parse;
import static org.apache.cassandra.db.compaction.CompactionAccordIteratorsTest.DurableBeforeType.MAJORITY;
import static org.apache.cassandra.db.compaction.CompactionAccordIteratorsTest.DurableBeforeType.NOT_DURABLE;
@ -95,6 +102,7 @@ import static org.apache.cassandra.service.accord.AccordKeyspace.COMMANDS;
import static org.apache.cassandra.service.accord.AccordKeyspace.COMMANDS_FOR_KEY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@ -104,6 +112,7 @@ import static org.mockito.Mockito.when;
public class CompactionAccordIteratorsTest
{
private static final Logger logger = LoggerFactory.getLogger(CompactionAccordIteratorsTest.class);
private static final long CLOCK_START = 44;
private static final long HLC_START = 41;
private static final int NODE = 1;
@ -117,8 +126,17 @@ public class CompactionAccordIteratorsTest
private static final TxnId[] TXN_IDS = new TxnId[] {TXN_ID, SECOND_TXN_ID};
private static final TxnId GT_SECOND_TXN_ID = AccordTestUtils.txnId(EPOCH, SECOND_TXN_ID.hlc() + 1, NODE);
static ColumnFamilyStore commands;
static ColumnFamilyStore commandsForKey;
static TableMetadata table;
static FullRoute route;
Random random;
/*
* Whether to compact all tables at once in a single merge or forcing two random tables
* to merge at a time
*/
private boolean singleCompaction;
@BeforeClass
public static void beforeClass() throws Throwable
@ -128,15 +146,40 @@ public class CompactionAccordIteratorsTest
SchemaLoader.createKeyspace("ks", KeyspaceParams.simple(1),
parse("CREATE TABLE tbl (k int, c int, v int, primary key (k, c))", "ks"));
StorageService.instance.initServer();
commands = ColumnFamilyStore.getIfExists(SchemaConstants.ACCORD_KEYSPACE_NAME, COMMANDS);
commands.disableAutoCompaction();
commandsForKey = ColumnFamilyStore.getIfExists(SchemaConstants.ACCORD_KEYSPACE_NAME, COMMANDS_FOR_KEY);
commandsForKey.disableAutoCompaction();
table = ColumnFamilyStore.getIfExists("ks", "tbl").metadata();
route = AccordTestUtils.keys(table, 42).toRoute(AccordTestUtils.key(table, 42).toUnseekable());
}
@Before
public void setUp()
{
// This attempt at determinism doesn't work because the order of the SSTableScanners is not determinisitc
long seed = System.nanoTime();
logger.info("Seed " + seed + "L");
random = new Random(seed);
}
// This isn't attempting to be an exhaustive test of Commands.shouldCleanup just that the return values
// are handled correctly and that the interaction between the CompactionIterator and shoudCleanup seems reasonable
@Test
public void testAccordCommandsPurger() throws Throwable
public void testAccordCommandsPurgerSingleCompaction() throws Throwable
{
testAccordCommandsPurger(true);
}
@Test
public void testAccordCommandsPurgerMultipleCompactions() throws Throwable
{
testAccordCommandsPurger(false);
}
private void testAccordCommandsPurger(boolean singleCompaction) throws Throwable
{
this.singleCompaction = singleCompaction;
// Null redudnant before should make no change since we have no information on this CommandStore
testAccordCommandsPurger(null, DurableBefore.EMPTY, expectAccordCommandsNoChange());
// Universally durable (and global to boot) should be erased since literally everyone knows about it
@ -156,7 +199,7 @@ public class CompactionAccordIteratorsTest
testAccordCommandsPurger(redundantBefore(LT_TXN_ID), durableBefore(DurableBeforeType.EMPTY), expectAccordCommandsNoChange());
}
private static void testAccordCommandsPurger(RedundantBefore redundantBefore, DurableBefore durableBefore, Consumer<List<Partition>> expectedResult) throws Throwable
private void testAccordCommandsPurger(RedundantBefore redundantBefore, DurableBefore durableBefore, Consumer<List<Partition>> expectedResult) throws Throwable
{
testWithCommandStore((commandStore) -> {
IAccordService mockAccordService = mockAccordService(commandStore, redundantBefore, durableBefore);
@ -167,8 +210,20 @@ public class CompactionAccordIteratorsTest
}
@Test
public void testAccordCommandsForKeyPurger() throws Throwable
public void testAccordCommandsForKeyPurgerSingleCompaction() throws Throwable
{
testAccordCommandsForKeyPurger(true);
}
@Test
public void testAccordCommandsForKeyPurgerMultipleCompactions() throws Throwable
{
testAccordCommandsForKeyPurger(false);
}
private void testAccordCommandsForKeyPurger(boolean singleCompaction) throws Throwable
{
this.singleCompaction = singleCompaction;
testAccordCommandsForKeyPurger(null, expectedAccordCommandsForKeyNoChange());
testAccordCommandsForKeyPurger(redundantBefore(LT_TXN_ID), expectedAccordCommandsForKeyNoChange());
testAccordCommandsForKeyPurger(redundantBefore(TXN_ID), expectedAccordCommandsForKeyNoChange());
@ -189,7 +244,7 @@ public class CompactionAccordIteratorsTest
assertEquals(TXN_ID.hlc(), CommandsForKeyRows.getLastExecutedMicros(staticRow));
assertEquals(4, Iterators.size(partition.unfilteredIterator()));
UnfilteredRowIterator rows = partition.unfilteredIterator();
// One row per series
// One row per txn per series
for (int i = 0; i < 2; i++)
for (TxnId txnId : TXN_IDS)
assertEquals(txnId, CommandsForKeyRows.getTimestamp((Row)rows.next()));
@ -202,10 +257,11 @@ public class CompactionAccordIteratorsTest
assertEquals(1, partitions.size());
Partition partition = partitions.get(0);
Row staticRow = partition.getRow(Clustering.STATIC_CLUSTERING);
assertEquals(4, Iterables.size(staticRow));
// Only expect one column to remain because the second transaction is a read
assertEquals(1, Iterables.size(staticRow));
assertEquals(SECOND_TXN_ID, CommandsForKeyRows.getMaxTimestamp(staticRow));
assertEquals(Timestamp.NONE, CommandsForKeyRows.getLastExecutedTimestamp(staticRow));
assertEquals(Timestamp.NONE, CommandsForKeyRows.getLastWriteTimestamp(staticRow));
assertNull(CommandsForKeyRows.getLastExecutedTimestamp(staticRow));
assertNull(CommandsForKeyRows.getLastWriteTimestamp(staticRow));
assertEquals(NO_LAST_EXECUTED_HLC, CommandsForKeyRows.getLastExecutedMicros(staticRow));
assertEquals(2, Iterators.size(partition.unfilteredIterator()));
UnfilteredRowIterator rows = partition.unfilteredIterator();
@ -219,7 +275,7 @@ public class CompactionAccordIteratorsTest
return partitions -> assertEquals(0, partitions.size());
}
private static void testAccordCommandsForKeyPurger(RedundantBefore redundantBefore, Consumer<List<Partition>> expectedResult) throws Throwable
private void testAccordCommandsForKeyPurger(RedundantBefore redundantBefore, Consumer<List<Partition>> expectedResult) throws Throwable
{
testWithCommandStore((commandStore) -> {
IAccordService mockAccordService = mockAccordService(commandStore, redundantBefore, DurableBefore.EMPTY);
@ -244,7 +300,9 @@ public class CompactionAccordIteratorsTest
assertEquals(1, Iterators.size(partition.unfilteredIterator()));
ByteBuffer[] partitionKeyComponents = CommandRows.splitPartitionKey(partition.partitionKey());
Row row = (Row) partition.unfilteredIterator().next();
assertEquals(6, row.columnCount());
assertEquals(CommandsColumns.TRUNCATE_FIELDS[1].length, row.columnCount());
for (ColumnMetadata cm : CommandsColumns.TRUNCATE_FIELDS[1])
assertNotNull(row.getColumnData(cm));
assertEquals(TXN_ID, CommandRows.getTxnId(partitionKeyComponents));
assertEquals(1, ((TxnData)CommandRows.getResult(row)).entrySet().size());
assertNotNull(CommandRows.getWrites(row));
@ -268,7 +326,9 @@ public class CompactionAccordIteratorsTest
assertEquals(1, Iterators.size(partition.unfilteredIterator()));
ByteBuffer[] partitionKeyComponents = CommandRows.splitPartitionKey(partition.partitionKey());
Row row = (Row)partition.unfilteredIterator().next();
assertEquals(4, row.columnCount());
assertEquals(CommandsColumns.TRUNCATE_FIELDS[0].length, row.columnCount());
for (ColumnMetadata cm : CommandsColumns.TRUNCATE_FIELDS[0])
assertNotNull(row.getColumnData(cm));
assertEquals(TXN_ID, CommandRows.getTxnId(partitionKeyComponents));
assertEquals(Durability.Local, CommandRows.getDurability(row));
assertEquals(TXN_ID, CommandRows.getExecuteAt(row));
@ -285,6 +345,9 @@ public class CompactionAccordIteratorsTest
assertEquals(1, Iterators.size(partition.unfilteredIterator()));
ByteBuffer[] partitionKeyComponents = CommandRows.splitPartitionKey(partition.partitionKey());
Row row = (Row)partition.unfilteredIterator().next();
assertEquals(commands.metadata().regularColumns().size(), row.columnCount());
for (ColumnMetadata cm : commands.metadata().regularColumns())
assertNotNull(row.getColumnData(cm));
assertEquals(TXN_ID, CommandRows.getTxnId(partitionKeyComponents));
assertEquals(SaveStatus.Applied, AccordKeyspace.CommandRows.getStatus(row));
};
@ -338,7 +401,21 @@ public class CompactionAccordIteratorsTest
void test(AccordCommandStore commandStore) throws Throwable;
}
private static void testWithCommandStore(TestWithCommandStore test, boolean additionalCommand) throws Throwable
private static void flush(AccordCommandStore commandStore)
{
commandStore.executeBlocking(() -> {
// clear cache and wait for post-eviction writes to complete
long cacheSize = commandStore.getCacheSize();
commandStore.setCacheSize(0);
commandStore.setCacheSize(cacheSize);
commandStore.cache().awaitSaveResults();
});
commands.forceBlockingFlush(FlushReason.UNIT_TESTS);
commandsForKey.forceBlockingFlush(FlushReason.UNIT_TESTS);
}
private void testWithCommandStore(TestWithCommandStore test, boolean additionalCommand) throws Throwable
{
Keyspace.open(ACCORD_KEYSPACE_NAME).getColumnFamilyStores().forEach(ColumnFamilyStore::truncateBlocking);
clock.set(CLOCK_START);
@ -350,57 +427,93 @@ public class CompactionAccordIteratorsTest
Seekable key = txn.keys().get(0);
PartialDeps partialDeps = Deps.NONE.slice(AccordTestUtils.fullRange(txn));
PartialTxn partialTxn = txn.slice(commandStore.unsafeRangesForEpoch().currentRanges(), true);
RoutingKey homeKey = key.someIntersectingRoutingKey(commandStore.unsafeRangesForEpoch().currentRanges());
PartialRoute partialRoute = route.slice(commandStore.unsafeRangesForEpoch().currentRanges());
getUninterruptibly(commandStore.submit(contextFor(txnId, txn.keys()), safe -> {
long originalCacheSize = getUninterruptibly(commandStore.submit(contextFor(txnId, txn.keys()), safe -> {
// clear cache
long cacheSize = commandStore.getCacheSize();
commandStore.setCacheSize(0);
CheckedCommands.preaccept(safe, txnId, partialTxn, route, null);
return cacheSize;
}).beginAsResult());
flush(commandStore);
getUninterruptibly(commandStore.execute(contextFor(txnId, txn.keys()), safe -> {
CheckedCommands.accept(safe, txnId, Ballot.ZERO, partialRoute, partialTxn.keys(), null, txnId, partialDeps);
}).beginAsResult());
flush(commandStore);
getUninterruptibly(commandStore.execute(contextFor(txnId, txn.keys()), safe -> {
CheckedCommands.commit(safe, txnId, route, null, partialTxn, txnId, partialDeps);
}).beginAsResult());
flush(commandStore);
getUninterruptibly(commandStore.execute(contextFor(txnId, txn.keys()), safe -> {
Pair<Writes, Result> result = AccordTestUtils.processTxnResultDirect(safe, txnId, partialTxn, txnId);
CheckedCommands.apply(safe, txnId, route, null, txnId, partialDeps, partialTxn, result.left, result.right);
return safe.get(txnId, homeKey).current();
}).beginAsResult());
flush(commandStore);
// The apply chain is asychronous so it is easiest to just spin until it is applied
// in order to have the updated state in the system table
spinAssertEquals(true, 5, () ->
getUninterruptibly(commandStore.submit(contextFor(txnId, txn.keys()), safe -> safe.get(txnId, route.homeKey()).current().hasBeen(Status.Applied)
).beginAsResult()));
flush(commandStore);
getUninterruptibly(commandStore.execute(contextFor(txnId, txn.keys()), safe -> {
commandStore.setCacheSize(originalCacheSize);
}).beginAsResult());
}
commandStore.executeBlocking(() -> {
// clear cache and wait for post-eviction writes to complete
long cacheSize = commandStore.getCacheSize();
commandStore.setCacheSize(0);
commandStore.setCacheSize(cacheSize);
commandStore.cache().awaitSaveResults();
});
UntypedResultSet commandsTable = QueryProcessor.executeInternal("SELECT * FROM " + ACCORD_KEYSPACE_NAME + "." + COMMANDS + ";");
logger.info(commandsTable.toStringUnsafe());
assertEquals(txnIds.length, commandsTable.size());
Iterator<UntypedResultSet.Row> commandsTableIterator = commandsTable.iterator();
for (TxnId txnId : txnIds)
assertEquals(txnId, AccordKeyspace.deserializeTimestampOrNull(commandsTableIterator.next().getBytes("txn_id"), TxnId::fromBits));
UntypedResultSet commandsForKeyTable = QueryProcessor.executeInternal("SELECT * FROM " + ACCORD_KEYSPACE_NAME + "." + COMMANDS_FOR_KEY + ";");
logger.info(commandsForKeyTable.toStringUnsafe());
assertEquals(txnIds.length * 2, commandsForKeyTable.size());
Iterator<UntypedResultSet.Row> commandsForKeyTableIterator = commandsTable.iterator();
for (TxnId txnId : txnIds)
assertEquals(txnId, AccordKeyspace.deserializeTimestampOrNull(commandsForKeyTableIterator.next().getBytes("txn_id"), TxnId::fromBits));
System.out.println(commandsForKeyTable);
test.test(commandStore);
}
private static List<Partition> compactCFS(IAccordService mockAccordService, ColumnFamilyStore cfs)
private List<Partition> compactCFS(IAccordService mockAccordService, ColumnFamilyStore cfs)
{
cfs.forceBlockingFlush(FlushReason.UNIT_TESTS);
List<ISSTableScanner> scanners = cfs.getLiveSSTables().stream().map(SSTableReader::getScanner).collect(Collectors.toList());
List<Partition> result = new ArrayList<>();
try (CompactionController controller = new CompactionController(ColumnFamilyStore.getIfExists(ACCORD_KEYSPACE_NAME, cfs.name), Collections.emptySet(), 0);
CompactionIterator compactionIterator = new CompactionIterator(OperationType.COMPACTION, scanners, controller, FBUtilities.nowInSeconds(), null, ActiveCompactionsTracker.NOOP, null, () -> mockAccordService))
int numScanners = scanners.size();
List<Partition> result = null;
do
{
while (compactionIterator.hasNext())
List<Partition> outputPartitions = new ArrayList<>();
List<ISSTableScanner> nextInputScanners = new ArrayList<>();
if (singleCompaction)
{
try (UnfilteredRowIterator partition = compactionIterator.next())
nextInputScanners = ImmutableList.copyOf(scanners);
scanners.clear();
}
else
{
// Process the rows only two sstables at a time to force compacting random slices of command state
nextInputScanners.add(scanners.remove(random.nextInt(scanners.size())));
nextInputScanners.add(scanners.remove(random.nextInt(scanners.size())));
}
try (CompactionController controller = new CompactionController(ColumnFamilyStore.getIfExists(ACCORD_KEYSPACE_NAME, cfs.name), Collections.emptySet(), 0);
CompactionIterator compactionIterator = new CompactionIterator(OperationType.COMPACTION, nextInputScanners, controller, FBUtilities.nowInSeconds(), null, ActiveCompactionsTracker.NOOP, null, () -> mockAccordService))
{
while (compactionIterator.hasNext())
{
result.add(ImmutableBTreePartition.create(partition));
try (UnfilteredRowIterator partition = compactionIterator.next())
{
outputPartitions.add(ImmutableBTreePartition.create(partition));
}
}
}
}
verify(mockAccordService, times(1)).getRedundantBeforesAndDurableBefore();
if (scanners.isEmpty())
result = outputPartitions;
else
scanners.add(random.nextInt(scanners.size()), new Scanner(cfs.metadata(), outputPartitions.stream().map(Partition::unfilteredIterator).collect(Collectors.toList())));
} while (!scanners.isEmpty());
verify(mockAccordService, times(singleCompaction ? 1 : numScanners - 1)).getRedundantBeforesAndDurableBefore();
return result;
}
}

View File

@ -17,18 +17,25 @@
*/
package org.apache.cassandra.db.compaction;
import static org.apache.cassandra.config.CassandraRelevantProperties.DIAGNOSTIC_SNAPSHOT_INTERVAL_NANOS;
import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.assertCommandIssued;
import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.makeRow;
import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.partition;
import static org.junit.Assert.*;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.common.collect.*;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import org.junit.BeforeClass;
import org.junit.Test;
@ -44,17 +51,30 @@ import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.partitions.AbstractUnfilteredPartitionIterator;
import org.apache.cassandra.db.rows.*;
import org.apache.cassandra.db.rows.RangeTombstoneBoundaryMarker;
import org.apache.cassandra.db.rows.Unfiltered;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.db.rows.UnfilteredRowsGenerator;
import org.apache.cassandra.io.sstable.ISSTableScanner;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.schema.KeyspaceParams;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.FBUtilities;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.apache.cassandra.config.CassandraRelevantProperties.DIAGNOSTIC_SNAPSHOT_INTERVAL_NANOS;
import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.assertCommandIssued;
import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.makeRow;
import static org.apache.cassandra.db.transform.DuplicateRowCheckerTest.partition;
public class CompactionIteratorTest extends CQLTester
{
@ -274,7 +294,7 @@ public class CompactionIteratorTest extends CQLTester
transformedSources.put(kk, Iterables.transform(tombstoneSources, list -> listToIterator(list, kk)));
try (CompactionController controller = new Controller(Keyspace.openAndGetStore(metadata), transformedSources, GC_BEFORE);
CompactionIterator iter = new CompactionIterator(OperationType.COMPACTION,
Lists.transform(content, x -> new Scanner(x)),
Lists.transform(content, x -> new Scanner(metadata, x)),
controller, NOW, null))
{
List<Unfiltered> result = new ArrayList<>();
@ -336,7 +356,7 @@ public class CompactionIteratorTest extends CQLTester
transformedSources.put(kk, Iterables.transform(tombstoneLists, list -> listToIterator(list, kk)));
try (CompactionController controller = new Controller(Keyspace.openAndGetStore(metadata), transformedSources, GC_BEFORE);
CompactionIterator iter = new CompactionIterator(OperationType.COMPACTION,
Lists.transform(content, x -> new Scanner(x)),
Lists.transform(content, x -> new Scanner(metadata, x)),
controller, NOW, null))
{
assertTrue(iter.hasNext());
@ -369,7 +389,7 @@ public class CompactionIteratorTest extends CQLTester
transformedSources.put(kk, Iterables.transform(tombstoneLists, list -> listToIterator(list, kk)));
try (CompactionController controller = new Controller(Keyspace.openAndGetStore(metadata), transformedSources, GC_BEFORE);
CompactionIterator iter = new CompactionIterator(OperationType.COMPACTION,
Lists.transform(content, x -> new Scanner(x)),
Lists.transform(content, x -> new Scanner(metadata, x)),
controller, NOW, null))
{
iter.stop();
@ -404,12 +424,14 @@ public class CompactionIteratorTest extends CQLTester
}
}
class Scanner extends AbstractUnfilteredPartitionIterator implements ISSTableScanner
static class Scanner extends AbstractUnfilteredPartitionIterator implements ISSTableScanner
{
Iterator<UnfilteredRowIterator> iter;
TableMetadata metadata;
Scanner(Iterable<UnfilteredRowIterator> content)
Scanner(TableMetadata metadata, Iterable<UnfilteredRowIterator> content)
{
this.metadata = metadata;
iter = content.iterator();
}
@ -500,7 +522,7 @@ public class CompactionIteratorTest extends CQLTester
DecoratedKey key = cfs.getPartitioner().decorateKey(ByteBufferUtil.bytes("key"));
try (CompactionController controller = new CompactionController(cfs, Integer.MAX_VALUE);
UnfilteredRowIterator rows = partition(cfs.metadata(), key, false, unfiltereds);
ISSTableScanner scanner = new Scanner(Collections.singletonList(rows));
ISSTableScanner scanner = new Scanner(cfs.metadata(), Collections.singletonList(rows));
CompactionIterator iter = new CompactionIterator(OperationType.COMPACTION,
Collections.singletonList(scanner),
controller, FBUtilities.nowInSeconds(), null))

View File

@ -494,7 +494,7 @@ public class ByteSourceComparisonTest extends ByteSourceTestBase
tt.pack(decomposeAndRandomPad(UTF8Type.instance, ""), decomposeAndRandomPad(Int32Type.instance, 0)),
// Note: a decomposed null (e.g. decomposeAndRandomPad(Int32Type.instance, null)) should not reach a tuple
tt.pack(decomposeAndRandomPad(UTF8Type.instance, ""), null),
tt.pack(null, decomposeAndRandomPad(Int32Type.instance, 0)),
tt.pack((ByteBuffer) null, decomposeAndRandomPad(Int32Type.instance, 0)),
tt.pack(decomposeAndRandomPad(UTF8Type.instance, "")),
tt.pack((ByteBuffer) null),
tt.pack()

View File

@ -442,7 +442,7 @@ public class ByteSourceConversionTest extends ByteSourceTestBase
tt.pack(decomposeAndRandomPad(UTF8Type.instance, ""), decomposeAndRandomPad(Int32Type.instance, 0)),
// Note: a decomposed null (e.g. decomposeAndRandomPad(Int32Type.instance, null)) should not reach a tuple
tt.pack(decomposeAndRandomPad(UTF8Type.instance, ""), null),
tt.pack(null, decomposeAndRandomPad(Int32Type.instance, 0)),
tt.pack((ByteBuffer) null, decomposeAndRandomPad(Int32Type.instance, 0)),
tt.pack(decomposeAndRandomPad(UTF8Type.instance, "")),
tt.pack((ByteBuffer) null),
tt.pack()