/*
* 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.db;
import java.io.DataInput;
import java.io.IOException;
import java.security.MessageDigest;
import java.util.Comparator;
import java.util.Iterator;
import com.google.common.base.Objects;
import com.google.common.collect.Iterators;
import org.apache.cassandra.cache.IMeasurableMemory;
import org.apache.cassandra.db.composites.CType;
import org.apache.cassandra.db.composites.Composite;
import org.apache.cassandra.io.IVersionedSerializer;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.ObjectSizes;
import org.apache.cassandra.utils.memory.AbstractAllocator;
/**
* A combination of a top-level (or row) tombstone and range tombstones describing the deletions
* within a {@link ColumnFamily} (or row).
*/
public class DeletionInfo implements IMeasurableMemory
{
private static final long EMPTY_SIZE = ObjectSizes.measure(new DeletionInfo(0, 0));
/**
* This represents a deletion of the entire row. We can't represent this within the RangeTombstoneList, so it's
* kept separately. This also slightly optimizes the common case of a full row deletion.
*/
private DeletionTime topLevel;
/**
* A list of range tombstones within the row. This is left as null if there are no range tombstones
* (to save an allocation (since it's a common case).
*/
private RangeTombstoneList ranges;
/**
* Creates a DeletionInfo with only a top-level (row) tombstone.
* @param markedForDeleteAt the time after which the entire row should be considered deleted
* @param localDeletionTime what time the deletion write was applied locally (for purposes of
* purging the tombstone after gc_grace_seconds).
*/
public DeletionInfo(long markedForDeleteAt, int localDeletionTime)
{
// Pre-1.1 node may return MIN_VALUE for non-deleted container, but the new default is MAX_VALUE
// (see CASSANDRA-3872)
this(new DeletionTime(markedForDeleteAt, localDeletionTime == Integer.MIN_VALUE ? Integer.MAX_VALUE : localDeletionTime));
}
public DeletionInfo(DeletionTime topLevel)
{
this(topLevel, null);
}
public DeletionInfo(Composite start, Composite end, Comparator<Composite> comparator, long markedForDeleteAt, int localDeletionTime)
{
this(DeletionTime.LIVE, new RangeTombstoneList(comparator, 1));
ranges.add(start, end, markedForDeleteAt, localDeletionTime);
}
public DeletionInfo(RangeTombstone rangeTombstone, Comparator<Composite> comparator)
{
this(rangeTombstone.min, rangeTombstone.max, comparator, rangeTombstone.data.markedForDeleteAt, rangeTombstone.data.localDeletionTime);
}
private DeletionInfo(DeletionTime topLevel, RangeTombstoneList ranges)
{
this.topLevel = topLevel;
this.ranges = ranges;
}
/**
* Returns a new DeletionInfo that has no top-level tombstone or any range tombstones.
*/
public static DeletionInfo live()
{
return new DeletionInfo(DeletionTime.LIVE);
}
public DeletionInfo copy()
{
return new DeletionInfo(topLevel, ranges == null ? null : ranges.copy());
}
public DeletionInfo copy(AbstractAllocator allocator)
{
RangeTombstoneList rangesCopy = null;
if (ranges != null)
rangesCopy = ranges.copy(allocator);
return new DeletionInfo(topLevel, rangesCopy);
}
/**
* Returns whether this DeletionInfo is live, that is deletes no columns.
*/
public boolean isLive()
{
return topLevel.isLive() && (ranges == null || ranges.isEmpty());
}
/**
* Return whether a given cell is deleted by the container having this deletion info.
*
* @param cell the cell to check.
* @return true if the cell is deleted, false otherwise
*/
public boolean isDeleted(Cell cell)
{
// We do rely on this test: if topLevel.markedForDeleteAt is MIN_VALUE, we should not
// consider the column deleted even if timestamp=MIN_VALUE, otherwise this break QueryFilter.isRelevant
if (isLive())
return false;
if (cell.timestamp() <= topLevel.markedForDeleteAt)
return true;
// No matter what the counter cell's timestamp is, a tombstone always takes precedence. See CASSANDRA-7346.
if (!topLevel.isLive() && cell instanceof CounterCell)
return true;
return ranges != null && ranges.isDeleted(cell);
}
/**
* Returns a new {@link InOrderTester} in forward order.
*/
public InOrderTester inOrderTester()
{
return inOrderTester(false);
}
/**
* Returns a new {@link InOrderTester} given the order in which
* columns will be passed to it.
*/
public InOrderTester inOrderTester(boolean reversed)
{
return new InOrderTester(reversed);
}
/**
* Purge every tombstones that are older than {@code gcbefore}.
*
* @param gcBefore timestamp (in seconds) before which tombstones should be purged
*/
public void purge(int gcBefore)
{
topLevel = topLevel.localDeletionTime < gcBefore ? DeletionTime.LIVE : topLevel;
if (ranges != null)
{
ranges.purge(gcBefore);
if (ranges.isEmpty())
ranges = null;
}
}
/**
* Evaluates difference between this deletion info and superset for read repair
*
* @return the difference between the two, or LIVE if no difference
*/
public DeletionInfo diff(DeletionInfo superset)
{
RangeTombstoneList rangeDiff = superset.ranges == null || superset.ranges.isEmpty()
? null
: ranges == null ? superset.ranges : ranges.diff(superset.ranges);
return topLevel.markedForDeleteAt != superset.topLevel.markedForDeleteAt || rangeDiff != null
? new DeletionInfo(superset.topLevel, rangeDiff)
: DeletionInfo.live();
}
/**
* Digests deletion info. Used to trigger read repair on mismatch.
*/
public void updateDigest(MessageDigest digest)
{
if (topLevel.markedForDeleteAt != Long.MIN_VALUE)
digest.update(ByteBufferUtil.bytes(topLevel.markedForDeleteAt));
if (ranges != null)
ranges.updateDigest(digest);
}
/**
* Returns true if {@code purge} would remove the top-level tombstone or any of the range
* tombstones, false otherwise.
* @param gcBefore timestamp (in seconds) before which tombstones should be purged
*/
public boolean hasPurgeableTombstones(int gcBefore)
{
if (topLevel.localDeletionTime < gcBefore)
return true;
return ranges != null && ranges.hasPurgeableTombstones(gcBefore);
}
/**
* Potentially replaces the top-level tombstone with another, keeping whichever has the higher markedForDeleteAt
* timestamp.
* @param newInfo
*/
public void add(DeletionTime newInfo)
{
if (topLevel.markedForDeleteAt < newInfo.markedForDeleteAt)
topLevel = newInfo;
}
public void add(RangeTombstone tombstone, Comparator<Composite> comparator)
{
if (ranges == null)
ranges = new RangeTombstoneList(comparator, 1);
ranges.add(tombstone);
}
/**
* Combines another DeletionInfo with this one and returns the result. Whichever top-level tombstone
* has the higher markedForDeleteAt timestamp will be kept, along with its localDeletionTime. The
* range tombstones will be combined.
*
* @return this object.
*/
public DeletionInfo add(DeletionInfo newInfo)
{
add(newInfo.topLevel);
if (ranges == null)
ranges = newInfo.ranges == null ? null : newInfo.ranges.copy();
else if (newInfo.ranges != null)
ranges.addAll(newInfo.ranges);
return this;
}
/**
* Returns the minimum timestamp in any of the range tombstones or the top-level tombstone.
*/
public long minTimestamp()
{
return ranges == null
? topLevel.markedForDeleteAt
: Math.min(topLevel.markedForDeleteAt, ranges.minMarkedAt());
}
/**
* Returns the maximum timestamp in any of the range tombstones or the top-level tombstone.
*/
public long maxTimestamp()
{
return ranges == null
? topLevel.markedForDeleteAt
: Math.max(topLevel.markedForDeleteAt, ranges.maxMarkedAt());
}
/**
* Returns the top-level (or "row") tombstone.
*/
public DeletionTime getTopLevelDeletion()
{
return topLevel;
}
// Use sparingly, not the most efficient thing
public Iterator<RangeTombstone> rangeIterator()
{
return ranges == null ? Iterators.<RangeTombstone>emptyIterator() : ranges.iterator();
}
public Iterator<RangeTombstone> rangeIterator(Composite start, Composite finish)
{
return ranges == null ? Iterators.<RangeTombstone>emptyIterator() : ranges.iterator(start, finish);
}
public RangeTombstone rangeCovering(Composite name)
{
return ranges == null ? null : ranges.search(name);
}
public int dataSize()
{
int size = TypeSizes.NATIVE.sizeof(topLevel.markedForDeleteAt);
return size + (ranges == null ? 0 : ranges.dataSize());
}
public boolean hasRanges()
{
return ranges != null && !ranges.isEmpty();
}
public int rangeCount()
{
return hasRanges() ? ranges.size() : 0;
}
/**
* Whether this deletion info may modify the provided one if added to it.
*/
public boolean mayModify(DeletionInfo delInfo)
{
return topLevel.compareTo(delInfo.topLevel) > 0 || hasRanges();
}
@Override
public String toString()
{
if (ranges == null || ranges.isEmpty())
return String.format("{%s}", topLevel);
else
return String.format("{%s, ranges=%s}", topLevel, rangesAsString());
}
private String rangesAsString()
{
assert !ranges.isEmpty();
StringBuilder sb = new StringBuilder();
CType type = (CType)ranges.comparator();
assert type != null;
Iterator<RangeTombstone> iter = rangeIterator();
while (iter.hasNext())
{
RangeTombstone i = iter.next();
sb.append("[");
sb.append(type.getString(i.min)).append("-");
sb.append(type.getString(i.max)).append(", ");
sb.append(i.data);
sb.append("]");
}
return sb.toString();
}
// Updates all the timestamp of the deletion contained in this DeletionInfo to be {@code timestamp}.
public void updateAllTimestamp(long timestamp)
{
if (topLevel.markedForDeleteAt != Long.MIN_VALUE)
topLevel = new DeletionTime(timestamp, topLevel.localDeletionTime);
if (ranges != null)
ranges.updateAllTimestamp(timestamp);
}
@Override
public boolean equals(Object o)
{
if(!(o instanceof DeletionInfo))
return false;
DeletionInfo that = (DeletionInfo)o;
return topLevel.equals(that.topLevel) && Objects.equal(ranges, that.ranges);
}
@Override
public final int hashCode()
{
return Objects.hashCode(topLevel, ranges);
}
@Override
public long unsharedHeapSize()
{
return EMPTY_SIZE + topLevel.unsharedHeapSize() + (ranges == null ? 0 : ranges.unsharedHeapSize());
}
public static class Serializer implements IVersionedSerializer<DeletionInfo>
{
private final RangeTombstoneList.Serializer rtlSerializer;
public Serializer(CType type)
{
this.rtlSerializer = new RangeTombstoneList.Serializer(type);
}
public void serialize(DeletionInfo info, DataOutputPlus out, int version) throws IOException
{
DeletionTime.serializer.serialize(info.topLevel, out);
rtlSerializer.serialize(info.ranges, out, version);
}
public DeletionInfo deserialize(DataInput in, int version) throws IOException
{
DeletionTime topLevel = DeletionTime.serializer.deserialize(in);
RangeTombstoneList ranges = rtlSerializer.deserialize(in, version);
return new DeletionInfo(topLevel, ranges);
}
public long serializedSize(DeletionInfo info, TypeSizes typeSizes, int version)
{
long size = DeletionTime.serializer.serializedSize(info.topLevel, typeSizes);
return size + rtlSerializer.serializedSize(info.ranges, typeSizes, version);
}
public long serializedSize(DeletionInfo info, int version)
{
return serializedSize(info, TypeSizes.NATIVE, version);
}
}
/**
* This object allow testing whether a given column (name/timestamp) is deleted
* or not by this DeletionInfo, assuming that the columns given to this
* object are passed in forward or reversed comparator sorted order.
*
* This is more efficient that calling DeletionInfo.isDeleted() repeatedly
* in that case.
*/
public class InOrderTester
{
/*
* Note that because because range tombstone are added to this DeletionInfo while we iterate,
* `ranges` may be null initially and we need to wait for the first range to create the tester (once
* created the test will pick up new tombstones however). We are guaranteed that a range tombstone
* will be added *before* we test any column that it may delete, so this is ok.
*/
private RangeTombstoneList.InOrderTester tester;
private final boolean reversed;
private InOrderTester(boolean reversed)
{
this.reversed = reversed;
}
public boolean isDeleted(Cell cell)
{
if (cell.timestamp() <= topLevel.markedForDeleteAt)
return true;
// No matter what the counter cell's timestamp is, a tombstone always takes precedence. See CASSANDRA-7346.
if (!topLevel.isLive() && cell instanceof CounterCell)
return true;
/*
* We don't optimize the reversed case for now because RangeTombstoneList
* is always in forward sorted order.
*/
if (reversed)
return DeletionInfo.this.isDeleted(cell);
// Maybe create the tester if we hadn't yet and we now have some ranges (see above).
if (tester == null && ranges != null)
tester = ranges.inOrderTester();
return tester != null && tester.isDeleted(cell);
}
}
}