add AntiEntropyService; patched by Stu Hood, reviewed by junrao for CASSANDRA-193

git-svn-id: https://svn.apache.org/repos/asf/incubator/cassandra/trunk@887575 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Jun Rao 2009-12-05 18:47:55 +00:00
parent f62d4ec14c
commit ac398bd0a7
5 changed files with 1211 additions and 90 deletions

View File

@ -42,6 +42,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.cassandra.service.StorageService;
import org.apache.cassandra.service.AntiEntropyService;
import org.apache.cassandra.utils.*;
import org.apache.cassandra.concurrent.DebuggableThreadPoolExecutor;
import org.apache.cassandra.concurrent.NamedThreadFactory;
@ -293,7 +294,8 @@ public final class ColumnFamilyStore implements ColumnFamilyStoreMBean
List<SSTableReader> forceAntiCompaction(Collection<Range> ranges, InetAddress target)
{
assert ranges != null;
Future<List<SSTableReader>> futurePtr = CompactionManager.instance().submit(ColumnFamilyStore.this, ranges, target);
Future<List<SSTableReader>> futurePtr = CompactionManager.instance().submitAnti(ColumnFamilyStore.this,
ranges, target);
List<SSTableReader> result;
try
@ -654,7 +656,8 @@ public final class ColumnFamilyStore implements ColumnFamilyStoreMBean
// if we have too many to compact all at once, compact older ones first -- this avoids
// re-compacting files we just created.
Collections.sort(sstables);
filesCompacted += doFileCompaction(sstables.subList(0, Math.min(sstables.size(), maxThreshold)));
boolean major = sstables.size() == ssTables_.size();
filesCompacted += doFileCompaction(sstables.subList(0, Math.min(sstables.size(), maxThreshold)), major);
}
logger_.debug(filesCompacted + " files compacted");
}
@ -679,7 +682,8 @@ public final class ColumnFamilyStore implements ColumnFamilyStoreMBean
void doMajorCompactionInternal(long skip) throws IOException
{
Collection<SSTableReader> sstables;
if (skip > 0L)
boolean major = skip < 1L;
if (!major)
{
sstables = new ArrayList<SSTableReader>();
for (SSTableReader sstable : ssTables_)
@ -695,7 +699,7 @@ public final class ColumnFamilyStore implements ColumnFamilyStoreMBean
sstables = ssTables_.getSSTables();
}
doFileCompaction(sstables);
doFileCompaction(sstables, major);
}
/*
@ -850,9 +854,9 @@ public final class ColumnFamilyStore implements ColumnFamilyStoreMBean
return results;
}
private int doFileCompaction(Collection<SSTableReader> sstables) throws IOException
private int doFileCompaction(Collection<SSTableReader> sstables, boolean major) throws IOException
{
return doFileCompaction(sstables, getDefaultGCBefore());
return doFileCompaction(sstables, getDefaultGCBefore(), major);
}
/*
@ -868,7 +872,7 @@ public final class ColumnFamilyStore implements ColumnFamilyStoreMBean
* The collection of sstables passed may be empty (but not null); even if
* it is not empty, it may compact down to nothing if all rows are deleted.
*/
int doFileCompaction(Collection<SSTableReader> sstables, int gcBefore) throws IOException
int doFileCompaction(Collection<SSTableReader> sstables, int gcBefore, boolean major) throws IOException
{
if (DatabaseDescriptor.isSnapshotBeforeCompaction())
Table.open(table_).snapshot("compact-" + columnFamily_);
@ -881,7 +885,7 @@ public final class ColumnFamilyStore implements ColumnFamilyStoreMBean
SSTableReader maxFile = getMaxSizeFile(sstables);
List<SSTableReader> smallerSSTables = new ArrayList<SSTableReader>(sstables);
smallerSSTables.remove(maxFile);
return doFileCompaction(smallerSSTables);
return doFileCompaction(smallerSSTables, gcBefore, false);
}
long startTime = System.currentTimeMillis();
@ -910,12 +914,18 @@ public final class ColumnFamilyStore implements ColumnFamilyStoreMBean
String newFilename = new File(compactionFileLocation, getTempSSTableFileName()).getAbsolutePath();
writer = new SSTableWriter(newFilename, expectedBloomFilterSize, StorageService.getPartitioner());
// validate the CF as we iterate over it
InetAddress initiator = major ? FBUtilities.getLocalAddress() : null;
AntiEntropyService.IValidator validator = AntiEntropyService.instance().getValidator(table_, columnFamily_, initiator);
validator.prepare();
while (nni.hasNext())
{
CompactionIterator.CompactedRow row = (CompactionIterator.CompactedRow) nni.next();
writer.append(row.key, row.buffer);
validator.add(row);
totalkeysWritten++;
}
validator.complete();
}
finally
{
@ -933,6 +943,34 @@ public final class ColumnFamilyStore implements ColumnFamilyStoreMBean
return sstables.size();
}
/**
* Performs a readonly compaction of all sstables in order to validate
* them on request, but without performing any writes.
*/
void doReadonlyCompaction(InetAddress initiator) throws IOException
{
Collection<SSTableReader> sstables = ssTables_.getSSTables();
CompactionIterator ci = new CompactionIterator(sstables, getDefaultGCBefore());
try
{
Iterator nni = new FilterIterator(ci, PredicateUtils.notNullPredicate());
// validate the CF as we iterate over it
AntiEntropyService.IValidator validator = AntiEntropyService.instance().getValidator(table_, columnFamily_, initiator);
validator.prepare();
while (nni.hasNext())
{
CompactionIterator.CompactedRow row = (CompactionIterator.CompactedRow) nni.next();
validator.add(row);
}
validator.complete();
}
finally
{
ci.close();
}
}
private long getTotalBytes(Iterable<SSTableReader> sstables)
{
long sum = 0;

View File

@ -72,92 +72,124 @@ public class CompactionManager implements CompactionManagerMBean
return instance_;
}
static class FileCompactor2 implements Callable<List<SSTableReader>>
static abstract class Compactor<T> implements Callable<T>
{
private ColumnFamilyStore columnFamilyStore_;
private Collection<Range> ranges_;
private InetAddress target_;
FileCompactor2(ColumnFamilyStore columnFamilyStore, Collection<Range> ranges, InetAddress target)
protected final ColumnFamilyStore cfstore;
public Compactor(ColumnFamilyStore columnFamilyStore)
{
columnFamilyStore_ = columnFamilyStore;
ranges_ = ranges;
target_ = target;
cfstore = columnFamilyStore;
}
public List<SSTableReader> call()
abstract T compact() throws IOException;
public T call()
{
List<SSTableReader> results;
T results;
if (logger_.isDebugEnabled())
logger_.debug("Started compaction ..."+columnFamilyStore_.columnFamily_);
logger_.debug("Starting " + this + ".");
try
{
results = columnFamilyStore_.doAntiCompaction(ranges_, target_);
results = compact();
}
catch (IOException e)
{
throw new RuntimeException(e);
}
if (logger_.isDebugEnabled())
logger_.debug("Finished compaction ..."+columnFamilyStore_.columnFamily_);
logger_.debug("Finished " + this + ".");
return results;
}
}
static class OnDemandCompactor implements Runnable
{
private ColumnFamilyStore columnFamilyStore_;
private long skip_ = 0L;
OnDemandCompactor(ColumnFamilyStore columnFamilyStore, long skip)
@Override
public String toString()
{
columnFamilyStore_ = columnFamilyStore;
skip_ = skip;
}
public void run()
{
if (logger_.isDebugEnabled())
logger_.debug("Started Major compaction for " + columnFamilyStore_.columnFamily_);
try
{
columnFamilyStore_.doMajorCompaction(skip_);
}
catch (IOException e)
{
throw new RuntimeException(e);
}
if (logger_.isDebugEnabled())
logger_.debug("Finished Major compaction for " + columnFamilyStore_.columnFamily_);
StringBuilder buff = new StringBuilder();
buff.append("<").append(getClass().getSimpleName());
buff.append(" for ").append(cfstore).append(">");
return buff.toString();
}
}
static class CleanupCompactor implements Runnable
static class AntiCompactor extends Compactor<List<SSTableReader>>
{
private ColumnFamilyStore columnFamilyStore_;
CleanupCompactor(ColumnFamilyStore columnFamilyStore)
private final Collection<Range> ranges;
private final InetAddress target;
AntiCompactor(ColumnFamilyStore cfstore, Collection<Range> ranges, InetAddress target)
{
columnFamilyStore_ = columnFamilyStore;
super(cfstore);
this.ranges = ranges;
this.target = target;
}
public void run()
public List<SSTableReader> compact() throws IOException
{
if (logger_.isDebugEnabled())
logger_.debug("Started compaction ..."+columnFamilyStore_.columnFamily_);
try
{
columnFamilyStore_.doCleanupCompaction();
}
catch (IOException e)
{
throw new RuntimeException(e);
}
if (logger_.isDebugEnabled())
logger_.debug("Finished compaction ..."+columnFamilyStore_.columnFamily_);
return cfstore.doAntiCompaction(ranges, target);
}
}
static class OnDemandCompactor extends Compactor<Object>
{
private final long skip;
OnDemandCompactor(ColumnFamilyStore cfstore, long skip)
{
super(cfstore);
this.skip = skip;
}
public Object compact() throws IOException
{
cfstore.doMajorCompaction(skip);
return this;
}
}
static class CleanupCompactor extends Compactor<Object>
{
CleanupCompactor(ColumnFamilyStore cfstore)
{
super(cfstore);
}
public Object compact() throws IOException
{
cfstore.doCleanupCompaction();
return this;
}
}
static class MinorCompactor extends Compactor<Integer>
{
private final int minimum;
private final int maximum;
MinorCompactor(ColumnFamilyStore cfstore, int minimumThreshold, int maximumThreshold)
{
super(cfstore);
minimum = minimumThreshold;
maximum = maximumThreshold;
}
public Integer compact() throws IOException
{
return cfstore.doCompaction(minimum, maximum);
}
}
static class ReadonlyCompactor extends Compactor<Object>
{
private final InetAddress initiator;
ReadonlyCompactor(ColumnFamilyStore cfstore, InetAddress initiator)
{
super(cfstore);
this.initiator = initiator;
}
public Object compact() throws IOException
{
cfstore.doReadonlyCompaction(initiator);
return this;
}
}
private ExecutorService compactor_ = new DebuggableThreadPoolExecutor("COMPACTION-POOL");
@ -173,29 +205,27 @@ public class CompactionManager implements CompactionManagerMBean
Future<Integer> submit(final ColumnFamilyStore columnFamilyStore, final int minThreshold, final int maxThreshold)
{
Callable<Integer> callable = new Callable<Integer>()
{
public Integer call() throws IOException
{
return columnFamilyStore.doCompaction(minThreshold, maxThreshold);
}
};
return compactor_.submit(callable);
return compactor_.submit(new MinorCompactor(columnFamilyStore, minThreshold, maxThreshold));
}
public void submitCleanup(ColumnFamilyStore columnFamilyStore)
public Future submitCleanup(ColumnFamilyStore columnFamilyStore)
{
compactor_.submit(new CleanupCompactor(columnFamilyStore));
return compactor_.submit(new CleanupCompactor(columnFamilyStore));
}
public Future<List<SSTableReader>> submit(ColumnFamilyStore columnFamilyStore, Collection<Range> ranges, InetAddress target)
public Future<List<SSTableReader>> submitAnti(ColumnFamilyStore columnFamilyStore, Collection<Range> ranges, InetAddress target)
{
return compactor_.submit( new FileCompactor2(columnFamilyStore, ranges, target) );
return compactor_.submit(new AntiCompactor(columnFamilyStore, ranges, target));
}
public void submitMajor(ColumnFamilyStore columnFamilyStore, long skip)
public Future submitMajor(ColumnFamilyStore columnFamilyStore, long skip)
{
compactor_.submit( new OnDemandCompactor(columnFamilyStore, skip) );
return compactor_.submit(new OnDemandCompactor(columnFamilyStore, skip));
}
public Future submitReadonly(ColumnFamilyStore columnFamilyStore, InetAddress initiator)
{
return compactor_.submit(new ReadonlyCompactor(columnFamilyStore, initiator));
}
/**

View File

@ -0,0 +1,780 @@
/**
* 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.service;
import java.io.*;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.*;
import org.apache.cassandra.concurrent.SingleThreadedStage;
import org.apache.cassandra.concurrent.StageManager;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.CompactionManager;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.Table;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.io.CompactionIterator.CompactedRow;
import org.apache.cassandra.io.DataInputBuffer;
import org.apache.cassandra.io.ICompactSerializer;
import org.apache.cassandra.io.SSTable;
import org.apache.cassandra.io.SSTableReader;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.net.IVerbHandler;
import org.apache.cassandra.net.Message;
import org.apache.cassandra.net.MessagingService;
import org.apache.cassandra.utils.Cachetable;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.LogUtil;
import org.apache.cassandra.utils.MerkleTree;
import org.apache.log4j.Logger;
import org.apache.commons.lang.StringUtils;
import com.google.common.collect.Collections2;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.reardencommerce.kernel.collections.shared.evictable.ConcurrentLinkedHashMap;
/**
* AntiEntropyService encapsulates "validating" (hashing) individual column families,
* exchanging MerkleTrees with remote nodes via a TreeRequest/Response conversation,
* and then triggering repairs for disagreeing ranges.
*
* Every Tree conversation has an 'initiator', where valid trees are sent after generation
* and where the local and remote tree will rendezvous in register(cf, endpoint, tree).
* Once the trees rendezvous, a Differencer is executed and the service can trigger repairs
* for disagreeing ranges.
*
* Tree comparison and repair triggering occur in the single threaded AE_SERVICE_STAGE.
*
* The steps taken to enact a repair are as follows:
* 1. A major compaction is triggered either via nodeprobe, or automatically:
* * Nodeprobe sends TreeRequest messages to all neighbors of the target node: when a node
* receives a TreeRequest, it will perform a readonly compaction to immediately validate
* the column family.
* * Automatic compactions will also validate a column family and broadcast TreeResponses, but
* since TreeRequest messages are not sent to neighboring nodes, repairs will only occur if two
* nodes happen to perform automatic compactions within TREE_CACHE_LIFETIME of one another.
* 2. The compaction process validates the column family by:
* * Calling getValidator(), which can return a NoopValidator if validation should not be performed,
* * Calling IValidator.prepare(), which samples the column family to determine key distribution,
* * Calling IValidator.add() in order for every row in the column family,
* * Calling IValidator.complete() to indicate that all rows have been added.
* * If getValidator decided that the column family should be validated, calling complete()
* indicates that a valid MerkleTree has been created for the column family.
* * The valid tree is broadcast to neighboring nodes via TreeResponse, and stored locally.
* 3. When a node receives a TreeResponse, it passes the tree to register(), which checks for trees to
* rendezvous with / compare to:
* * If the tree is local, it is cached, and compared to any trees that were received from neighbors.
* * If the tree is remote, it is immediately compared to a local tree if one is cached. Otherwise,
* the remote tree is stored until a local tree can be generated.
* * A Differencer object is enqueued for each comparison.
* 4. Differencers are executed in AE_SERVICE_STAGE, to compare the two trees.
* * Based on the fraction of disagreement between the trees, the differencer will
* either perform repair via the io.Streaming api, or via RangeCommand read repairs.
* 5. TODO: Because a local tree is stored for TREE_CACHE_LIFETIME, it is possible to perform
* redundant repairs when repairs are triggered manually. Because of the SSTable architecture,
* this doesn't cause any problems except excess data transfer, but:
* * One possible solution is to maintain the local tree in memory by invalidating ranges when they
* change, and only performing partial compactions/validations.
* * Another would be to only communicate with one neighbor at a time, meaning that an additional
* compaction is required for every neighbor.
*/
public class AntiEntropyService
{
private static final Logger logger = Logger.getLogger(AntiEntropyService.class);
public final static String AE_SERVICE_STAGE = "AE-SERVICE-STAGE";
public final static String TREE_REQUEST_VERB = "TREE-REQUEST-VERB";
public final static String TREE_RESPONSE_VERB = "TREE-RESPONSE-VERB";
// millisecond lifetime to store remote trees before they become stale
public final static long TREE_CACHE_LIFETIME = 600000;
// singleton enforcement
private static volatile AntiEntropyService aeService;
/**
* Map of endpoints to recently generated trees for their column families.
* Remote trees are removed from the map once they have been compared to
* local trees, but local trees are cached for multiple comparisons.
*/
private final ConcurrentLinkedHashMap<InetAddress, Cachetable<CFTuple, MerkleTree>> trees;
public static AntiEntropyService instance()
{
if ( aeService == null )
{
synchronized ( AntiEntropyService.class )
{
if ( aeService == null )
{
aeService = new AntiEntropyService();
}
}
}
return aeService;
}
/**
* Private constructor. Use AntiEntropyService.instance()
*/
private AntiEntropyService()
{
StageManager.registerStage(AE_SERVICE_STAGE, new SingleThreadedStage(AE_SERVICE_STAGE));
MessagingService.instance().registerVerbHandlers(TREE_REQUEST_VERB, new TreeRequestVerbHandler());
MessagingService.instance().registerVerbHandlers(TREE_RESPONSE_VERB, new TreeResponseVerbHandler());
trees = ConcurrentLinkedHashMap.create(ConcurrentLinkedHashMap.EvictionPolicy.LRU,
DatabaseDescriptor.getReplicationFactor()+1);
}
/**
* @param endpoint Endpoint to fetch trees for.
* @return The store of trees for the given endpoint.
*/
private Cachetable<CFTuple, MerkleTree> cacheForEndpoint(InetAddress endpoint)
{
Cachetable<CFTuple, MerkleTree> etrees = trees.get(endpoint);
if (etrees == null)
{
// double check the creation
Cachetable<CFTuple, MerkleTree> probable =
new Cachetable<CFTuple, MerkleTree>(TREE_CACHE_LIFETIME);
if ((etrees = trees.putIfAbsent(endpoint, probable)) == null)
// created new store for this endpoint
etrees = probable;
}
return etrees;
}
/**
* Register a tree from the given endpoint to be compared to neighbor trees
* in AE_SERVICE_STAGE when they become available.
*
* @param cf The column family of the tree.
* @param endpoint The endpoint which owns the given tree.
* @param tree The tree for the endpoint.
*/
void register(CFTuple cf, InetAddress endpoint, MerkleTree tree)
{
InetAddress LOCAL = FBUtilities.getLocalAddress();
// store the tree, possibly replacing an older copy
Cachetable<CFTuple, MerkleTree> etrees = cacheForEndpoint(endpoint);
List<Differencer> differencers = new ArrayList<Differencer>();
if (LOCAL.equals(endpoint))
{
// we stored a local tree: queue differencing for all remote trees
for (Map.Entry<InetAddress, Cachetable<CFTuple, MerkleTree>> entry : trees.entrySet())
{
if (LOCAL.equals(entry.getKey()))
// don't compare to ourself
continue;
MerkleTree remotetree = entry.getValue().remove(cf);
if (remotetree == null)
// no tree stored for this endpoint at the moment
continue;
differencers.add(new Differencer(cf, LOCAL, entry.getKey(), tree, remotetree));
}
etrees.put(cf, tree);
logger.debug("Cached local tree for " + cf);
}
else
{
// we stored a remote tree: queue differencing for local tree
MerkleTree localtree = cacheForEndpoint(LOCAL).get(cf);
if (localtree != null)
// compare immediately
differencers.add(new Differencer(cf, LOCAL, endpoint, localtree, tree));
else
{
// cache for later comparison
etrees.put(cf, tree);
logger.debug("Cached remote tree from " + endpoint + " for " + cf);
}
}
for (Differencer differencer : differencers)
{
logger.debug("Queueing comparison " + differencer);
StageManager.getStage(AE_SERVICE_STAGE).execute(differencer);
}
}
/**
* Called by a Validator to send a valid tree to endpoints storing
* replicas of local data.
*
* @param validator A locally generated validator.
* @param local The local endpoint.
* @param neighbors A list of neighbor endpoints to send the tree to.
*/
void notifyNeighbors(Validator validator, InetAddress local, Collection<InetAddress> neighbors)
{
MessagingService ms = MessagingService.instance();
try
{
Message message = TreeResponseVerbHandler.makeVerb(local, validator);
logger.info("Sending AEService tree for " + validator.cf + " to: " + neighbors);
for (InetAddress neighbor : neighbors)
ms.sendOneWay(message, neighbor);
}
catch (Exception e)
{
logger.error("Could not send valid tree to endpoints: " + neighbors, e);
}
}
/**
* Should only be used in AE_SERVICE_STAGE or for testing.
*
* @param table Table containing cf.
* @param cf The column family.
* @param endpoint The endpoint that generated the tree.
* @return the cached tree for the given cf and endpoint.
*/
MerkleTree getCachedTree(String table, String cf, InetAddress endpoint)
{
return cacheForEndpoint(endpoint).get(new CFTuple(table, cf));
}
/**
* Return a Validator object which can be used to collect hashes for a column family.
* A Validator must be prepared() before use, and completed() afterward.
*
* @param table The table name containing the column family.
* @param cf The column family name.
* @param initiator Endpoint that initially triggered this validation, or null.
* @return A Validator.
*/
public IValidator getValidator(String table, String cf, InetAddress initiator)
{
if (initiator == null)
return new NoopValidator();
else if (StorageService.instance().getTokenMetadata().sortedTokens().size() < 1)
// gossiper isn't started
return new NoopValidator();
else
return new Validator(new CFTuple(table, cf), initiator);
}
/**
* A Strategy to handle building and validating a merkle tree for a column family.
*
* Lifecycle:
* 1. prepare() - Initialize tree with samples.
* 2. add() - 0 or more times, to add hashes to the tree.
* 3. complete() - Enqueues any operations that were blocked waiting for a valid tree.
*/
public static interface IValidator
{
public void prepare();
public void add(CompactedRow row);
public void complete();
}
/**
* The IValidator to be used in normal operation.
*/
public static class Validator implements IValidator, Callable<Object>
{
public final CFTuple cf;
public final InetAddress initiator;
public final MerkleTree tree;
private transient final List<MerkleTree.RowHash> rows;
// the minimum token sorts first, but falls into the last range
private transient List<MerkleTree.RowHash> minrows;
// null when all rows with the min token have been consumed
private transient Token mintoken;
private transient long validated;
private transient MerkleTree.TreeRangeIterator ranges;
public final static Predicate<DecoratedKey> DKPRED = Predicates.alwaysTrue();
Validator(CFTuple cf, InetAddress initiator)
{
this(cf, initiator,
// TODO: memory usage (maxsize) should either be tunable per
// CF, globally, or as shared for all CFs in a cluster
new MerkleTree(DatabaseDescriptor.getPartitioner(),
MerkleTree.RECOMMENDED_DEPTH,
(int)Math.pow(2,15)));
}
Validator(CFTuple cf, InetAddress initiator, MerkleTree tree)
{
assert cf != null && initiator != null && tree != null;
this.cf = cf;
this.initiator = initiator;
this.tree = tree;
rows = new ArrayList<MerkleTree.RowHash>();
minrows = new ArrayList<MerkleTree.RowHash>();
mintoken = null;
validated = 0;
ranges = null;
}
public void prepare()
{
Predicate<SSTable> cfpred = new Predicate<SSTable>(){
public boolean apply(SSTable ss)
{
return cf.table.equals(ss.getTableName()) && cf.cf.equals(ss.getColumnFamilyName());
}
};
List<DecoratedKey> keys = SSTableReader.getIndexedDecoratedKeysFor(cfpred, DKPRED);
if (keys.isEmpty())
// use an even tree distribution
tree.init();
else
{
int numkeys = keys.size();
Random random = new Random();
// sample the column family using random keys from the index
while (true)
{
DecoratedKey dk = keys.get(random.nextInt(numkeys));
if (!tree.split(dk.token))
break;
}
}
logger.debug("Prepared AEService tree of size " + tree.size() + " for " + cf);
mintoken = tree.partitioner().getMinimumToken();
ranges = tree.invalids(new Range(mintoken, mintoken));
}
/**
* Called (in order) for every row present in the CF.
* Hashes the row, and adds it to the tree being built.
*
* There are three possible cases:
* 1. Token is greater than range.right (we haven't generated a range for it yet),
* 2. Token is less than/equal to range.left (the range was valid),
* 3. Token is contained in the range (the range is in progress).
*
* Additionally, there is a special case for the minimum token, because
* although it sorts first, it is contained in the last possible range.
*
* @param row The row.
*/
public void add(CompactedRow row)
{
if (mintoken != null)
{
assert ranges != null : "Validator was not prepared()";
// check for the minimum token special case
if (row.key.token.compareTo(mintoken) == 0)
{
// and store it to be appended when we complete
minrows.add(rowHash(row));
validated++;
return;
}
mintoken = null;
}
if (!ranges.hasNext())
return;
MerkleTree.TreeRange range = ranges.peek();
// generate new ranges as long as case 1 is true
while (range.right().compareTo(row.key.token) < 0)
{
// token is past the current range: finalize
range.validate(rows);
rows.clear();
// and generate a new range
ranges.next();
if (!ranges.hasNext())
return;
range = ranges.peek();
}
// if case 2 is true, ignore the token
if (row.key.token.compareTo(range.left()) <= 0)
return;
// case 3 must be true: buffer the hashed row
rows.add(rowHash(row));
validated++;
}
private MerkleTree.RowHash rowHash(CompactedRow row)
{
byte[] rowhash = FBUtilities.hash("MD5", row.key.key.getBytes(),
row.buffer.getData());
return new MerkleTree.RowHash(row.key.token, rowhash);
}
/**
* Depending on the initiator for the validation, either registers
* trees to be compared locally in AE_SERVICE_STAGE, or remotely.
*/
public void complete()
{
assert ranges != null : "Validator was not prepared()";
// finish validating remaining rows
while (ranges.hasNext())
{
MerkleTree.TreeRange range = ranges.next();
if (!ranges.hasNext() && !minrows.isEmpty() &&
range.contains(tree.partitioner().getMinimumToken()))
{
// append rows with the minimum token into the last range
rows.addAll(minrows);
minrows.clear();
}
range.validate(rows);
rows.clear();
}
assert rows.isEmpty() && minrows.isEmpty();
StageManager.getStage(AE_SERVICE_STAGE).execute(this);
logger.debug("Validated " + validated + " rows into AEService tree for " + cf);
}
/**
* Called after the valdation lifecycle to trigger additional action
* with the now valid tree. Runs in AE_SERVICE_STAGE: depending on
* which node initiated validation, performs different actions.
*
* @return A meaningless object.
*/
public Object call() throws Exception
{
AntiEntropyService aes = AntiEntropyService.instance();
InetAddress local = FBUtilities.getLocalAddress();
StorageService ss = StorageService.instance();
Collection<InetAddress> neighbors =
Collections2.filter(ss.getNaturalEndpoints(ss.getLocalToken()),
Predicates.not(Predicates.equalTo(local)));
// cache the local tree
aes.register(cf, local, tree);
if (!local.equals(initiator))
// one of our neighbors initiated: broadcast the tree to all of them
aes.notifyNeighbors(this, local, neighbors);
// else: we initiated this validation session: wait for responses
// return any old object
return AntiEntropyService.class;
}
}
/**
* The IValidator to be used before a cluster has stabilized, or when repairs
* are disabled.
*/
public static class NoopValidator implements IValidator
{
/**
* Does nothing.
*/
public void prepare()
{
// noop
}
/**
* Does nothing.
*/
public void add(CompactedRow row)
{
// noop
}
/**
* Does nothing.
*/
public void complete()
{
// noop
}
}
/**
* Compares two trees, and launches repairs for disagreeing ranges.
*/
public static class Differencer implements Runnable
{
public final CFTuple cf;
public final InetAddress local;
public final InetAddress remote;
public final MerkleTree ltree;
public final MerkleTree rtree;
public final List<Range> differences;
public Differencer(CFTuple cf, InetAddress local, InetAddress remote, MerkleTree ltree, MerkleTree rtree)
{
this.cf = cf;
this.local = local;
this.remote = remote;
this.ltree = ltree;
this.rtree = rtree;
differences = new ArrayList<Range>();
}
/**
* Compares our trees, and triggers repairs for any ranges that mismatch.
*/
public void run()
{
StorageService ss = StorageService.instance();
Token minimum = ss.getPartitioner().getMinimumToken();
// restore partitioners (in case we were serialized)
if (ltree.partitioner() == null)
ltree.partitioner(ss.getPartitioner());
if (rtree.partitioner() == null)
rtree.partitioner(ss.getPartitioner());
// determine the ranges where responsibility overlaps
Set<Range> interesting = new HashSet(ss.getRangesForEndPoint(local));
interesting.retainAll(ss.getRangesForEndPoint(remote));
// compare trees, and filter out uninteresting differences
for (Range diff : MerkleTree.difference(ltree, rtree))
{
for (Range localrange: interesting)
{
if (diff.intersects(localrange))
{
differences.add(diff);
break; // the inner loop
}
}
}
// TODO: calculating a percentage here would be all kinds of awesome
logger.info("Found " + differences.size() + " differing ranges between local " +
local + " and remote " + remote + " endpoints for " + cf + ".");
// FIXME: trigger repairs!
}
public String toString()
{
return "#<Differencer " + cf + " local=" + local + " remote=" + remote + ">";
}
}
/**
* Handler for requests from remote nodes to generate a valid tree.
*
* The payload is an EndpointCF triple representing the columnfamily to validate
* and the initiating endpoint.
*/
public static class TreeRequestVerbHandler implements IVerbHandler, ICompactSerializer<CFTuple>
{
public static final TreeRequestVerbHandler SERIALIZER = new TreeRequestVerbHandler();
static Message makeVerb(String table, String cf)
{
try
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
SERIALIZER.serialize(new CFTuple(table, cf), dos);
return new Message(FBUtilities.getLocalAddress(), AE_SERVICE_STAGE,
TREE_REQUEST_VERB, bos.toByteArray());
}
catch(IOException e)
{
throw new RuntimeException(e);
}
}
public void serialize(CFTuple treerequest, DataOutputStream dos) throws IOException
{
dos.writeUTF(treerequest.table);
dos.writeUTF(treerequest.cf);
}
public CFTuple deserialize(DataInputStream dis) throws IOException
{
return new CFTuple(dis.readUTF(), dis.readUTF());
}
/**
* If we have a recently generated cached tree, respond with it immediately:
* Otherwise, trigger a readonly compaction which will broadcast the tree
* upon completion.
*/
public void doVerb(Message message)
{
byte[] bytes = message.getMessageBody();
DataInputBuffer buffer = new DataInputBuffer();
buffer.reset(bytes, bytes.length);
try
{
CFTuple request = this.deserialize(buffer);
// check for cached local tree
InetAddress local = FBUtilities.getLocalAddress();
MerkleTree cached =
AntiEntropyService.instance().getCachedTree(request.table,
request.cf,
local);
if (cached != null)
{
if (local.equals(message.getFrom()))
// we are the requestor, and we already have a cached tree
return;
// respond immediately with the recently generated tree
Validator valid = new Validator(request, message.getFrom(), cached);
Message response = TreeResponseVerbHandler.makeVerb(local, valid);
MessagingService.instance().sendOneWay(response, message.getFrom());
logger.debug("Answered request from " + message.getFrom() +
" for " + request + " with cached tree.");
return;
}
// trigger readonly-compaction
logger.debug("Queueing readonly compaction for request from " +
message.getFrom() + " for " + request);
Table table = Table.open(request.table);
CompactionManager.instance().submitReadonly(table.getColumnFamilyStore(request.cf),
message.getFrom());
}
catch (Exception e)
{
logger.warn(LogUtil.throwableToString(e));
}
}
}
/**
* Handler for responses from remote nodes that contain a valid tree.
*
* The payload is a completed Validator object from the remote endpoint.
*/
public static class TreeResponseVerbHandler implements IVerbHandler, ICompactSerializer<Validator>
{
public static final TreeResponseVerbHandler SERIALIZER = new TreeResponseVerbHandler();
static Message makeVerb(InetAddress local, Validator validator)
{
try
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
SERIALIZER.serialize(validator, dos);
return new Message(local, AE_SERVICE_STAGE, TREE_RESPONSE_VERB, bos.toByteArray());
}
catch(IOException e)
{
throw new RuntimeException(e);
}
}
public void serialize(Validator v, DataOutputStream dos) throws IOException
{
TreeRequestVerbHandler.SERIALIZER.serialize(v.cf, dos);
ObjectOutputStream oos = new ObjectOutputStream(dos);
oos.writeObject(v.initiator);
oos.writeObject(v.tree);
oos.flush();
}
public Validator deserialize(DataInputStream dis) throws IOException
{
final CFTuple cf = TreeRequestVerbHandler.SERIALIZER.deserialize(dis);
ObjectInputStream ois = new ObjectInputStream(dis);
try
{
Validator v = new Validator(cf, (InetAddress)ois.readObject(),
(MerkleTree)ois.readObject());
return v;
}
catch(Exception e)
{
throw new RuntimeException(e);
}
}
public void doVerb(Message message)
{
byte[] bytes = message.getMessageBody();
DataInputBuffer buffer = new DataInputBuffer();
buffer.reset(bytes, bytes.length);
try
{
// deserialize the remote tree, and register it
Validator rvalidator = this.deserialize(buffer);
AntiEntropyService.instance().register(rvalidator.cf, message.getFrom(),
rvalidator.tree);
}
catch (Exception e)
{
logger.warn(LogUtil.throwableToString(e));
}
}
}
/**
* A tuple of table and cf.
*/
static final class CFTuple
{
public final String table;
public final String cf;
public CFTuple(String table, String cf)
{
assert table != null && cf != null;
this.table = table;
this.cf = cf;
}
@Override
public int hashCode()
{
int hashCode = 31 + table.hashCode();
return 31*hashCode + cf.hashCode();
}
@Override
public boolean equals(Object o)
{
if(!(o instanceof CFTuple))
return false;
CFTuple that = (CFTuple)o;
return table.equals(that.table) && cf.equals(that.cf);
}
@Override
public String toString()
{
return "[" + table + "][" + cf + "]";
}
}
}

