package org.infinispan.functional; import java.io.Serializable; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import org.infinispan.AdvancedCache; import org.infinispan.Cache; import org.infinispan.commands.VisitableCommand; import org.infinispan.commands.functional.TxReadOnlyKeyCommand; import org.infinispan.commands.functional.TxReadOnlyManyCommand; import org.infinispan.commons.api.functional.EntryView.ReadEntryView; import org.infinispan.commons.api.functional.EntryView.ReadWriteEntryView; import org.infinispan.commons.api.functional.EntryView.WriteEntryView; import org.infinispan.commons.api.functional.FunctionalMap; import org.infinispan.commons.api.functional.FunctionalMap.ReadWriteMap; import org.infinispan.commons.api.functional.FunctionalMap.WriteOnlyMap; import org.infinispan.context.InvocationContext; import org.infinispan.functional.impl.ReadOnlyMapImpl; import org.infinispan.functional.impl.ReadWriteMapImpl; import org.infinispan.functional.impl.WriteOnlyMapImpl; import org.infinispan.interceptors.BaseCustomAsyncInterceptor; import org.infinispan.interceptors.impl.CallInterceptor; import org.infinispan.remoting.transport.Address; import org.infinispan.util.CountingCARD; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static org.testng.Assert.assertEquals; /** * @author Radim Vansa <rvansa@redhat.com> && Krzysztof Sobolewski <Krzysztof.Sobolewski@atende.pl> */ @Test(groups = "functional", testName = "functional.AbstractFunctionalOpTest") public abstract class AbstractFunctionalOpTest extends AbstractFunctionalTest { static ConcurrentMap<Class<? extends AbstractFunctionalOpTest>, AtomicInteger> invocationCounts = new ConcurrentHashMap<>(); FunctionalMap.ReadOnlyMap<Object, String> ro; FunctionalMap.ReadOnlyMap<Integer, String> lro; WriteOnlyMap<Object, String> wo; ReadWriteMap<Object, String> rw; AdvancedCache<Object, String> cache; WriteOnlyMap<Integer, String> lwo; ReadWriteMap<Integer, String> lrw; List<CountingCARD> countingCARDs; public AbstractFunctionalOpTest() { numNodes = 4; numDistOwners = 2; } @DataProvider(name = "writeMethods") public static Object[][] writeMethods() { return Stream.of(WriteMethod.values()).map(m -> new Object[]{m}).toArray(Object[][]::new); } @DataProvider(name = "owningModeAndWriteMethod") public static Object[][] owningModeAndWriteMethod() { return Stream.of(Boolean.TRUE, Boolean.FALSE) .flatMap(isSourceOwner -> Stream.of(WriteMethod.values()) .map(method -> new Object[]{isSourceOwner, method})) .toArray(Object[][]::new); } @DataProvider(name = "readMethods") public static Object[][] methods() { return Stream.of(ReadMethod.values()).map(m -> new Object[] { m }).toArray(Object[][]::new); } @DataProvider(name = "owningModeAndReadMethod") public static Object[][] owningModeAndMethod() { return Stream.of(Boolean.TRUE, Boolean.FALSE) .flatMap(isSourceOwner -> Stream.of(ReadMethod.values()) .map(method -> new Object[] { isSourceOwner, method })) .toArray(Object[][]::new); } @DataProvider(name = "owningModeAndReadWrites") public static Object[][] owningModeAndReadWrites() { return Stream.of(Boolean.TRUE, Boolean.FALSE) .flatMap(isSourceOwner -> Stream.of(WriteMethod.values()).filter(m -> m.doesRead) .map(method -> new Object[]{isSourceOwner, method})) .toArray(Object[][]::new); } static Address getAddress(Cache<Object, Object> cache) { return cache.getAdvancedCache().getRpcManager().getAddress(); } @BeforeMethod public void resetInvocationCount() { AtomicInteger counter = invocationCounts.get(getClass()); if (counter != null) counter.set(0); } @Override @BeforeMethod public void createBeforeMethod() throws Throwable { super.createBeforeMethod(); this.ro = ReadOnlyMapImpl.create(fmapD1); this.lro = ReadOnlyMapImpl.create(fmapL1); this.wo = WriteOnlyMapImpl.create(fmapD1); this.rw = ReadWriteMapImpl.create(fmapD1); this.cache = cacheManagers.get(0).<Object, String>getCache(DIST).getAdvancedCache(); this.lwo = WriteOnlyMapImpl.create(fmapL1); this.lrw = ReadWriteMapImpl.create(fmapL1); } @Override protected void createCacheManagers() throws Throwable { super.createCacheManagers(); countingCARDs = cacheManagers.stream().map(cm -> CountingCARD.replaceDispatcher(cm)).collect(Collectors.toList()); Stream.of(null, DIST, REPL).forEach(name -> caches(name).forEach(c -> { c.getAdvancedCache().getAsyncInterceptorChain().addInterceptorBefore(new CommandCachingInterceptor(), CallInterceptor.class); })); } protected void advanceGenerationsAndAwait(long timeout, TimeUnit timeUnit) throws InterruptedException { long now = System.currentTimeMillis(); long deadline = now + timeUnit.toMillis(timeout); for (CountingCARD card : countingCARDs) { card.advanceGenerationAndAwait(deadline - now, TimeUnit.MILLISECONDS); now = System.currentTimeMillis(); } } protected Object getKey(boolean isOwner) { Object key; if (isOwner) { // this is simple: find a key that is local to the originating node key = getKeyForCache(0, DIST); } else { // this is more complicated: we need a key that is *not* local to the originating node key = IntStream.iterate(0, i -> i + 1) .mapToObj(i -> "key" + i) .filter(k -> !cache(0, DIST).getAdvancedCache().getDistributionManager().getLocality(k).isLocal()) .findAny() .get(); } return key; } protected void assertInvocations(int expected) { AtomicInteger counter = invocationCounts.get(getClass()); assertEquals(counter == null ? 0 : counter.get(), expected); } // we don't want to increment the invocation count if it's the non-modifying invocation during transactional read-writes private static boolean isModifying() { VisitableCommand current = CommandCachingInterceptor.getCurrent(); return !(current instanceof TxReadOnlyKeyCommand || current instanceof TxReadOnlyManyCommand); } private static void incrementInvocationCount(Class<? extends AbstractFunctionalOpTest> clazz) { invocationCounts.computeIfAbsent(clazz, k -> new AtomicInteger()).incrementAndGet(); } enum WriteMethod { WO_EVAL(false, (key, wo, rw, read, write, clazz) -> wo.eval(key, (Consumer<WriteEntryView<String>> & Serializable) view -> { if (isModifying()) { incrementInvocationCount(clazz); } write.accept(view, null); }).join()), WO_EVAL_VALUE(false, (key, wo, rw, read, write, clazz) -> wo.eval(key, null, (BiConsumer<String, WriteEntryView<String>> & Serializable) (v, view) -> { if (isModifying()) { incrementInvocationCount(clazz); } write.accept(view, null); }).join()), WO_EVAL_MANY(false, (key, wo, rw, read, write, clazz) -> wo.evalMany(Collections.singleton(key), (Consumer<WriteEntryView<String>> & Serializable) view -> { if (isModifying()) { incrementInvocationCount(clazz); } write.accept(view, null); }).join()), WO_EVAL_MANY_ENTRIES(false, (key, wo, rw, read, write, clazz) -> wo.evalMany(Collections.singletonMap(key, null), (BiConsumer<String, WriteEntryView<String>> & Serializable) (v, view) -> { if (isModifying()) { incrementInvocationCount(clazz); } write.accept(view, null); }).join()), RW_EVAL(true, (key, wo, rw, read, write, clazz) -> rw.eval(key, (Function<ReadWriteEntryView<Object, String>, Object> & Serializable) view -> { if (isModifying()) { incrementInvocationCount(clazz); } Object ret = read.apply(view); write.accept(view, ret); return ret; }).join()), RW_EVAL_VALUE(true, (key, wo, rw, read, write, clazz) -> rw.eval(key, null, (BiFunction<String, ReadWriteEntryView<Object, String>, Object> & Serializable) (v, view) -> { if (isModifying()) { incrementInvocationCount(clazz); } Object ret = read.apply(view); write.accept(view, ret); return ret; }).join()), RW_EVAL_MANY(true, (key, wo, rw, read, write, clazz) -> rw.evalMany(Collections.singleton(key), (Function<ReadWriteEntryView<Object, String>, Object> & Serializable) view -> { if (isModifying()) { incrementInvocationCount(clazz); } Object ret = read.apply(view); write.accept(view, ret); return ret; }).filter(Objects::nonNull).findAny().orElse(null)), RW_EVAL_MANY_ENTRIES(true, (key, wo, rw, read, write, clazz) -> rw.evalMany(Collections.singletonMap(key, null), (BiFunction<String, ReadWriteEntryView<Object, String>, Object> & Serializable) (v, view) -> { if (isModifying()) { incrementInvocationCount(clazz); } Object ret = read.apply(view); write.accept(view, ret); return ret; }).filter(Objects::nonNull).findAny().orElse(null)),; final Performer action; final boolean doesRead; WriteMethod(boolean doesRead, Performer action) { this.doesRead = doesRead; this.action = action; } @FunctionalInterface interface Performer<K, R> { R eval(K key, WriteOnlyMap<K, String> wo, ReadWriteMap<K, String> rw, Function<ReadEntryView<K, String>, R> read, BiConsumer<WriteEntryView<String>, R> write, Class<? extends AbstractFunctionalOpTest> clazz); } } enum ReadMethod { RO_EVAL((key, ro, read) -> ro.eval(key, read).join()), RO_EVAL_MANY((key, ro, read) -> ro.evalMany(Collections.singleton(key), read).filter(Objects::nonNull).findAny().orElse(null)), ; final Performer action; ReadMethod(Performer action) { this.action = action; } @FunctionalInterface interface Performer<K, R> { R eval(K key, FunctionalMap.ReadOnlyMap<K, String> ro, Function<ReadEntryView<Object, String>, R> read); } } protected static class CommandCachingInterceptor extends BaseCustomAsyncInterceptor { private static ThreadLocal<VisitableCommand> current = new ThreadLocal<>(); public static VisitableCommand getCurrent() { return current.get(); } @Override protected Object handleDefault(InvocationContext ctx, VisitableCommand command) throws Throwable { current.set(command); return invokeNextAndFinally(ctx, command, (rCtx, rCommand, rv, t) -> current.remove()); } } }