Protect against overflow of local expiration time

Patch by Paulo Motta; Reviewed by Sam Tunnicliffe for CASSANDRA-14092
This commit is contained in:
Paulo Motta 2018-02-01 04:01:28 -02:00 committed by Paulo Motta
parent 5ba9e6da94
commit b2949439ec
59 changed files with 898 additions and 46 deletions

81
CASSANDRA-14092.txt Normal file
View File

@ -0,0 +1,81 @@
CASSANDRA-14092: MAXIMUM TTL EXPIRATION DATE
---------------------------------------------
The maximum expiration timestamp that can be represented by the storage engine is
2038-01-19T03:14:06+00:00, which means that INSERTS using TTL that would expire
after this date are not currently supported.
# Expiration Date Overflow Policy
We plan to lift this limitation in newer versions, but while the fix is not available,
operators can decide which policy to apply when dealing with inserts with TTL exceeding
the maximum supported expiration date:
- REJECT: this is the default policy and will reject any requests with expiration
date timestamp after 2038-01-19T03:14:06+00:00.
- CAP: any insert with TTL expiring after 2038-01-19T03:14:06+00:00 will expire on
2038-01-19T03:14:06+00:00 and the client will receive a warning.
- CAP_NOWARN: same as previous, except that the client warning will not be emitted.
These policies may be specified via the -Dcassandra.expiration_date_overflow_policy=POLICY
startup option.
# Potential data loss on earlier versions
Prior to 3.0.16 (3.0.X) and 3.11.2 (3.11.x), there was no protection against
INSERTS with TTL expiring after the maximum supported date, causing the expiration
time field to overflow and the records to expire immediately. Expired records due
to overflow will not be queryable and will be permanently removed after a compaction.
2.1.X, 2.2.X and earlier series are not subject to this bug when assertions are enabled
since an AssertionError is thrown during INSERT when the expiration time field overflows
on these versions. When assertions are disabled then it is possible to INSERT entries
with overflowed local expiration time and even the earlier versions are subject to data
loss due to this bug.
This issue only affected INSERTs with very large TTLs, close to the maximum allowed value
of 630720000 seconds (20 years), starting from 2018-01-19T03:14:06+00:00. As time progresses,
the maximum supported TTL will be gradually reduced as the maximum expiration date approaches.
For instance, a user on an affected version on 2028-01-19T03:14:06 with a TTL of 10 years
will be affected by this bug, so we urge users of very large TTLs to upgrade to a version
where this issue is addressed as soon as possible.
# Data Recovery
SSTables from Cassandra versions prior to 2.1.20/2.2.12/3.0.16/3.11.2 containing entries
with overflowed expiration time that were backed up or did not go through compaction can
be recovered by reinserting overflowed entries with a valid expiration time and a higher
timestamp, since tombstones may have been generated with the original timestamp.
To find out if an SSTable has an entry with overflowed expiration, inspect it with the
sstable2json tool and look for a negative "local deletion time" field. SSTables in this
condition should be backed up immediately, as they are subject to data loss during
compaction.
A "--reinsert-overflowed-ttl" option was added to scrub to rewrite SSTables containing
rows with overflowed expiration time with the maximum expiration date of
2038-01-19T03:14:06+00:00 and the original timestamp + 1 (ms). Two methods are offered
for recovery of SSTables via scrub:
- Offline scrub:
- Clone the data directory tree to another location, keeping only the folders and the
contents of the system tables.
- Clone the configuration directory to another location, setting the data_file_directories
property to the cloned data directory in the cloned cassandra.yaml.
- Copy the affected SSTables to the cloned data location of the affected table.
- Set the environment variable CASSANDRA_CONF=<cloned configuration directory>.
- Execute "sstablescrub --reinsert-overflowed-ttl <keyspace> <table>".
WARNING: not specifying --reinsert-overflowed-ttl is equivalent to a single-sstable
compaction, so the data with overflowed will be removed - make sure to back up
your SSTables before running scrub.
- Once the scrub is completed, copy the resulting SSTables to the original data directory.
- Execute "nodetool refresh" in a live node to load the recovered SSTables.
- Online scrub:
- Disable compaction on the node with "nodetool disableautocompaction" - this step is crucial
as otherwise, the data may be removed permanently during compaction.
- Copy the SSTables containing entries with overflowed expiration time to the data directory.
- run "nodetool refresh" to load the SSTables.
- run "nodetool scrub --reinsert-overflowed-ttl <keyspace> <table>".
- Re-enable compactions after verifying that scrub recovered the missing entries.
See https://issues.apache.org/jira/browse/CASSANDRA-14092 for more details about this issue.

View File

@ -1,4 +1,5 @@
2.1.20 2.1.20
* Protect against overflow of local expiration time (CASSANDRA-14092)
* More PEP8 compliance for cqlsh (CASSANDRA-14021) * More PEP8 compliance for cqlsh (CASSANDRA-14021)
* RPM package spec: fix permissions for installed jars and config files (CASSANDRA-14181) * RPM package spec: fix permissions for installed jars and config files (CASSANDRA-14181)

View File

@ -1,3 +1,23 @@
PLEASE READ: MAXIMUM TTL EXPIRATION DATE NOTICE (CASSANDRA-14092)
------------------------------------------------------------------
(General upgrading instructions are available in the next section)
The maximum expiration timestamp that can be represented by the storage engine is
2038-01-19T03:14:06+00:00, which means that inserts with TTL thatl expire after
this date are not currently supported. By default, INSERTS with TTL exceeding the
maximum supported date are rejected, but it's possible to choose a different
expiration overflow policy. See CASSANDRA-14092.txt for more details.
Prior to 3.0.16 (3.0.X) and 3.11.2 (3.11.x) there was no protection against INSERTS
with TTL expiring after the maximum supported date, causing the expiration time
field to overflow and the records to expire immediately. Clusters in the 2.X and
lower series are not subject to this when assertions are enabled. Backed up SSTables
can be potentially recovered and recovery instructions can be found on the
CASSANDRA-14092.txt file.
If you use or plan to use very large TTLS (10 to 20 years), read CASSANDRA-14092.txt
for more information.
GENERAL UPGRADING ADVICE FOR ANY VERSION GENERAL UPGRADING ADVICE FOR ANY VERSION
======================================== ========================================
@ -18,8 +38,7 @@ using the provided 'sstableupgrade' tool.
Upgrading Upgrading
--------- ---------
- Nothing specific to this release, but please see 2.1 if you are upgrading - See MAXIMUM TTL EXPIRATION DATE NOTICE above.
from a previous version.
2.1.19 2.1.19
====== ======

View File

