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

grails.util.CacheEntry Maven / Gradle / Ivy

There is a newer version: 6.2.0
Show newest version
/*
 * Copyright 2011 the original author or authors.
 *
 * 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 grails.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Wrapper for a value inside a cache that adds timestamp information
 * for expiration and prevents "cache storms" with a Lock.
 *
 * JMM happens-before is ensured with AtomicReference.
 *
 * Objects in cache are assumed to not change after publication.
 *
 * @author Lari Hotari
 * @since 2.3.4
 */
public class CacheEntry {
    private static final Logger LOG = LoggerFactory.getLogger(CacheEntry.class);
    private final AtomicReference valueRef=new AtomicReference(null);
    private long createdMillis;
    private final ReadWriteLock lock=new ReentrantReadWriteLock();
    private final Lock readLock=lock.readLock();
    private final Lock writeLock=lock.writeLock();
    private volatile boolean initialized=false;

    public CacheEntry() {
        expire();
    }
    
    public CacheEntry(V value) {
        setValue(value);
    }
    
    /**
     * Gets a value from cache. If the key doesn't exist, it will create the value using the updater callback
     * Prevents cache storms with a lock 
     * 
     * The key is always added to the cache. Null return values will also be cached.
     * You can use this together with ConcurrentLinkedHashMap to create a bounded LRU cache
     *
     * @param map the cache map
     * @param key the key to look up
     * @param timeoutMillis cache entry timeout
     * @param updater callback to create/update value
     * @param cacheEntryClass CacheEntry implementation class to use
     * @param returnExpiredWhileUpdating when true, return expired value while updating new value
     * @param cacheRequestObject context object that gets passed to hasExpired, shouldUpdate and updateValue methods, not used in default implementation
     * @return
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static  V getValue(ConcurrentMap> map, K key, long timeoutMillis, Callable updater, Callable cacheEntryFactory, boolean returnExpiredWhileUpdating, Object cacheRequestObject) {
        CacheEntry cacheEntry = map.get(key);
        if(cacheEntry==null) {
            try {
                cacheEntry = cacheEntryFactory.call();
            }
            catch (Exception e) {
                throw new UpdateException(e);
            }
            CacheEntry previousEntry = map.putIfAbsent(key, cacheEntry);
            if(previousEntry != null) {
                cacheEntry = previousEntry;
            }
        }
        try {
            return cacheEntry.getValue(timeoutMillis, updater, returnExpiredWhileUpdating, cacheRequestObject);
        }
        catch (UpdateException e) {
            e.rethrowRuntimeException();
            // make compiler happy
            return null;
        }
    }
    
    @SuppressWarnings("rawtypes")
    private static final Callable DEFAULT_CACHE_ENTRY_FACTORY = new Callable() {
        @Override
        public CacheEntry call() throws Exception {
            return new CacheEntry();
        }
    };
    
    public static  V getValue(ConcurrentMap> map, K key, long timeoutMillis, Callable updater) {
        return getValue(map, key, timeoutMillis, updater, DEFAULT_CACHE_ENTRY_FACTORY, true, null);
    }

    public static  V getValue(ConcurrentMap> map, K key, long timeoutMillis, Callable updater, boolean returnExpiredWhileUpdating) {
        return getValue(map, key, timeoutMillis, updater, DEFAULT_CACHE_ENTRY_FACTORY, returnExpiredWhileUpdating, null);
    }
    
    public V getValue(long timeout, Callable updater) {
        return getValue(timeout, updater, true, null);
    }
    
    /**
     * gets the current value from the entry and updates it if it's older than timeout
     *
     * updater is a callback for creating an updated value.
     *
     * @param timeout
     * @param updater
     * @param returnExpiredWhileUpdating
     * @param cacheRequestObject
     * @return the current value
     */
    public V getValue(long timeout, Callable updater, boolean returnExpiredWhileUpdating, Object cacheRequestObject) {
        if (!isInitialized() || hasExpired(timeout, cacheRequestObject)) {
            boolean lockAcquired = false;
            try {
                long beforeLockingCreatedMillis = createdMillis;
                if(returnExpiredWhileUpdating) {
                    if(!writeLock.tryLock()) {
                        if(isInitialized()) {
                            return getValueWhileUpdating(cacheRequestObject);
                        } else {
                            if(LOG.isDebugEnabled()) {
                                LOG.debug("Locking cache for update");
                            }
                            writeLock.lock();
                        }
                    }
                } else {
                    LOG.debug("Locking cache for update");
                    writeLock.lock();
                }
                lockAcquired = true;
                V value;
                if (!isInitialized() || shouldUpdate(beforeLockingCreatedMillis, cacheRequestObject)) {
                    try {
                        value = updateValue(getValue(), updater, cacheRequestObject);
                        if(LOG.isDebugEnabled()) {
                            LOG.debug("Updating cache for value [{}]", value);
                        }
                        setValue(value);
                    }
                    catch (Exception e) {
                        throw new UpdateException(e);
                    }
                } else {
                    value = getValue();
                    resetTimestamp(false);
                }
                return value;
            } finally {
                if(lockAcquired) {
                    if(LOG.isDebugEnabled()) {
                        LOG.debug("Unlocking cache for update");
                    }
                    writeLock.unlock();
                }
            }
        } else {
            return getValue();
        }
    }
    
    protected V getValueWhileUpdating(Object cacheRequestObject) {
        return valueRef.get();
    }

    protected V updateValue(V oldValue, Callable updater, Object cacheRequestObject) throws Exception {
        return updater != null ? updater.call() : oldValue;
    }

    public V getValue() {
        try {
            readLock.lock();
            return valueRef.get();
        } finally {
            readLock.unlock();
        }
    }
    
    public void setValue(V val) {
        try{
            writeLock.lock();
            valueRef.set(val);
            setInitialized(true);
            resetTimestamp(true);
        } finally {
            writeLock.unlock();
        }
    }

    protected boolean hasExpired(long timeout, Object cacheRequestObject) {
        return timeout >= 0 && System.currentTimeMillis() - timeout > createdMillis;
    }

    protected boolean shouldUpdate(long beforeLockingCreatedMillis, Object cacheRequestObject) {
        return beforeLockingCreatedMillis == createdMillis || createdMillis == 0L;
    }

    protected void resetTimestamp(boolean updated) {
        if(updated) {
            createdMillis = System.currentTimeMillis();
        }
    }

    public long getCreatedMillis() {
        return createdMillis;
    }

    public void expire() {
        createdMillis = 0L;
    }
    
    public boolean isInitialized() {
        return initialized;
    }

    public void setInitialized(boolean initialized) {
        this.initialized = initialized;
    }

    public static final class UpdateException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public UpdateException(String message, Throwable cause) {
            super(message, cause);
        }

        public UpdateException(Throwable cause) {
            super(cause);
        }

        public void rethrowCause() throws Exception {
            if (getCause() instanceof Exception) {
                throw (Exception)getCause();
            }

            throw this;
        }
        
        public void rethrowRuntimeException() {
            if (getCause() instanceof RuntimeException) {
                throw (RuntimeException)getCause();
            }
            throw this;
        }
        
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy