/*
* INESC-ID, Instituto de Engenharia de Sistemas e Computadores Investigação e Desevolvimento em Lisboa
* Copyright 2013 INESC-ID and/or its affiliates and other
* contributors as indicated by the @author tags. All rights reserved.
* See the copyright.txt in the distribution for a full listing of
* individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 3.0 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.infinispan.tx.gmu;
import org.infinispan.Cache;
import org.infinispan.commands.tx.CommitCommand;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.VersioningScheme;
import org.infinispan.container.DataContainer;
import org.infinispan.container.gmu.GMUDataContainer;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.distribution.DistributionManager;
import org.infinispan.distribution.ch.ConsistentHash;
import org.infinispan.interceptors.TxInterceptor;
import org.infinispan.interceptors.base.BaseCustomInterceptor;
import org.infinispan.interceptors.base.CommandInterceptor;
import org.infinispan.remoting.transport.Address;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.transaction.LocalTransaction;
import org.infinispan.transaction.TransactionTable;
import org.infinispan.transaction.gmu.manager.SortedTransactionQueue;
import org.infinispan.transaction.gmu.manager.TransactionCommitManager;
import org.infinispan.transaction.xa.GlobalTransaction;
import org.infinispan.util.concurrent.IsolationLevel;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
import static org.infinispan.distribution.DistributionTestHelper.addressOf;
import static org.infinispan.distribution.DistributionTestHelper.isOwner;
/**
* // TODO: Document this
*
* @author Pedro Ruivo
* @since 5.2
*/
public abstract class AbstractGMUTest extends MultipleCacheManagersTest {
protected static final String KEY_1 = "key_1";
protected static final String KEY_2 = "key_2";
protected static final String KEY_3 = "key_3";
protected static final String VALUE_1 = "value_1";
protected static final String VALUE_2 = "value_2";
protected static final String VALUE_3 = "value_3";
private static final AtomicInteger KEY_ID = new AtomicInteger(0);
@Override
protected final void createCacheManagers() throws Throwable {
ConfigurationBuilder dcc = defaultGMUConfiguration();
decorate(dcc);
createCluster(dcc, initialClusterSize());
waitForClusterToForm();
}
protected abstract void decorate(ConfigurationBuilder builder);
protected abstract int initialClusterSize();
protected abstract boolean syncCommitPhase();
protected abstract CacheMode cacheMode();
protected final void assertCachesValue(int executedOn, Object key, Object value) {
for (int i = 0; i < cacheManagers.size(); ++i) {
if (i == executedOn || syncCommitPhase()) {
assertEquals(value, cache(i).get(key));
} else {
assertEventuallyEquals(i, key, value);
}
}
}
protected final void assertCacheValuesNull(Object... keys) {
for (int i = 0; i < cacheManagers.size(); ++i) {
for (Object key : keys) {
assertNull(cache(i).get(key));
}
}
}
protected final void assertAtLeastCaches(int size) {
assert cacheManagers.size() >= size;
}
protected final void printDataContainer() {
if (log.isDebugEnabled()) {
StringBuilder stringBuilder = new StringBuilder("\n\n===================\n");
for (int i = 0; i < cacheManagers.size(); ++i) {
DataContainer dataContainer = cache(i).getAdvancedCache().getDataContainer();
if (dataContainer instanceof GMUDataContainer) {
stringBuilder.append(dataContainerToString((GMUDataContainer) dataContainer))
.append("\n")
.append("===================\n");
} else {
return;
}
}
log.debugf(stringBuilder.toString());
}
}
protected final void put(int cacheIndex, Object key, Object value, Object returnValue) {
txPut(cacheIndex, key, value, returnValue);
assertCachesValue(cacheIndex, key, value);
}
protected final void txPut(int cacheIndex, Object key, Object value, Object returnValue) {
Object oldValue = cache(cacheIndex).put(key, value);
assertEquals(returnValue, oldValue);
}
protected final void putIfAbsent(int cacheIndex, Object key, Object value, Object returnValue, Object expectedValue) {
Object oldValue = cache(cacheIndex).putIfAbsent(key, value);
assertCachesValue(cacheIndex, key, expectedValue);
assertEquals(returnValue, oldValue);
}
protected final void putAll(int cacheIndex, Map<Object, Object> map) {
cache(cacheIndex).putAll(map);
for (Map.Entry<Object, Object> entry : map.entrySet()) {
assertCachesValue(cacheIndex, entry.getKey(), entry.getValue());
}
}
protected final void remove(int cacheIndex, Object key, Object returnValue) {
Object oldValue = cache(cacheIndex).remove(key);
assertCachesValue(cacheIndex, key, null);
assertEquals(returnValue, oldValue);
}
protected final void replace(int cacheIndex, Object key, Object value, Object returnValue) {
Object oldValue = cache(cacheIndex).replace(key, value);
assertCachesValue(cacheIndex, key, value);
assertEquals(returnValue, oldValue);
}
protected final void replaceIf(int cacheIndex, Object key, Object value, Object ifValue, Object returnValue, boolean success) {
boolean result = cache(cacheIndex).replace(key, ifValue, value);
assertCachesValue(cacheIndex, key, returnValue);
assertEquals(result, success);
}
protected final void removeIf(int cacheIndex, Object key, Object ifValue, Object returnValue, boolean success) {
boolean result = cache(cacheIndex).remove(key, ifValue);
assertCachesValue(cacheIndex, key, returnValue);
assertEquals(result, success);
}
protected final void safeRollback(int cacheIndex) {
safeRollback(tm(cacheIndex));
}
protected final void safeRollback(TransactionManager transactionManager) {
try {
transactionManager.rollback();
} catch (Exception e) {
log.warn("Exception suppressed when rollback: " + e.getMessage());
}
}
protected final Object newKey(int mapTo, int notMapTo) {
return newKey(Collections.singleton(mapTo), Collections.singleton(notMapTo));
}
protected final Object newKey(int mapTo, Collection<Integer> notMapTo) {
return newKey(Collections.singleton(mapTo), notMapTo);
}
protected final Object newKey(Collection<Integer> mapTo, Collection<Integer> notMapTo) {
return new GMUMagicKey(caches(mapTo), caches(notMapTo), "KEY_" + KEY_ID.incrementAndGet());
}
protected final Object newKey(int mapTo) {
return newKey(Collections.singleton(mapTo), Collections.<Integer>emptyList());
}
protected final void assertKeyOwners(Object key, int mapTo, int notMapTo) {
assertKeyOwners(key, Collections.singleton(mapTo), Collections.singleton(notMapTo));
}
protected final void assertKeyOwners(Object key, Collection<Integer> mapTo, Collection<Integer> notMapTo) {
if (mapTo != null) {
for (int index : mapTo) {
if (cache(index).getAdvancedCache().getDistributionManager() != null) {
assert isOwner(cache(index), key) : key + " does not belong to " + addressOf(cache(index));
}
}
}
if (notMapTo != null) {
for (int index : notMapTo) {
if (cache(index).getAdvancedCache().getDistributionManager() != null) {
assert !isOwner(cache(index), key) : key + " belong to " + addressOf(cache(index));
}
}
}
}
protected final <T> T getComponent(int cacheIndex, Class<T> tClass) {
return TestingUtil.extractComponent(cache(cacheIndex), tClass);
}
protected final void logKeysUsedInTest(String testName, Object... keys) {
log.debugf("Test [%s] in class [%s] will use %s", testName, getClass().getSimpleName(), Arrays.asList(keys));
}
protected final Collection<Cache> caches(Collection<Integer> cacheIndexes) {
if (cacheIndexes == null || cacheIndexes.isEmpty()) {
return Collections.emptyList();
}
List<Cache> list = new LinkedList<Cache>();
for (int index : cacheIndexes) {
list.add(cache(index));
}
return list;
}
protected final void rewireMagicKeyAwareConsistentHash() {
for (int i = 0; i < cacheManagers.size(); ++i) {
DistributionManager distributionManager = advancedCache(i).getDistributionManager();
//only in distributed mode we have DistributionManager
if (distributionManager != null) {
ConsistentHash delegate = distributionManager.getConsistentHash();
MagicKeyAwareConsistentHash ch = new MagicKeyAwareConsistentHash(delegate);
distributionManager.setConsistentHash(ch);
}
}
}
protected final GlobalTransaction globalTransaction(int cacheIndex) {
TransactionTable transactionTable = advancedCache(cacheIndex).getComponentRegistry()
.getComponent(TransactionTable.class);
LocalTransaction localTransaction = transactionTable.getLocalTransaction(tx(cacheIndex));
return localTransaction == null ? null : localTransaction.getGlobalTransaction();
}
protected final Thread prepareInAllNodes(final Transaction tx, final DelayCommit delayCommit, final int cacheIndex)
throws InterruptedException {
Thread thread = new Thread("Prepare-Only-" + cacheIndex + "-" + tx) {
@Override
public void run() {
try {
tm(cacheIndex).resume(tx);
delayCommit.blockTransaction(globalTransaction(cacheIndex));
tm(cacheIndex).commit();
} catch (Exception e) {
e.printStackTrace();
delayCommit.setCommitBlocked(true);
}
}
};
thread.start();
delayCommit.awaitUntilCommitIsBlocked();
return thread;
}
protected final DelayCommit addDelayCommit(int cacheIndex, int delay) {
DelayCommit delayCommit = new DelayCommit(delay);
advancedCache(cacheIndex).removeInterceptor(DelayCommit.class);
advancedCache(cacheIndex).addInterceptorAfter(delayCommit, TxInterceptor.class);
return delayCommit;
}
private ConfigurationBuilder defaultGMUConfiguration() {
ConfigurationBuilder builder = getDefaultClusteredCacheConfig(cacheMode(), true);
builder.locking().isolationLevel(IsolationLevel.SERIALIZABLE);
builder.versioning().enable().scheme(VersioningScheme.GMU);
builder.transaction().syncCommitPhase(syncCommitPhase());
builder.clustering().l1().disable();
return builder;
}
private String dataContainerToString(GMUDataContainer dataContainer) {
return dataContainer.stateToString();
}
protected class MagicKeyAwareConsistentHash implements ConsistentHash {
private final ConsistentHash delegate;
public MagicKeyAwareConsistentHash(ConsistentHash delegate) {
this.delegate = delegate;
}
@Override
public Set<Address> getCaches() {
return delegate.getCaches();
}
@Override
public void setCaches(Set<Address> caches) {
delegate.setCaches(caches);
}
@Override
public List<Address> locate(Object key, int replCount) {
log.debugf("MagicKeyAware.locate(%s,%s) ==> ??", key, replCount);
if (key instanceof GMUMagicKey) {
GMUMagicKey magicKey = (GMUMagicKey) key;
List<Address> owners = delegate.locate(key, replCount + 2);
List<Address> returnOwners = new ArrayList<Address>(replCount);
for (Address mustBeOwner : magicKey.getMapTo()) {
if (delegate.getCaches().contains(mustBeOwner)) {
returnOwners.add(mustBeOwner);
}
}
int insertedOwners = returnOwners.size();
for (Address owner : owners) {
if (insertedOwners >= replCount) {
break;
} else if (magicKey.getNotMapTo().contains(owner)) {
//not to be inside
continue;
} else if (magicKey.getMapTo().contains(owner)) {
//already inside
continue;
}
returnOwners.add(owner);
insertedOwners++;
}
log.debugf("MagicKeyAware.locate(%s,%s) ==> %s", key, replCount, returnOwners);
return returnOwners;
}
return delegate.locate(key, replCount);
}
@Override
public Map<Object, List<Address>> locateAll(Collection<Object> keys, int replCount) {
Map<Object, List<Address>> owners = new HashMap<Object, List<Address>>(keys.size());
for (Object key : keys) {
owners.put(key, locate(key, replCount));
}
return owners;
}
@Override
public boolean isKeyLocalToAddress(Address a, Object key, int replCount) {
return locate(key, replCount).contains(a);
}
@Override
public List<Integer> getHashIds(Address a) {
return delegate.getHashIds(a);
}
@Override
@Deprecated
public List<Address> getStateProvidersOnLeave(Address leaver, int replCount) {
return delegate.getStateProvidersOnLeave(leaver, replCount);
}
@Override
@Deprecated
public List<Address> getStateProvidersOnJoin(Address joiner, int replCount) {
return delegate.getStateProvidersOnJoin(joiner, replCount);
}
@Override
@Deprecated
public List<Address> getBackupsForNode(Address node, int replCount) {
return delegate.getBackupsForNode(node, replCount);
}
@Override
public Address primaryLocation(Object key) {
return locate(key, 1).get(0);
}
}
protected class DelayCommit extends CommandInterceptor {
private final long delay;
private final Object commitLock = new Object();
private final Object hasCommitLock = new Object();
private volatile GlobalTransaction transactionToBlock;
private boolean hasCommitBlocked;
private DelayCommit(long delay) {
this.delay = delay;
}
@Override
public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable {
if (transactionToBlock != null && transactionToBlock.equals(command.getGlobalTransaction())) {
blockCommit();
}
return invokeNextInterceptor(ctx, command);
}
public void blockTransaction(GlobalTransaction globalTransaction) {
this.transactionToBlock = globalTransaction;
}
public void awaitUntilCommitIsBlocked() throws InterruptedException {
synchronized (hasCommitLock) {
while (!hasCommitBlocked) {
hasCommitLock.wait();
}
}
}
public void blockCommit() {
synchronized (commitLock) {
setCommitBlocked(true);
try {
if (delay <= 0) {
commitLock.wait();
} else {
commitLock.wait(delay);
}
} catch (Exception e) {
//ignore
}
transactionToBlock = null;
setCommitBlocked(false);
}
}
public void unblock() {
synchronized (commitLock) {
commitLock.notify();
}
}
private void setCommitBlocked(boolean value) {
synchronized (hasCommitLock) {
hasCommitBlocked = value;
hasCommitLock.notifyAll();
}
}
}
protected class ObtainTransactionEntry extends BaseCustomInterceptor {
private final TransactionCommitManager transactionCommitManager;
private SortedTransactionQueue.TransactionEntry transactionEntry;
private Thread expectedThread;
public ObtainTransactionEntry(Cache<?, ?> cache) {
this.transactionCommitManager = cache.getAdvancedCache().getComponentRegistry()
.getComponent(TransactionCommitManager.class);
cache.getAdvancedCache().addInterceptorAfter(this, TxInterceptor.class);
}
@Override
public Object visitCommitCommand(TxInvocationContext ctx, CommitCommand command) throws Throwable {
setTransactionEntry(transactionCommitManager.getTransactionEntry(command.getGlobalTransaction()));
return invokeNextInterceptor(ctx, command);
}
public synchronized SortedTransactionQueue.TransactionEntry getTransactionEntry() throws InterruptedException {
while (transactionEntry == null) {
wait();
}
return transactionEntry;
}
private synchronized void setTransactionEntry(SortedTransactionQueue.TransactionEntry transactionEntry) {
if (Thread.currentThread().equals(expectedThread)) {
log.debugf("Setting transactions entry: %s", transactionEntry);
this.transactionEntry = transactionEntry;
notifyAll();
} else {
log.debugf("Not setting transaction entry. Thread does not match %s and %s.", expectedThread, Thread.currentThread());
}
}
public synchronized void expectedThisThread() {
this.expectedThread = Thread.currentThread();
}
public synchronized void reset() {
transactionEntry = null;
}
}
}