All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cesecore.util.ConcurrentCache Maven / Gradle / Ivy

/*************************************************************************
 *                                                                       *
 *  CESeCore: CE Security Core                                           *
 *                                                                       *
 *  This software 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 any later version.                    *
 *                                                                       *
 *  See terms of license at gnu.org.                                     *
 *                                                                       *
 *************************************************************************/  
package org.cesecore.util;

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.log4j.Logger;

/**
 * A concurrent cache allows multiple threads to cache data. Only one thread
 * will be allowed to generate data for one particular key, and other threads
 * will block.
 * 
 * All methods in this class and inner classes are thread-safe.
 * 
 * @version $Id: ConcurrentCache.java 27456 2017-12-07 10:59:06Z samuellb $
 *
 */
public final class ConcurrentCache {

    private static final Logger log = Logger.getLogger(ConcurrentCache.class);
    
    /**
     * Internal entries are stored in the ConcurrentMap
     */
    private final static class InternalEntry {
        final V value;
        volatile long expire;
        
        private InternalEntry(final V value) {
            this.value = value;
            this.expire = Long.MAX_VALUE;
        }
    }
    
    /**
     * A reference to a cache entry, with a get and put method to read/write data from/to the cache.
     * 
     * All methods are thread safe, but only one thread should operate on an Entry object.
     */
    public final class Entry {
        private final K key;
        private InternalEntry entry;
        /** If non-null, then other threads are waiting on this semaphore for data on the same key in the cache. */
        private final Object ourSemaphore;
        
        private Entry(final K key, final InternalEntry entry) {
            this.key = key;
            this.entry = entry;
            this.ourSemaphore = null;
        }
        
        private Entry(final K key, final InternalEntry entry, final Object ourSemaphore) {
            if (ourSemaphore == null) {
                throw new IllegalArgumentException("ourSemaphore may not be null");
            }
            this.key = key;
            this.entry = entry;
            this.ourSemaphore = ourSemaphore;
        }
        
        /**
         * @return true if the key existed in the cache.
         */
        public boolean isInCache() {
            return entry != null;
        }
        
        /**
         * @return the value read from the cache when the Entry was created. Calls to putValue() on this particular Entry change the return value.
         */
        public V getValue() {
            if (entry == null) {
                throw new IllegalStateException("Tried to read from non-existent cache entry");
            }
            return entry.value;
        }
        
        /**
         * Updates the value in this Entry as well as in the underlying cache.
         * The expire time is set to be "infinite". Thread-safe.
         */
        public void putValue(final V value) {
            if (key != null) {
                entry = new InternalEntry<>(value);
                cache.put(key, entry);
            }
        }
        
        /**
         * Sets the validity of the value. After the cache entry expires, the next request for it will
         * fail (on purpose) so it can be updated. Requests that happen while the expired entry is being
         * updated will still use the expired value, so they don't have to block.
         * @param validFor Cache validity in milliseconds.
         */
        public void setCacheValidity(long validFor) {
            if (entry != null) {
                entry.expire = System.currentTimeMillis() + validFor;
            }
        }
        
        /**
         * Must be called if other threads might be waiting for this cache entry
         * (i.e. if isInCache() returns false)
         */
        public void close() {
            if (ourSemaphore != null) {
                synchronized (ourSemaphore) {
                    semaphores.remove(key);
                    ourSemaphore.notifyAll();
                }
            }
        }
    }
    
    private final ConcurrentHashMap> cache = new ConcurrentHashMap<>();
    private final ConcurrentMap semaphores = new ConcurrentHashMap<>();
    
    public static final long NO_LIMIT = -1L;
    
    /** @see #setEnabled */
    private volatile boolean enabled = true;
    /** @see #setCloseOnEviction */
    private volatile boolean closeOnEviction = false;
    /** @see setMaxEntries */
    private volatile long maxEntries = NO_LIMIT;
    
    private AtomicLong numEntries = new AtomicLong(0L);
    private final Set pendingRemoval = Collections.newSetFromMap(new ConcurrentHashMap());
    private final Lock isCleaning = new ReentrantLock();
    private volatile long lastCleanup = 0L;
    private volatile long cleanupInterval = 1000L;
    
    /** Creates an empty concurrent cache */
    public ConcurrentCache() {
        // Do nothing
    }
    
    /**
     * Creates a concurrent cache initialized with the mapping defined in the given map.
     * Can be used for rebuilding the cache in the background for instance.
     * 
     * @param map
     * @param validFor Time in milliseconds which the entry will be valid for, or -1L for forever.
     * @see ConcurrentCache#getKeys()
     */
    public ConcurrentCache(final Map map, long validFor) {
        for (Map.Entry mapEntry : map.entrySet()) {
            final InternalEntry intEntry = new InternalEntry(mapEntry.getValue());
            if (validFor != -1L) {
                intEntry.expire = System.currentTimeMillis() + validFor;
            }
            cache.put(mapEntry.getKey(), intEntry);
        }
    }
    