@ -20,11 +20,14 @@ package org.apache.cassandra.cql;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.db.IMutation; import org.apache.cassandra.db.IMutation;
import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.exceptions.UnauthorizedException; import org.apache.cassandra.exceptions.UnauthorizedException;
import org.apache.cassandra.thrift.ThriftClientState; import org.apache.cassandra.thrift.ThriftClientState;
import org.w3c.dom.Attr;
public abstract class AbstractModification public abstract class AbstractModification
{ {
@ -89,6 +92,7 @@ public abstract class AbstractModification
public int getTimeToLive() public int getTimeToLive()
{ {
Attributes.maybeApplyExpirationDateOverflowPolicy(keyspace, columnFamily, timeToLive);
return timeToLive; return timeToLive;
} }

View File

@ -17,7 +17,10 @@
*/ */
package org.apache.cassandra.cql; package org.apache.cassandra.cql;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.db.ConsistencyLevel; import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.exceptions.InvalidRequestException;
/** /**
* Class to contain attributes for statements * Class to contain attributes for statements
@ -73,4 +76,20 @@ public class Attributes
return String.format("Attributes(consistency=%s, timestamp=%s, timeToLive=%s)", cLevel, timestamp, timeToLive); return String.format("Attributes(consistency=%s, timestamp=%s, timeToLive=%s)", cLevel, timestamp, timeToLive);
} }
public static void maybeApplyExpirationDateOverflowPolicy(String keyspace, String columnFamily, Integer timeToLive)
{
CFMetaData metadata = Schema.instance.getCFMetaData(keyspace, columnFamily);
if (metadata != null)
{
try
{
org.apache.cassandra.cql3.Attributes.maybeApplyExpirationDateOverflowPolicy(metadata, timeToLive, false);
}
catch (InvalidRequestException e)
{
throw new RuntimeException(e);
}
}
}
} }

View File

@ -72,6 +72,10 @@ public class BatchStatement
public int getTimeToLive() public int getTimeToLive()
{ {
for (AbstractModification statement : statements)
{
Attributes.maybeApplyExpirationDateOverflowPolicy(statement.keyspace, statement.columnFamily, timeToLive);
}
return timeToLive; return timeToLive;
} }

View File

@ -28,6 +28,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.db.ExpiringCell;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.db.compaction.AbstractCompactionStrategy; import org.apache.cassandra.db.compaction.AbstractCompactionStrategy;
import org.apache.cassandra.db.marshal.AbstractType; import org.apache.cassandra.db.marshal.AbstractType;
@ -191,6 +192,12 @@ public class CFPropDefs {
KW_DEFAULT_TIME_TO_LIVE, KW_DEFAULT_TIME_TO_LIVE,
0, 0,
CFMetaData.DEFAULT_DEFAULT_TIME_TO_LIVE)); CFMetaData.DEFAULT_DEFAULT_TIME_TO_LIVE));
if (defaultTimeToLive > ExpiringCell.MAX_TTL)
throw new InvalidRequestException(String.format("%s must be less than or equal to %d (got %s)",
KW_DEFAULT_TIME_TO_LIVE,
ExpiringCell.MAX_TTL,
defaultTimeToLive));
} }
CFMetaData.validateCompactionOptions(compactionStrategyClass, compactionStrategyOptions); CFMetaData.validateCompactionOptions(compactionStrategyClass, compactionStrategyOptions);

View File

@ -18,13 +18,19 @@
package org.apache.cassandra.cql3; package org.apache.cassandra.cql3;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.concurrent.TimeUnit;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.db.ExpiringCell; import org.apache.cassandra.db.ExpiringCell;
import org.apache.cassandra.db.marshal.Int32Type; import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.LongType; import org.apache.cassandra.db.marshal.LongType;
import org.apache.cassandra.exceptions.InvalidRequestException; import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.serializers.MarshalException; import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.utils.NoSpamLogger;
/** /**
* Utility class for the Parser to gather attributes for modification * Utility class for the Parser to gather attributes for modification
@ -32,6 +38,41 @@ import org.apache.cassandra.serializers.MarshalException;
*/ */
public class Attributes public class Attributes
{ {
private static final int EXPIRATION_OVERFLOW_WARNING_INTERVAL_MINUTES = Integer.getInteger("cassandra.expiration_overflow_warning_interval_minutes", 5);
private static final Logger logger = LoggerFactory.getLogger(Attributes.class);
public enum ExpirationDateOverflowPolicy
{
REJECT, CAP
}
@VisibleForTesting
public static ExpirationDateOverflowPolicy policy;
static {
String policyAsString = System.getProperty("cassandra.expiration_date_overflow_policy", ExpirationDateOverflowPolicy.REJECT.name());
try
{
policy = ExpirationDateOverflowPolicy.valueOf(policyAsString.toUpperCase());
}
catch (RuntimeException e)
{
logger.warn("Invalid expiration date overflow policy: {}. Using default: {}", policyAsString, ExpirationDateOverflowPolicy.REJECT.name());
policy = ExpirationDateOverflowPolicy.REJECT;
}
}
public static final String MAXIMUM_EXPIRATION_DATE_EXCEEDED_WARNING = "Request on table {}.{} with {}ttl of {} seconds exceeds maximum supported expiration " +
"date of 2038-01-19T03:14:06+00:00 and will have its expiration capped to that date. " +
"In order to avoid this use a lower TTL or upgrade to a version where this limitation " +
"is fixed. See CASSANDRA-14092 for more details.";
public static final String MAXIMUM_EXPIRATION_DATE_EXCEEDED_REJECT_MESSAGE = "Request on table %s.%s with %sttl of %d seconds exceeds maximum supported expiration " +
"date of 2038-01-19T03:14:06+00:00. In order to avoid this use a lower TTL, change " +
"the expiration date overflow policy or upgrade to a version where this limitation " +
"is fixed. See CASSANDRA-14092 for more details.";
private final Term timestamp; private final Term timestamp;
private final Term timeToLive; private final Term timeToLive;
@ -77,10 +118,13 @@ public class Attributes
return LongType.instance.compose(tval); return LongType.instance.compose(tval);
} }
public int getTimeToLive(QueryOptions options) throws InvalidRequestException public int getTimeToLive(QueryOptions options, CFMetaData metadata) throws InvalidRequestException
{ {
if (timeToLive == null) if (timeToLive == null)
return 0; {
maybeApplyExpirationDateOverflowPolicy(metadata, metadata.getDefaultTimeToLive(), true);
return metadata.getDefaultTimeToLive();
}
ByteBuffer tval = timeToLive.bindAndGet(options); ByteBuffer tval = timeToLive.bindAndGet(options);
if (tval == null) if (tval == null)
@ -102,6 +146,8 @@ public class Attributes
if (ttl > ExpiringCell.MAX_TTL) if (ttl > ExpiringCell.MAX_TTL)
throw new InvalidRequestException(String.format("ttl is too large. requested (%d) maximum (%d)", ttl, ExpiringCell.MAX_TTL)); throw new InvalidRequestException(String.format("ttl is too large. requested (%d) maximum (%d)", ttl, ExpiringCell.MAX_TTL));
maybeApplyExpirationDateOverflowPolicy(metadata, ttl, false);
return ttl; return ttl;
} }
@ -135,4 +181,33 @@ public class Attributes
return new ColumnSpecification(ksName, cfName, new ColumnIdentifier("[ttl]", true), Int32Type.instance); return new ColumnSpecification(ksName, cfName, new ColumnIdentifier("[ttl]", true), Int32Type.instance);
} }
} }
public static void maybeApplyExpirationDateOverflowPolicy(CFMetaData metadata, int ttl, boolean isDefaultTTL) throws InvalidRequestException
{
if (ttl == 0)
return;
// Check for localExpirationTime overflow (CASSANDRA-14092)
int nowInSecs = (int)(System.currentTimeMillis() / 1000);
if (ttl + nowInSecs < 0)
{
switch (policy)
{
case CAP:
/**
* Capping at this stage is basically not rejecting the request. The actual capping is done
* by {@link org.apache.cassandra.db.BufferExpiringCell#computeLocalExpirationTime(int)},
* which converts the negative TTL to {@link org.apache.cassandra.db.BufferExpiringCell#MAX_DELETION_TIME}
*/
NoSpamLogger.log(logger, NoSpamLogger.Level.WARN, EXPIRATION_OVERFLOW_WARNING_INTERVAL_MINUTES,
TimeUnit.MINUTES, MAXIMUM_EXPIRATION_DATE_EXCEEDED_WARNING,
metadata.ksName, metadata.cfName, isDefaultTTL? "default " : "", ttl);
return;
default: //REJECT
throw new InvalidRequestException(String.format(MAXIMUM_EXPIRATION_DATE_EXCEEDED_REJECT_MESSAGE, metadata.ksName, metadata.cfName,
isDefaultTTL? "default " : "", ttl));
}
}
}
} }

View File

@ -22,6 +22,7 @@ import java.util.*;
import org.apache.cassandra.cache.CachingOptions; import org.apache.cassandra.cache.CachingOptions;
import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.CFMetaData.SpeculativeRetry; import org.apache.cassandra.config.CFMetaData.SpeculativeRetry;
import org.apache.cassandra.db.ExpiringCell;
import org.apache.cassandra.db.compaction.AbstractCompactionStrategy; import org.apache.cassandra.db.compaction.AbstractCompactionStrategy;
import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.exceptions.SyntaxException; import org.apache.cassandra.exceptions.SyntaxException;
@ -127,6 +128,12 @@ public class CFPropDefs extends PropertyDefinitions
} }
validateMinimumInt(KW_DEFAULT_TIME_TO_LIVE, 0, CFMetaData.DEFAULT_DEFAULT_TIME_TO_LIVE); validateMinimumInt(KW_DEFAULT_TIME_TO_LIVE, 0, CFMetaData.DEFAULT_DEFAULT_TIME_TO_LIVE);
Integer defaultTimeToLive = getInt(KW_DEFAULT_TIME_TO_LIVE, 0);
if (defaultTimeToLive > ExpiringCell.MAX_TTL)
throw new ConfigurationException(String.format("%s must be less than or equal to %d (got %s)",
KW_DEFAULT_TIME_TO_LIVE,
ExpiringCell.MAX_TTL,
defaultTimeToLive));
Integer minIndexInterval = getInt(KW_MIN_INDEX_INTERVAL, null); Integer minIndexInterval = getInt(KW_MIN_INDEX_INTERVAL, null);
Integer maxIndexInterval = getInt(KW_MAX_INDEX_INTERVAL, null); Integer maxIndexInterval = getInt(KW_MAX_INDEX_INTERVAL, null);

View File

@ -124,7 +124,7 @@ public abstract class ModificationStatement implements CQLStatement
public int getTimeToLive(QueryOptions options) throws InvalidRequestException public int getTimeToLive(QueryOptions options) throws InvalidRequestException
{ {
return attrs.getTimeToLive(options); return attrs.getTimeToLive(options, cfm);
} }
public void checkAccess(ClientState state) throws InvalidRequestException, UnauthorizedException public void checkAccess(ClientState state) throws InvalidRequestException, UnauthorizedException

View File

