-
Notifications
You must be signed in to change notification settings - Fork 3.7k
CASSANDRA-20667: Accord: BEGIN TRANSACTIONs IF condition logic does not properly support meaningless emptiness and null values #4175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
003933c
c0c899d
ab4bdd5
809a4dc
364b2cb
6f71245
b4ed52e
9740e1c
b831784
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -236,6 +236,8 @@ protected Bound(ColumnMetadata column, TableMetadata table, Operator operator, B | |
*/ | ||
public abstract boolean appliesTo(Row row); | ||
|
||
public abstract boolean isNull(Row row); | ||
|
||
public abstract BoundKind kind(); | ||
|
||
public static final ParameterisedUnversionedSerializer<Bound, TableMetadatas> serializer = new ParameterisedUnversionedSerializer<>() { | ||
|
@@ -294,6 +296,12 @@ public boolean appliesTo(Row row) | |
return operator.isSatisfiedBy(column.type, rowValue(row), value); | ||
} | ||
|
||
@Override | ||
public boolean isNull(Row row) | ||
{ | ||
return column.type.isNull(rowValue(row)); | ||
} | ||
|
||
protected ByteBuffer rowValue(Row row) | ||
{ | ||
// If we're asking for a given cell, and we didn't get any row from our read, it's | ||
|
@@ -399,10 +407,21 @@ public BoundKind kind() | |
@Override | ||
public boolean appliesTo(Row row) | ||
{ | ||
ByteBuffer element = ((MultiElementType<?>) column.type).getElement(columnData(row), keyOrIndex); | ||
ByteBuffer element = elementValue(row); | ||
return operator.isSatisfiedBy(elementType, element, value); | ||
} | ||
|
||
private ByteBuffer elementValue(Row row) | ||
{ | ||
return ((MultiElementType<?>) column.type).getElement(columnData(row), keyOrIndex); | ||
} | ||
|
||
@Override | ||
public boolean isNull(Row row) | ||
{ | ||
return column.type.isNull(elementValue(row)); | ||
} | ||
|
||
/** | ||
* Returns the column data for the given row. | ||
* @param row the row | ||
|
@@ -454,6 +473,12 @@ public boolean appliesTo(Row row) | |
return operator.isSatisfiedBy((MultiElementType<?>) column.type, columnData, value); | ||
} | ||
|
||
@Override | ||
public boolean isNull(Row row) | ||
{ | ||
return row == null || row.getComplexColumnData(column) == null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is all filtered data, so can this data be non-live? |
||
} | ||
|
||
@Override | ||
public boolean equals(Object o) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -300,7 +300,7 @@ public Setter(ColumnMetadata column, Term t) | |
@Override | ||
public boolean requiresTimestamp() | ||
{ | ||
return true; | ||
return column.type.isMultiCell(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bug found in fuzz testing, frozen lists were requiring timestamp which made it hit this block
this is a immutable bb so can avoid converting to a reference (i think it fails as a reference but forgot) |
||
} | ||
|
||
public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException | ||
|
@@ -397,7 +397,7 @@ public void execute(DecoratedKey partitionKey, UpdateParameters params) throws I | |
@Override | ||
public boolean requiresTimestamp() | ||
{ | ||
return true; | ||
return column.type.isMultiCell(); | ||
} | ||
|
||
static void doAppend(Term.Terminal value, ColumnMetadata column, UpdateParameters params) throws InvalidRequestException | ||
|
@@ -455,7 +455,7 @@ public Prepender(ColumnMetadata column, Term t) | |
@Override | ||
public boolean requiresTimestamp() | ||
{ | ||
return true; | ||
return column.type.isMultiCell(); | ||
} | ||
|
||
public void execute(DecoratedKey partitionKey, UpdateParameters params) throws InvalidRequestException | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ | |
import org.apache.cassandra.service.accord.api.PartitionKey; | ||
import org.apache.cassandra.service.accord.txn.RetryWithNewProtocolResult; | ||
import org.apache.cassandra.tracing.Tracing; | ||
import org.apache.cassandra.utils.JVMStabilityInspector; | ||
import org.apache.cassandra.utils.concurrent.AsyncFuture; | ||
|
||
import static org.apache.cassandra.utils.Clock.Global.nanoTime; | ||
|
@@ -142,6 +143,8 @@ else if (isTxnRequest && coordinationFailed instanceof TopologyMismatch) | |
} | ||
else | ||
{ | ||
logger.error("Unexpected exception", fail); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was seeing that logs were clean but exceptions were happening, and found that this code block was hiding all these exceptions. The error isn't a |
||
JVMStabilityInspector.inspectThrowable(fail); | ||
report = bookkeeping.newFailed(txnId, keysOrRanges); | ||
} | ||
report.addSuppressed(fail); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
import java.io.IOException; | ||
import java.nio.ByteBuffer; | ||
import java.util.Collection; | ||
import java.util.EnumSet; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
|
@@ -38,6 +39,7 @@ | |
import org.apache.cassandra.db.Clustering; | ||
import org.apache.cassandra.db.TypeSizes; | ||
import org.apache.cassandra.db.marshal.AbstractType; | ||
import org.apache.cassandra.db.marshal.CollectionType; | ||
import org.apache.cassandra.db.marshal.UserType; | ||
import org.apache.cassandra.db.partitions.FilteredPartition; | ||
import org.apache.cassandra.db.rows.Cell; | ||
|
@@ -289,54 +291,99 @@ public boolean applies(TxnData data) | |
FilteredPartition partition = reference.getPartition(data); | ||
boolean exists = partition != null && !partition.isEmpty(); | ||
|
||
Row row = null; | ||
if (exists) | ||
if (reference.column() != null | ||
&& !reference.column().isPartitionKey()) | ||
{ | ||
row = reference.getRow(partition); | ||
exists = row != null && !row.isEmpty(); | ||
} | ||
|
||
if (exists && reference.selectsColumn()) | ||
{ | ||
ColumnData columnData = reference.getColumnData(row); | ||
|
||
if (columnData == null) | ||
Row row = null; | ||
if (exists) | ||
{ | ||
exists = false; | ||
row = reference.getRow(partition); | ||
exists = row != null && !row.isEmpty(); | ||
} | ||
else if (columnData.column().isComplex()) | ||
|
||
if (exists && reference.selectsColumn()) | ||
{ | ||
if (reference.isElementSelection() || reference.isFieldSelection()) | ||
ColumnData columnData = reference.getColumnData(row); | ||
|
||
if (columnData == null) | ||
{ | ||
exists = false; | ||
} | ||
else if (columnData.column().isComplex()) | ||
{ | ||
if (reference.isElementSelection()) | ||
{ | ||
Cell<?> cell = (Cell<?>) columnData; | ||
exists = !cell.isTombstone(); | ||
// Collections don't support NULL but meangingless null types are supported, so byte[0] is allowed! | ||
// This is NULL when touched, so need to still check each value | ||
if (exists) | ||
{ | ||
CollectionType<?> type = (CollectionType<?>) reference.column().type.unwrap(); | ||
switch (type.kind) | ||
{ | ||
case MAP: | ||
{ | ||
exists = !type.nameComparator().isNull(cell.path().get(0)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. im still not 100% sold on this, here is the test that hits this
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we have a particularly good principled behaviour we can implement here, but I think treating empty as NULL wherever it is safe to do so, and unsafe to treat it as a raw value, is perhaps as good as we can get. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't thought about this in a while, but can we just reject the txn if we use a meaningless key? If we can't, then agree... |
||
if (exists) | ||
exists = !type.valueComparator().isNull(cell.buffer()); | ||
} | ||
break; | ||
case SET: | ||
{ | ||
exists = !type.nameComparator().isNull(cell.path().get(0)); | ||
} | ||
break; | ||
case LIST: | ||
{ | ||
exists = !type.valueComparator().isNull(cell.buffer()); | ||
} | ||
break; | ||
default: | ||
throw new UnsupportedOperationException(type.kind.name()); | ||
} | ||
} | ||
} | ||
else if (reference.isFieldSelection()) | ||
{ | ||
Cell<?> cell = (Cell<?>) columnData; | ||
exists = exists(cell, reference.getFieldSelectionType()); | ||
} | ||
else | ||
{ | ||
// TODO: Is this even necessary, given the partition is already filtered? | ||
if (!((ComplexColumnData) columnData).complexDeletion().isLive()) | ||
exists = false; | ||
} | ||
} | ||
else if (reference.isElementSelection()) | ||
{ | ||
// This is frozen, so check if the Cell is a tombstone and that the element is present. | ||
Cell<?> cell = (Cell<?>) columnData; | ||
exists = exists(cell, reference.column().type); | ||
if (exists) | ||
{ | ||
ByteBuffer element = reference.getFrozenCollectionElement(cell); | ||
exists = !reference.getFrozenCollectionElementType().isNull(element); | ||
} | ||
} | ||
else if (reference.isFieldSelection()) | ||
Comment on lines
+359
to
+370
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. im not sure if this is allowed in CQL, but it was already here in the code so i made sure it worked |
||
{ | ||
// This is frozen, so check if the Cell is a tombstone and that the field is present. | ||
Cell<?> cell = (Cell<?>) columnData; | ||
exists = !cell.isTombstone(); | ||
exists = exists(cell, reference.column().type); | ||
if (exists) | ||
{ | ||
ByteBuffer fieldValue = reference.getFrozenFieldValue(cell); | ||
exists = !reference.getFieldSelectionType().isNull(fieldValue); | ||
} | ||
} | ||
else | ||
{ | ||
// TODO: Is this even necessary, given the partition is already filtered? | ||
if (!((ComplexColumnData) columnData).complexDeletion().isLive()) | ||
exists = false; | ||
Cell<?> cell = (Cell<?>) columnData; | ||
exists = exists(cell, reference.column().type); | ||
} | ||
} | ||
else if (reference.isElementSelection()) | ||
{ | ||
// This is frozen, so check if the Cell is a tombstone and that the element is present. | ||
Cell<?> cell = (Cell<?>) columnData; | ||
ByteBuffer element = reference.getFrozenCollectionElement(cell); | ||
exists = element != null && !cell.isTombstone(); | ||
} | ||
else if (reference.isFieldSelection()) | ||
{ | ||
// This is frozen, so check if the Cell is a tombstone and that the field is present. | ||
Cell<?> cell = (Cell<?>) columnData; | ||
ByteBuffer fieldValue = reference.getFrozenFieldValue(cell); | ||
exists = fieldValue != null && !cell.isTombstone(); | ||
} | ||
else | ||
{ | ||
Cell<?> cell = (Cell<?>) columnData; | ||
exists = !cell.isTombstone(); | ||
} | ||
} | ||
|
||
switch (kind()) | ||
|
@@ -350,6 +397,11 @@ else if (reference.isFieldSelection()) | |
} | ||
} | ||
|
||
private static boolean exists(Cell<?> cell, AbstractType<?> type) | ||
{ | ||
return !cell.isTombstone() && !type.unwrap().isNull(cell.buffer()); | ||
} | ||
|
||
private static final ConditionSerializer<Exists> serializer = new ConditionSerializer<Exists>() | ||
{ | ||
@Override | ||
|
@@ -442,9 +494,9 @@ public long serializedSize(ColumnConditionsAdapter condition, TableMetadatas tab | |
|
||
public static class Value extends TxnCondition | ||
{ | ||
private static final Set<Kind> KINDS = ImmutableSet.of(Kind.EQUAL, Kind.NOT_EQUAL, | ||
Kind.GREATER_THAN, Kind.GREATER_THAN_OR_EQUAL, | ||
Kind.LESS_THAN, Kind.LESS_THAN_OR_EQUAL); | ||
private static final EnumSet<Kind> KINDS = EnumSet.of(Kind.EQUAL, Kind.NOT_EQUAL, | ||
Kind.GREATER_THAN, Kind.GREATER_THAN_OR_EQUAL, | ||
Kind.LESS_THAN, Kind.LESS_THAN_OR_EQUAL); | ||
|
||
private final TxnReference reference; | ||
private final ByteBuffer value; | ||
|
@@ -460,6 +512,11 @@ public Value(TxnReference reference, Kind kind, ByteBuffer value, ProtocolVersio | |
this.version = version; | ||
} | ||
|
||
public static EnumSet<Kind> supported() | ||
{ | ||
return EnumSet.copyOf(KINDS); | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) | ||
{ | ||
|
@@ -532,7 +589,13 @@ else if (type.isUDT()) | |
@Override | ||
public boolean applies(TxnData data) | ||
{ | ||
return getBounds(data).appliesTo(reference.getRow(data)); | ||
Bound bounds = getBounds(data); | ||
if (reference.column().type.unwrap().isNull(bounds.value)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. semantic difference with CAS IF. if input or value are null, applies is
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is OK, because bound cannot be an IS NULL or IS NOT NULL check. |
||
return false; | ||
Row row = reference.getRow(data); | ||
if (bounds.isNull(row)) | ||
return false; | ||
return bounds.appliesTo(row); | ||
} | ||
|
||
private static final ConditionSerializer<Value> serializer = new ConditionSerializer<>() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isNull
has the following semanticsnull
andempty
are bothnull
null
andempty
are bothnull
null
isnull
The main difference between 1/2 is that in 1
empty
is rejected during validation, so normally means you passed a tombstone here by mistake (historic bug on the read path inRowFilter
). 2 allowsempty
during validation