View Javadoc

1   package org.apache.jcs.engine.memory.shrinking;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.IOException;
23  import java.io.Serializable;
24  import java.util.ArrayList;
25  import java.util.Set;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.apache.jcs.engine.behavior.ICacheElement;
30  import org.apache.jcs.engine.behavior.IElementAttributes;
31  import org.apache.jcs.engine.control.event.ElementEvent;
32  import org.apache.jcs.engine.control.event.behavior.ElementEventType;
33  import org.apache.jcs.engine.control.event.behavior.IElementEvent;
34  import org.apache.jcs.engine.control.event.behavior.IElementEventHandler;
35  import org.apache.jcs.engine.memory.behavior.IMemoryCache;
36  
37  /**
38   * A background memory shrinker. Memory problems and concurrent modification exception caused by
39   * acting directly on an iterator of the underlying memory cache should have been solved.
40   * @version $Id: ShrinkerThread.java 1406724 2012-11-07 17:19:03Z tv $
41   */
42  public class ShrinkerThread<K extends Serializable, V extends Serializable>
43      implements Runnable
44  {
45      /** The logger */
46      private final static Log log = LogFactory.getLog( ShrinkerThread.class );
47  
48      /** The MemoryCache instance which this shrinker is watching */
49      private final IMemoryCache<K, V> cache;
50  
51      /** Maximum memory idle time for the whole cache */
52      private final long maxMemoryIdleTime;
53  
54      /** Maximum number of items to spool per run. Default is -1, or no limit. */
55      private final int maxSpoolPerRun;
56  
57      /** Should we limit the number spooled per run. If so, the maxSpoolPerRun will be used. */
58      private boolean spoolLimit = false;
59  
60      /**
61       * Constructor for the ShrinkerThread object.
62       * <p>
63       * @param cache The MemoryCache which the new shrinker should watch.
64       */
65      public ShrinkerThread( IMemoryCache<K, V> cache )
66      {
67          super();
68  
69          this.cache = cache;
70  
71          long maxMemoryIdleTimeSeconds = cache.getCacheAttributes().getMaxMemoryIdleTimeSeconds();
72  
73          if ( maxMemoryIdleTimeSeconds < 0 )
74          {
75              this.maxMemoryIdleTime = -1;
76          }
77          else
78          {
79              this.maxMemoryIdleTime = maxMemoryIdleTimeSeconds * 1000;
80          }
81  
82          this.maxSpoolPerRun = cache.getCacheAttributes().getMaxSpoolPerRun();
83          if ( this.maxSpoolPerRun != -1 )
84          {
85              this.spoolLimit = true;
86          }
87  
88      }
89  
90      /**
91       * Main processing method for the ShrinkerThread object
92       */
93      public void run()
94      {
95          shrink();
96      }
97  
98      /**
99       * This method is called when the thread wakes up. First the method obtains an array of keys for
100      * the cache region. It iterates through the keys and tries to get the item from the cache
101      * without affecting the last access or position of the item. The item is checked for
102      * expiration, the expiration check has 3 parts:
103      * <ol>
104      * <li>Has the cacheattributes.MaxMemoryIdleTimeSeconds defined for the region been exceeded? If
105      * so, the item should be move to disk.</li> <li>Has the item exceeded MaxLifeSeconds defined in
106      * the element attributes? If so, remove it.</li> <li>Has the item exceeded IdleTime defined in
107      * the element attributes? If so, remove it. If there are event listeners registered for the
108      * cache element, they will be called.</li>
109      * </ol>
110      * @todo Change element event handling to use the queue, then move the queue to the region and
111      *       access via the Cache.
112      */
113     protected void shrink()
114     {
115         if ( log.isDebugEnabled() )
116         {
117             if ( this.cache.getCompositeCache() != null )
118             {
119                 log.debug( "Shrinking memory cache for: " + this.cache.getCompositeCache().getCacheName() );
120             }
121         }
122 
123         try
124         {
125             Set<K> keys = cache.getKeySet();
126             int size = keys.size();
127             if ( log.isDebugEnabled() )
128             {
129                 log.debug( "Keys size: " + size );
130             }
131 
132             ICacheElement<K, V> cacheElement;
133             IElementAttributes attributes;
134 
135             int spoolCount = 0;
136 
137             for (K key : keys)
138             {
139                 cacheElement = cache.getQuiet( key );
140 
141                 if ( cacheElement == null )
142                 {
143                     continue;
144                 }
145 
146                 attributes = cacheElement.getElementAttributes();
147 
148                 boolean remove = false;
149 
150                 long now = System.currentTimeMillis();
151 
152                 // If the element is not eternal, check if it should be
153                 // removed and remove it if so.
154                 if ( !cacheElement.getElementAttributes().getIsEternal() )
155                 {
156                     remove = checkForRemoval( cacheElement, now );
157 
158                     if ( remove )
159                     {
160                         cache.remove( cacheElement.getKey() );
161                     }
162                 }
163 
164                 // If the item is not removed, check is it has been idle
165                 // long enough to be spooled.
166 
167                 if ( !remove && ( maxMemoryIdleTime != -1 ) )
168                 {
169                     if ( !spoolLimit || ( spoolCount < this.maxSpoolPerRun ) )
170                     {
171                         final long lastAccessTime = attributes.getLastAccessTime();
172 
173                         if ( lastAccessTime + maxMemoryIdleTime < now )
174                         {
175                             if ( log.isDebugEnabled() )
176                             {
177                                 log.debug( "Exceeded memory idle time: " + cacheElement.getKey() );
178                             }
179 
180                             // Shouldn't we ensure that the element is
181                             // spooled before removing it from memory?
182                             // No the disk caches have a purgatory. If it fails
183                             // to spool that does not affect the
184                             // responsibilities of the memory cache.
185 
186                             spoolCount++;
187 
188                             cache.remove( cacheElement.getKey() );
189 
190                             cache.waterfal( cacheElement );
191 
192                             key = null;
193                             cacheElement = null;
194                         }
195                     }
196                     else
197                     {
198                         if ( log.isDebugEnabled() )
199                         {
200                             log.debug( "spoolCount = '" + spoolCount + "'; " + "maxSpoolPerRun = '" + maxSpoolPerRun
201                                 + "'" );
202                         }
203 
204                         // stop processing if limit has been reached.
205                         if ( spoolLimit && ( spoolCount >= this.maxSpoolPerRun ) )
206                         {
207                             keys = null;
208                             return;
209                         }
210                     }
211                 }
212             }
213         }
214         catch ( Throwable t )
215         {
216             log.info( "Unexpected trouble in shrink cycle", t );
217 
218             // concurrent modifications should no longer be a problem
219             // It is up to the IMemoryCache to return an array of keys
220 
221             // stop for now
222             return;
223         }
224     }
225 
226     /**
227      * Check if either lifetime or idletime has expired for the provided event, and remove it from
228      * the cache if so.
229      * <p>
230      * @param cacheElement Element to check for expiration
231      * @param now Time to consider expirations relative to. This makes it easier to test.
232      * @return true if the element should be removed, or false.
233      * @throws IOException
234      */
235     protected boolean checkForRemoval( ICacheElement<?, ?> cacheElement, long now )
236         throws IOException
237     {
238         IElementAttributes attributes = cacheElement.getElementAttributes();
239 
240         final long maxLifeSeconds = attributes.getMaxLifeSeconds();
241         final long createTime = attributes.getCreateTime();
242 
243         // Check if maxLifeSeconds has been exceeded
244         if ( maxLifeSeconds != -1 && now - createTime > maxLifeSeconds * 1000 )
245         {
246             if ( log.isInfoEnabled() )
247             {
248                 log.info( "Exceeded maxLifeSeconds: " + cacheElement.getKey() );
249             }
250 
251             handleElementEvents( cacheElement, ElementEventType.EXCEEDED_MAXLIFE_BACKGROUND );
252 
253             return true;
254         }
255 
256         final long idleTime = attributes.getIdleTime();
257         final long lastAccessTime = attributes.getLastAccessTime();
258 
259         // Check maxIdleTime has been exceeded
260         if ( idleTime != -1 && now - lastAccessTime > idleTime * 1000 )
261         {
262             if ( log.isInfoEnabled() )
263             {
264                 log.info( "Exceeded maxIdleTime " + cacheElement.getKey() );
265             }
266 
267             handleElementEvents( cacheElement, ElementEventType.EXCEEDED_IDLETIME_BACKGROUND );
268 
269             return true;
270         }
271 
272         return false;
273     }
274 
275     /**
276      * Handle any events registered for the given element of the given event type.
277      * <p>
278      * @param cacheElement Element to handle events for
279      * @param eventType Type of event to handle
280      * @throws IOException If an error occurs
281      */
282     private void handleElementEvents( ICacheElement<?, ?> cacheElement, ElementEventType eventType )
283         throws IOException
284     {
285         IElementAttributes attributes = cacheElement.getElementAttributes();
286 
287         ArrayList<IElementEventHandler> eventHandlers = attributes.getElementEventHandlers();
288 
289         if ( eventHandlers != null )
290         {
291             if ( log.isDebugEnabled() )
292             {
293                 log.debug( "Handlers are registered, type: " + eventType );
294             }
295 
296             IElementEvent event = new ElementEvent( cacheElement, eventType );
297 
298             for (IElementEventHandler hand : eventHandlers)
299             {
300                 // extra safety
301                 // TODO we shouldn't be operating on a variable of another class.
302                 // we did this to get away from the singleton composite cache.
303                 // we will need to create an event manager and pass it around instead.
304                 if ( cache.getCompositeCache() != null )
305                 {
306                     cache.getCompositeCache().addElementEvent( hand, event );
307                 }
308             }
309         }
310     }
311 }