@ -575,6 +575,12 @@ public abstract class AbstractNativeCell extends AbstractCell implements CellNam
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public Cell withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
{
throw new UnsupportedOperationException();
}
protected long internalSize() protected long internalSize()
{ {
return MemoryUtil.getInt(peer); return MemoryUtil.getInt(peer);

View File

@ -68,6 +68,12 @@ public class BufferCell extends AbstractCell
return new BufferCell(name, value, newTimestamp); return new BufferCell(name, value, newTimestamp);
} }
@Override
public Cell withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
{
throw new UnsupportedOperationException();
}
@Override @Override
public CellName name() { public CellName name() {
return name; return name;

View File

@ -53,6 +53,12 @@ public class BufferDeletedCell extends BufferCell implements DeletedCell
return new BufferDeletedCell(name, value, newTimestamp); return new BufferDeletedCell(name, value, newTimestamp);
} }
@Override
public Cell withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
{
throw new UnsupportedOperationException();
}
@Override @Override
public boolean isLive() public boolean isLive()
{ {

View File

@ -31,19 +31,23 @@ import org.apache.cassandra.utils.memory.MemtableAllocator;
public class BufferExpiringCell extends BufferCell implements ExpiringCell public class BufferExpiringCell extends BufferCell implements ExpiringCell
{ {
public static final int MAX_DELETION_TIME = Integer.MAX_VALUE - 1;
private final int localExpirationTime; private final int localExpirationTime;
private final int timeToLive; private final int timeToLive;
public BufferExpiringCell(CellName name, ByteBuffer value, long timestamp, int timeToLive) public BufferExpiringCell(CellName name, ByteBuffer value, long timestamp, int timeToLive)
{ {
this(name, value, timestamp, timeToLive, (int) (System.currentTimeMillis() / 1000) + timeToLive); super(name, value, timestamp);
assert timeToLive > 0 : timeToLive;
this.timeToLive = timeToLive;
this.localExpirationTime = computeLocalExpirationTime(timeToLive);
} }
public BufferExpiringCell(CellName name, ByteBuffer value, long timestamp, int timeToLive, int localExpirationTime) public BufferExpiringCell(CellName name, ByteBuffer value, long timestamp, int timeToLive, int localExpirationTime)
{ {
super(name, value, timestamp); super(name, value, timestamp);
assert timeToLive > 0 : timeToLive; assert timeToLive > 0 : timeToLive;
assert localExpirationTime > 0 : localExpirationTime;
this.timeToLive = timeToLive; this.timeToLive = timeToLive;
this.localExpirationTime = localExpirationTime; this.localExpirationTime = localExpirationTime;
} }
@ -65,6 +69,12 @@ public class BufferExpiringCell extends BufferCell implements ExpiringCell
return new BufferExpiringCell(name(), value(), newTimestamp, timeToLive, localExpirationTime); return new BufferExpiringCell(name(), value(), newTimestamp, timeToLive, localExpirationTime);
} }
@Override
public Cell withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime)
{
return new BufferExpiringCell(name(), value(), newTimestamp, timeToLive, newLocalDeletionTime);
}
@Override @Override
public int cellDataSize() public int cellDataSize()
{ {
@ -176,7 +186,9 @@ public class BufferExpiringCell extends BufferCell implements ExpiringCell
/** @return Either a DeletedCell, or an ExpiringCell. */ /** @return Either a DeletedCell, or an ExpiringCell. */
public static Cell create(CellName name, ByteBuffer value, long timestamp, int timeToLive, int localExpirationTime, int expireBefore, ColumnSerializer.Flag flag) public static Cell create(CellName name, ByteBuffer value, long timestamp, int timeToLive, int localExpirationTime, int expireBefore, ColumnSerializer.Flag flag)
{ {
if (localExpirationTime >= expireBefore || flag == ColumnSerializer.Flag.PRESERVE_SIZE) // CASSANDRA-14092 may have written rows with negative localExpirationTime, so we don't turn them into tombstones yet
// to be able to recover them with scrub.
if (localExpirationTime < 0 || localExpirationTime >= expireBefore || flag == ColumnSerializer.Flag.PRESERVE_SIZE)
return new BufferExpiringCell(name, value, timestamp, timeToLive, localExpirationTime); return new BufferExpiringCell(name, value, timestamp, timeToLive, localExpirationTime);
// The column is now expired, we can safely return a simple tombstone. Note that // The column is now expired, we can safely return a simple tombstone. Note that
// as long as the expiring column and the tombstone put together live longer than GC grace seconds, // as long as the expiring column and the tombstone put together live longer than GC grace seconds,
@ -184,4 +196,20 @@ public class BufferExpiringCell extends BufferCell implements ExpiringCell
// http://cassandra-user-incubator-apache-org.3065146.n2.nabble.com/repair-compaction-and-tombstone-rows-td7583481.html // http://cassandra-user-incubator-apache-org.3065146.n2.nabble.com/repair-compaction-and-tombstone-rows-td7583481.html
return new BufferDeletedCell(name, localExpirationTime - timeToLive, timestamp); return new BufferDeletedCell(name, localExpirationTime - timeToLive, timestamp);
} }
/**
* This method computes the {@link #localExpirationTime}, maybe capping to the maximum representable value
* which is {@link #MAX_DELETION_TIME}.
*
* Please note that the {@link org.apache.cassandra.cql3.Attributes.ExpirationDateOverflowPolicy} is applied
* during {@link org.apache.cassandra.cql3.Attributes#maybeApplyExpirationDateOverflowPolicy(CFMetaData, int, boolean)},
* so if the request was not denied it means it's expiration date should be capped.
*
* See CASSANDRA-14092
*/
private int computeLocalExpirationTime(int timeToLive)
{
int localExpirationTime = (int) (System.currentTimeMillis() / 1000) + timeToLive;
return localExpirationTime >= 0? localExpirationTime : MAX_DELETION_TIME;
}
} }

View File

@ -38,6 +38,8 @@ public interface Cell extends OnDiskAtom
public Cell withUpdatedTimestamp(long newTimestamp); public Cell withUpdatedTimestamp(long newTimestamp);
public Cell withUpdatedTimestampAndLocalDeletionTime(long newTimestamp, int newLocalDeletionTime);
@Override @Override
public CellName name(); public CellName name();

View File

@ -1516,12 +1516,12 @@ public class ColumnFamilyStore implements ColumnFamilyStoreMBean
return CompactionManager.instance.performCleanup(ColumnFamilyStore.this, jobs); return CompactionManager.instance.performCleanup(ColumnFamilyStore.this, jobs);
} }
public CompactionManager.AllSSTableOpStatus scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, int jobs) throws ExecutionException, InterruptedException public CompactionManager.AllSSTableOpStatus scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, boolean reinsertOverflowedTTLRows, int jobs) throws ExecutionException, InterruptedException
{ {
// skip snapshot creation during scrub, SEE JIRA 5891 // skip snapshot creation during scrub, SEE JIRA 5891
if(!disableSnapshot) if(!disableSnapshot)
snapshotWithoutFlush("pre-scrub-" + System.currentTimeMillis()); snapshotWithoutFlush("pre-scrub-" + System.currentTimeMillis());
return CompactionManager.instance.performScrub(ColumnFamilyStore.this, skipCorrupted, checkData, jobs); return CompactionManager.instance.performScrub(ColumnFamilyStore.this, skipCorrupted, checkData, reinsertOverflowedTTLRows, jobs);
} }
public CompactionManager.AllSSTableOpStatus sstablesRewrite(boolean excludeCurrentVersion, int jobs) throws ExecutionException, InterruptedException public CompactionManager.AllSSTableOpStatus sstablesRewrite(boolean excludeCurrentVersion, int jobs) throws ExecutionException, InterruptedException

View File

@ -60,6 +60,7 @@ public class DeletionTime implements Comparable<DeletionTime>, IMeasurableMemory
@VisibleForTesting @VisibleForTesting
public DeletionTime(long markedForDeleteAt, int localDeletionTime) public DeletionTime(long markedForDeleteAt, int localDeletionTime)
{ {
assert localDeletionTime >= 0 : localDeletionTime;
this.markedForDeleteAt = markedForDeleteAt; this.markedForDeleteAt = markedForDeleteAt;
this.localDeletionTime = localDeletionTime; this.localDeletionTime = localDeletionTime;
} }

View File

