001package org.apache.commons.jcs3.engine.memory.shrinking;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.util.Set;
023
024import org.apache.commons.jcs3.engine.behavior.ICacheElement;
025import org.apache.commons.jcs3.engine.behavior.IElementAttributes;
026import org.apache.commons.jcs3.engine.control.CompositeCache;
027import org.apache.commons.jcs3.engine.control.event.behavior.ElementEventType;
028import org.apache.commons.jcs3.engine.memory.behavior.IMemoryCache;
029import org.apache.commons.jcs3.log.Log;
030import org.apache.commons.jcs3.log.LogManager;
031
032/**
033 * A background memory shrinker. Memory problems and concurrent modification exception caused by
034 * acting directly on an iterator of the underlying memory cache should have been solved.
035 */
036public class ShrinkerThread<K, V>
037    implements Runnable
038{
039    /** The logger */
040    private static final Log log = LogManager.getLog( ShrinkerThread.class );
041
042    /** The CompositeCache instance which this shrinker is watching */
043    private final CompositeCache<K, V> cache;
044
045    /** Maximum memory idle time for the whole cache */
046    private final long maxMemoryIdleTime;
047
048    /** Maximum number of items to spool per run. Default is -1, or no limit. */
049    private final int maxSpoolPerRun;
050
051    /** Should we limit the number spooled per run. If so, the maxSpoolPerRun will be used. */
052    private boolean spoolLimit;
053
054    /**
055     * Constructor for the ShrinkerThread object.
056     * <p>
057     * @param cache The MemoryCache which the new shrinker should watch.
058     */
059    public ShrinkerThread( final CompositeCache<K, V> cache )
060    {
061        this.cache = cache;
062
063        final long maxMemoryIdleTimeSeconds = cache.getCacheAttributes().getMaxMemoryIdleTimeSeconds();
064
065        if ( maxMemoryIdleTimeSeconds < 0 )
066        {
067            this.maxMemoryIdleTime = -1;
068        }
069        else
070        {
071            this.maxMemoryIdleTime = maxMemoryIdleTimeSeconds * 1000;
072        }
073
074        this.maxSpoolPerRun = cache.getCacheAttributes().getMaxSpoolPerRun();
075        if ( this.maxSpoolPerRun != -1 )
076        {
077            this.spoolLimit = true;
078        }
079
080    }
081
082    /**
083     * Main processing method for the ShrinkerThread object
084     */
085    @Override
086    public void run()
087    {
088        shrink();
089    }
090
091    /**
092     * This method is called when the thread wakes up. First the method obtains an array of keys for
093     * the cache region. It iterates through the keys and tries to get the item from the cache
094     * without affecting the last access or position of the item. The item is checked for
095     * expiration, the expiration check has 3 parts:
096     * <ol>
097     * <li>Has the cacheattributes.MaxMemoryIdleTimeSeconds defined for the region been exceeded? If
098     * so, the item should be move to disk.</li> <li>Has the item exceeded MaxLifeSeconds defined in
099     * the element attributes? If so, remove it.</li> <li>Has the item exceeded IdleTime defined in
100     * the element attributes? If so, remove it. If there are event listeners registered for the
101     * cache element, they will be called.</li>
102     * </ol>
103     * TODO Change element event handling to use the queue, then move the queue to the region and
104     *       access via the Cache.
105     */
106    protected void shrink()
107    {
108        log.debug( "Shrinking memory cache for: {0}", this.cache::getCacheName);
109
110        final IMemoryCache<K, V> memCache = cache.getMemoryCache();
111
112        try
113        {
114            final Set<K> keys = memCache.getKeySet();
115            final int size = keys.size();
116            log.debug( "Keys size: {0}", size );
117
118            int spoolCount = 0;
119
120            for (final K key : keys)
121            {
122                final ICacheElement<K, V> cacheElement = memCache.getQuiet( key );
123
124                if ( cacheElement == null )
125                {
126                    continue;
127                }
128
129                final IElementAttributes attributes = cacheElement.getElementAttributes();
130
131                boolean remove = false;
132
133                final long now = System.currentTimeMillis();
134
135                // If the element is not eternal, check if it should be
136                // removed and remove it if so.
137                if ( !attributes.getIsEternal() )
138                {
139                    remove = cache.isExpired( cacheElement, now,
140                            ElementEventType.EXCEEDED_MAXLIFE_BACKGROUND,
141                            ElementEventType.EXCEEDED_IDLETIME_BACKGROUND );
142
143                    if ( remove )
144                    {
145                        memCache.remove( key );
146                    }
147                }
148
149                // If the item is not removed, check is it has been idle
150                // long enough to be spooled.
151
152                if ( !remove && maxMemoryIdleTime != -1 )
153                {
154                    if ( !spoolLimit || spoolCount < this.maxSpoolPerRun )
155                    {
156                        final long lastAccessTime = attributes.getLastAccessTime();
157
158                        if ( lastAccessTime + maxMemoryIdleTime < now )
159                        {
160                            log.debug( "Exceeded memory idle time: {0}", key );
161
162                            // Shouldn't we ensure that the element is
163                            // spooled before removing it from memory?
164                            // No the disk caches have a purgatory. If it fails
165                            // to spool that does not affect the
166                            // responsibilities of the memory cache.
167
168                            spoolCount++;
169
170                            memCache.remove( key );
171                            memCache.waterfal( cacheElement );
172                        }
173                    }
174                    else
175                    {
176                        log.debug( "spoolCount = \"{0}\"; maxSpoolPerRun = \"{1}\"",
177                                spoolCount, maxSpoolPerRun );
178
179                        // stop processing if limit has been reached.
180                        if ( spoolLimit && spoolCount >= this.maxSpoolPerRun )
181                        {
182                            return;
183                        }
184                    }
185                }
186            }
187        }
188        catch ( final Throwable t )
189        {
190            log.info( "Unexpected trouble in shrink cycle", t );
191
192            // concurrent modifications should no longer be a problem
193            // It is up to the IMemoryCache to return an array of keys
194
195            // stop for now
196            return;
197        }
198    }
199}