/* * Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved. * * Licensed 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 com.hazelcast.map; import com.hazelcast.config.Config; import com.hazelcast.config.InMemoryFormat; import com.hazelcast.config.MapConfig; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.ICompletableFuture; import com.hazelcast.core.IMap; import com.hazelcast.core.Offloadable; import com.hazelcast.core.ReadOnly; import com.hazelcast.test.HazelcastParametersRunnerFactory; import com.hazelcast.test.HazelcastTestSupport; import com.hazelcast.test.TestHazelcastInstanceFactory; import com.hazelcast.test.annotation.ParallelTest; import com.hazelcast.test.annotation.QuickTest; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import static com.hazelcast.config.InMemoryFormat.BINARY; import static com.hazelcast.config.InMemoryFormat.OBJECT; import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @RunWith(Parameterized.class) @Parameterized.UseParametersRunnerFactory(HazelcastParametersRunnerFactory.class) @Category({QuickTest.class, ParallelTest.class}) public class EntryProcessorOffloadableTest extends HazelcastTestSupport { public static final String MAP_NAME = "EntryProcessorOffloadableTest"; private HazelcastInstance[] instances; @Parameterized.Parameter(0) public InMemoryFormat inMemoryFormat; @Parameterized.Parameter(1) public int syncBackupCount; @Parameterized.Parameter(2) public int asyncBackupCount; @Rule public ExpectedException expectedException = ExpectedException.none(); @Parameterized.Parameters(name = "{index}: {0} sync={1} async={2}") public static Collection<Object[]> data() { return asList(new Object[][]{ {BINARY, 0, 0}, {OBJECT, 0, 0}, {BINARY, 1, 0}, {OBJECT, 1, 0}, {BINARY, 0, 1}, {OBJECT, 0, 1} }); } private boolean isBackup() { return syncBackupCount + asyncBackupCount > 0; } @Override public Config getConfig() { Config config = super.getConfig(); MapConfig mapConfig = new MapConfig(MAP_NAME); mapConfig.setInMemoryFormat(inMemoryFormat); mapConfig.setAsyncBackupCount(asyncBackupCount); mapConfig.setBackupCount(syncBackupCount); config.addMapConfig(mapConfig); return config; } @Before public void before() { TestHazelcastInstanceFactory factory = createHazelcastInstanceFactory(2); instances = factory.newInstances(getConfig()); } @Test public void testEntryProcessorWithKey_offloadable_setValue() { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); SimpleValue expectedValue = new SimpleValue(2); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); map.put(key, givenValue); Object result = map.executeOnKey(key, new EntryIncOffloadable()); assertEquals(expectedValue, map.get(key)); assertBackupEventually(instances[1], MAP_NAME, key, isBackup() ? expectedValue : null); assertEquals(givenValue.i, result); instances[0].shutdown(); assertEquals(expectedValue, map.get(key)); assertEquals(givenValue.i, result); } private static class EntryIncOffloadable implements EntryProcessor<String, SimpleValue>, Offloadable, EntryBackupProcessor<String, SimpleValue> { @Override public Object process(final Map.Entry<String, SimpleValue> entry) { final SimpleValue value = entry.getValue(); int result = value.i; value.i++; entry.setValue(value); return result; } @Override public EntryBackupProcessor<String, SimpleValue> getBackupProcessor() { return this; } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } } @Test public void testEntryProcessorWithKey_offloadable_withoutSetValue() { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); SimpleValue expectedValue = new SimpleValue(1); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); map.put(key, givenValue); Object result = map.executeOnKey(key, new EntryIncOffloadableNoSetValue()); assertEquals(expectedValue, map.get(key)); if (inMemoryFormat.equals(OBJECT)) { assertBackupEventually(instances[1], MAP_NAME, key, isBackup() ? new SimpleValue(2) : null); } else { assertBackupEventually(instances[1], MAP_NAME, key, isBackup() ? expectedValue : null); } assertEquals(givenValue.i, result); instances[0].shutdown(); assertEquals(expectedValue, map.get(key)); } private static class EntryIncOffloadableNoSetValue implements EntryProcessor<String, SimpleValue>, Offloadable, EntryBackupProcessor<String, SimpleValue> { @Override public Object process(final Map.Entry<String, SimpleValue> entry) { final SimpleValue value = entry.getValue(); int result = value.i; value.i++; return result; } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } @Override public EntryBackupProcessor getBackupProcessor() { return this; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } } @Test public void testEntryProcessorWithKey_offloadableReadOnly_setValue() { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); map.put(key, givenValue); expectedException.expect(UnsupportedOperationException.class); map.executeOnKey(key, new EntryIncOffloadableReadOnly()); } private static class EntryIncOffloadableReadOnly implements EntryProcessor<String, SimpleValue>, Offloadable, ReadOnly, EntryBackupProcessor<String, SimpleValue> { @Override public Object process(final Map.Entry<String, SimpleValue> entry) { final SimpleValue value = entry.getValue(); int result = value.i; value.i++; entry.setValue(value); return result; } @Override public EntryBackupProcessor<String, SimpleValue> getBackupProcessor() { return null; } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } } @Test public void testEntryProcessorWithKey_offloadableReadOnly_withoutSetValue() { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); SimpleValue expectedValue = new SimpleValue(1); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); map.put(key, givenValue); Object result = map.executeOnKey(key, new EntryIncOffloadableReadOnlyNoSetValue()); assertEquals(expectedValue, map.get(key)); assertBackupEventually(instances[1], MAP_NAME, key, isBackup() ? expectedValue : null); assertEquals(givenValue.i, result); instances[0].shutdown(); assertEquals(expectedValue, map.get(key)); } private static class EntryIncOffloadableReadOnlyNoSetValue implements EntryProcessor<String, SimpleValue>, Offloadable, ReadOnly, EntryBackupProcessor<String, SimpleValue> { @Override public Object process(final Map.Entry<String, SimpleValue> entry) { final SimpleValue value = entry.getValue(); int result = value.i; value.i++; return result; } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } } @Test public void testEntryProcessorWithKey_offloadableReadOnly_returnValue() { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); SimpleValue expectedValue = new SimpleValue(1); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); map.put(key, givenValue); Object result = map.executeOnKey(key, new EntryIncOffloadableReadOnlyReturnValue()); assertEquals(expectedValue, map.get(key)); assertBackupEventually(instances[1], MAP_NAME, key, isBackup() ? expectedValue : null); assertEquals(givenValue.i, result); instances[0].shutdown(); assertEquals(expectedValue, map.get(key)); } private static class EntryIncOffloadableReadOnlyReturnValue implements EntryProcessor<String, SimpleValue>, Offloadable, ReadOnly, EntryBackupProcessor<String, SimpleValue> { @Override public Object process(final Map.Entry<String, SimpleValue> entry) { final SimpleValue value = entry.getValue(); return value.i; } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } } @Test public void testEntryProcessorWithKey_offloadableReadOnly_noReturnValue() { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); SimpleValue expectedValue = new SimpleValue(1); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); map.put(key, givenValue); Object result = map.executeOnKey(key, new EntryIncOffloadableReadOnlyNoReturnValue()); assertEquals(expectedValue, map.get(key)); assertBackupEventually(instances[1], MAP_NAME, key, isBackup() ? expectedValue : null); assertNull(result); instances[0].shutdown(); assertEquals(expectedValue, map.get(key)); } private static class EntryIncOffloadableReadOnlyNoReturnValue implements EntryProcessor<String, SimpleValue>, Offloadable, ReadOnly, EntryBackupProcessor<String, SimpleValue> { @Override public Object process(final Map.Entry<String, SimpleValue> entry) { return null; } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } } @Test public void testEntryProcessorWithKey_offloadableReadOnly_throwsException() { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); map.put(key, givenValue); expectedException.expect(RuntimeException.class); map.executeOnKey(key, new EntryIncOffloadableReadOnlyException()); assertFalse(map.isLocked(key)); } private static class EntryIncOffloadableReadOnlyException implements EntryProcessor<String, SimpleValue>, Offloadable, ReadOnly, EntryBackupProcessor<String, SimpleValue> { @Override public Object process(final Map.Entry<String, SimpleValue> entry) { throw new RuntimeException("EP exception"); } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } } @Test public void testEntryProcessorWithKey_offloadableModifying_throwsException_keyNotLocked() { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); map.put(key, givenValue); expectedException.expect(RuntimeException.class); map.executeOnKey(key, new EntryIncOffloadableException()); assertFalse(map.isLocked(key)); } private static class EntryIncOffloadableException implements EntryProcessor<String, SimpleValue>, Offloadable, EntryBackupProcessor<String, SimpleValue> { @Override public Object process(final Map.Entry<String, SimpleValue> entry) { throw new RuntimeException("EP exception"); } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } } void assertBackupEventually(final HazelcastInstance instance, final String mapName, final Object key, Object expected) { assertEqualsEventually(new Callable<Object>() { @Override public Object call() throws Exception { return readFromMapBackup(instance, mapName, key); } }, expected); } @Test public void testEntryProcessorWithKey_offloadable_otherModifyingWillWait() throws InterruptedException { final String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); SimpleValue expectedValue = new SimpleValue(4); final IMap<Object, Object> map = instances[0].getMap(MAP_NAME); map.put(key, givenValue); final CountDownLatch epStarted = new CountDownLatch(1); final CountDownLatch epStopped = new CountDownLatch(1); new Thread() { public void run() { map.executeOnKey(key, new EntryLatchModifying(epStarted, epStopped)); } }.start(); epStarted.await(); map.executeOnKey(key, new EntryLatchVerifying(epStarted, epStopped, 4)); // verified EPs not out-of-order, and not at the same time assertEqualsEventually(new Callable<Object>() { @Override public Object call() throws Exception { return map.get(key); } }, expectedValue); } private static class EntryLatchModifying implements EntryProcessor<String, SimpleValue>, Offloadable, EntryBackupProcessor<String, SimpleValue> { private final CountDownLatch start; private final CountDownLatch stop; public EntryLatchModifying(CountDownLatch start, CountDownLatch stop) { this.start = start; this.stop = stop; } @Override public Object process(final Map.Entry<String, SimpleValue> entry) { start.countDown(); try { final SimpleValue value = entry.getValue(); value.i++; entry.setValue(value); return null; } finally { stop.countDown(); } } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } } private static class EntryLatchVerifying implements EntryProcessor<String, SimpleValue>, Offloadable, EntryBackupProcessor<String, SimpleValue> { private final CountDownLatch otherStarted; private final CountDownLatch otherStopped; private final int valueToSet; public EntryLatchVerifying(CountDownLatch otherStarted, CountDownLatch otherStopped, final int value) { this.otherStarted = otherStarted; this.otherStopped = otherStopped; this.valueToSet = value; } @Override public Object process(final Map.Entry<String, SimpleValue> entry) { if (otherStarted.getCount() != 0 || otherStopped.getCount() != 0) { throw new RuntimeException("Wrong threading order"); } final SimpleValue value = entry.getValue(); value.i = valueToSet; entry.setValue(value); return null; } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } } @Test public void testEntryProcessorWithKey_offloadable_otherReadingWillNotWait() throws InterruptedException { final String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); SimpleValue expectedValue = new SimpleValue(2); final IMap<Object, Object> map = instances[0].getMap(MAP_NAME); map.put(key, givenValue); final CountDownLatch epStarted = new CountDownLatch(1); final CountDownLatch epWaitToProceed = new CountDownLatch(1); final CountDownLatch epStopped = new CountDownLatch(1); new Thread() { public void run() { map.executeOnKey(key, new EntryLatchModifyingOtherReading(epStarted, epWaitToProceed, epStopped)); } }.start(); epStarted.await(); map.executeOnKey(key, new EntryLatchReadOnlyVerifyingWhileOtherWriting(epStarted, epWaitToProceed, epStopped)); epStopped.await(); // verified EPs not out-of-order, and not at the same time assertEqualsEventually(new Callable<Object>() { @Override public Object call() throws Exception { return map.get(key); } }, expectedValue); } private static class EntryLatchModifyingOtherReading implements EntryProcessor<String, SimpleValue>, Offloadable, EntryBackupProcessor<String, SimpleValue> { private final CountDownLatch start; private final CountDownLatch stop; private final CountDownLatch waitToProceed; public EntryLatchModifyingOtherReading(CountDownLatch start, CountDownLatch waitToProceed, CountDownLatch stop) { this.start = start; this.stop = stop; this.waitToProceed = waitToProceed; } @Override public Object process(final Map.Entry<String, SimpleValue> entry) { start.countDown(); try { waitToProceed.await(); final SimpleValue value = entry.getValue(); value.i++; entry.setValue(value); return null; } catch (InterruptedException e) { throw new RuntimeException(e); } finally { stop.countDown(); } } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } } private static class EntryLatchReadOnlyVerifyingWhileOtherWriting implements EntryProcessor<String, SimpleValue>, EntryBackupProcessor<String, SimpleValue>, Offloadable, ReadOnly { private final CountDownLatch otherStarted; private final CountDownLatch otherWaitingToProceed; private final CountDownLatch otherStopped; public EntryLatchReadOnlyVerifyingWhileOtherWriting(CountDownLatch otherStarted, CountDownLatch otherWaitingToProceed, CountDownLatch otherStopped) { this.otherStarted = otherStarted; this.otherWaitingToProceed = otherWaitingToProceed; this.otherStopped = otherStopped; } @Override public Object process(final Map.Entry<String, SimpleValue> entry) { if (otherStarted.getCount() != 0 || otherStopped.getCount() != 1) { throw new RuntimeException("Wrong threading order"); } otherWaitingToProceed.countDown(); return null; } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } } private String init() { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); map.put(key, givenValue); return key; } @Test public void testEntryProcessorWithKey_lockedVsUnlocked() { String key = init(); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); // not locked -> will offload String thread = (String) map.executeOnKey(key, new ThreadSneakingOffloadableEntryProcessor()); assertTrue(thread.contains("cached.thread")); // locked -> won't offload map.lock(key); thread = (String) map.executeOnKey(key, new ThreadSneakingOffloadableEntryProcessor()); assertTrue(thread.contains("partition-operation.thread")); } @Test public void testEntryProcessorWithKey_lockedVsUnlocked_ReadOnly() { String key = init(); IMap<Object, Object> map = instances[1].getMap(MAP_NAME); // not locked -> will offload String thread = (String) map.executeOnKey(key, new ThreadSneakingOffloadableReadOnlyEntryProcessor()); assertTrue(thread.contains("cached.thread")); // locked -> will offload map.lock(key); thread = (String) map.executeOnKey(key, new ThreadSneakingOffloadableReadOnlyEntryProcessor()); assertTrue(thread.contains("cached.thread")); } private static class ThreadSneakingOffloadableEntryProcessor extends AbstractEntryProcessor<String, SimpleValue> implements Offloadable { @Override public Object process(Map.Entry<String, SimpleValue> entry) { // returns the name of thread it runs on return Thread.currentThread().getName(); } @Override public String getExecutorName() { return OFFLOADABLE_EXECUTOR; } } private static class ThreadSneakingOffloadableReadOnlyEntryProcessor implements EntryProcessor<String, SimpleValue>, Offloadable, ReadOnly { @Override public Object process(Map.Entry<String, SimpleValue> entry) { // returns the name of thread it runs on return Thread.currentThread().getName(); } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public String getExecutorName() { return OFFLOADABLE_EXECUTOR; } } @Test public void testEntryProcessorWithKey_localNotReentrant() throws ExecutionException, InterruptedException { String key = init(); IMap<Object, SimpleValue> map = instances[1].getMap(MAP_NAME); int count = 100; // when List<ICompletableFuture> futures = new ArrayList<ICompletableFuture>(); for (int i = 0; i < count; i++) { futures.add(map.submitToKey(key, new IncrementingOffloadableEP())); } for (ICompletableFuture future : futures) { future.get(); } // then assertEquals(count + 1, map.get(key).i); } private static class IncrementingOffloadableEP implements EntryProcessor<String, SimpleValue>, Offloadable, EntryBackupProcessor<String, SimpleValue> { @Override public Object process(final Map.Entry<String, SimpleValue> entry) { SimpleValue value = entry.getValue(); value.i++; entry.setValue(value); return null; } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } @Override public EntryBackupProcessor getBackupProcessor() { return this; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } } @Test public void testEntryProcessorWithKey_localNotReentrant_latchTest() throws ExecutionException, InterruptedException { String key = generateKeyOwnedBy(instances[0]); SimpleValue givenValue = new SimpleValue(1); IMap<Object, Object> map = instances[0].getMap(MAP_NAME); map.put(key, givenValue); CountDownLatch mayStart = new CountDownLatch(1); CountDownLatch stopped = new CountDownLatch(1); ICompletableFuture first = map.submitToKey(key, new EntryLatchAwaitingModifying(mayStart, stopped)); mayStart.countDown(); ICompletableFuture second = map.submitToKey(key, new EntryOtherStoppedVerifying(stopped)); while (!(first.isDone() && second.isDone())) { sleepAtLeastMillis(1); } // verifies that the other has stopped before the first one started assertEquals(0L, second.get()); } private static class EntryLatchAwaitingModifying implements EntryProcessor<String, SimpleValue>, Offloadable, EntryBackupProcessor<String, SimpleValue> { private final CountDownLatch mayStart; private final CountDownLatch stop; public EntryLatchAwaitingModifying(CountDownLatch mayStart, CountDownLatch stop) { this.mayStart = mayStart; this.stop = stop; } @Override public Object process(final Map.Entry<String, SimpleValue> entry) { try { mayStart.await(); } catch (InterruptedException e) { } try { entry.setValue(entry.getValue()); return null; } finally { stop.countDown(); } } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public void processBackup(Map.Entry<String, SimpleValue> entry) { process(entry); } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } } private static class EntryOtherStoppedVerifying implements EntryProcessor<String, SimpleValue>, Offloadable { private final CountDownLatch otherStopped; public EntryOtherStoppedVerifying(CountDownLatch otherStopped) { this.otherStopped = otherStopped; } @Override public Object process(final Map.Entry<String, SimpleValue> entry) { return otherStopped.getCount(); } @Override public EntryBackupProcessor getBackupProcessor() { return null; } @Override public String getExecutorName() { return Offloadable.OFFLOADABLE_EXECUTOR; } } private static class SimpleValue implements Serializable { public int i; SimpleValue() { } SimpleValue(final int i) { this.i = i; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } SimpleValue that = (SimpleValue) o; if (i != that.i) { return false; } return true; } @Override public String toString() { return "value: " + i; } } }