@ -358,7 +358,14 @@ public class CompactionManager implements CompactionManagerMBean
} }
} }
@Deprecated
public AllSSTableOpStatus performScrub(final ColumnFamilyStore cfs, final boolean skipCorrupted, final boolean checkData, int jobs) throws InterruptedException, ExecutionException public AllSSTableOpStatus performScrub(final ColumnFamilyStore cfs, final boolean skipCorrupted, final boolean checkData, int jobs) throws InterruptedException, ExecutionException
{
return performScrub(cfs, skipCorrupted, checkData, false, jobs);
}
public AllSSTableOpStatus performScrub(final ColumnFamilyStore cfs, final boolean skipCorrupted, final boolean checkData,
final boolean reinsertOverflowedTTLRows, int jobs) throws InterruptedException, ExecutionException
{ {
assert !cfs.isIndex(); assert !cfs.isIndex();
return parallelAllSSTableOperation(cfs, new OneSSTableOperation() return parallelAllSSTableOperation(cfs, new OneSSTableOperation()
@ -372,7 +379,7 @@ public class CompactionManager implements CompactionManagerMBean
@Override @Override
public void execute(SSTableReader input) throws IOException public void execute(SSTableReader input) throws IOException
{ {
scrubOne(cfs, input, skipCorrupted, checkData); scrubOne(cfs, input, skipCorrupted, checkData, reinsertOverflowedTTLRows);
} }
}, jobs); }, jobs);
} }
@ -710,9 +717,9 @@ public class CompactionManager implements CompactionManagerMBean
} }
} }
private void scrubOne(ColumnFamilyStore cfs, SSTableReader sstable, boolean skipCorrupted, boolean checkData) throws IOException private void scrubOne(ColumnFamilyStore cfs, SSTableReader sstable, boolean skipCorrupted, boolean checkData, boolean reinsertOverflowedTTLRows) throws IOException
{ {
Scrubber scrubber = new Scrubber(cfs, sstable, skipCorrupted, false, checkData); Scrubber scrubber = new Scrubber(cfs, sstable, skipCorrupted, false, checkData, reinsertOverflowedTTLRows);
CompactionInfo.Holder scrubInfo = scrubber.getScrubInfo(); CompactionInfo.Holder scrubInfo = scrubber.getScrubInfo();
metrics.beginCompaction(scrubInfo); metrics.beginCompaction(scrubInfo);
@ -1352,7 +1359,7 @@ public class CompactionManager implements CompactionManagerMBean
public void afterExecute(Runnable r, Throwable t) public void afterExecute(Runnable r, Throwable t)
{ {
DebuggableThreadPoolExecutor.maybeResetTraceSessionWrapper(r); DebuggableThreadPoolExecutor.maybeResetTraceSessionWrapper(r);
if (t == null) if (t == null)
t = DebuggableThreadPoolExecutor.extractThrowable(r); t = DebuggableThreadPoolExecutor.extractThrowable(r);

View File

@ -28,6 +28,7 @@ import com.google.common.collect.Sets;
import org.apache.cassandra.db.*; import org.apache.cassandra.db.*;
import org.apache.cassandra.db.columniterator.OnDiskAtomIterator; import org.apache.cassandra.db.columniterator.OnDiskAtomIterator;
import org.apache.cassandra.db.composites.CellNames;
import org.apache.cassandra.io.sstable.*; import org.apache.cassandra.io.sstable.*;
import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.io.util.RandomAccessReader; import org.apache.cassandra.io.util.RandomAccessReader;
@ -35,6 +36,7 @@ import org.apache.cassandra.service.ActiveRepairService;
import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.OutputHandler; import org.apache.cassandra.utils.OutputHandler;
import org.apache.cassandra.utils.memory.HeapAllocator;
public class Scrubber implements Closeable public class Scrubber implements Closeable
{ {
@ -43,6 +45,7 @@ public class Scrubber implements Closeable
private final File destination; private final File destination;
private final boolean skipCorrupted; private final boolean skipCorrupted;
public final boolean validateColumns; public final boolean validateColumns;
private final boolean reinsertOverflowedTTLRows;
private final CompactionController controller; private final CompactionController controller;
private final boolean isCommutative; private final boolean isCommutative;
@ -67,6 +70,7 @@ public class Scrubber implements Closeable
long nextRowPositionFromIndex; long nextRowPositionFromIndex;
private final OutputHandler outputHandler; private final OutputHandler outputHandler;
private NegativeLocalDeletionInfoMetrics negativeLocalDeletionInfoMetrics = new NegativeLocalDeletionInfoMetrics();
private static final Comparator<Row> rowComparator = new Comparator<Row>() private static final Comparator<Row> rowComparator = new Comparator<Row>()
{ {
@ -79,10 +83,17 @@ public class Scrubber implements Closeable
public Scrubber(ColumnFamilyStore cfs, SSTableReader sstable, boolean skipCorrupted, boolean isOffline, boolean checkData) throws IOException public Scrubber(ColumnFamilyStore cfs, SSTableReader sstable, boolean skipCorrupted, boolean isOffline, boolean checkData) throws IOException
{ {
this(cfs, sstable, skipCorrupted, new OutputHandler.LogOutput(), isOffline, checkData); this(cfs, sstable, skipCorrupted, isOffline, checkData, false);
} }
public Scrubber(ColumnFamilyStore cfs, SSTableReader sstable, boolean skipCorrupted, OutputHandler outputHandler, boolean isOffline, boolean checkData) throws IOException public Scrubber(ColumnFamilyStore cfs, SSTableReader sstable, boolean skipCorrupted, boolean isOffline, boolean checkData,
boolean reinsertOverflowedTTLRows) throws IOException
{
this(cfs, sstable, skipCorrupted, new OutputHandler.LogOutput(), isOffline, checkData, reinsertOverflowedTTLRows);
}
public Scrubber(ColumnFamilyStore cfs, SSTableReader sstable, boolean skipCorrupted, OutputHandler outputHandler, boolean isOffline, boolean checkData,
boolean reinsertOverflowedTTLRows) throws IOException
{ {
this.cfs = cfs; this.cfs = cfs;
this.sstable = sstable; this.sstable = sstable;
@ -90,6 +101,7 @@ public class Scrubber implements Closeable
this.skipCorrupted = skipCorrupted; this.skipCorrupted = skipCorrupted;
this.isOffline = isOffline; this.isOffline = isOffline;
this.validateColumns = checkData; this.validateColumns = checkData;
this.reinsertOverflowedTTLRows = reinsertOverflowedTTLRows;
List<SSTableReader> toScrub = Collections.singletonList(sstable); List<SSTableReader> toScrub = Collections.singletonList(sstable);
@ -131,6 +143,9 @@ public class Scrubber implements Closeable
this.currentRowPositionFromIndex = 0; this.currentRowPositionFromIndex = 0;
this.nextRowPositionFromIndex = 0; this.nextRowPositionFromIndex = 0;
if (reinsertOverflowedTTLRows)
outputHandler.output("Starting scrub with reinsert overflowed TTL option");
} }
public void scrub() public void scrub()
@ -311,6 +326,8 @@ public class Scrubber implements Closeable
else else
{ {
outputHandler.output("Scrub of " + sstable + " complete: " + goodRows + " rows in new sstable and " + emptyRows + " empty (tombstoned) rows dropped"); outputHandler.output("Scrub of " + sstable + " complete: " + goodRows + " rows in new sstable and " + emptyRows + " empty (tombstoned) rows dropped");
if (negativeLocalDeletionInfoMetrics.fixedRows > 0)
outputHandler.output("Fixed " + negativeLocalDeletionInfoMetrics.fixedRows + " rows with overflowed local deletion time.");
if (badRows > 0) if (badRows > 0)
outputHandler.warn("Unable to recover " + badRows + " rows that were skipped. You can attempt manual recovery from the pre-scrub snapshot. You can also run nodetool repair to transfer the data from a healthy replica, if any"); outputHandler.warn("Unable to recover " + badRows + " rows that were skipped. You can attempt manual recovery from the pre-scrub snapshot. You can also run nodetool repair to transfer the data from a healthy replica, if any");
} }
@ -322,7 +339,7 @@ public class Scrubber implements Closeable
// OrderCheckerIterator will check, at iteration time, that the cells are in the proper order. If it detects // OrderCheckerIterator will check, at iteration time, that the cells are in the proper order. If it detects
// that one cell is out of order, it will stop returning them. The remaining cells will be sorted and added // that one cell is out of order, it will stop returning them. The remaining cells will be sorted and added
// to the outOfOrderRows that will be later written to a new SSTable. // to the outOfOrderRows that will be later written to a new SSTable.
OrderCheckerIterator atoms = new OrderCheckerIterator(new SSTableIdentityIterator(sstable, dataFile, key, dataSize, validateColumns), OrderCheckerIterator atoms = new OrderCheckerIterator(getIterator(key, dataSize),
cfs.metadata.comparator.onDiskAtomComparator()); cfs.metadata.comparator.onDiskAtomComparator());
if (prevKey != null && prevKey.compareTo(key) > 0) if (prevKey != null && prevKey.compareTo(key) > 0)
{ {
@ -342,6 +359,18 @@ public class Scrubber implements Closeable
return true; return true;
} }
/**
* Only wrap with {@link FixNegativeLocalDeletionTimeIterator} if {@link #reinsertOverflowedTTLRows} option
* is specified
*/
private OnDiskAtomIterator getIterator(DecoratedKey key, long dataSize)
{
SSTableIdentityIterator sstableIdentityIterator = new SSTableIdentityIterator(sstable, dataFile, key, dataSize, validateColumns);
return reinsertOverflowedTTLRows ? new FixNegativeLocalDeletionTimeIterator(sstableIdentityIterator,
outputHandler,
negativeLocalDeletionInfoMetrics) : sstableIdentityIterator;
}
private void updateIndexKey() private void updateIndexKey()
{ {
currentIndexKey = nextIndexKey; currentIndexKey = nextIndexKey;
@ -516,6 +545,11 @@ public class Scrubber implements Closeable
} }
} }
public class NegativeLocalDeletionInfoMetrics
{
public volatile int fixedRows = 0;
}
/** /**
* In some case like CASSANDRA-12127 the cells might have been stored in the wrong order. This decorator check the * In some case like CASSANDRA-12127 the cells might have been stored in the wrong order. This decorator check the
* cells order and collect the out of order cells to correct the problem. * cells order and collect the out of order cells to correct the problem.
@ -601,4 +635,61 @@ public class Scrubber implements Closeable
return cf; return cf;
} }
} }
/**
* This iterator converts negative {@link BufferExpiringCell#getLocalDeletionTime()} into {@link BufferExpiringCell#MAX_DELETION_TIME}
*
* This is to recover entries with overflowed localExpirationTime due to CASSANDRA-14092
*/
private static final class FixNegativeLocalDeletionTimeIterator extends AbstractIterator<OnDiskAtom> implements OnDiskAtomIterator
{
/**
* The decorated iterator.
*/
private final OnDiskAtomIterator iterator;
private final OutputHandler outputHandler;
private final NegativeLocalDeletionInfoMetrics negativeLocalExpirationTimeMetrics;
public FixNegativeLocalDeletionTimeIterator(OnDiskAtomIterator iterator, OutputHandler outputHandler,
NegativeLocalDeletionInfoMetrics negativeLocalDeletionInfoMetrics)
{
this.iterator = iterator;
this.outputHandler = outputHandler;
this.negativeLocalExpirationTimeMetrics = negativeLocalDeletionInfoMetrics;
}
public ColumnFamily getColumnFamily()
{
return iterator.getColumnFamily();
}
public DecoratedKey getKey()
{
return iterator.getKey();
}
public void close() throws IOException
{
iterator.close();
}
@Override
protected OnDiskAtom computeNext()
{
if (!iterator.hasNext())
return endOfData();
OnDiskAtom next = iterator.next();
if (next instanceof ExpiringCell && next.getLocalDeletionTime() < 0)
{
outputHandler.debug(String.format("Found cell with negative local expiration time: %s", ((ExpiringCell) next).getString(getColumnFamily().getComparator()), getColumnFamily()));
negativeLocalExpirationTimeMetrics.fixedRows++;
next = ((Cell) next).localCopy(getColumnFamily().metadata(), HeapAllocator.instance).withUpdatedTimestampAndLocalDeletionTime(next.timestamp() + 1, BufferExpiringCell.MAX_DELETION_TIME);
}
return next;
}
}
} }

View File

@ -28,9 +28,11 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.Attributes;
import org.apache.cassandra.db.*; import org.apache.cassandra.db.*;
import org.apache.cassandra.db.context.CounterContext; import org.apache.cassandra.db.context.CounterContext;
import org.apache.cassandra.dht.IPartitioner; import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.io.sstable.metadata.MetadataCollector; import org.apache.cassandra.io.sstable.metadata.MetadataCollector;
import org.apache.cassandra.service.ActiveRepairService; import org.apache.cassandra.service.ActiveRepairService;
import org.apache.cassandra.utils.CounterId; import org.apache.cassandra.utils.CounterId;
@ -155,7 +157,20 @@ public abstract class AbstractSSTableSimpleWriter implements Closeable
*/ */
public void addExpiringColumn(ByteBuffer name, ByteBuffer value, long timestamp, int ttl, long expirationTimestampMS) throws IOException public void addExpiringColumn(ByteBuffer name, ByteBuffer value, long timestamp, int ttl, long expirationTimestampMS) throws IOException
{ {
addColumn(new BufferExpiringCell(metadata.comparator.cellFromByteBuffer(name), value, timestamp, ttl, (int)(expirationTimestampMS / 1000))); int localExpirationTime = (int) (expirationTimestampMS / 1000);
try
{
// This will throw exception if policy is REJECT and now() + ttl is higher than MAX_DELETION_TIME
Attributes.maybeApplyExpirationDateOverflowPolicy(metadata, ttl, false);
// If exception was not thrown, this means the policy was CAP, so we check for overflow and cap if that's the case
if (localExpirationTime < 0)
localExpirationTime = BufferExpiringCell.MAX_DELETION_TIME;
}
catch (InvalidRequestException e)
{
throw new RuntimeException(e);
}
addColumn(new BufferExpiringCell(metadata.comparator.cellFromByteBuffer(name), value, timestamp, ttl, localExpirationTime));
} }
/** /**

View File

@ -2414,11 +2414,17 @@ public class StorageService extends NotificationBroadcasterSupport implements IE
} }
public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException
{
return scrub(disableSnapshot, skipCorrupted, checkData, false, jobs, keyspaceName, columnFamilies);
}
public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, boolean reinsertOverflowedTTLRows,
int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException
{ {
CompactionManager.AllSSTableOpStatus status = CompactionManager.AllSSTableOpStatus.SUCCESSFUL; CompactionManager.AllSSTableOpStatus status = CompactionManager.AllSSTableOpStatus.SUCCESSFUL;
for (ColumnFamilyStore cfStore : getValidColumnFamilies(false, false, keyspaceName, columnFamilies)) for (ColumnFamilyStore cfStore : getValidColumnFamilies(false, false, keyspaceName, columnFamilies))
{ {
CompactionManager.AllSSTableOpStatus oneStatus = cfStore.scrub(disableSnapshot, skipCorrupted, checkData, jobs); CompactionManager.AllSSTableOpStatus oneStatus = cfStore.scrub(disableSnapshot, skipCorrupted, checkData, reinsertOverflowedTTLRows, jobs);
if (oneStatus != CompactionManager.AllSSTableOpStatus.SUCCESSFUL) if (oneStatus != CompactionManager.AllSSTableOpStatus.SUCCESSFUL)
status = oneStatus; status = oneStatus;
} }

View File

@ -219,16 +219,16 @@ public interface StorageServiceMBean extends NotificationEmitter
/** /**
* Takes the snapshot of a multiple column family from different keyspaces. A snapshot name must be specified. * Takes the snapshot of a multiple column family from different keyspaces. A snapshot name must be specified.
* *
* @param tag * @param tag
* the tag given to the snapshot; may not be null or empty * the tag given to the snapshot; may not be null or empty
* @param columnFamilyList * @param columnFamilyList
* list of columnfamily from different keyspace in the form of ks1.cf1 ks2.cf2 * list of columnfamily from different keyspace in the form of ks1.cf1 ks2.cf2
*/ */
public void takeMultipleColumnFamilySnapshot(String tag, String... columnFamilyList) throws IOException; public void takeMultipleColumnFamilySnapshot(String tag, String... columnFamilyList) throws IOException;
/** /**
* Remove the snapshot with the given name from the given keyspaces. * Remove the snapshot with the given name from the given keyspaces.
* If no tag is specified we will remove all snapshots. * If no tag is specified we will remove all snapshots.
@ -274,8 +274,11 @@ public interface StorageServiceMBean extends NotificationEmitter
public int scrub(boolean disableSnapshot, boolean skipCorrupted, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException; public int scrub(boolean disableSnapshot, boolean skipCorrupted, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException;
@Deprecated @Deprecated
public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException; public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException;
@Deprecated
public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException; public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException;
public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, boolean reinsertOverflowedTTLRows, int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException;
/** /**
* Rewrite all sstables to the latest version. * Rewrite all sstables to the latest version.
* Unlike scrub, it doesn't skip bad rows and do not snapshot sstables first. * Unlike scrub, it doesn't skip bad rows and do not snapshot sstables first.
@ -381,11 +384,11 @@ public interface StorageServiceMBean extends NotificationEmitter
* If level cannot be parsed, then the level will be defaulted to DEBUG<br> * If level cannot be parsed, then the level will be defaulted to DEBUG<br>
* <br> * <br>
* The logback configuration should have < jmxConfigurator /> set * The logback configuration should have < jmxConfigurator /> set
* *
* @param classQualifier The logger's classQualifer * @param classQualifier The logger's classQualifer
* @param level The log level * @param level The log level
* @throws Exception * @throws Exception
* *
* @see ch.qos.logback.classic.Level#toLevel(String) * @see ch.qos.logback.classic.Level#toLevel(String)
*/ */
public void setLoggingLevel(String classQualifier, String level) throws Exception; public void setLoggingLevel(String classQualifier, String level) throws Exception;

View File

@ -24,6 +24,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.*; import org.apache.cassandra.config.*;
import org.apache.cassandra.cql3.Attributes;
import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.db.*; import org.apache.cassandra.db.*;
import org.apache.cassandra.db.composites.*; import org.apache.cassandra.db.composites.*;
@ -314,7 +315,7 @@ public class ThriftValidation
if (isCommutative) if (isCommutative)
throw new org.apache.cassandra.exceptions.InvalidRequestException("invalid operation for commutative columnfamily " + metadata.cfName); throw new org.apache.cassandra.exceptions.InvalidRequestException("invalid operation for commutative columnfamily " + metadata.cfName);
validateTtl(cosc.column); validateTtl(metadata, cosc.column);
validateColumnPath(metadata, new ColumnPath(metadata.cfName).setSuper_column((ByteBuffer)null).setColumn(cosc.column.name)); validateColumnPath(metadata, new ColumnPath(metadata.cfName).setSuper_column((ByteBuffer)null).setColumn(cosc.column.name));
validateColumnData(metadata, key, null, cosc.column); validateColumnData(metadata, key, null, cosc.column);
} }
@ -349,7 +350,7 @@ public class ThriftValidation
} }
} }
private static void validateTtl(Column column) throws org.apache.cassandra.exceptions.InvalidRequestException private static void validateTtl(CFMetaData metadata, Column column) throws org.apache.cassandra.exceptions.InvalidRequestException
{ {
if (column.isSetTtl()) if (column.isSetTtl())
{ {
@ -358,9 +359,11 @@ public class ThriftValidation
if (column.ttl > ExpiringCell.MAX_TTL) if (column.ttl > ExpiringCell.MAX_TTL)
throw new org.apache.cassandra.exceptions.InvalidRequestException(String.format("ttl is too large. requested (%d) maximum (%d)", column.ttl, ExpiringCell.MAX_TTL)); throw new org.apache.cassandra.exceptions.InvalidRequestException(String.format("ttl is too large. requested (%d) maximum (%d)", column.ttl, ExpiringCell.MAX_TTL));
Attributes.maybeApplyExpirationDateOverflowPolicy(metadata, column.ttl, false);
} }
else else
{ {
Attributes.maybeApplyExpirationDateOverflowPolicy(metadata, metadata.getDefaultTimeToLive(), true);
// if it's not set, then it should be zero -- here we are just checking to make sure Thrift doesn't change that contract with us. // if it's not set, then it should be zero -- here we are just checking to make sure Thrift doesn't change that contract with us.
assert column.ttl == 0; assert column.ttl == 0;
} }
@ -434,7 +437,7 @@ public class ThriftValidation
*/ */
public static void validateColumnData(CFMetaData metadata, ByteBuffer key, ByteBuffer scName, Column column) throws org.apache.cassandra.exceptions.InvalidRequestException public static void validateColumnData(CFMetaData metadata, ByteBuffer key, ByteBuffer scName, Column column) throws org.apache.cassandra.exceptions.InvalidRequestException
{ {
validateTtl(column); validateTtl(metadata, column);
if (!column.isSetValue()) if (!column.isSetValue())
throw new org.apache.cassandra.exceptions.InvalidRequestException("Column value is required"); throw new org.apache.cassandra.exceptions.InvalidRequestException("Column value is required");
if (!column.isSetTimestamp()) if (!column.isSetTimestamp())

View File

@ -243,9 +243,9 @@ public class NodeProbe implements AutoCloseable
return ssProxy.forceKeyspaceCleanup(jobs, keyspaceName, columnFamilies); return ssProxy.forceKeyspaceCleanup(jobs, keyspaceName, columnFamilies);
} }
public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException public int scrub(boolean disableSnapshot, boolean skipCorrupted, boolean checkData, boolean reinsertOverflowedTTLRows, int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException
{ {
return ssProxy.scrub(disableSnapshot, skipCorrupted, checkData, jobs, keyspaceName, columnFamilies); return ssProxy.scrub(disableSnapshot, skipCorrupted, checkData, reinsertOverflowedTTLRows, jobs, keyspaceName, columnFamilies);
} }
public int upgradeSSTables(String keyspaceName, boolean excludeCurrentVersion, int jobs, String... columnFamilies) throws IOException, ExecutionException, InterruptedException public int upgradeSSTables(String keyspaceName, boolean excludeCurrentVersion, int jobs, String... columnFamilies) throws IOException, ExecutionException, InterruptedException
@ -268,10 +268,10 @@ public class NodeProbe implements AutoCloseable
} }
} }
public void scrub(PrintStream out, boolean disableSnapshot, boolean skipCorrupted, boolean checkData, int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException public void scrub(PrintStream out, boolean disableSnapshot, boolean skipCorrupted, boolean checkData, boolean reinsertOverflowedTTLRows, int jobs, String keyspaceName, String... columnFamilies) throws IOException, ExecutionException, InterruptedException
{ {
checkJobs(out, jobs); checkJobs(out, jobs);
if (scrub(disableSnapshot, skipCorrupted, checkData, jobs, keyspaceName, columnFamilies) != 0) if (scrub(disableSnapshot, skipCorrupted, checkData, reinsertOverflowedTTLRows, jobs, keyspaceName, columnFamilies) != 0)
{ {
failed = true; failed = true;
out.println("Aborted scrubbing atleast one column family in keyspace "+keyspaceName+", check server logs for more information."); out.println("Aborted scrubbing atleast one column family in keyspace "+keyspaceName+", check server logs for more information.");
@ -562,7 +562,7 @@ public class NodeProbe implements AutoCloseable
/** /**
* Take a snapshot of all column family from different keyspaces. * Take a snapshot of all column family from different keyspaces.
* *
* @param snapshotName * @param snapshotName
* the name of the snapshot. * the name of the snapshot.
* @param columnfamilylist * @param columnfamilylist
@ -1302,7 +1302,7 @@ public class NodeProbe implements AutoCloseable
} }
catch (Exception e) catch (Exception e)
{ {
throw new RuntimeException("Error setting log for " + classQualifier +" on level " + level +". Please check logback configuration and ensure to have <jmxConfigurator /> set", e); throw new RuntimeException("Error setting log for " + classQualifier +" on level " + level +". Please check logback configuration and ensure to have <jmxConfigurator /> set", e);
} }
} }

View File

@ -551,20 +551,20 @@ public class NodeTool
try try
{ {
ownerships = probe.effectiveOwnership(keyspace); ownerships = probe.effectiveOwnership(keyspace);
} }
catch (IllegalStateException ex) catch (IllegalStateException ex)
{ {
ownerships = probe.getOwnership(); ownerships = probe.getOwnership();
errors.append("Note: " + ex.getMessage() + "%n"); errors.append("Note: " + ex.getMessage() + "%n");
showEffectiveOwnership = false; showEffectiveOwnership = false;
} }
catch (IllegalArgumentException ex) catch (IllegalArgumentException ex)
{ {
System.out.printf("%nError: " + ex.getMessage() + "%n"); System.out.printf("%nError: " + ex.getMessage() + "%n");
return; return;
} }
System.out.println(); System.out.println();
for (Entry<String, SetHostStat> entry : getOwnershipByDc(probe, resolveIp, tokensToEndpoints, ownerships).entrySet()) for (Entry<String, SetHostStat> entry : getOwnershipByDc(probe, resolveIp, tokensToEndpoints, ownerships).entrySet())
printDc(probe, format, entry.getKey(), endpointsToTokens, entry.getValue(),showEffectiveOwnership); printDc(probe, format, entry.getKey(), endpointsToTokens, entry.getValue(),showEffectiveOwnership);
@ -1282,6 +1282,11 @@ public class NodeTool
description = "Number of sstables to scrub simultanously, set to 0 to use all available compaction threads") description = "Number of sstables to scrub simultanously, set to 0 to use all available compaction threads")
private int jobs = 2; private int jobs = 2;
@Option(title = "reinsert_overflowed_ttl",
name = {"r", "--reinsert-overflowed-ttl"},
description = StandaloneScrubber.REINSERT_OVERFLOWED_TTL_OPTION_DESCRIPTION)
private boolean reinsertOverflowedTTL = false;
@Override @Override
public void execute(NodeProbe probe) public void execute(NodeProbe probe)
{ {
@ -1292,7 +1297,7 @@ public class NodeTool
{ {
try try
{ {
probe.scrub(System.out, disableSnapshot, skipCorrupted, !noValidation, jobs, keyspace, cfnames); probe.scrub(System.out, disableSnapshot, skipCorrupted, reinsertOverflowedTTL , !noValidation, jobs, keyspace, cfnames);
} catch (Exception e) } catch (Exception e)
{ {
throw new RuntimeException("Error occurred during flushing", e); throw new RuntimeException("Error occurred during flushing", e);
@ -2197,7 +2202,7 @@ public class NodeTool
unreachableNodes = probe.getUnreachableNodes(); unreachableNodes = probe.getUnreachableNodes();
hostIDMap = probe.getHostIdMap(); hostIDMap = probe.getHostIdMap();
epSnitchInfo = probe.getEndpointSnitchInfoProxy(); epSnitchInfo = probe.getEndpointSnitchInfoProxy();
StringBuffer errors = new StringBuffer(); StringBuffer errors = new StringBuffer();
Map<InetAddress, Float> ownerships = null; Map<InetAddress, Float> ownerships = null;
@ -2249,9 +2254,9 @@ public class NodeTool
printNode(endpoint.getHostAddress(), owns, tokens, hasEffectiveOwns, isTokenPerNode); printNode(endpoint.getHostAddress(), owns, tokens, hasEffectiveOwns, isTokenPerNode);
} }
} }
System.out.printf("%n" + errors.toString()); System.out.printf("%n" + errors.toString());
} }
private void findMaxAddressLength(Map<String, SetHostStat> dcs) private void findMaxAddressLength(Map<String, SetHostStat> dcs)
@ -2726,7 +2731,7 @@ public class NodeTool
probe.truncateHints(endpoint); probe.truncateHints(endpoint);
} }
} }
@Command(name = "setlogginglevel", description = "Set the log level threshold for a given class. If both class and level are empty/null, it will reset to the initial configuration") @Command(name = "setlogginglevel", description = "Set the log level threshold for a given class. If both class and level are empty/null, it will reset to the initial configuration")
public static class SetLoggingLevel extends NodeToolCmd public static class SetLoggingLevel extends NodeToolCmd
{ {
@ -2741,7 +2746,7 @@ public class NodeTool
probe.setLoggingLevel(classQualifier, level); probe.setLoggingLevel(classQualifier, level);
} }
} }
@Command(name = "getlogginglevels", description = "Get the runtime logging levels") @Command(name = "getlogginglevels", description = "Get the runtime logging levels")
public static class GetLoggingLevels extends NodeToolCmd public static class GetLoggingLevels extends NodeToolCmd
{ {

View File

@ -47,6 +47,12 @@ import static org.apache.cassandra.tools.BulkLoader.CmdLineOptions;
public class StandaloneScrubber public class StandaloneScrubber
{ {
public static final String REINSERT_OVERFLOWED_TTL_OPTION_DESCRIPTION = "Rewrites rows with overflowed expiration date affected by CASSANDRA-14092 with " +
"the maximum supported expiration date of 2038-01-19T03:14:06+00:00. " +
"The rows are rewritten with the original timestamp incremented by one millisecond " +
"to override/supersede any potential tombstone that may have been generated " +
"during compaction of the affected rows.";
private static final String TOOL_NAME = "sstablescrub"; private static final String TOOL_NAME = "sstablescrub";
private static final String VERBOSE_OPTION = "verbose"; private static final String VERBOSE_OPTION = "verbose";
private static final String DEBUG_OPTION = "debug"; private static final String DEBUG_OPTION = "debug";
@ -54,6 +60,7 @@ public class StandaloneScrubber
private static final String MANIFEST_CHECK_OPTION = "manifest-check"; private static final String MANIFEST_CHECK_OPTION = "manifest-check";
private static final String SKIP_CORRUPTED_OPTION = "skip-corrupted"; private static final String SKIP_CORRUPTED_OPTION = "skip-corrupted";
private static final String NO_VALIDATE_OPTION = "no-validate"; private static final String NO_VALIDATE_OPTION = "no-validate";
private static final String REINSERT_OVERFLOWED_TTL_OPTION = "reinsert-overflowed-ttl";
public static void main(String args[]) public static void main(String args[])
{ {
@ -110,7 +117,7 @@ public class StandaloneScrubber
{ {
try try
{ {
Scrubber scrubber = new Scrubber(cfs, sstable, options.skipCorrupted, handler, true, !options.noValidate); Scrubber scrubber = new Scrubber(cfs, sstable, options.skipCorrupted, handler, true, !options.noValidate, options.reinsertOverflowedTTL);
try try
{ {
scrubber.scrub(); scrubber.scrub();
@ -192,6 +199,7 @@ public class StandaloneScrubber
public boolean manifestCheckOnly; public boolean manifestCheckOnly;
public boolean skipCorrupted; public boolean skipCorrupted;
public boolean noValidate; public boolean noValidate;
public boolean reinsertOverflowedTTL;
private Options(String keyspaceName, String cfName) private Options(String keyspaceName, String cfName)
{ {
@ -232,6 +240,7 @@ public class StandaloneScrubber
opts.manifestCheckOnly = cmd.hasOption(MANIFEST_CHECK_OPTION); opts.manifestCheckOnly = cmd.hasOption(MANIFEST_CHECK_OPTION);
opts.skipCorrupted = cmd.hasOption(SKIP_CORRUPTED_OPTION); opts.skipCorrupted = cmd.hasOption(SKIP_CORRUPTED_OPTION);
opts.noValidate = cmd.hasOption(NO_VALIDATE_OPTION); opts.noValidate = cmd.hasOption(NO_VALIDATE_OPTION);
opts.reinsertOverflowedTTL = cmd.hasOption(REINSERT_OVERFLOWED_TTL_OPTION);
return opts; return opts;
} }
@ -258,6 +267,7 @@ public class StandaloneScrubber
options.addOption("m", MANIFEST_CHECK_OPTION, "only check and repair the leveled manifest, without actually scrubbing the sstables"); options.addOption("m", MANIFEST_CHECK_OPTION, "only check and repair the leveled manifest, without actually scrubbing the sstables");
options.addOption("s", SKIP_CORRUPTED_OPTION, "skip corrupt rows in counter tables"); options.addOption("s", SKIP_CORRUPTED_OPTION, "skip corrupt rows in counter tables");
options.addOption("n", NO_VALIDATE_OPTION, "do not validate columns using column validator"); options.addOption("n", NO_VALIDATE_OPTION, "do not validate columns using column validator");
options.addOption("r", REINSERT_OVERFLOWED_TTL_OPTION, REINSERT_OVERFLOWED_TTL_OPTION_DESCRIPTION);
return options; return options;
} }

View File

@ -0,0 +1,8 @@
Index.db
Data.db
Statistics.db
TOC.txt
Summary.db
Digest.sha1
CompressionInfo.db
Filter.db

View File

@ -0,0 +1,8 @@
Index.db
Data.db
Statistics.db
TOC.txt
Summary.db
Digest.sha1
CompressionInfo.db
Filter.db

View File

@ -0,0 +1,8 @@
Index.db
Data.db
Statistics.db
TOC.txt
Summary.db
Digest.sha1
CompressionInfo.db
Filter.db

View File

@ -0,0 +1,8 @@
Index.db
Data.db
Statistics.db
TOC.txt
Summary.db
Digest.sha1
CompressionInfo.db
Filter.db

View File

@ -0,0 +1,410 @@
package org.apache.cassandra.cql3.validation.operations;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.apache.cassandra.cql3.Attributes;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.UntypedResultSet;
import org.apache.cassandra.db.BufferExpiringCell;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.ExpiringCell;
import org.apache.cassandra.db.Keyspace;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.utils.FBUtilities;
import org.junit.Test;
public class TTLTest extends CQLTester
{
public static String NEGATIVE_LOCAL_EXPIRATION_TEST_DIR = "test/data/negative-local-expiration-test/%s";
public static int MAX_TTL = ExpiringCell.MAX_TTL;
public static final String SIMPLE_NOCLUSTERING = "table1";
public static final String SIMPLE_CLUSTERING = "table2";
public static final String COMPLEX_NOCLUSTERING = "table3";
public static final String COMPLEX_CLUSTERING = "table4";
@Test
public void testTTLPerRequestLimit() throws Throwable
{
createTable("CREATE TABLE %s (k int PRIMARY KEY, i int)");
// insert with low TTL should not be denied
execute("INSERT INTO %s (k, i) VALUES (1, 1) USING TTL ?", 10); // max ttl
try
{
execute("INSERT INTO %s (k, i) VALUES (1, 1) USING TTL ?", MAX_TTL + 1);
fail("Expect InvalidRequestException");
}
catch (InvalidRequestException e)
{
assertTrue(e.getMessage().contains("ttl is too large."));
}
try
{
execute("INSERT INTO %s (k, i) VALUES (1, 1) USING TTL ?", -1);
fail("Expect InvalidRequestException");
}
catch (InvalidRequestException e)
{
assertTrue(e.getMessage().contains("A TTL must be greater or equal to 0"));
}
execute("TRUNCATE %s");
// insert with low TTL should not be denied
execute("UPDATE %s USING TTL ? SET i = 1 WHERE k = 2", 5); // max ttl
try
{
execute("UPDATE %s USING TTL ? SET i = 1 WHERE k = 2", MAX_TTL + 1);
fail("Expect InvalidRequestException");
}
catch (InvalidRequestException e)
{
assertTrue(e.getMessage().contains("ttl is too large."));
}
try
{
execute("UPDATE %s USING TTL ? SET i = 1 WHERE k = 2", -1);
fail("Expect InvalidRequestException");
}
catch (InvalidRequestException e)
{
assertTrue(e.getMessage().contains("A TTL must be greater or equal to 0"));
}
}
@Test
public void testTTLDefaultLimit() throws Throwable
{
try
{
createTable("CREATE TABLE %s (k int PRIMARY KEY, i int) WITH default_time_to_live=-1");
fail("Expect Invalid schema");
}
catch (RuntimeException e)
{
assertTrue(e.getCause()
.getCause()
.getMessage()
.contains("default_time_to_live cannot be smaller than 0"));
}
try
{
createTable("CREATE TABLE %s (k int PRIMARY KEY, i int) WITH default_time_to_live="
+ (MAX_TTL + 1));
fail("Expect Invalid schema");
}
catch (RuntimeException e)
{
assertTrue(e.getCause()
.getCause()
.getMessage()
.contains("default_time_to_live must be less than or equal to " + MAX_TTL + " (got "
+ (MAX_TTL + 1) + ")"));
}
// table with default low TTL should not be denied
createTable("CREATE TABLE %s (k int PRIMARY KEY, i int) WITH default_time_to_live=" + 5);
execute("INSERT INTO %s (k, i) VALUES (1, 1)");
}
@Test
public void testRejectExpirationDateOverflowPolicy() throws Throwable
{
Attributes.policy = Attributes.ExpirationDateOverflowPolicy.REJECT;
createTable("CREATE TABLE %s (k int PRIMARY KEY, i int)");
try
{
execute("INSERT INTO %s (k, i) VALUES (1, 1) USING TTL " + MAX_TTL);
}
catch (InvalidRequestException e)
{
assertTrue(e.getMessage().contains("exceeds maximum supported expiration date"));
}
try
{
createTable("CREATE TABLE %s (k int PRIMARY KEY, i int) WITH default_time_to_live=" + MAX_TTL);
execute("INSERT INTO %s (k, i) VALUES (1, 1)");
}
catch (InvalidRequestException e)
{
assertTrue(e.getMessage().contains("exceeds maximum supported expiration date"));
}
}
@Test
public void testCapExpirationDatePolicyDefaultTTL() throws Throwable
{
Attributes.policy = Attributes.ExpirationDateOverflowPolicy.CAP;
createTable("CREATE TABLE %s (k int PRIMARY KEY, i int) WITH default_time_to_live=" + MAX_TTL);
execute("INSERT INTO %s (k, i) VALUES (1, 1)");
checkTTLIsCapped("i");
Attributes.policy = Attributes.ExpirationDateOverflowPolicy.REJECT;
}
@Test
public void testCapExpirationDatePolicyPerRequest() throws Throwable
{
// Test cap policy
Attributes.policy = Attributes.ExpirationDateOverflowPolicy.CAP;
// simple column, clustering, flush
baseCapExpirationDateOverflowTest(true, true, true);
// simple column, clustering, noflush
baseCapExpirationDateOverflowTest(true, true, false);
// simple column, noclustering, flush
baseCapExpirationDateOverflowTest(true, false, true);
// simple column, noclustering, noflush
baseCapExpirationDateOverflowTest(true, false, false);
// complex column, clustering, flush
baseCapExpirationDateOverflowTest(false, true, true);
// complex column, clustering, noflush
baseCapExpirationDateOverflowTest(false, true, false);
// complex column, noclustering, flush
baseCapExpirationDateOverflowTest(false, false, true);
// complex column, noclustering, noflush
baseCapExpirationDateOverflowTest(false, false, false);
// complex column, noclustering, flush
baseCapExpirationDateOverflowTest(false, false, false);
// Return to previous policy
Attributes.policy = Attributes.ExpirationDateOverflowPolicy.REJECT;
}
@Test
public void testRecoverOverflowedExpirationWithScrub() throws Throwable
{
createTable(true, true);
createTable(true, false);
createTable(false, true);
createTable(false, false);
baseTestRecoverOverflowedExpiration(false, false);
baseTestRecoverOverflowedExpiration(true, false);
baseTestRecoverOverflowedExpiration(true, true);
}
public void baseCapExpirationDateOverflowTest(boolean simple, boolean clustering, boolean flush) throws Throwable
{
// Create Table
if (simple)
{
if (clustering)
createTable("create table %s (k int, a int, b int, primary key(k, a))");
else
createTable("create table %s (k int primary key, a int, b int)");
}
else
{
if (clustering)
createTable("create table %s (k int, a int, b set<text>, primary key(k, a))");
else
createTable("create table %s (k int primary key, a int, b set<text>)");
}
// Insert data with INSERT and UPDATE
if (simple)
{
execute("INSERT INTO %s (k, a, b) VALUES (?, ?, ?) USING TTL " + MAX_TTL, 2, 2, 2);
if (clustering)
execute("UPDATE %s USING TTL " + MAX_TTL + " SET b = 1 WHERE k = 1 AND a = 1;");
else
execute("UPDATE %s USING TTL " + MAX_TTL + " SET a = 1, b = 1 WHERE k = 1;");
}
else
{
execute("INSERT INTO %s (k, a, b) VALUES (?, ?, ?) USING TTL " + MAX_TTL, 2, 2, set("v21", "v22", "v23", "v24"));
if (clustering)
execute("UPDATE %s USING TTL " + MAX_TTL + " SET b = ? WHERE k = 1 AND a = 1;", set("v11", "v12", "v13", "v14"));
else
execute("UPDATE %s USING TTL " + MAX_TTL + " SET a = 1, b = ? WHERE k = 1;", set("v11", "v12", "v13", "v14"));
}
// Maybe Flush
Keyspace ks = Keyspace.open(keyspace());
if (flush)
FBUtilities.waitOnFutures(ks.flush());
// Verify data
verifyData(simple);
// Maybe major compact
if (flush)
{
// Major compact and check data is still present
ks.getColumnFamilyStore(currentTable()).forceMajorCompaction();
// Verify data again
verifyData(simple);
}
}
public void baseTestRecoverOverflowedExpiration(boolean runScrub, boolean reinsertOverflowedTTL) throws Throwable
{
// simple column, clustering
testRecoverOverflowedExpirationWithScrub(true, true, runScrub, reinsertOverflowedTTL);
// simple column, noclustering
testRecoverOverflowedExpirationWithScrub(true, false, runScrub, reinsertOverflowedTTL);
// complex column, clustering
testRecoverOverflowedExpirationWithScrub(false, true, runScrub, reinsertOverflowedTTL);
// complex column, noclustering
testRecoverOverflowedExpirationWithScrub(false, false, runScrub, reinsertOverflowedTTL);
}
private void verifyData(boolean simple) throws Throwable
{
if (simple)
{
assertRows(execute("SELECT * from %s"), row(1, 1, 1), row(2, 2, 2));
}
else
{
assertRows(execute("SELECT * from %s"), row(1, 1, set("v11", "v12", "v13", "v14")), row(2, 2, set("v21", "v22", "v23", "v24")));
}
// Cannot retrieve TTL from collections
if (simple)
checkTTLIsCapped("b");
}
/**
* Verify that the computed TTL is approximately equal to the maximum allowed ttl given the
* {@link ExpiringCell#getLocalDeletionTime()} field limitation (CASSANDRA-14092)
*/
private void checkTTLIsCapped(String field) throws Throwable
{
// TTL is computed dynamically from row expiration time, so if it is
// equal or higher to the minimum max TTL we compute before the query
// we are fine.
int minMaxTTL = computeMaxTTL();
UntypedResultSet execute = execute("SELECT ttl(" + field + ") FROM %s");
for (UntypedResultSet.Row row : execute)
{
int ttl = row.getInt("ttl(" + field + ")");
assertTrue(ttl >= minMaxTTL);
}
}
/**
* The max TTL is computed such that the TTL summed with the current time is equal to the maximum
* allowed expiration time {@link BufferExpiringCell#getLocalDeletionTime()} (2038-01-19T03:14:06+00:00)
*/
private int computeMaxTTL()
{
int nowInSecs = (int) (System.currentTimeMillis() / 1000);
return BufferExpiringCell.MAX_DELETION_TIME - nowInSecs;
}
public void testRecoverOverflowedExpirationWithScrub(boolean simple, boolean clustering, boolean runScrub, boolean reinsertOverflowedTTL) throws Throwable
{
if (reinsertOverflowedTTL)
{
assert runScrub;
}
Keyspace keyspace = Keyspace.open(KEYSPACE);
ColumnFamilyStore cfs = keyspace.getColumnFamilyStore(getTableName(simple, clustering));
assertEquals(0, cfs.getLiveSSTableCount());
copySSTablesToTableDir(simple, clustering);
cfs.loadNewSSTables();
if (runScrub)
{
cfs.scrub(true, false, false, reinsertOverflowedTTL, 1);
}
if (reinsertOverflowedTTL)
{
if (simple)
{
UntypedResultSet execute = execute(String.format("SELECT * from %s.%s", KEYSPACE, getTableName(simple, clustering)));
assertRows(execute, row(1, 1, 1), row(2, 2, 2));
}
else
assertRows(execute(String.format("SELECT * from %s.%s", KEYSPACE, getTableName(simple, clustering))), row(1, 1, set("v11", "v12", "v13", "v14")), row(2, 2, set("v21", "v22", "v23", "v24")));
cfs.forceMajorCompaction();
if (simple)
assertRows(execute(String.format("SELECT * from %s.%s", KEYSPACE, getTableName(simple, clustering))), row(1, 1, 1), row(2, 2, 2));
else
assertRows(execute(String.format("SELECT * from %s.%s", KEYSPACE, getTableName(simple, clustering))), row(1, 1, set("v11", "v12", "v13", "v14")), row(2, 2, set("v21", "v22", "v23", "v24")));
}
else
{
assertEmpty(execute(String.format("SELECT * from %s.%s", KEYSPACE, getTableName(simple, clustering))));
}
cfs.truncateBlocking(); //cleanup for next tests
}
private void copySSTablesToTableDir(boolean simple, boolean clustering) throws IOException
{
File destDir = Keyspace.open(KEYSPACE).getColumnFamilyStore(getTableName(simple, clustering)).directories.getCFDirectories().iterator().next();
File sourceDir = getTableDir(simple, clustering);
for (File file : sourceDir.listFiles())
{
copyFile(file, destDir);
}
}
private void createTable(boolean simple, boolean clustering) throws Throwable
{
if (simple)
{
if (clustering)
execute(String.format("create table %s.%s (k int, a int, b int, primary key(k, a))", KEYSPACE, getTableName(simple, clustering)));
else
execute(String.format("create table %s.%s (k int primary key, a int, b int)", KEYSPACE, getTableName(simple, clustering)));
}
else
{
if (clustering)
execute(String.format("create table %s.%s (k int, a int, b set<text>, primary key(k, a))", KEYSPACE, getTableName(simple, clustering)));
else
execute(String.format("create table %s.%s (k int primary key, a int, b set<text>)", KEYSPACE, getTableName(simple, clustering)));
}
}
private static File getTableDir(boolean simple, boolean clustering)
{
return new File(String.format(NEGATIVE_LOCAL_EXPIRATION_TEST_DIR, getTableName(simple, clustering)));
}
private static void copyFile(File src, File dest) throws IOException
{
byte[] buf = new byte[65536];
if (src.isFile())
{
File target = new File(dest, src.getName());
int rd;
FileInputStream is = new FileInputStream(src);
FileOutputStream os = new FileOutputStream(target);
while ((rd = is.read(buf)) >= 0)
os.write(buf, 0, rd);
}
}
public static String getTableName(boolean simple, boolean clustering)
{
if (simple)
return clustering ? SIMPLE_CLUSTERING : SIMPLE_NOCLUSTERING;
else
return clustering ? COMPLEX_CLUSTERING : COMPLEX_NOCLUSTERING;
}
}