    /**
     * "Opens" a cache entry. If the entry already exists, then an {@link Entry} that
     * maps to the existing entry is returned. Otherwise, a semaphore is used
     * to prevent multiple threads from creating the new cache entry. Only the
     * first thread is returned an Entry with isInCache()==false, later threads
     * will block and wait for the first thread.
     * 
     * For non-existent entries (i.e. isInCache()==false), the caller is expected to put
     * a value in it and call close() on the Entry.
     *  
     * @param key      Key in the cache.
     * @param timeout  Timeout in milliseconds. The call will only be allowed
     *                 to block for (approximately) this amount of time.
     * @return An Entry object that maps to an entry in the cache (existing
     *         or blank), or null if a timeout occurred. 
     * @throws NullPointerException if key is null.
     */
    public Entry openCacheEntry(final K key, final long timeout) {
        final long timeAtEntry = System.currentTimeMillis();
        if (key == null) {
            throw new NullPointerException("key may not be null");
        }
        
        if (!enabled) {
            return new Entry(null, null);
        }
        
        if (maxEntries != NO_LIMIT) {
            pendingRemoval.remove(key); // always mark as used
        }
        
        // Fast path if cached
        InternalEntry entry = cache.get(key);
        final long toExpire = (entry != null ? entry.expire : 0L);
        if (entry != null && toExpire > timeAtEntry) {
            // Found valid entry in cache
            if (log.isDebugEnabled()) {
                log.debug("Found valid entry in cache for key "+key);
                if (log.isTraceEnabled()) {
                    log.debug("Value: "+entry.value);
                    log.trace(" getKeys() {
        return new HashSet<>(cache.keySet());
    }
    
    /**
     * 

Enables or disables caching. If disabled, nothing will be cached and openCacheEntry will * always immediately return an non-existent entry (this may also cause concurrent attempts * to fetch/build/etc the same object).

* *

Disabling the cache doesn't stop any currently "open" cache entries from being written to.

* *

The default is enabled.

*/ public void setEnabled(boolean enabled) { this.enabled = enabled; } /** @see ConcurrentCache#setEnabled */ public boolean isEnabled() { return enabled; } /** *

Turns on or off automatic closure of values on eviction from the cache. * Automatic closure can only be done on objects that implement the {@link Closeable} interface. * Exceptions from the close() method are debug logged and swallowed.

* *

The default is false.

*/ public void setCloseOnEviction(boolean closeOnEviction) { this.closeOnEviction = closeOnEviction; } /** @see ConcurrentCache#setCloseOnEviction */ public boolean isCloseOnEviction() { return closeOnEviction; } /** *

Sets the desired maximum number of entries in the cache. This is not a * strict limit, and the cache may temporarily exceed this number.

* *

The value {@link ConcurrentCache#NO_LIMIT} (-1) is the default.

*/ public void setMaxEntries(long maxEntries) { if (maxEntries == NO_LIMIT || maxEntries > 0L) { this.maxEntries = maxEntries; } else { throw new IllegalArgumentException("max entries must be either a positive value or -1"); } } /** @see ConcurrentCache#setMaxEntries */ public long getMaxEntries() { return maxEntries; } /** * Sets the minimum time in milliseconds between two cleanup runs. * * The default is 1000 (= 1 second). */ public void setCleanupInterval(long milliseconds) { cleanupInterval = milliseconds; } /** @see ConcurrentCache#setCleanupInterval */ public long getCleanupInterval() { return cleanupInterval; } private void cleanupIfNeeded() { if (maxEntries != NO_LIMIT && numEntries.get() > maxEntries) { cleanup(); } } private void cleanupIfHighlyNeeded() { // More than 1.5 times the limit if (maxEntries != NO_LIMIT && 2L*numEntries.get() > 3L*maxEntries) { cleanup(); } } /** Used internally for testing. */ void checkNumberOfEntries(long min, long max) { long a = numEntries.get(); long b = cache.size(); if (a != b) { throw new IllegalStateException("cache.size() and numEntries does not match ("+a+" and "+b+")"); } if (a < min) { throw new IllegalStateException("number of entries ("+a+") is less than minimum ("+min+")."); } if (a > max) { throw new IllegalStateException("number of entries ("+a+") is greater than maximum ("+max+")."); } } /** * Removes expired entries, and randomly selected entries that have not been used since the last call. */ private void cleanup() { List valuesToClose = null; final long startTime = System.currentTimeMillis(); if (startTime < lastCleanup+cleanupInterval || !isCleaning.tryLock()) { return; } try { final float ratioToRemove; final Random random; if (maxEntries == NO_LIMIT) { ratioToRemove = 0; random = null; } else { // Remove a bit extra ratioToRemove = Math.max(0.0F, 1.0F-0.8F*maxEntries/numEntries.get()); // Remove items that have not been accessed since they were last marked as "pending removal" if (closeOnEviction) { valuesToClose = new ArrayList<>(); } for (K key : pendingRemoval) { InternalEntry evicted = cache.remove(key); if (closeOnEviction && evicted.value instanceof Closeable) { valuesToClose.add((Closeable)evicted.value); } numEntries.decrementAndGet(); } pendingRemoval.clear(); random = new Random(System.nanoTime()); } final long now = System.currentTimeMillis(); final Iterator>> iter = cache.entrySet().iterator(); while (iter.hasNext()) { final Map.Entry> mapEntry = iter.next(); if (mapEntry.getValue().expire <= now) { iter.remove(); numEntries.decrementAndGet(); } else if (maxEntries != NO_LIMIT && random.nextFloat() < ratioToRemove) { pendingRemoval.add(mapEntry.getKey()); } } } finally { isCleaning.unlock(); final long endTime = System.currentTimeMillis(); lastCleanup = endTime; if (log.isDebugEnabled()) { log.debug("Clean up took "+(endTime - startTime)+" ms"); } } if (valuesToClose != null) { for (final Closeable closable : valuesToClose) { try { closable.close(); } catch (IOException e) { log.debug("Exception ocurring when closing evicted value.", e); } } } } /** * Removes all entries in the cache */ public void clear() { if (closeOnEviction) { isCleaning.lock(); try { for (final InternalEntry entry : cache.values()) { if (entry.value instanceof Closeable) { try { ((Closeable)entry.value).close(); } catch (IOException e) { log.debug("Exception ocurring when closing value during cache clearing.", e); } } } cache.clear(); } finally { isCleaning.unlock(); } } else { cache.clear(); } numEntries.set(0L); pendingRemoval.clear(); lastCleanup = 0L; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy