package org.infinispan.functional; import static org.infinispan.test.Exceptions.assertException; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertNotNull; import static org.testng.AssertJUnit.assertTrue; import java.io.Serializable; import java.util.Collections; import java.util.concurrent.CompletionException; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Stream; import javax.transaction.RollbackException; import javax.transaction.Status; import javax.transaction.Transaction; import javax.transaction.xa.XAException; import org.infinispan.Cache; import org.infinispan.commons.api.functional.EntryView; import org.infinispan.commons.api.functional.FunctionalMap; import org.infinispan.remoting.RemoteException; import org.infinispan.transaction.LockingMode; import org.infinispan.transaction.WriteSkewException; import org.infinispan.util.concurrent.IsolationLevel; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @Test(groups = "functional") public class FunctionalWriteSkewInMemoryTest extends FunctionalTxInMemoryTest { public FunctionalWriteSkewInMemoryTest() { transactional(true).lockingMode(LockingMode.OPTIMISTIC).isolationLevel(IsolationLevel.REPEATABLE_READ); } @Override protected String parameters() { return null; } @DataProvider(name = "readCombos") public static Object[][] readCombos() { return Stream.of(Boolean.TRUE, Boolean.FALSE).flatMap( isOwner -> Stream.of(ReadOp.values()).flatMap( op1 -> Stream.of(ReadOp.values()).map( op2 -> new Object[]{isOwner, op1, op2}))) .filter(args -> ((ReadOp) args[1]).isFunctional() || ((ReadOp) args[2]).isFunctional()) .toArray(Object[][]::new); } @Test(dataProvider = "readCombos") public void testWriteSkew(boolean isOwner, ReadOp op1, ReadOp op2) throws Throwable { Object key = getKey(isOwner); cache(0, DIST).put(key, "value0"); tm.begin(); assertEquals("value0", op1.action.eval(cache(0, DIST), key, ro, rw)); Transaction transaction = tm.suspend(); cache(0, DIST).put(key, "value1"); tm.resume(transaction); try { assertEquals("value0", op2.action.eval(cache(0, DIST), key, ro, rw)); } catch (CompletionException e) { assertException(WriteSkewException.class, e.getCause()); // this is fine; either we read the old value, or we can't read it and we throw tm.rollback(); } catch (WriteSkewException e) { // synchronous get is invoked using synchronous API, without wrapping into CompletionExceptions assert op2 == ReadOp.GET; } if (tm.getStatus() == Status.STATUS_ACTIVE) { try { tm.commit(); } catch (RollbackException e) { Throwable[] suppressed = e.getSuppressed(); assertTrue(suppressed != null && suppressed.length == 1); assertEquals(XAException.class, suppressed[0].getClass()); Throwable cause = suppressed[0].getCause(); while (cause instanceof RemoteException) { cause = cause.getCause(); } assertNotNull(cause); assertEquals(WriteSkewException.class, cause.getClass()); } } } enum ReadOp { READ(true, (cache, key, ro, rw) -> ro.eval(key, (Serializable & Function<EntryView.ReadEntryView, Object>) EntryView.ReadEntryView::get).join()), READ_MANY(true, (cache, key, ro, rw) -> ro.evalMany(Collections.singleton(key), (Serializable & Function<EntryView.ReadEntryView, Object>) EntryView.ReadEntryView::get).findAny().get()), READ_WRITE_KEY(true, (cache, key, ro, rw) -> rw.eval(key, (Serializable & Function<EntryView.ReadWriteEntryView, Object>) EntryView.ReadEntryView::get).join()), READ_WRITE_KEY_VALUE(true, (cache, key, ro, rw) -> rw.eval(key, null, (Serializable & BiFunction<Object, EntryView.ReadWriteEntryView, Object>) (value, v) -> v.get()).join()), READ_WRITE_MANY(true, (cache, key, ro, rw) -> rw.evalMany(Collections.singleton(key), (Serializable & Function<EntryView.ReadWriteEntryView, Object>) EntryView.ReadEntryView::get).findAny().get()), READ_WRITE_MANY_ENTRIES(true, (cache, key, ro, rw) -> rw.evalMany(Collections.singletonMap(key, null), (Serializable & BiFunction<Object, EntryView.ReadWriteEntryView, Object>) (value, v) -> v.get()).findAny().get()), GET(false, (cache, key, ro, rw) -> cache.get(key)) ; final Performer action; final boolean functional; ReadOp(boolean functional, Performer action) { this.functional = functional; this.action = action; } public boolean isFunctional() { return functional; } @FunctionalInterface private interface Performer { Object eval(Cache cache, Object key, FunctionalMap.ReadOnlyMap ro, FunctionalMap.ReadWriteMap rw) throws Throwable; } } }