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 }