package org.infinispan.query.backend; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import javax.transaction.Transaction; import org.infinispan.AdvancedCache; import org.infinispan.Cache; import org.infinispan.commons.CacheException; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.Configuration; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.context.Flag; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.registry.InternalCacheRegistry; import org.infinispan.remoting.transport.jgroups.SuspectException; import org.infinispan.transaction.TransactionMode; import org.infinispan.util.KeyValuePair; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import net.jcip.annotations.ThreadSafe; // TODO [anistor] This class should be removed if we manage to remove autodetection of entity types. /** * Stores all entity classes known to query module in a replicated cache. The entry value is a boolean which indicates * if the type is indexable. The key is a KeyValuePair composed of the cache name and the class. This cache is 'append only'. * <p> * Write operations are expected to happen only exceptionally, therefore this code * is heavily optimized for reads (at cost of writes). * Also we're assuming all entries are small: there is no size limit nor cleanup strategy. * <p> * This is not caching the fact that some key is not defined: that would be tricky to * get right and is not needed for our use case. * * @author Sanne Grinovero (C) 2013 Red Hat Inc. * @author anistor@redhat.com */ @ThreadSafe public final class QueryKnownClasses { private static final Log log = LogFactory.getLog(QueryKnownClasses.class); public static final String QUERY_KNOWN_CLASSES_CACHE_NAME = "___query_known_classes"; private final Set<Class<?>> indexedEntities; private final String cacheName; private final EmbeddedCacheManager cacheManager; private final InternalCacheRegistry internalCacheRegistry; private volatile SearchFactoryHandler searchFactoryHandler; /** * A replicated cache that is lazily instantiated on first access. */ private volatile AdvancedCache<KeyValuePair<String, Class<?>>, Boolean> knownClassesCache; private volatile TransactionHelper transactionHelper; /** * A second level cache. Not using a ConcurrentHashMap as this will degenerate into a read-only Map at runtime; * in the Query specific case we're only adding new class types while they are being discovered, * after this initial phase this is supposed to be a read-only immutable map. */ private final AtomicReference<Map<Class<?>, Boolean>> localCache; /** * Constructor used only in pre-declared mode. */ QueryKnownClasses(Set<Class<?>> indexedEntities) { this.indexedEntities = Collections.unmodifiableSet(new HashSet<>(indexedEntities)); this.cacheName = null; this.cacheManager = null; this.internalCacheRegistry = null; this.localCache = null; } /** * Constructor used only in autodetect mode. */ @Deprecated QueryKnownClasses(String cacheName, EmbeddedCacheManager cacheManager, InternalCacheRegistry internalCacheRegistry) { this.indexedEntities = null; this.cacheName = cacheName; this.cacheManager = cacheManager; this.internalCacheRegistry = internalCacheRegistry; this.localCache = new AtomicReference<>(Collections.emptyMap()); } String getCacheName() { return cacheName; } boolean isAutodetectEnabled() { return indexedEntities == null; } void start(SearchFactoryHandler searchFactoryHandler) { if (indexedEntities != null) { throw new IllegalStateException("Cannot start internal cache unless we are in autodetect mode"); } if (searchFactoryHandler == null) { throw new IllegalArgumentException("null argument not allowed"); } this.searchFactoryHandler = searchFactoryHandler; startInternalCache(); knownClassesCache.addListener(searchFactoryHandler.getCacheListener(), key -> key.getKey().equals(cacheName)); } void stop() { if (knownClassesCache != null) { if (searchFactoryHandler != null) { knownClassesCache.removeListener(searchFactoryHandler.getCacheListener()); searchFactoryHandler = null; } knownClassesCache = null; } } Set<Class<?>> keys() { if (indexedEntities != null) { return indexedEntities; } startInternalCache(); Set<Class<?>> result = new HashSet<>(); Transaction tx = transactionHelper.suspendTxIfExists(); try { for (KeyValuePair<String, Class<?>> key : knownClassesCache.keySet()) { if (key.getKey().equals(cacheName)) { result.add(key.getValue()); } } return result; } finally { transactionHelper.resume(tx); } } boolean containsKey(final Class<?> clazz) { if (indexedEntities != null) { return indexedEntities.contains(clazz); } return localCache.get().containsKey(clazz); } Boolean get(final Class<?> clazz) { if (indexedEntities != null) { return indexedEntities.contains(clazz); } return localCache.get().get(clazz); } void put(final Class<?> clazz, final Boolean value) { if (indexedEntities != null) { throw new IllegalStateException("Autodetect mode is not enabled"); } if (value == null) { throw new IllegalArgumentException("Null values are not allowed"); } startInternalCache(); Transaction tx = transactionHelper.suspendTxIfExists(); try { runCommand(() -> knownClassesCache.put(new KeyValuePair<>(cacheName, clazz), value)); } finally { transactionHelper.resume(tx); } localCacheInsert(clazz, value); } private void localCacheInsert(final Class<?> key, final Boolean value) { synchronized (localCache) { final Map<Class<?>, Boolean> currentContent = localCache.get(); final int currentSize = currentContent.size(); if (currentSize == 0) { localCache.lazySet(Collections.singletonMap(key, value)); } else { Map<Class<?>, Boolean> updatedContent = new HashMap<>(currentSize + 1); updatedContent.putAll(currentContent); updatedContent.put(key, value); localCache.lazySet(Collections.unmodifiableMap(updatedContent)); } } } /** * Start the internal cache lazily. */ private void startInternalCache() { if (knownClassesCache == null) { synchronized (this) { if (knownClassesCache == null) { internalCacheRegistry.registerInternalCache(QUERY_KNOWN_CLASSES_CACHE_NAME, getInternalCacheConfig()); Cache<KeyValuePair<String, Class<?>>, Boolean> knownClassesCache = SecurityActions.getCache(cacheManager, QUERY_KNOWN_CLASSES_CACHE_NAME); this.knownClassesCache = knownClassesCache.getAdvancedCache().withFlags(Flag.IGNORE_RETURN_VALUES); transactionHelper = new TransactionHelper(this.knownClassesCache.getTransactionManager()); } } } } private Configuration getInternalCacheConfig() { ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); // allow the registry to work for local caches as well as clustered caches CacheMode cacheMode = cacheManager.getGlobalComponentRegistry().getGlobalConfiguration().isClustered() ? CacheMode.REPL_SYNC : CacheMode.LOCAL; configurationBuilder.clustering().cacheMode(cacheMode); // use invocation batching (cache-only transactions) for high consistency as writes are expected to be rare in this cache configurationBuilder.transaction().transactionMode(TransactionMode.TRANSACTIONAL) .transactionManagerLookup(null).invocationBatching().enable(); configurationBuilder.security().authorization().disable(); return configurationBuilder.build(); } /** * Run command and retry if suspect exception was thrown. */ private void runCommand(Runnable runnable) { while (true) { try { runnable.run(); break; } catch (CacheException e) { if (SuspectException.isSuspectExceptionInChain(e)) { // Retry the command log.trace("Ignoring suspect exception and retrying operation for internal cache."); } else { throw e; } } } } }