View File

@ -19,8 +19,10 @@
package org.apache.cassandra.db;
import java.io.IOException;
import java.net.InetAddress;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.ArrayList;
import java.util.Set;
import java.util.HashSet;
@ -28,17 +30,23 @@ import org.junit.Test;
import org.apache.cassandra.io.SSTableReader;
import org.apache.cassandra.CleanupHelper;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.filter.QueryPath;
import org.apache.cassandra.db.filter.IdentityQueryFilter;
import org.apache.cassandra.utils.FBUtilities;
import static junit.framework.Assert.assertEquals;
public class CompactionsTest extends CleanupHelper
{
public static final String TABLE1 = "Keyspace1";
public static final String TABLE2 = "Keyspace2";
public static final InetAddress LOCAL = FBUtilities.getLocalAddress();
@Test
public void testCompactions() throws IOException, ExecutionException, InterruptedException
{
// this test does enough rows to force multiple block indexes to be used
Table table = Table.open("Keyspace1");
Table table = Table.open(TABLE1);
ColumnFamilyStore store = table.getColumnFamilyStore("Standard1");
final int ROWS_PER_SSTABLE = 10;
@ -46,7 +54,7 @@ public class CompactionsTest extends CleanupHelper
for (int j = 0; j < (SSTableReader.indexInterval() * 3) / ROWS_PER_SSTABLE; j++) {
for (int i = 0; i < ROWS_PER_SSTABLE; i++) {
String key = String.valueOf(i % 2);
RowMutation rm = new RowMutation("Keyspace1", key);
RowMutation rm = new RowMutation(TABLE1, key);
rm.add(new QueryPath("Standard1", null, String.valueOf(i / 2).getBytes()), new byte[0], j * ROWS_PER_SSTABLE + i);
rm.apply();
inserted.add(key);
@ -72,7 +80,7 @@ public class CompactionsTest extends CleanupHelper
{
CompactionManager.instance().disableCompactions();
Table table = Table.open("Keyspace1");
Table table = Table.open(TABLE1);
String cfName = "Standard1";
ColumnFamilyStore store = table.getColumnFamilyStore(cfName);
@ -80,7 +88,7 @@ public class CompactionsTest extends CleanupHelper
RowMutation rm;
// inserts
rm = new RowMutation("Keyspace1", key);
rm = new RowMutation(TABLE1, key);
for (int i = 0; i < 10; i++)
{
rm.add(new QueryPath(cfName, null, String.valueOf(i).getBytes()), new byte[0], 0);
@ -91,20 +99,20 @@ public class CompactionsTest extends CleanupHelper
// deletes
for (int i = 0; i < 10; i++)
{
rm = new RowMutation("Keyspace1", key);
rm = new RowMutation(TABLE1, key);
rm.delete(new QueryPath(cfName, null, String.valueOf(i).getBytes()), 1);
rm.apply();
}
store.forceBlockingFlush();
// resurrect one row
rm = new RowMutation("Keyspace1", key);
rm = new RowMutation(TABLE1, key);
rm.add(new QueryPath(cfName, null, String.valueOf(5).getBytes()), new byte[0], 2);
rm.apply();
store.forceBlockingFlush();
// compact and test that all columns but the resurrected one is completely gone
store.doFileCompaction(store.getSSTables(), Integer.MAX_VALUE);
store.doFileCompaction(store.getSSTables(), Integer.MAX_VALUE, false);
ColumnFamily cf = table.getColumnFamilyStore(cfName).getColumnFamily(new IdentityQueryFilter(key, new QueryPath(cfName)));
assert cf.getColumnCount() == 1;
assert cf.getColumn(String.valueOf(5).getBytes()) != null;
@ -115,7 +123,7 @@ public class CompactionsTest extends CleanupHelper
{
CompactionManager.instance().disableCompactions();
Table table = Table.open("Keyspace1");
Table table = Table.open(TABLE1);
String cfName = "Standard2";
ColumnFamilyStore store = table.getColumnFamilyStore(cfName);
@ -123,7 +131,7 @@ public class CompactionsTest extends CleanupHelper
RowMutation rm;
// inserts
rm = new RowMutation("Keyspace1", key);
rm = new RowMutation(TABLE1, key);
for (int i = 0; i < 5; i++)
{
rm.add(new QueryPath(cfName, null, String.valueOf(i).getBytes()), new byte[0], 0);
@ -133,7 +141,7 @@ public class CompactionsTest extends CleanupHelper
// deletes
for (int i = 0; i < 5; i++)
{
rm = new RowMutation("Keyspace1", key);
rm = new RowMutation(TABLE1, key);
rm.delete(new QueryPath(cfName, null, String.valueOf(i).getBytes()), 1);
rm.apply();
}
@ -142,9 +150,36 @@ public class CompactionsTest extends CleanupHelper
assert store.getSSTables().size() == 1 : store.getSSTables(); // inserts & deletes were in the same memtable -> only deletes in sstable
// compact and test that the row is completely gone
store.doFileCompaction(store.getSSTables(), Integer.MAX_VALUE);
store.doFileCompaction(store.getSSTables(), Integer.MAX_VALUE, false);
assert store.getSSTables().isEmpty();
ColumnFamily cf = table.getColumnFamilyStore(cfName).getColumnFamily(new IdentityQueryFilter(key, new QueryPath(cfName)));
assert cf == null : cf;
}
@Test
public void testCompactionReadonly() throws IOException, ExecutionException, InterruptedException
{
Table table = Table.open(TABLE2);
ColumnFamilyStore store = table.getColumnFamilyStore("Standard1");
final int ROWS_PER_SSTABLE = 10;
Set<String> inserted = new HashSet<String>();
for (int j = 0; j < (SSTableReader.indexInterval() * 3) / ROWS_PER_SSTABLE; j++) {
for (int i = 0; i < ROWS_PER_SSTABLE; i++) {
String key = String.valueOf(i % 2);
RowMutation rm = new RowMutation(TABLE2, key);
rm.add(new QueryPath("Standard1", null, String.valueOf(i / 2).getBytes()), new byte[0], j * ROWS_PER_SSTABLE + i);
rm.apply();
inserted.add(key);
}
store.forceBlockingFlush();
assertEquals(inserted.size(), table.getColumnFamilyStore("Standard1").getKeyRange("", "", 10000).keys.size());
}
// perform readonly compaction and confirm that no sstables changed
ArrayList<SSTableReader> oldsstables = new ArrayList<SSTableReader>(store.getSSTables());
store.doReadonlyCompaction(LOCAL);
assertEquals(oldsstables, new ArrayList<SSTableReader>(store.getSSTables()));
assertEquals(inserted.size(), table.getColumnFamilyStore("Standard1").getKeyRange("", "", 10000).keys.size());
}
}

View File

@ -0,0 +1,238 @@
/*
* 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.service;
import java.io.IOException;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.*;
import org.apache.cassandra.concurrent.StageManager;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.db.filter.QueryPath;
import org.apache.cassandra.db.ColumnFamilyStoreUtils;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.CompactionManager;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.RowMutation;
import org.apache.cassandra.db.Table;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Range;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.io.CompactionIterator.CompactedRow;
import org.apache.cassandra.io.DataOutputBuffer;
import org.apache.cassandra.io.SSTableReader;
import static org.apache.cassandra.service.AntiEntropyService.*;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.MerkleTree;
import org.apache.cassandra.CleanupHelper;
import org.apache.cassandra.io.SSTableUtils;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
public class AntiEntropyServiceTest extends CleanupHelper
{
public static InetAddress LOCAL = FBUtilities.getLocalAddress();
// table and column family to test against
public AntiEntropyService aes;
public String tablename;
public String cfname;
static
{
try
{
StorageService.instance().initServer();
}
catch(Exception e)
{
throw new RuntimeException(e);
}
}
@Before
public void prepare() throws Exception
{
aes = AntiEntropyService.instance();
tablename = DatabaseDescriptor.getTables().get(0);
cfname = Table.open(tablename).getColumnFamilies().iterator().next();
}
@Test
public void testInstance() throws Throwable
{
assert null != aes;
assert aes == AntiEntropyService.instance();
}
@Test
public void testValidatorPrepare() throws Throwable
{
Validator validator;
// open an SSTable to give us something to sample
TreeMap<String, byte[]> map = new TreeMap<String,byte[]>();
for ( int i = 0; i < 1000; i++ )
{
map.put(Integer.toString(i), "blah".getBytes());
}
// write
SSTableReader ssTable =
SSTableUtils.writeSSTable(cfname, map, 1000,
StorageService.instance().getPartitioner(), 0.01);
tablename = ssTable.getTableName();
// sample
validator = new Validator(new CFTuple(tablename, cfname), LOCAL);
validator.prepare();
// and confirm that the tree was split
assertTrue(validator.tree.size() > 1);
}
@Test
public void testValidatorComplete() throws Throwable
{
Validator validator = new Validator(new CFTuple(tablename, cfname), LOCAL);
validator.prepare();
validator.complete();
// confirm that the tree was validated
Token min = validator.tree.partitioner().getMinimumToken();
assert null != validator.tree.hash(new Range(min, min));
// wait for queued operations to be flushed
flushAES().get(5000, TimeUnit.MILLISECONDS);
}
@Test
public void testValidatorAdd() throws Throwable
{
Validator validator = new Validator(new CFTuple(tablename, cfname),
LOCAL);
IPartitioner part = validator.tree.partitioner();
Token min = part.getMinimumToken();
Token mid = part.midpoint(min, min);
validator.prepare();
// add a row with the minimum token
validator.add(new CompactedRow(new DecoratedKey(min, "nonsense!"),
new DataOutputBuffer()));
// and a row after it
validator.add(new CompactedRow(new DecoratedKey(mid, "inconceivable!"),
new DataOutputBuffer()));
validator.complete();
// confirm that the tree was validated
assert null != validator.tree.hash(new Range(min, min));
}
/**
* Build a column family with 2 or more SSTables, and then force a major compaction
*/
@Test
public void testTreeCaching() throws Throwable
{
// populate column family
List<RowMutation> rms = new LinkedList<RowMutation>();
RowMutation rm = new RowMutation(tablename, "key");
rm.add(new QueryPath(cfname, null, "Column1".getBytes()), "asdf".getBytes(), 0);
rms.add(rm);
// with two SSTables
ColumnFamilyStoreUtils.writeColumnFamily(rms);
ColumnFamilyStore store = ColumnFamilyStoreUtils.writeColumnFamily(rms);
// force a major compaction, and wait for it to finish
MerkleTree old = aes.getCachedTree(tablename, cfname, LOCAL);
CompactionManager.instance().submitMajor(store, 0).get(5000, TimeUnit.MILLISECONDS);
// check that a tree was created and cached
flushAES().get(5000, TimeUnit.MILLISECONDS);
assert old != aes.getCachedTree(tablename, cfname, LOCAL);
}
@Test
public void testNotifyNeighbors() throws Throwable
{
// generate empty tree
Validator validator = new Validator(new CFTuple(tablename, cfname), LOCAL);
validator.prepare();
validator.complete();
// grab reference to the tree
MerkleTree tree = validator.tree;
// notify ourself (should immediately be delivered into AE_STAGE)
aes.notifyNeighbors(validator, LOCAL, Arrays.asList(LOCAL));
flushAES().get(5, TimeUnit.SECONDS);
// confirm that our reference is not equal to the original due
// to (de)serialization
assert tree != aes.getCachedTree(tablename, cfname, LOCAL);
}
@Test
public void testDifferencer() throws Throwable
{
// generate a tree
Validator validator = new Validator(new CFTuple("ltable", "lcf"), LOCAL);
validator.prepare();
// create a clone with no values filled
validator.complete();
MerkleTree ltree = validator.tree;
validator = new Validator(new CFTuple("rtable", "rcf"), LOCAL);
validator.prepare();
validator.complete();
MerkleTree rtree = validator.tree;
// change a range in one of the trees
Token min = StorageService.instance().getPartitioner().getMinimumToken();
ltree.invalidate(min);
MerkleTree.TreeRange changed = ltree.invalids(new Range(min, min)).next();
changed.hash("non-empty hash!".getBytes());
// difference the trees
Differencer diff = new Differencer(new CFTuple(tablename, cfname),
LOCAL, LOCAL, ltree, rtree);
diff.run();
// ensure that the changed range was recorded
assertEquals("Wrong number of differing ranges", 1, diff.differences.size());
assertEquals("Wrong differing range", changed, diff.differences.get(0));
}
Future<Object> flushAES()
{
return StageManager.getStage(AE_SERVICE_STAGE).execute(new Callable<Object>(){
public Boolean call()
{
return true;
}
});
}
}