/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.ignite.internal.processors.cache.query.continuous; import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Random; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import javax.cache.configuration.Factory; import javax.cache.configuration.FactoryBuilder; import javax.cache.configuration.MutableCacheEntryListenerConfiguration; import javax.cache.event.CacheEntryCreatedListener; import javax.cache.event.CacheEntryEvent; import javax.cache.event.CacheEntryExpiredListener; import javax.cache.event.CacheEntryListener; import javax.cache.event.CacheEntryListenerException; import javax.cache.event.CacheEntryRemovedListener; import javax.cache.event.CacheEntryUpdatedListener; import javax.cache.event.EventType; import javax.cache.processor.EntryProcessor; import javax.cache.processor.MutableEntry; import org.apache.ignite.Ignite; import org.apache.ignite.IgniteCache; import org.apache.ignite.binary.BinaryObject; import org.apache.ignite.cache.CacheEntryEventSerializableFilter; import org.apache.ignite.cache.query.ContinuousQuery; import org.apache.ignite.cache.query.QueryCursor; import org.apache.ignite.configuration.CacheConfiguration; import org.apache.ignite.configuration.IgniteConfiguration; import org.apache.ignite.internal.binary.BinaryMarshaller; import org.apache.ignite.internal.util.typedef.CI1; import org.apache.ignite.internal.util.typedef.G; import org.apache.ignite.internal.util.typedef.PA; import org.apache.ignite.internal.util.typedef.internal.S; import org.apache.ignite.lang.IgniteAsyncCallback; import org.apache.ignite.lang.IgniteInClosure; import org.apache.ignite.testframework.GridTestUtils; import org.apache.ignite.testframework.junits.IgniteCacheConfigVariationsAbstractTest; import org.apache.ignite.transactions.Transaction; import org.apache.ignite.transactions.TransactionConcurrency; import org.apache.ignite.transactions.TransactionIsolation; import org.jetbrains.annotations.Nullable; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static javax.cache.event.EventType.CREATED; import static javax.cache.event.EventType.REMOVED; import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL; import static org.apache.ignite.internal.processors.cache.query.continuous.CacheContinuousQueryVariationsTest.SerializableFilter.isAccepted; import static org.apache.ignite.testframework.junits.IgniteConfigVariationsAbstractTest.DataMode.EXTERNALIZABLE; import static org.apache.ignite.transactions.TransactionIsolation.READ_COMMITTED; import static org.apache.ignite.transactions.TransactionIsolation.REPEATABLE_READ; import static org.apache.ignite.transactions.TransactionIsolation.SERIALIZABLE; /** * */ public class CacheContinuousQueryVariationsTest extends IgniteCacheConfigVariationsAbstractTest { /** */ private static final int ITERATION_CNT = 20; /** */ private static final int KEYS = 50; /** */ private static final int VALS = 10; /** */ public static boolean singleNode = false; /** {@inheritDoc} */ @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); cfg.setClientMode(igniteInstanceName.endsWith("0") && !singleNode); return cfg; } /** * @throws Exception If failed. */ public void testRandomOperationJCacheApiKeepBinary() throws Exception { testRandomOperation(true, false, false, false, true); } /** * @throws Exception If failed. */ public void testRandomOperationJCacheApiAsyncCallback() throws Exception { testRandomOperation(true, false, false, true, false); } /** * @throws Exception If failed. */ public void testRandomOperationJCacheApiWithFilter() throws Exception { testRandomOperation(true, false, true, false, false); } /** * @throws Exception If failed. */ public void testRandomOperationJCacheApiWithFilterAsyncCallback() throws Exception { testRandomOperation(true, false, true, true, false); } /** * @throws Exception If failed. */ public void testRandomOperationJCacheApiSyncWithFilter() throws Exception { testRandomOperation(true, true, true, false, false); } /** * @throws Exception If failed. */ public void testRandomOperation() throws Exception { testRandomOperation(true, true, false, false, false); } /** * @throws Exception If failed. */ public void testRandomOperationWithKeepBinary() throws Exception { testRandomOperation(true, true, false, false, true); } /** * @throws Exception If failed. */ public void testRandomOperationWithAsyncCallback() throws Exception { testRandomOperation(true, true, false, true, false); } /** * @throws Exception If failed. */ public void testRandomOperationWithFilter() throws Exception { testRandomOperation(true, true, true, false, false); } /** * @throws Exception If failed. */ public void testRandomOperationWithFilterWithKeepBinary() throws Exception { testRandomOperation(true, true, true, false, true); } /** * @throws Exception If failed. */ public void testRandomOperationWithFilterAsyncCallback() throws Exception { testRandomOperation(true, true, true, true, false); } /** * @param jcacheApi Use JCache API. * @param syncNtf Use sync notification. * @param withFilter Use filter. * @param asyncCallback Filter is annotated IgniteAsyncCallback * @param keepBinary Keep binary. * @throws Exception If failed. */ private void testRandomOperation(final boolean jcacheApi, final boolean syncNtf, final boolean withFilter, final boolean asyncCallback, final boolean keepBinary) throws Exception { if (keepBinary && !(getConfiguration().getMarshaller() == null || getConfiguration().getMarshaller().getClass() == BinaryMarshaller.class)) return; runInAllDataModes(new TestRunnable() { @Override public void run() throws Exception { long seed = System.currentTimeMillis(); Random rnd = new Random(seed); log.info("Random seed: " + seed); // Register listener on all nodes. List<BlockingQueue<CacheEntryEvent<?, ?>>> evtsQueues = new ArrayList<>(); Collection<QueryCursor<?>> curs = new ArrayList<>(); Collection<MutableCacheEntryListenerConfiguration> lsnrCfgs = new ArrayList<>(); for (int idx = 0; idx < G.allGrids().size(); idx++) { final BlockingQueue<CacheEntryEvent<?, ?>> evtsQueue = new ArrayBlockingQueue<>(50_000); CI1<Iterable<CacheEntryEvent<?, ?>>> clsr = new CI1<Iterable<CacheEntryEvent<?, ?>>>() { @Override public void apply(Iterable<CacheEntryEvent<?, ?>> evts) { for (CacheEntryEvent<?, ?> evt : evts) evtsQueue.add(evt); } }; final CacheEntryUpdatedListener<Object, Object> lsnr = asyncCallback ? new AsyncLocalNonSerializableListener(clsr): new LocalNonSerializableListener(clsr); IgniteCache<Object, Object> jcache = keepBinary ? jcache(idx).withKeepBinary() : jcache(idx); if (jcacheApi) { MutableCacheEntryListenerConfiguration<Object, Object> lsnrCfg = new MutableCacheEntryListenerConfiguration<>( new Factory<CacheEntryListener<? super Object, ? super Object>>() { @Override public CacheEntryListener<? super Object, ? super Object> create() { return lsnr; } }, withFilter ? FactoryBuilder.factoryOf( asyncCallback ? new AsyncSerializableFilter(keepBinary, dataMode) : new SerializableFilter(keepBinary, dataMode)) : null, true, syncNtf ); jcache.registerCacheEntryListener(lsnrCfg); lsnrCfgs.add(lsnrCfg); evtsQueues.add(evtsQueue); } else { ContinuousQuery<Object, Object> qry = new ContinuousQuery<>(); qry.setLocalListener(lsnr); qry.setRemoteFilterFactory(withFilter ? FactoryBuilder.factoryOf( asyncCallback ? new AsyncSerializableFilter(keepBinary, dataMode) : new SerializableFilter(keepBinary, dataMode)) : null); curs.add(jcache.query(qry)); evtsQueues.add(evtsQueue); } } ConcurrentMap<Object, Object> expData = new ConcurrentHashMap<>(); try { for (int i = 0; i < ITERATION_CNT; i++) { if (i % 5 == 0) log.info("Iteration: " + i); for (int idx = 0; idx < G.allGrids().size(); idx++) randomUpdate(rnd, evtsQueues, expData, keepBinary ? jcache(idx).withKeepBinary() : jcache(idx), keepBinary, withFilter); } } catch (Exception e) { log.error("Got unexpected error: ", e); throw e; } finally { for (QueryCursor<?> cur : curs) cur.close(); for (int i = 0; i < G.allGrids().size(); i++) { for (MutableCacheEntryListenerConfiguration cfg : lsnrCfgs) jcache(i).deregisterCacheEntryListener(cfg); } } } }); } /** * @param rnd Random generator. * @param evtsQueues Events queue. * @param expData Expected cache data. * @param cache Cache. * @throws Exception If failed. */ private void randomUpdate( Random rnd, List<BlockingQueue<CacheEntryEvent<?, ?>>> evtsQueues, ConcurrentMap<Object, Object> expData, IgniteCache<Object, Object> cache, boolean keepBinary, boolean withFilter ) throws Exception { Object key = key(rnd.nextInt(KEYS)); Object newVal = value(rnd.nextInt()); Object oldVal = expData.get(key); int op = rnd.nextInt(11); Ignite ignite = cache.unwrap(Ignite.class); Transaction tx = null; if (cache.getConfiguration(CacheConfiguration.class).getAtomicityMode() == TRANSACTIONAL && rnd.nextBoolean()) tx = ignite.transactions().txStart(txRandomConcurrency(rnd), txRandomIsolation(rnd)); try { // log.info("Random operation [key=" + key + ", op=" + op + ']'); switch (op) { case 0: { cache.put(key, newVal); if (tx != null) tx.commit(); waitAndCheckEvent(evtsQueues, key, newVal, oldVal, keepBinary, withFilter); expData.put(key, newVal); break; } case 1: { cache.getAndPut(key, newVal); if (tx != null) tx.commit(); waitAndCheckEvent(evtsQueues, key, newVal, oldVal, keepBinary, withFilter); expData.put(key, newVal); break; } case 2: { cache.remove(key); if (tx != null) tx.commit(); waitAndCheckEvent(evtsQueues, key, null, oldVal, keepBinary, withFilter); expData.remove(key); break; } case 3: { cache.getAndRemove(key); if (tx != null) tx.commit(); waitAndCheckEvent(evtsQueues, key, null, oldVal, keepBinary, withFilter); expData.remove(key); break; } case 4: { cache.invoke(key, new EntrySetValueProcessor(newVal, rnd.nextBoolean())); if (tx != null) tx.commit(); waitAndCheckEvent(evtsQueues, key, newVal, oldVal, keepBinary, withFilter); expData.put(key, newVal); break; } case 5: { cache.invoke(key, new EntrySetValueProcessor(null, rnd.nextBoolean())); if (tx != null) tx.commit(); waitAndCheckEvent(evtsQueues, key, null, oldVal, keepBinary, withFilter); expData.remove(key); break; } case 6: { cache.putIfAbsent(key, newVal); if (tx != null) tx.commit(); if (oldVal == null) { waitAndCheckEvent(evtsQueues, key, newVal, null, keepBinary, withFilter); expData.put(key, newVal); } else checkNoEvent(evtsQueues); break; } case 7: { cache.getAndPutIfAbsent(key, newVal); if (tx != null) tx.commit(); if (oldVal == null) { waitAndCheckEvent(evtsQueues, key, newVal, null, keepBinary, withFilter); expData.put(key, newVal); } else checkNoEvent(evtsQueues); break; } case 8: { cache.replace(key, newVal); if (tx != null) tx.commit(); if (oldVal != null) { waitAndCheckEvent(evtsQueues, key, newVal, oldVal, keepBinary, withFilter); expData.put(key, newVal); } else checkNoEvent(evtsQueues); break; } case 9: { cache.getAndReplace(key, newVal); if (tx != null) tx.commit(); if (oldVal != null) { waitAndCheckEvent(evtsQueues, key, newVal, oldVal, keepBinary, withFilter); expData.put(key, newVal); } else checkNoEvent(evtsQueues); break; } case 10: { if (oldVal != null) { Object replaceVal = value(rnd.nextInt(VALS)); boolean success = replaceVal.equals(oldVal); if (success) { cache.replace(key, replaceVal, newVal); if (tx != null) tx.commit(); waitAndCheckEvent(evtsQueues, key, newVal, oldVal, keepBinary, withFilter); expData.put(key, newVal); } else { cache.replace(key, replaceVal, newVal); if (tx != null) tx.commit(); checkNoEvent(evtsQueues); } } else { cache.replace(key, value(rnd.nextInt(VALS)), newVal); if (tx != null) tx.commit(); checkNoEvent(evtsQueues); } break; } default: fail("Op:" + op); } } finally { if (tx != null) tx.close(); } } /** {@inheritDoc} */ @Override protected long getTestTimeout() { return TimeUnit.MINUTES.toMillis(5); } /** * @param rnd {@link Random}. * @return {@link TransactionConcurrency}. */ private TransactionConcurrency txRandomConcurrency(Random rnd) { return rnd.nextBoolean() ? TransactionConcurrency.OPTIMISTIC : TransactionConcurrency.PESSIMISTIC; } /** * @param rnd {@link Random}. * @return {@link TransactionIsolation}. */ private TransactionIsolation txRandomIsolation(Random rnd) { int val = rnd.nextInt(3); if (val == 0) return READ_COMMITTED; else if (val == 1) return REPEATABLE_READ; else return SERIALIZABLE; } /** * @param evtsQueues Event queue. * @param key Key. * @param val Value. * @param oldVal Old value. * @param keepBinary Keep binary. * @param withFilter With filter. * @throws Exception If failed. */ private void waitAndCheckEvent(List<BlockingQueue<CacheEntryEvent<?, ?>>> evtsQueues, Object key, Object val, Object oldVal, boolean keepBinary, boolean withFilter) throws Exception { if (val == null && oldVal == null || (withFilter && val != null && !isAccepted(val, false, dataMode))) { checkNoEvent(evtsQueues); return; } for (BlockingQueue<CacheEntryEvent<?, ?>> evtsQueue : evtsQueues) { CacheEntryEvent<?, ?> evt = evtsQueue.poll(5, SECONDS); assertNotNull("Failed to wait for event [key=" + key + ", val=" + val + ", oldVal=" + oldVal + ']', evt); Object actKey = evt.getKey(); Object actVal = evt.getValue(); Object actOldVal = evt.getOldValue(); if (keepBinary) { actKey = checkAndGetObject(actKey); actVal = checkAndGetObject(actVal); actOldVal = checkAndGetObject(actOldVal); } assertEquals(key, actKey); assertEquals(val, actVal); assertEquals(oldVal, actOldVal); } } /** * @param obj Binary object. * @return Deserialize value. */ private Object checkAndGetObject(@Nullable Object obj) { if (obj != null) { assert obj instanceof BinaryObject || dataMode == EXTERNALIZABLE: obj; if (obj instanceof BinaryObject) obj = ((BinaryObject)obj).deserialize(); } return obj; } /** * @param evtsQueues Event queue. * @throws Exception If failed. */ private void checkNoEvent(List<BlockingQueue<CacheEntryEvent<?, ?>>> evtsQueues) throws Exception { for (BlockingQueue<CacheEntryEvent<?, ?>> evtsQueue : evtsQueues) { CacheEntryEvent<?, ?> evt = evtsQueue.poll(10, MILLISECONDS); assertNull(evt); } } /** * @throws Exception If failed. */ public void testRemoveRemoveScenario() throws Exception { runInAllDataModes(new TestRunnable() { @Override public void run() throws Exception { IgniteCache<Object, Object> cache = jcache(); ContinuousQuery<Object, Object> qry = new ContinuousQuery<>(); final List<CacheEntryEvent<?, ?>> evts = new CopyOnWriteArrayList<>(); qry.setLocalListener(new CacheEntryUpdatedListener<Object, Object>() { @Override public void onUpdated(Iterable<CacheEntryEvent<?, ?>> events) throws CacheEntryListenerException { for (CacheEntryEvent<?, ?> e : events) evts.add(e); } }); Object key = key(1); try (QueryCursor qryCur = cache.query(qry)) { for (int i = 0; i < ITERATION_CNT; i++) { log.info("Start iteration: " + i); // Not events. cache.invoke(key, new EntrySetValueProcessor(true)); // Get events. cache.put(key, value(1)); cache.remove(key); // Not events. cache.invoke(key, new EntrySetValueProcessor(null, false)); cache.invoke(key, new EntrySetValueProcessor(null, false)); cache.invoke(key, new EntrySetValueProcessor(true)); cache.remove(key); // Get events. cache.put(key, value(2)); // Not events. cache.invoke(key, new EntrySetValueProcessor(true)); // Get events. cache.invoke(key, new EntrySetValueProcessor(null, false)); // Not events. cache.remove(key); // Get events. cache.put(key, value(3)); cache.put(key, value(4)); // Not events. cache.invoke(key, new EntrySetValueProcessor(true)); cache.putIfAbsent(key, value(5)); cache.putIfAbsent(key, value(5)); cache.putIfAbsent(key, value(5)); cache.invoke(key, new EntrySetValueProcessor(true)); cache.remove(key, value(5)); // Get events. cache.remove(key, value(4)); cache.putIfAbsent(key, value(5)); // Not events. cache.replace(key, value(3), value(2)); cache.replace(key, value(3), value(2)); cache.replace(key, value(3), value(2)); // Get events. cache.replace(key, value(5), value(6)); assert GridTestUtils.waitForCondition(new PA() { @Override public boolean apply() { return evts.size() == 9; } }, 5_000); checkEvent(evts.get(0), CREATED, value(1), null); checkEvent(evts.get(1), REMOVED, null, value(1)); checkEvent(evts.get(2), CREATED, value(2), null); checkEvent(evts.get(3), REMOVED, null, value(2)); checkEvent(evts.get(4), CREATED, value(3), null); checkEvent(evts.get(5), EventType.UPDATED, value(4), value(3)); checkEvent(evts.get(6), REMOVED, null, value(4)); checkEvent(evts.get(7), CREATED, value(5), null); checkEvent(evts.get(8), EventType.UPDATED, value(6), value(5)); cache.remove(key); cache.remove(key); //Wait when remove event will be added to evts while (evts.size() != 10) { Thread.sleep(100); } evts.clear(); log.info("Finish iteration: " + i); } } } }); } /** * @param event Event. * @param type Event type. * @param val Value. * @param oldVal Old value. */ private void checkEvent(CacheEntryEvent<?, ?> event, EventType type, Object val, Object oldVal) { assertEquals(event.getEventType(), type); assertEquals(event.getValue(), val); assertEquals(event.getOldValue(), oldVal); } /** * */ protected static class EntrySetValueProcessor implements EntryProcessor<Object, Object, Object> { /** */ private Object val; /** */ private boolean retOld; /** */ private boolean skipModify; /** * @param skipModify If {@code true} then entry will not be modified. */ public EntrySetValueProcessor(boolean skipModify) { this.skipModify = skipModify; } /** * @param val Value to set. * @param retOld Return old value flag. */ public EntrySetValueProcessor(Object val, boolean retOld) { this.val = val; this.retOld = retOld; } /** {@inheritDoc} */ @Override public Object process(MutableEntry<Object, Object> e, Object... args) { if (skipModify) return null; Object old = retOld ? e.getValue() : null; if (val != null) e.setValue(val); else e.remove(); return old; } /** {@inheritDoc} */ @Override public String toString() { return S.toString(EntrySetValueProcessor.class, this); } } /** * */ @IgniteAsyncCallback public static class AsyncSerializableFilter extends SerializableFilter { /** * */ public AsyncSerializableFilter() { // No-op. } /** * @param keepBinary Keep binary. * @param dataMode Data mode. */ public AsyncSerializableFilter(boolean keepBinary, DataMode dataMode) { super(keepBinary, dataMode); } } /** * */ public static class SerializableFilter implements CacheEntryEventSerializableFilter<Object, Object> { /** */ private boolean keepBinary; /** */ private DataMode dataMode; /** */ public SerializableFilter() { // No-op. } /** * @param keepBinary Keep binary. * @param dataMode Data mode. */ public SerializableFilter(boolean keepBinary, DataMode dataMode) { this.keepBinary = keepBinary; this.dataMode = dataMode; } /** {@inheritDoc} */ @Override public boolean evaluate(CacheEntryEvent<?, ?> event) throws CacheEntryListenerException { return isAccepted(event.getValue(), keepBinary, dataMode); } /** * @param val Value. * @param keepBinary Keep binary. * @param dataMode Data mode. * @return {@code True} if value is even. */ public static boolean isAccepted(Object val, boolean keepBinary, DataMode dataMode) { if (val != null) { int val0 = 0; if (val instanceof TestObject) { assert !keepBinary || dataMode == EXTERNALIZABLE : val; val0 = valueOf(val); } else if (val instanceof BinaryObject) { assert keepBinary : val; val0 = ((BinaryObject)val).field("val"); } else fail("Unexpected object: " + val); return val0 % 2 == 0; } return true; } } /** * */ @IgniteAsyncCallback public static class AsyncLocalNonSerializableListener extends LocalNonSerializableListener { /** * @param clsr Closure. */ AsyncLocalNonSerializableListener(IgniteInClosure<Iterable<CacheEntryEvent<?, ?>>> clsr) { super(clsr); } /** * */ public AsyncLocalNonSerializableListener() { // No-op. } } /** * */ public static class LocalNonSerializableListener implements CacheEntryUpdatedListener<Object, Object>, CacheEntryCreatedListener<Object, Object>, CacheEntryExpiredListener<Object, Object>, CacheEntryRemovedListener<Object, Object>, Externalizable { /** */ IgniteInClosure<Iterable<CacheEntryEvent<?, ?>>> clsr; /** * @param clsr Closure. */ LocalNonSerializableListener(IgniteInClosure<Iterable<CacheEntryEvent<?, ?>>> clsr) { this.clsr = clsr; } /** */ public LocalNonSerializableListener() { // No-op. } /** {@inheritDoc} */ @Override public void onCreated(Iterable<CacheEntryEvent<?, ?>> evts) throws CacheEntryListenerException { onEvents(evts); } /** {@inheritDoc} */ @Override public void onExpired(Iterable<CacheEntryEvent<?, ?>> evts) throws CacheEntryListenerException { onEvents(evts); } /** {@inheritDoc} */ @Override public void onRemoved(Iterable<CacheEntryEvent<?, ?>> evts) throws CacheEntryListenerException { onEvents(evts); } /** {@inheritDoc} */ @Override public void onUpdated(Iterable<CacheEntryEvent<?, ?>> evts) throws CacheEntryListenerException { onEvents(evts); } /** * @param evts Events. */ private void onEvents(Iterable<CacheEntryEvent<?, ?>> evts) { clsr.apply(evts); } /** {@inheritDoc} */ @Override public void writeExternal(ObjectOutput out) throws IOException { throw new UnsupportedOperationException("Failed. Listener should not be marshaled."); } /** {@inheritDoc} */ @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { throw new UnsupportedOperationException("Failed. Listener should not be unmarshaled."); } } }