/* * JBoss, Home of Professional Open Source. * Copyright 2014, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.domain.management.security; import static org.jboss.as.domain.management.logging.DomainManagementLogger.SECURITY_LOGGER; import java.io.IOException; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import javax.naming.NamingException; import org.jboss.as.domain.management.security.LdapSearcherCache.AttachmentKey; import org.jboss.as.domain.management.security.LdapSearcherCache.SearchResult; import org.jboss.msc.service.Service; import org.jboss.msc.service.StartContext; import org.jboss.msc.service.StartException; import org.jboss.msc.service.StopContext; /** * The {@link Service} that handles caching results of LDAP searches. * * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> */ class LdapCacheService<R, K> implements Service<LdapSearcherCache<R, K>> { private static volatile int THREAD_COUNT = 1; private final LdapSearcher<R, K> searcher; private volatile CacheMode mode; private volatile int evictionTime; private volatile boolean cacheFailures; private volatile int maxCacheSize; /* * Controlled by the service lifecycle. */ private volatile ExtendedLdapSearcherCache<R, K> cacheImplementation; private ScheduledExecutorService executorService; private LdapCacheService(final LdapSearcher<R, K> searcher, final CacheMode mode, final int evictionTime, final boolean cacheFailures, final int maxCacheSize) { this.searcher = searcher; this.mode = mode; this.evictionTime = evictionTime; this.cacheFailures = cacheFailures; this.maxCacheSize = maxCacheSize; } /* * Factory Methods */ static <R, K> LdapCacheService<R, K> createNoCacheService(final LdapSearcher<R, K> searcher) { return new LdapCacheService<R, K>(searcher, CacheMode.OFF, 0, false, 0); } static <R, K> LdapCacheService<R, K> createBySearchCacheService(final LdapSearcher<R, K> searcher, final int evictionTime, final boolean cacheFailure, final int maxSize) { return new LdapCacheService<R, K>(searcher, CacheMode.BY_SEARCH, evictionTime, cacheFailure, maxSize); } static <R, K> LdapCacheService<R, K> createByAccessCacheService(final LdapSearcher<R, K> searcher, final int evictionTime, final boolean cacheFailure, final int maxSize) { return new LdapCacheService<R, K>(searcher, CacheMode.BY_ACCESS, evictionTime, cacheFailure, maxSize); } /* * MSC Service and Lifecycle Methods */ @Override public LdapSearcherCache<R, K> getValue() throws IllegalStateException, IllegalArgumentException { return cacheImplementation; } @Override public void start(final StartContext context) throws StartException { switch (mode) { case OFF: cacheImplementation = new NoCacheCache(); break; case BY_ACCESS: cacheImplementation = new ByAccessCache(evictionTime, cacheFailures, maxCacheSize); break; case BY_SEARCH: cacheImplementation = new BySearchCache(evictionTime, cacheFailures, maxCacheSize); break; default: // Should not actually hit this. throw new IllegalStateException(String.format("Unknown cache mode '%s'", mode)); } /* * This is only used to trigger evictions, if one eviction is stuck waiting for the lock on the table there is no point * having many threads concurrently waiting on the same lock. */ if (evictionTime > 0) { executorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, String.format("LDAP Cache Eviction Thread (%d)", THREAD_COUNT++)); } }); } } @Override public void stop(final StopContext context) { try { context.execute(new Runnable() { @Override public void run() { try { cacheImplementation.clearAll(); cacheImplementation = null; if (executorService != null) { // FIXME context.execute() should not be used for blocking tasks. Inject a scheduled executor // and get rid of this executorService.shutdown(); executorService = null; } } finally { context.complete(); } } }); } finally { context.asynchronous(); } } /* * Accessor Methods */ public CacheMode getMode() { return mode; } public void setMode(CacheMode mode) { this.mode = mode; } public int getEvictionTime() { return evictionTime; } public void setEvictionTime(int evictionTime) { this.evictionTime = evictionTime; } public boolean isCacheFailures() { return cacheFailures; } public void setCacheFailures(boolean cacheFailures) { this.cacheFailures = cacheFailures; } public int getMaxCacheSize() { return maxCacheSize; } public void setMaxCacheSize(int maxCacheSize) { this.maxCacheSize = maxCacheSize; } private enum CacheMode { OFF, BY_SEARCH, BY_ACCESS } private R internalSearch(LdapConnectionHandler connectionHandler, K key) throws IOException, NamingException { return searcher.search(connectionHandler, key); } private interface ExtendedLdapSearcherCache<R, K> extends LdapSearcherCache<R, K> { int getCurrentSize(); void clearAll(); void clear(K key); void clear(Predicate<K> predicate); boolean contains(K key); int count(Predicate<K> predicate); Set<K> currentKeys(); } private class NoCacheCache implements ExtendedLdapSearcherCache<R, K> { @Override public SearchResult<R> search(LdapConnectionHandler connectionHandler, K key) throws IOException, NamingException { SECURITY_LOGGER.tracef("Non caching search for '%s'", key); R result = searcher.search(connectionHandler, key); return new SearchResultImpl<R>(result); } @Override public int getCurrentSize() { return 0; } @Override public void clearAll() { } @Override public void clear(K key) { } @Override public boolean contains(K key) { return false; } @Override public Set<K> currentKeys() { return Collections.emptySet(); } @Override public void clear(Predicate<K> predicate) { } @Override public int count(Predicate<K> predicate) { return 0; } } private abstract class BaseSearchCache implements ExtendedLdapSearcherCache<R, K> { protected final int evictionTime; protected final boolean cacheFailures; protected final int maxSize; protected final LinkedHashMap<K, CacheEntry> theCache = new LinkedHashMap<K, CacheEntry>(); private BaseSearchCache(final int evictionTime, final boolean cacheFailures, final int maxSize) { this.evictionTime = evictionTime; this.cacheFailures = cacheFailures; this.maxSize = maxSize; } @Override public int getCurrentSize() { synchronized (theCache) { return theCache.size(); } } @Override public void clearAll() { synchronized (theCache) { Iterator<CacheEntry> it = theCache.values().iterator(); while (it.hasNext()) { CacheEntry current = it.next(); current.cancelFuture(); it.remove(); } } SECURITY_LOGGER.trace("Cleared whole cache."); } @Override public void clear(K key) { synchronized (theCache) { CacheEntry entry = theCache.remove(key); if (entry != null) { entry.cancelFuture(); } } SECURITY_LOGGER.tracef("Cleared entry from cache with key '%s'", key); } @Override public void clear(Predicate<K> predicate) { synchronized (theCache) { Iterator<Entry<K, CacheEntry>> it = theCache.entrySet().iterator(); while (it.hasNext()) { Entry<K, CacheEntry> current = it.next(); K key = current.getKey(); if (predicate.matches(key)) { it.remove(); current.getValue().cancelFuture(); SECURITY_LOGGER.tracef("Cleared entry from cache with key '%s' based on predicate match.", key); } } } } @Override public boolean contains(K key) { synchronized (theCache) { return theCache.containsKey(key); } } @Override public int count(Predicate<K> predicate) { int count = 0; synchronized (theCache) { Iterator<K> it = theCache.keySet().iterator(); while (it.hasNext()) { K current = it.next(); if (predicate.matches(current)) { count++; } } } return count; } @Override public Set<K> currentKeys() { return Collections.unmodifiableSet(theCache.keySet()); } protected class CacheEntry { private volatile NamingException failure; private volatile SearchResult<R> result; private ScheduledFuture<?> future; public SearchResult<R> getSearchResult(LdapConnectionHandler connectionHandler, K key) throws IOException, NamingException { if (failure != null) { SECURITY_LOGGER.tracef("Using cached failure for search with key '%s'", key); throw failure; } else if (result != null) { SECURITY_LOGGER.tracef("Using cached result for search with key '%s'", key); return result; } synchronized (this) { if (failure != null) { SECURITY_LOGGER.tracef("Using cached failure for search with key '%s'", key); throw failure; } else if (result != null) { SECURITY_LOGGER.tracef("Using cached result for search with key '%s'", key); return result; } try { R result = internalSearch(connectionHandler, key); SECURITY_LOGGER.tracef("New search for entry with key '%s'", key); SearchResult<R> searchResult = new SearchResultImpl<R>(result); return this.result = searchResult; } catch (NamingException e) { if (cacheFailures) { failure = e; } throw e; } } } /** * Set the {@link ScheduledFuture} for the eviction of this entry. * * Note: This method should only be called by a {@link Thread} that has already obtained a lock to the cache. * * @param future - The {@link ScheduledFuture} for the eviction of this entry. */ public void setFuture(ScheduledFuture<?> future) { this.future = future; } /** * Cancel the {@link ScheduledFuture} for the eviction of this entry. * * This could be called either because the entry is being manually evicted from the cache or because eviction is * being rescheduled. * * Note: This method should only be called by a {@link Thread} that has already obtained a lock to the cache. */ public void cancelFuture() { if (future != null) { future.cancel(true); } } } } private class BySearchCache extends BaseSearchCache { private BySearchCache(final int evictionTime, final boolean cacheFailures, final int maxSize) { super(evictionTime, cacheFailures, maxSize); } @Override public SearchResult<R> search(LdapConnectionHandler connectionHandler, final K key) throws IOException, NamingException { CacheEntry entry = null; synchronized (theCache) { entry = theCache.get(key); if (entry == null) { SECURITY_LOGGER.tracef("Entry for '%s' not found in cache.", key); entry = new CacheEntry(); theCache.put(key, entry); if (maxSize > 0 && theCache.size() > maxSize) { boolean trace = SECURITY_LOGGER.isTraceEnabled(); Iterator<Entry<K, CacheEntry>> it = theCache.entrySet().iterator(); while (theCache.size() > maxSize) { Entry<K, CacheEntry> current = it.next(); current.getValue().cancelFuture(); it.remove(); if (trace) { SECURITY_LOGGER.tracef( "Entry with key '%s' evicted from cache due to cache being above maximum size.", current.getKey()); } } } if (evictionTime > 0) { entry.setFuture(executorService.schedule(new Runnable() { @Override public void run() { synchronized (theCache) { CacheEntry entry = theCache.remove(key); if (entry == null) { SECURITY_LOGGER.tracef("Entry with key '%s' not in cache at time of timeout.", key); } else { SECURITY_LOGGER.tracef("Evicted entry with key '%s' due to eviction timeout.", key); } } } }, evictionTime, TimeUnit.SECONDS)); } } else { SECURITY_LOGGER.tracef("Cached entry for '%s' found in cache.", key); } } // The individual entry will handle it's own synchronization now. return entry.getSearchResult(connectionHandler, key); } } private class ByAccessCache extends BaseSearchCache { private ByAccessCache(final int evictionTime, final boolean cacheFailures, final int maxSize) { super(evictionTime, cacheFailures, maxSize); } @Override public SearchResult<R> search(LdapConnectionHandler connectionHandler, final K key) throws IOException, NamingException { CacheEntry entry = null; synchronized (theCache) { // Always remove the cached entry so it can be re-added and moved to the end of the list. entry = theCache.remove(key); if (entry == null) { SECURITY_LOGGER.tracef("Entry for '%s' not found in cache.", key); entry = new CacheEntry(); if (maxSize > 0 && theCache.size() + 1 > maxSize) { boolean trace = SECURITY_LOGGER.isTraceEnabled(); Iterator<Entry<K, CacheEntry>> it = theCache.entrySet().iterator(); while (theCache.size() + 1 > maxSize) { Entry<K, CacheEntry> current = it.next(); current.getValue().cancelFuture(); it.remove(); if (trace) { SECURITY_LOGGER.tracef( "Entry with key '%s' evicted from cache due to cache being above maximum size.", current.getKey()); } } } } else { SECURITY_LOGGER.tracef("Cached entry for '%s' found in cache.", key); } theCache.put(key, entry); if (evictionTime > 0) { entry.cancelFuture(); entry.setFuture(executorService.schedule(new Runnable() { @Override public void run() { synchronized (theCache) { CacheEntry entry = theCache.remove(key); if (entry == null) { SECURITY_LOGGER.tracef("Entry with key '%s' not in cache at time of timeout.", key); } else { SECURITY_LOGGER.tracef("Evicted entry with key '%s' due to eviction timeout.", key); } } } }, evictionTime, TimeUnit.SECONDS)); } } // The individual entry will handle it's own synchronization now. return entry.getSearchResult(connectionHandler, key); } } private class SearchResultImpl<R> implements SearchResult<R> { private final ConcurrentMap<AttachmentKey<?>, Object> valueAttachments = new ConcurrentHashMap<AttachmentKey<?>, Object>(); private final R result; private SearchResultImpl(R result) { this.result = result; } @Override public R getResult() { return result; } @Override public <T> T getAttachment(AttachmentKey<T> key) { return key.cast(valueAttachments.get(key)); } @Override public <T> T attach(AttachmentKey<T> key, T value) { return key.cast(valueAttachments.put(key, value)); } @Override public <T> T detach(AttachmentKey<T> key) { return key.cast(valueAttachments.remove(key)); } } }