/* * 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.ringbuffer.impl; import com.hazelcast.config.Config; import com.hazelcast.config.InMemoryFormat; import com.hazelcast.config.RingbufferConfig; import com.hazelcast.config.RingbufferStoreConfig; import com.hazelcast.core.HazelcastException; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.core.RingbufferStore; import com.hazelcast.core.RingbufferStoreFactory; import com.hazelcast.nio.serialization.Data; import com.hazelcast.ringbuffer.OverflowPolicy; import com.hazelcast.ringbuffer.Ringbuffer; import com.hazelcast.test.HazelcastParallelClassRunner; import com.hazelcast.test.HazelcastTestSupport; import com.hazelcast.test.TestHazelcastInstanceFactory; import com.hazelcast.test.annotation.ParallelTest; import com.hazelcast.test.annotation.QuickTest; import com.hazelcast.util.ConcurrencyUtil; import com.hazelcast.util.ConstructorFunction; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; import static com.hazelcast.config.InMemoryFormat.BINARY; import static com.hazelcast.config.InMemoryFormat.OBJECT; import static com.hazelcast.config.RingbufferConfig.DEFAULT_CAPACITY; import static com.hazelcast.instance.TestUtil.terminateInstance; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @RunWith(HazelcastParallelClassRunner.class) @Category({QuickTest.class, ParallelTest.class}) public class RingbufferStoreTest extends HazelcastTestSupport { private static Config getConfig(String ringbufferName, int capacity, InMemoryFormat inMemoryFormat, RingbufferStoreConfig ringbufferStoreConfig) { final Config config = new Config(); final RingbufferConfig rbConfig = config .getRingbufferConfig(ringbufferName) .setInMemoryFormat(inMemoryFormat) .setCapacity(capacity); rbConfig.setRingbufferStoreConfig(ringbufferStoreConfig); return config; } @Test public void testRingbufferStore() throws Exception { final int numItems = 2000; final TestRingbufferStore<Integer> rbStore = new TestRingbufferStore<Integer>(2000, 0, 2000); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setEnabled(true) .setStoreImplementation(rbStore); final Config config = getConfig("testRingbufferStore", DEFAULT_CAPACITY, OBJECT, rbStoreConfig); final TestHazelcastInstanceFactory factory = createHazelcastInstanceFactory(2); // add items to the ring buffer and the store and shut down final HazelcastInstance instance = factory.newHazelcastInstance(config); final Ringbuffer<Object> ringbuffer = instance.getRingbuffer("testRingbufferStore"); for (int i = 0; i < numItems; i++) { ringbuffer.add(i); } instance.shutdown(); // now get a new ring buffer and read the items from the store final HazelcastInstance instance2 = factory.newHazelcastInstance(config); final Ringbuffer<Object> ringbuffer2 = instance2.getRingbuffer("testRingbufferStore"); // the actual ring buffer is empty but we can still load items from it assertEquals(0, ringbuffer2.size()); assertEquals(DEFAULT_CAPACITY, ringbuffer2.remainingCapacity()); assertEquals(numItems, rbStore.store.size()); for (int i = 0; i < numItems; i++) { assertEquals(i, ringbuffer2.readOne(i)); } rbStore.assertAwait(3); } @Test public void testRingbufferStoreAllAndReadFromMemory() throws Exception { final int numItems = 200; final WriteOnlyRingbufferStore<Integer> rbStore = new WriteOnlyRingbufferStore<Integer>(); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setEnabled(true) .setStoreImplementation(rbStore); final Config config = getConfig("testRingbufferStore", DEFAULT_CAPACITY, OBJECT, rbStoreConfig); final TestHazelcastInstanceFactory factory = createHazelcastInstanceFactory(2); final HazelcastInstance instance = factory.newHazelcastInstance(config); final HazelcastInstance instance2 = factory.newHazelcastInstance(config); warmUpPartitions(instance, instance2); // add items to both ring buffers (master and backup) and shut down the master final Ringbuffer<Object> ringbuffer = instance.getRingbuffer("testRingbufferStore"); final ArrayList<Integer> items = new ArrayList<Integer>(); for (int i = 0; i < numItems; i++) { items.add(i); } ringbuffer.addAllAsync(items, OverflowPolicy.OVERWRITE).get(); terminateInstance(instance); // now read items from the backup final Ringbuffer<Object> ringbuffer2 = instance2.getRingbuffer("testRingbufferStore"); assertEquals(numItems, ringbuffer2.size()); assertEquals(numItems, rbStore.store.size()); // assert that the backup has all items in memory, without loading from the store for (int i = 0; i < numItems; i++) { assertEquals(i, ringbuffer2.readOne(i)); } } @Test public void testRingbufferStoreMoreThanCapacity() throws Exception { final int capacity = 1000; final TestRingbufferStore<Integer> rbStore = new TestRingbufferStore<Integer>(capacity * 2, 0, 0); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setEnabled(true) .setStoreImplementation(rbStore); final Config config = getConfig("testRingbufferStore", capacity, OBJECT, rbStoreConfig); final HazelcastInstance instance = createHazelcastInstance(config); final Ringbuffer<Object> ringbuffer = instance.getRingbuffer("testRingbufferStore"); for (int i = 0; i < capacity * 2; i++) { ringbuffer.add(i); } assertEquals(capacity, ringbuffer.size()); assertEquals(capacity * 2, rbStore.store.size()); for (int i = 0; i < capacity * 2; i++) { assertEquals(i, ringbuffer.readOne(i)); } rbStore.assertAwait(3); } @Test public void testStoreId_whenNodeDown() throws InterruptedException { final IdCheckerRingbufferStore rbStore = new IdCheckerRingbufferStore(); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setEnabled(true) .setStoreImplementation(rbStore); final Config config = getConfig("default", DEFAULT_CAPACITY, OBJECT, rbStoreConfig); final TestHazelcastInstanceFactory factory = createHazelcastInstanceFactory(2); final HazelcastInstance instance1 = factory.newHazelcastInstance(config); final HazelcastInstance instance2 = factory.newHazelcastInstance(config); final String name = generateKeyOwnedBy(instance1); final Ringbuffer<Object> ringbuffer = instance2.getRingbuffer(name); final HashMap<Long, String> addedItems = new HashMap<Long, String>(); for (int i = 0; i < 3; i++) { final String item = randomString(); addedItems.put(ringbuffer.add(item), item); } instance1.shutdown(); final String item = randomString(); addedItems.put(ringbuffer.add(item), item); for (Entry<Long, String> e : addedItems.entrySet()) { assertEquals("The ring buffer returned a different object than the one which was stored", e.getValue(), ringbuffer.readOne(e.getKey())); } } @Test public void testStoreId_writeToMasterAndReadFromBackup() throws InterruptedException { final IdCheckerRingbufferStore rbStore = new IdCheckerRingbufferStore(); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setEnabled(true) .setStoreImplementation(rbStore); final Config config = getConfig("default", DEFAULT_CAPACITY, OBJECT, rbStoreConfig); final TestHazelcastInstanceFactory factory = createHazelcastInstanceFactory(3); final HazelcastInstance instance1 = factory.newHazelcastInstance(config); final HazelcastInstance instance2 = factory.newHazelcastInstance(config); warmUpPartitions(instance1, instance2); final String name = generateKeyOwnedBy(instance1); final Ringbuffer<Object> masterRB = instance1.getRingbuffer(name); final HashMap<Long, Integer> addedItems = new HashMap<Long, Integer>(); for (int i = 0; i < 100; i++) { addedItems.put(masterRB.add(i), i); } terminateInstance(instance1); final Ringbuffer<Object> backupRB = instance2.getRingbuffer(name); for (Entry<Long, Integer> e : addedItems.entrySet()) { assertEquals("The ring buffer returned a different object than the one which was stored", e.getValue(), backupRB.readOne(e.getKey())); } } @Test public void testRingbufferStoreFactory() { final String ringbufferName = randomString(); final SimpleRingbufferStoreFactory rbStoreFactory = new SimpleRingbufferStoreFactory(); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setEnabled(true) .setFactoryImplementation(rbStoreFactory); final Config config = getConfig(ringbufferName, DEFAULT_CAPACITY, OBJECT, rbStoreConfig); final HazelcastInstance instance = createHazelcastInstance(config); final Ringbuffer<Object> ringbuffer = instance.getRingbuffer(ringbufferName); ringbuffer.add(1); assertEquals(1, rbStoreFactory.stores.size()); final TestRingbufferStore ringbufferStore = (TestRingbufferStore) rbStoreFactory.stores.get(ringbufferName); int size = ringbufferStore.store.size(); assertEquals("Ring buffer store size should be 1 but found " + size, 1, size); } @Test public void testRingbufferStoreFactoryIsNotInitialized_whenDisabledInRingbufferStoreConfig() { final String ringbufferName = randomString(); final SimpleRingbufferStoreFactory rbStoreFactory = new SimpleRingbufferStoreFactory(); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setEnabled(false) .setFactoryImplementation(rbStoreFactory); final Config config = getConfig(ringbufferName, DEFAULT_CAPACITY, OBJECT, rbStoreConfig); final HazelcastInstance instance = createHazelcastInstance(config); final Ringbuffer<Object> ringbuffer = instance.getRingbuffer(ringbufferName); ringbuffer.add(1); assertEquals("Expected that the ring buffer store would not be initialized" + " since we disabled it in the ring buffer store config but found initialized ", 0, rbStoreFactory.stores.size()); } @Test public void testRingbufferStore_withBinaryModeOn() throws InterruptedException { final String ringbufferName = randomString(); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setStoreImplementation(new TestRingbufferStore<Data>()) .setEnabled(true); final Config config = getConfig(ringbufferName, DEFAULT_CAPACITY, BINARY, rbStoreConfig); final HazelcastInstance node = createHazelcastInstance(config); final Ringbuffer<Object> ringbuffer = node.getRingbuffer(ringbufferName); ringbuffer.add(1); ringbuffer.add(2); final long lastSequence = ringbuffer.add(3); assertEquals(3, ringbuffer.readOne(lastSequence)); } @Test(expected = HazelcastException.class) public void testRingbufferStore_addThrowsException() throws InterruptedException { final String ringbufferName = randomString(); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setStoreImplementation(new ExceptionThrowingRingbufferStore()) .setEnabled(true); final Config config = getConfig(ringbufferName, DEFAULT_CAPACITY, OBJECT, rbStoreConfig); final HazelcastInstance node = createHazelcastInstance(config); final Ringbuffer<Object> ringbuffer = node.getRingbuffer(ringbufferName); ringbuffer.add(1); } @Test(expected = ExecutionException.class) public void testRingbufferStore_addAllThrowsException() throws Exception { final String ringbufferName = randomString(); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setStoreImplementation(new ExceptionThrowingRingbufferStore()) .setEnabled(true); final Config config = getConfig(ringbufferName, DEFAULT_CAPACITY, OBJECT, rbStoreConfig); final HazelcastInstance node = createHazelcastInstance(config); final Ringbuffer<Object> ringbuffer = node.getRingbuffer(ringbufferName); ringbuffer.addAllAsync(Arrays.asList(1, 2), OverflowPolicy.OVERWRITE).get(); } @Test(expected = HazelcastException.class) public void testRingbufferStore_getLargestSequenceThrowsException() throws Exception { final String ringbufferName = randomString(); final RingbufferStoreConfig rbStoreConfig = new RingbufferStoreConfig() .setStoreImplementation(new ExceptionThrowingRingbufferStore(true)) .setEnabled(true); final Config config = getConfig(ringbufferName, DEFAULT_CAPACITY, OBJECT, rbStoreConfig); final HazelcastInstance node = createHazelcastInstance(config); node.getRingbuffer(ringbufferName).size(); } static class SimpleRingbufferStoreFactory implements RingbufferStoreFactory<Integer> { private final ConcurrentMap<String, RingbufferStore> stores = new ConcurrentHashMap<String, RingbufferStore>(); @Override @SuppressWarnings("unchecked") public RingbufferStore<Integer> newRingbufferStore(String name, Properties properties) { return ConcurrencyUtil.getOrPutIfAbsent(stores, name, new ConstructorFunction<String, RingbufferStore>() { @Override public RingbufferStore<Integer> createNew(String arg) { return new TestRingbufferStore<Integer>(); } }); } } static class IdCheckerRingbufferStore<T> implements RingbufferStore<T> { long lastKey = -1; final Map<Long, T> store = new LinkedHashMap<Long, T>(); @Override public void store(final long sequence, final T value) { if (lastKey >= sequence) { throw new RuntimeException("key[" + sequence + "] is already stored"); } lastKey = sequence; store.put(sequence, value); } @Override public void storeAll(long firstItemSequence, T[] items) { throw new UnsupportedOperationException(); } @Override public T load(final long sequence) { return store.get(sequence); } @Override public long getLargestSequence() { return lastKey; } } static class ExceptionThrowingRingbufferStore<T> implements RingbufferStore<T> { private final boolean getLargestSequenceThrowsException; public ExceptionThrowingRingbufferStore() { this(false); } ExceptionThrowingRingbufferStore(boolean getLargestSequenceThrowsException) { this.getLargestSequenceThrowsException = getLargestSequenceThrowsException; } @Override public void store(long sequence, T data) { throw new RuntimeException(); } @Override public void storeAll(long firstItemSequence, T[] items) { throw new RuntimeException(); } @Override public T load(long sequence) { throw new RuntimeException(); } @Override public long getLargestSequence() { if (getLargestSequenceThrowsException) { throw new RuntimeException(); } return -1; } } static class TestRingbufferStore<T> implements RingbufferStore<T> { final Map<Long, T> store = new LinkedHashMap<Long, T>(); final AtomicInteger callCount = new AtomicInteger(); final AtomicInteger destroyCount = new AtomicInteger(); final CountDownLatch latchStore; final CountDownLatch latchStoreAll; final CountDownLatch latchLoad; public TestRingbufferStore() { this(0, 0, 0); } TestRingbufferStore(int expectedStore, int expectedStoreAll, int expectedLoad) { latchStore = new CountDownLatch(expectedStore); latchStoreAll = new CountDownLatch(expectedStoreAll); latchLoad = new CountDownLatch(expectedLoad); } public void destroy() { destroyCount.incrementAndGet(); } void assertAwait(int seconds) throws Exception { assertTrue("Store remaining: " + latchStore.getCount(), latchStore.await(seconds, SECONDS)); assertTrue("Store-all remaining: " + latchStoreAll.getCount(), latchStoreAll.await(seconds, SECONDS)); assertTrue("Load remaining: " + latchLoad.getCount(), latchLoad.await(seconds, SECONDS)); } Map<Long, T> getStore() { return store; } @Override public void store(long sequence, T data) { store.put(sequence, data); callCount.incrementAndGet(); latchStore.countDown(); } @Override public void storeAll(long firstItemSequence, T[] items) { for (int i = 0; i < items.length; i++) { store.put(firstItemSequence + i, items[i]); } callCount.incrementAndGet(); latchStoreAll.countDown(); } @Override public T load(long sequence) { callCount.incrementAndGet(); latchLoad.countDown(); return store.get(sequence); } @Override public long getLargestSequence() { final Set<Long> coll = store.keySet(); return coll.isEmpty() ? -1 : Collections.max(coll); } } static class WriteOnlyRingbufferStore<T> implements RingbufferStore<T> { final Map<Long, T> store = new LinkedHashMap<Long, T>(); public WriteOnlyRingbufferStore() { } Map<Long, T> getStore() { return store; } @Override public void store(long sequence, T data) { store.put(sequence, data); } @Override public void storeAll(long firstItemSequence, T[] items) { for (int i = 0; i < items.length; i++) { store.put(firstItemSequence + i, items[i]); } } @Override public T load(long sequence) { throw new UnsupportedOperationException(); } @Override public long getLargestSequence() { final Set<Long> coll = store.keySet(); return coll.isEmpty() ? -1 : Collections.max(coll); } } }