1   /**
2    *  Copyright 2003-2007 Greg Luck
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  package net.sf.ehcache.event;
18  
19  import junit.framework.TestCase;
20  import net.sf.ehcache.AbstractCacheTest;
21  import net.sf.ehcache.CacheException;
22  import net.sf.ehcache.CacheManager;
23  import net.sf.ehcache.Ehcache;
24  import net.sf.ehcache.Element;
25  
26  import java.io.IOException;
27  import java.io.Serializable;
28  import java.util.Date;
29  import java.util.List;
30  
31  /**
32   * Tests the cache listener functionality
33   * @author Greg Luck
34   * @version $Id: CacheEventListenerTest.java 512 2007-07-10 09:18:45Z gregluck $
35   */
36  public class CacheEventListenerTest extends TestCase {
37  
38      /**
39       * manager
40       */
41      protected CacheManager manager;
42      /**
43       * the cache name we wish to test
44       */
45      protected String cacheName = "sampleCache1";
46      /**
47       * the cache we wish to test
48       */
49      protected Ehcache cache;
50  
51      /**
52       * {@inheritDoc}
53       *
54       * @throws Exception
55       */
56      protected void setUp() throws Exception {
57          CountingCacheEventListener.resetCounters();
58          manager = CacheManager.create(AbstractCacheTest.TEST_CONFIG_DIR + "ehcache-countinglisteners.xml");
59          cache = manager.getCache(cacheName);
60          cache.removeAll();
61      }
62  
63  
64      /**
65       * {@inheritDoc}
66       *
67       * @throws Exception
68       */
69      protected void tearDown() throws Exception {
70          CountingCacheEventListener.resetCounters();
71          manager.shutdown();
72      }
73  
74  
75      /**
76       * Tests the put listener.
77       */
78      public void testPutNotifications() {
79  
80          Serializable key = new Date();
81          Serializable value = new Date();
82          Element element = new Element(key, value);
83  
84          //Put
85          cache.put(element);
86  
87          List notifications = CountingCacheEventListener.getCacheElementsPut(cache);
88  
89          assertTrue(notifications.size() == 1);
90          assertEquals(key, ((Element) notifications.get(0)).getObjectKey());
91          assertEquals(element.getObjectValue(), ((Element) notifications.get(0)).getObjectValue());
92  
93          //A put which updates records as one put, because the second one is an update
94          cache.put(element);
95          notifications = CountingCacheEventListener.getCacheElementsPut(cache);
96          assertTrue(notifications.size() == 1);
97  
98      }
99  
100     /**
101      * Tests the put and update listeners.
102      */
103     public void testUpdateNotifications() {
104 
105         //Put and update
106         for (int i = 0; i < 11; i++) {
107             cache.put(new Element("" + i, "" + i));
108             cache.put(new Element("" + i, "" + i));
109         }
110 
111         //Put with no update
112         cache.put(new Element("20", "20"));
113 
114         //Should get 12 puts and 11 updates
115         List notifications = CountingCacheEventListener.getCacheElementsPut(cache);
116         assertTrue(notifications.size() == 12);
117         assertEquals("0", ((Element) notifications.get(0)).getObjectKey());
118         assertEquals("0", ((Element) notifications.get(0)).getObjectValue());
119 
120         notifications = CountingCacheEventListener.getCacheElementsUpdated(cache);
121         assertTrue(notifications.size() == 11);
122         assertEquals("0", ((Element) notifications.get(0)).getObjectKey());
123         assertEquals("0", ((Element) notifications.get(0)).getObjectValue());
124 
125 
126     }
127 
128     /**
129      * Tests the remove notifier
130      */
131     public void testRemoveNotifications() {
132 
133         Serializable key = "1";
134         Serializable value = new Date();
135         Element element = new Element(key, value);
136 
137         //Put
138         cache.put(element);
139 
140         //Check removal from MemoryStore
141         cache.remove(key);
142 
143 
144         List notifications = CountingCacheEventListener.getCacheElementsRemoved(cache);
145         assertEquals(element, notifications.get(0));
146 
147         //An unsuccessful remove should notify
148         cache.remove(key);
149         notifications = CountingCacheEventListener.getCacheElementsRemoved(cache);
150         assertEquals(2, notifications.size());
151 
152         //check for NPE
153         cache.remove(null);
154 
155     }
156 
157 
158     /**
159      * Tests the eviction notifier.
160      * sampleCache2 does not overflow, so an evict should trigger a notification
161      */
162     public void testEvictNotificationsWhereNoOverflow() {
163 
164         Ehcache cache2 = manager.getCache("sampleCache2");
165 
166         //Put 11. 1 should be evicted
167         Element element = null;
168         for (int i = 0; i < 11; i++) {
169             element = new Element("" + i, new Date());
170             cache2.put(element);
171         }
172 
173         List notifications = CountingCacheEventListener.getCacheElementsEvicted(cache2);
174         assertEquals(1, notifications.size());
175     }
176 
177     /**
178      * Tests the eviction notifier.
179      * sampleCache1 overflows, so the evict should overflow to disk and not trigger a notification
180      */
181     public void testEvictNotificationsWhereOverflow() {
182 
183         Ehcache cache1 = manager.getCache("sampleCache1");
184 
185         //Put 11. 1 should be evicted
186         Element element = null;
187         for (int i = 0; i < 11; i++) {
188             element = new Element("" + i, new Date());
189             cache1.put(element);
190         }
191 
192         List notifications = CountingCacheEventListener.getCacheElementsEvicted(cache1);
193         assertEquals(0, notifications.size());
194     }
195 
196     /**
197      * Tests the removeAll notifier.
198      */
199     public void testRemoveAllNotification() {
200 
201         Ehcache cache2 = manager.getCache("sampleCache2");
202 
203         //Put 11.
204         Element element = null;
205         for (int i = 0; i < 11; i++) {
206             element = new Element("" + i, new Date());
207             cache2.put(element);
208         }
209 
210         List notifications = CountingCacheEventListener.getCacheRemoveAlls(cache2);
211         assertEquals(0, notifications.size());
212 
213         //Remove all
214         cache2.removeAll();
215         notifications = CountingCacheEventListener.getCacheRemoveAlls(cache2);
216         assertEquals(1, notifications.size());
217     }
218 
219 
220     /**
221      * Tests the remove notifier where the element does not exist in the local cache.
222      * Listener notification is required for correct operation of cluster invalidation.
223      */
224     public void testRemoveNotificationWhereElementDidNotExist() {
225 
226         Serializable key = "1";
227 
228         //Don't Put
229         //cache.put(element);
230 
231         //Check removal from MemoryStore
232         cache.remove(key);
233 
234 
235         List notifications = CountingCacheEventListener.getCacheElementsRemoved(cache);
236         assertEquals(key, ((Element) notifications.get(0)).getKey());
237 
238         //An unsuccessful remove should notify
239         cache.remove(key);
240         notifications = CountingCacheEventListener.getCacheElementsRemoved(cache);
241         assertEquals(2, notifications.size());
242 
243         //check for NPE
244         cache.remove(null);
245 
246     }
247 
248 
249     /**
250      * Tests the expiry notifier. Check a reported scenario
251      */
252     public void testExpiryNotifications() throws InterruptedException {
253 
254         Serializable key = "1";
255         Serializable value = new Date();
256         Element element = new Element(key, value);
257 
258         cache.getCacheEventNotificationService().registerListener(new TestCacheEventListener());
259 
260         //Put
261         cache.put(element);
262 
263         //expire
264         Thread.sleep(1020);
265 
266         //force expiry
267         Element expiredElement = cache.get(key);
268         assertEquals(null, expiredElement);
269 
270         //the TestCacheEventListener does a put of a new Element with the same key on expiry
271         Element newElement = cache.get(key);
272         assertEquals("set on notify", newElement.getValue());
273         assertNotNull(newElement);
274 
275         //Check counting listener
276         List notifications = CountingCacheEventListener.getCacheElementsExpired(cache);
277         assertEquals(element, notifications.get(0));
278         assertEquals(1, notifications.size());
279 
280         //check for NPE
281         cache.remove(null);
282 
283     }
284 
285     /**
286      * Used to do work on notifyRemoved for the above test.
287      */
288     class TestCacheEventListener implements CacheEventListener {
289 
290         /**
291          * {@inheritDoc}
292          */
293         public void notifyElementRemoved(final Ehcache cache, final Element element) throws CacheException {
294             //
295         }
296 
297         /**
298          * Called immediately after an element has been put into the cache. The {@link net.sf.ehcache.Cache#put(net.sf.ehcache.Element)} method
299          * will block until this method returns.
300          * <p/>
301          * Implementers may wish to have access to the Element's fields, including value, so the element is provided.
302          * Implementers should be careful not to modify the element. The effect of any modifications is undefined.
303          *
304          * @param cache   the cache emitting the notification
305          * @param element the element which was just put into the cache.
306          */
307         public void notifyElementPut(final Ehcache cache, final Element element) throws CacheException {
308             //
309         }
310 
311         /**
312          * Called immediately after an element has been put into the cache and the element already
313          * existed in the cache. This is thus an update.
314          * <p/>
315          * The {@link net.sf.ehcache.Cache#put(net.sf.ehcache.Element)} method
316          * will block until this method returns.
317          * <p/>
318          * Implementers may wish to have access to the Element's fields, including value, so the element is provided.
319          * Implementers should be careful not to modify the element. The effect of any modifications is undefined.
320          *
321          * @param cache   the cache emitting the notification
322          * @param element the element which was just put into the cache.
323          */
324         public void notifyElementUpdated(final Ehcache cache, final Element element) throws CacheException {
325             //
326         }
327 
328         /**
329          * Called immediately after an element is <i>found</i> to be expired. The
330          * {@link net.sf.ehcache.Cache#remove(Object)} method will block until this method returns.
331          * <p/>
332          * As the {@link net.sf.ehcache.Element} has been expired, only what was the key of the element is known.
333          * <p/>
334          * Elements are checked for expiry in ehcache at the following times:
335          * <ul>
336          * <li>When a get request is made
337          * <li>When an element is spooled to the diskStore in accordance with a MemoryStore eviction policy
338          * <li>In the DiskStore when the expiry thread runs, which by default is
339          * {@link net.sf.ehcache.Cache#DEFAULT_EXPIRY_THREAD_INTERVAL_SECONDS}
340          * </ul>
341          * If an element is found to be expired, it is deleted and this method is notified.
342          *
343          * @param cache   the cache emitting the notification
344          * @param element the element that has just expired
345          *                <p/>
346          *                Deadlock Warning: expiry will often come from the <code>DiskStore</code> expiry thread. It holds a lock to the
347          *                DiskStorea the time the notification is sent. If the implementation of this method calls into a
348          *                synchronized <code>Cache</code> method and that subsequently calls into DiskStore a deadlock will result.
349          *                Accordingly implementers of this method should not call back into Cache.
350          */
351         public void notifyElementExpired(final Ehcache cache, final Element element) {
352             cache.put(new Element(element.getKey(), "set on notify"));
353         }
354 
355         /**
356          * {@inheritDoc}
357          */
358         public void notifyElementEvicted(final Ehcache cache, final Element element) {
359             //
360         }
361 
362         /**
363          * {@inheritDoc}
364          */
365         public void notifyRemoveAll(final Ehcache cache) {
366             //
367         }
368 
369         /**
370          * Give the replicator a chance to cleanup and free resources when no longer needed
371          */
372         public void dispose() {
373             //
374         }
375 
376         /**
377          * Creates and returns a copy of this object.  The precise meaning
378          * of "copy" may depend on the class of the object. The general
379          * intent is that, for any object <tt>x</tt>, the expression:
380          * <blockquote>
381          * <pre>
382          * x.clone() != x</pre></blockquote>
383          * will be true, and that the expression:
384          * <blockquote>
385          * <pre>
386          * x.clone().getClass() == x.getClass()</pre></blockquote>
387          * will be <tt>true</tt>, but these are not absolute requirements.
388          * While it is typically the case that:
389          * <blockquote>
390          * <pre>
391          * x.clone().equals(x)</pre></blockquote>
392          * will be <tt>true</tt>, this is not an absolute requirement.
393          * <p/>
394          * By convention, the returned object should be obtained by calling
395          * <tt>super.clone</tt>.  If a class and all of its superclasses (except
396          * <tt>Object</tt>) obey this convention, it will be the case that
397          * <tt>x.clone().getClass() == x.getClass()</tt>.
398          * <p/>
399          * By convention, the object returned by this method should be independent
400          * of this object (which is being cloned).  To achieve this independence,
401          * it may be necessary to modify one or more fields of the object returned
402          * by <tt>super.clone</tt> before returning it.  Typically, this means
403          * copying any mutable objects that comprise the internal "deep structure"
404          * of the object being cloned and replacing the references to these
405          * objects with references to the copies.  If a class contains only
406          * primitive fields or references to immutable objects, then it is usually
407          * the case that no fields in the object returned by <tt>super.clone</tt>
408          * need to be modified.
409          * <p/>
410          * The method <tt>clone</tt> for class <tt>Object</tt> performs a
411          * specific cloning operation. First, if the class of this object does
412          * not implement the interface <tt>Cloneable</tt>, then a
413          * <tt>CloneNotSupportedException</tt> is thrown. Note that all arrays
414          * are considered to implement the interface <tt>Cloneable</tt>.
415          * Otherwise, this method creates a new instance of the class of this
416          * object and initializes all its fields with exactly the contents of
417          * the corresponding fields of this object, as if by assignment; the
418          * contents of the fields are not themselves cloned. Thus, this method
419          * performs a "shallow copy" of this object, not a "deep copy" operation.
420          * <p/>
421          * The class <tt>Object</tt> does not itself implement the interface
422          * <tt>Cloneable</tt>, so calling the <tt>clone</tt> method on an object
423          * whose class is <tt>Object</tt> will result in throwing an
424          * exception at run time.
425          *
426          * @return a clone of this instance.
427          * @throws CloneNotSupportedException if the object's class does not
428          *                                    support the <code>Cloneable</code> interface. Subclasses
429          *                                    that override the <code>clone</code> method can also
430          *                                    throw this exception to indicate that an instance cannot
431          *                                    be cloned.
432          * @see Cloneable
433          */
434         public Object clone() throws CloneNotSupportedException {
435             return super.clone();
436         }
437     }
438 
439     /**
440      * When the <code>MemoryStore</code> overflows, and there is no disk
441      * store, then the element gets automatically removed. This should
442      * trigger an eviction notification.
443      */
444     public void testEvictionFromLRUMemoryStoreNoExpiry() throws IOException, CacheException, InterruptedException {
445         String sampleCache2 = "sampleCache2";
446         cache = manager.getCache(sampleCache2);
447         cache.removeAll();
448         for (int i = 0; i < 10; i++) {
449             cache.put(new Element(i + "", new Date()));
450         }
451         cache.put(new Element(11 + "", new Date()));
452         List evictionNotifications = CountingCacheEventListener.getCacheElementsEvicted(cache);
453         assertEquals(1, evictionNotifications.size());
454 
455         List expiryNotifications = CountingCacheEventListener.getCacheElementsExpired(cache);
456         assertEquals(0, expiryNotifications.size());
457     }
458 
459     /**
460      * When the <code>MemoryStore</code> overflows, and there is no disk
461      * store, then the element gets automatically evicted. This should
462      * trigger an eviction notification.
463      */
464     public void testEvictionFromLRUMemoryStoreNotSerializable() throws IOException, CacheException, InterruptedException {
465         String sampleCache1 = "sampleCache1";
466         cache = manager.getCache(sampleCache1);
467         cache.removeAll();
468 
469         //should trigger a removal notification because it is not Serializable and will be evicted
470         cache.put(new Element(12 + "", new Object()));
471 
472         for (int i = 0; i < 10; i++) {
473             cache.put(new Element(i + "", new Date()));
474         }
475 
476         List evictionNotifications = CountingCacheEventListener.getCacheElementsEvicted(cache);
477         assertEquals(1, evictionNotifications.size());
478 
479         List expiryNotifications = CountingCacheEventListener.getCacheElementsExpired(cache);
480         assertEquals(0, expiryNotifications.size());
481     }
482 
483     /**
484      * When the <code>MemoryStore</code> overflows, and there is no disk
485      * store, then the element gets automatically removed. This should
486      * trigger a remove notification.
487      * <p/>
488      * If the element has expired, it should instead trigger an expiry notification.
489      */
490     public void testEvictionFromLRUMemoryStoreExpiry() throws IOException, CacheException, InterruptedException {
491         String sampleCache2 = "sampleCache2";
492         cache = manager.getCache(sampleCache2);
493         cache.removeAll();
494         for (int i = 0; i < 10; i++) {
495             cache.put(new Element(i + "", new Date()));
496         }
497 
498         Thread.sleep(1030);
499         cache.put(new Element(11 + "", new Date()));
500 
501         List removalNotifications = CountingCacheEventListener.getCacheElementsEvicted(cache);
502         assertEquals(0, removalNotifications.size());
503 
504         List expiryNotifications = CountingCacheEventListener.getCacheElementsExpired(cache);
505         assertEquals(1, expiryNotifications.size());
506     }
507 
508     /**
509      * When the <code>MemoryStore</code> overflows, and there is no disk
510      * store, then the element gets automatically evicted. This should
511      * trigger a notification.
512      */
513     public void testEvictionFromFIFOMemoryStoreNoExpiry() throws IOException, CacheException {
514         String sampleCache3 = "sampleCache3";
515         cache = manager.getCache(sampleCache3);
516         cache.removeAll();
517         for (int i = 0; i < 10; i++) {
518             cache.put(new Element(i + "", new Date()));
519         }
520 
521         cache.put(new Element(11 + "", new Date()));
522 
523         List removalNotifications = CountingCacheEventListener.getCacheElementsEvicted(cache);
524         assertEquals(1, removalNotifications.size());
525 
526         List expiryNotifications = CountingCacheEventListener.getCacheElementsExpired(cache);
527         assertEquals(0, expiryNotifications.size());
528     }
529 
530     /**
531      * When the <code>MemoryStore</code> overflows, and there is no disk
532      * store, then the element gets automatically evicted. This should
533      * trigger a notification.
534      * <p/>
535      * If the element has expired, it should instead trigger an expiry notification.
536      */
537     public void testEvictionFromFIFOMemoryStoreExpiry() throws IOException, CacheException, InterruptedException {
538         String sampleCache3 = "sampleCache3";
539         cache = manager.getCache(sampleCache3);
540         cache.removeAll();
541         for (int i = 0; i < 10; i++) {
542             cache.put(new Element(i + "", new Date()));
543         }
544 
545         Thread.sleep(1020);
546         cache.put(new Element(11 + "", new Date()));
547 
548         List notifications = CountingCacheEventListener.getCacheElementsEvicted(cache);
549         assertEquals(0, notifications.size());
550 
551         List expiryNotifications = CountingCacheEventListener.getCacheElementsExpired(cache);
552         assertEquals(1, expiryNotifications.size());
553     }
554 
555     /**
556      * When the <code>MemoryStore</code> overflows, and there is no disk
557      * store, then the element gets automatically evicted. This should
558      * trigger a notification.
559      */
560     public void testEvictionFromLFUMemoryStoreNoExpiry() throws IOException, CacheException {
561         String sampleCache4 = "sampleCache4";
562         cache = manager.getCache(sampleCache4);
563         cache.removeAll();
564         for (int i = 0; i < 10; i++) {
565             cache.put(new Element(i + "", new Date()));
566         }
567 
568         cache.put(new Element(11 + "", new Date()));
569 
570         List notifications = CountingCacheEventListener.getCacheElementsEvicted(cache);
571         assertEquals(1, notifications.size());
572 
573         List expiryNotifications = CountingCacheEventListener.getCacheElementsExpired(cache);
574         assertEquals(0, expiryNotifications.size());
575     }
576 
577     /**
578      * When the <code>MemoryStore</code> overflows, and there is no disk
579      * store, then the element gets automatically removed. This should
580      * trigger a notification.
581      * <p/>
582      * If the element has expired, it should instead trigger an expiry notification.
583      */
584     public void testEvictionFromLFUMemoryStoreExpiry() throws IOException, CacheException, InterruptedException {
585         String sampleCache4 = "sampleCache4";
586         cache = manager.getCache(sampleCache4);
587         cache.removeAll();
588         for (int i = 0; i < 10; i++) {
589             cache.put(new Element(i + "", new Date()));
590         }
591 
592         Thread.sleep(1020);
593         cache.put(new Element(11 + "", new Date()));
594 
595         List notifications = CountingCacheEventListener.getCacheElementsEvicted(cache);
596         assertEquals(0, notifications.size());
597 
598         List expiryNotifications = CountingCacheEventListener.getCacheElementsExpired(cache);
599         assertEquals(1, expiryNotifications.size());
600     }
601 
602 
603     /**
604      * Tests expiry notification is hooked up to searchInMemoryStore
605      *
606      * @throws InterruptedException
607      * @throws CacheException
608      */
609     public void testExpiryViaMemoryStoreCheckingOnGet() throws InterruptedException, CacheException, IOException {
610 
611         cache.removeAll();
612         CountingCacheEventListener.resetCounters();
613 
614         Serializable key = "1";
615         Serializable value = new Date();
616         Element element = new Element(key, value);
617 
618         //Check expiry from memory store in 1 second
619         cache.put(element);
620         Thread.sleep(1020);
621 
622         //Trigger expiry
623         cache.get(key);
624         List notifications = CountingCacheEventListener.getCacheElementsExpired(cache);
625         assertEquals(1, notifications.size());
626         assertEquals(element, notifications.get(0));
627     }
628 
629     /**
630      * Tests expiry notification is hooked up to searchInDiskStore.
631      * This test is not exact, because the expiry thread may also expire some.
632      *
633      * @throws InterruptedException
634      * @throws CacheException
635      */
636     public void testExpiryViaDiskStoreCheckingOnGet() throws InterruptedException, CacheException, IOException {
637         //Overflow 10 elements to disk store
638         for (int i = 0; i < 20; i++) {
639             Element element = new Element("" + i, new Date());
640             cache.put(element);
641         }
642 
643         //Wait for expiry
644         Thread.sleep(1020);
645 
646         //Trigger expiry
647         for (int i = 0; i < 20; i++) {
648             cache.get("" + i);
649         }
650 
651         List notifications = CountingCacheEventListener.getCacheElementsExpired(cache);
652         for (int i = 0; i < notifications.size(); i++) {
653             Element element = (Element) notifications.get(i);
654             element.getObjectKey();
655         }
656         assertTrue(notifications.size() >= 10);
657     }
658 
659 
660     /**
661      * Tests expiry thread expiry
662      *
663      * @throws InterruptedException
664      */
665     public void testExpiryViaDiskStoreExpiryThread() throws InterruptedException {
666         //Overflow 10 elements to disk store
667         for (int i = 0; i < 20; i++) {
668             Element element = new Element("" + i, new Date());
669             cache.put(element);
670         }
671 
672         //Wait for expiry and expiry thread
673         Thread.sleep(2120);
674 
675         List notifications = CountingCacheEventListener.getCacheElementsExpired(cache);
676         for (int i = 0; i < notifications.size(); i++) {
677             Element element = (Element) notifications.get(i);
678             element.getObjectKey();
679         }
680         assertEquals(10, notifications.size());
681 
682     }
683 
684 
685     /**
686      * Tests that elements evicted from disk are notified to any listeners.
687      * fails from full ant builds?
688      */
689     public void xTestEvictionFromDiskStoreWithExpiry() throws IOException, CacheException, InterruptedException {
690 
691         cache.removeAll();
692         //Overflow 10 elements to disk store which is maximum
693         for (int i = 0; i < 20; i++) {
694             Element element = new Element("" + i, new Date());
695             cache.put(element);
696         }
697         cache.put(new Element(21 + "", new Date()));
698         Thread.sleep(2100);
699 
700         List notifications = CountingCacheEventListener.getCacheElementsEvicted(cache);
701         assertEquals(1, notifications.size());
702 
703         List expiryNotifications = CountingCacheEventListener.getCacheElementsExpired(cache);
704         assertEquals(10, expiryNotifications.size());
705     }
706 
707 }