Added memory-based caching implementation.
authorwassa <wassa@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Wed, 4 Oct 2006 16:47:49 +0000 (16:47 +0000)
committerwassa <wassa@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Wed, 4 Oct 2006 16:47:49 +0000 (16:47 +0000)
git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/trunk@2054 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

src/edu/internet2/middleware/shibboleth/common/Cache.java
src/edu/internet2/middleware/shibboleth/common/provider/BaseCache.java
src/edu/internet2/middleware/shibboleth/common/provider/CookieCache.java
src/edu/internet2/middleware/shibboleth/common/provider/MemoryCache.java [new file with mode: 0644]
src/edu/internet2/middleware/shibboleth/common/provider/ServletSessionCache.java
tests/edu/internet2/middleware/shibboleth/common/provider/CacheTests.java

index c13cbb2..aecabe2 100644 (file)
@@ -17,6 +17,8 @@
 package edu.internet2.middleware.shibboleth.common;
 
 /**
+ * Defines an IdP-wide caching mechanism.
+ * 
  * @author Walter Hoehn
  */
 public interface Cache {
@@ -25,15 +27,47 @@ public interface Cache {
                CLIENT_SIDE, SERVER_SIDE, CLIENT_SERVER_SHARED
        }
 
+       /**
+        * Returns the identifier for the cache. This will commonly be a string name for the subsytem that is accessing the
+        * cache. Effectively acts as a namespace for the caching mechanisms.
+        */
        public String getName();
 
+       /**
+        * Returns an indication of how the cache stores its data. Subsystems may enforce storage requirements on caches via
+        * this mechanism.
+        */
        public CacheType getCacheType();
 
+       /**
+        * Causes the cache to return a value associated with a given key.
+        * 
+        * @throws CacheException
+        *             if an error was encountered while reading the cache
+        */
        public String retrieve(String key) throws CacheException;
 
+       /**
+        * Causes the cache to remove a value associated with a given key.
+        * 
+        * @throws CacheException
+        *             if an error was encountered while removing the value from the cache
+        */
        public void remove(String key) throws CacheException;
 
+       /**
+        * Boolean indication of whether or not the cache contains a value tied to a specified key.
+        * 
+        * @throws CacheException
+        *             if an error was encountered while reading the cache
+        */
        public boolean contains(String key) throws CacheException;
 
+       /**
+        * Causes the cache to associate a value with a given key for a specified number of seconds.
+        * 
+        * @throws CacheException
+        *             if the value could not be written to the cache
+        */
        public void store(String key, String value, long duration) throws CacheException;
 }
index eb61ac8..88ac7e7 100644 (file)
@@ -65,5 +65,10 @@ public abstract class BaseCache implements Cache {
                        this.value = value;
                        this.expiration = expireAt;
                }
+
+               protected boolean isExpired() {
+
+                       return (new Date().after(expiration));
+               }
        }
 }
index 64c92fe..d43fb84 100644 (file)
@@ -127,7 +127,7 @@ public class CookieCache extends BaseCache implements Cache {
                if (entry == null) { return false; }
 
                // Clean cache if it is expired
-               if (new Date().after(((CacheEntry) entry).expiration)) {
+               if ((((CacheEntry) entry).isExpired())) {
                        log.debug("Found expired object.  Deleting...");
                        totalCookies--;
                        dataCache.remove(key);
@@ -145,7 +145,7 @@ public class CookieCache extends BaseCache implements Cache {
                if (entry == null) { return null; }
 
                // Clean cache if it is expired
-               if (new Date().after(((CacheEntry) entry).expiration)) {
+               if ((((CacheEntry) entry).isExpired())) {
                        log.debug("Found expired object.  Deleting...");
                        totalCookies--;
                        dataCache.remove(key);
diff --git a/src/edu/internet2/middleware/shibboleth/common/provider/MemoryCache.java b/src/edu/internet2/middleware/shibboleth/common/provider/MemoryCache.java
new file mode 100644 (file)
index 0000000..536d562
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
+ *
+ * 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 edu.internet2.middleware.shibboleth.common.provider;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import org.apache.log4j.Logger;
+
+import edu.internet2.middleware.shibboleth.common.Cache;
+import edu.internet2.middleware.shibboleth.common.CacheException;
+
+/**
+ * <code>Cache</code> implementation that uses java objects to cache data. This implementation will reap expired
+ * entries.
+ * 
+ * @author Walter Hoehn
+ */
+
+public class MemoryCache extends BaseCache implements Cache {
+
+       private MemoryCacheCleaner cleaner = new MemoryCacheCleaner();
+       private static Logger log = Logger.getLogger(MemoryCache.class.getName());
+       private Map<String, CacheEntry> entries = Collections.synchronizedMap(new HashMap<String, CacheEntry>());
+
+       public MemoryCache(String name) {
+
+               super(name, Cache.CacheType.SERVER_SIDE);
+       }
+
+       public boolean contains(String key) throws CacheException {
+
+               CacheEntry entry = entries.get(key);
+               if (entry == null) { return false; }
+
+               // Clean cache if it is expired
+               if (entry.isExpired()) {
+                       log.debug("Found expired object.  Deleting...");
+                       entries.remove(key);
+                       return false;
+               }
+
+               // OK, we have it
+               return true;
+       }
+
+       public void remove(String key) throws CacheException {
+
+               entries.remove(key);
+       }
+
+       public String retrieve(String key) throws CacheException {
+
+               CacheEntry entry = entries.get(key);
+               if (entry == null) { return null; }
+
+               // Clean cache if it is expired
+               if (entry.isExpired()) {
+                       log.debug("Found expired object.  Deleting...");
+                       entries.remove(key);
+                       return null;
+               }
+
+               return entry.value;
+       }
+
+       public void store(String key, String value, long duration) throws CacheException {
+
+               entries.put(key, new CacheEntry(value, duration));
+       }
+
+       protected void destroy() {
+
+               synchronized (cleaner) {
+                       if (cleaner != null) {
+                               cleaner.shutdown = true;
+                               cleaner.interrupt();
+                       }
+               }
+       }
+
+       protected void finalize() throws Throwable {
+
+               super.finalize();
+               destroy();
+       }
+
+       private class MemoryCacheCleaner extends Thread {
+
+               private boolean shutdown = false;
+               private Thread master;
+
+               private MemoryCacheCleaner() {
+
+                       super("edu.internet2.middleware.shibboleth.idp.common.provider.MemoryCache.MemoryCacheCleaner");
+                       this.master = Thread.currentThread();
+                       setDaemon(true);
+                       if (getPriority() > Thread.MIN_PRIORITY) {
+                               setPriority(getPriority() - 1);
+                       }
+                       log.debug("Starting memory-based cache cleanup thread (" + getName() + ").");
+                       start();
+               }
+
+               public void run() {
+
+                       try {
+                               sleep(60 * 1000); // one minute
+                       } catch (InterruptedException e) {
+                               log.debug("Memory-based cache cleanup interrupted (" + getName() + ").");
+                       }
+                       while (true) {
+                               try {
+                                       if (!master.isAlive()) {
+                                               shutdown = true;
+                                               log.debug("Memory-based cache cleaner is orphaned (" + getName() + ").");
+                                       }
+                                       if (shutdown) {
+                                               log.debug("Stopping Memory-based cache cleanup thread (" + getName() + ").");
+                                               return;
+                                       }
+                                       log.debug("Memory-based cache cleanup thread searching for stale entries (" + getName() + ").");
+                                       Set<String> needsDeleting = new HashSet<String>();
+                                       synchronized (entries) {
+                                               Iterator<Entry<String, CacheEntry>> iterator = entries.entrySet().iterator();
+                                               while (iterator.hasNext()) {
+                                                       Entry<String, CacheEntry> entry = iterator.next();
+                                                       CacheEntry cacheEntry = entry.getValue();
+                                                       if (cacheEntry.isExpired()) {
+                                                               needsDeleting.add(entry.getKey());
+                                                       }
+                                               }
+
+                                       }
+                                       // release the lock to be friendly
+                                       Iterator deleteIterator = needsDeleting.iterator();
+                                       while (deleteIterator.hasNext()) {
+                                               synchronized (entries) {
+                                                       log.debug("Expiring an entry from the memory cache (" + getName() + ").");
+                                                       entries.remove(deleteIterator.next());
+                                               }
+                                       }
+                                       sleep(60 * 1000); // one minute
+                               } catch (InterruptedException e) {
+                                       log.debug("Memory-based cache cleanup interrupted (" + getName() + ").");
+                               }
+                       }
+               }
+       }
+}
index 483ca9f..5186783 100644 (file)
@@ -16,8 +16,6 @@
 
 package edu.internet2.middleware.shibboleth.common.provider;
 
-import java.util.Date;
-
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 
@@ -60,7 +58,7 @@ public class ServletSessionCache extends BaseCache implements Cache {
                if (object == null || !(object instanceof CacheEntry)) { return false; }
 
                // Clean cache if it is expired
-               if (new Date().after(((CacheEntry) object).expiration)) {
+               if (((CacheEntry) object).isExpired()) {
                        log.debug("Found expired object.  Deleting...");
                        session.removeAttribute(getInternalKeyName(key));
                        return false;
@@ -77,7 +75,7 @@ public class ServletSessionCache extends BaseCache implements Cache {
                if (object == null || !(object instanceof CacheEntry)) { return null; }
 
                // Clean cache if it is expired
-               if (new Date().after(((CacheEntry) object).expiration)) {
+               if (((CacheEntry) object).isExpired()) {
                        log.debug("Found expired object.  Deleting...");
                        session.removeAttribute(getInternalKeyName(key));
                        return null;
index 14533d5..1f94ee7 100644 (file)
@@ -56,7 +56,7 @@ public class CacheTests extends TestCase {
                super(name);
                BasicConfigurator.resetConfiguration();
                BasicConfigurator.configure();
-               Logger.getRootLogger().setLevel(Level.DEBUG);
+               Logger.getRootLogger().setLevel(Level.OFF);
        }
 
        public static void main(String[] args) {
@@ -105,6 +105,35 @@ public class CacheTests extends TestCase {
                }
        }
 
+       public void testMemoryCache() {
+
+               try {
+                       // Startup the cache
+                       Cache cache = new MemoryCache("foobar");
+
+                       // Make sure the cache starts clean
+                       assertNull("Cache contained errant record.", cache.retrieve("foo"));
+
+                       // Store and retrieve
+                       cache.store("foo", "bar", 99999);
+                       assertTrue("Cache expected to contain record.", cache.contains("foo"));
+                       assertEquals("Cache expected to contain record.", "bar", cache.retrieve("foo"));
+
+                       // Make sure expiration works
+                       cache.store("bar", "foo", 1);
+                       try {
+                               Thread.sleep(2000);
+                       } catch (InterruptedException e) {
+                               // Who cares
+                       }
+                       assertFalse("Cache expected to expire record.", cache.contains("bar"));
+                       assertEquals("Cache expected to expire record.", null, cache.retrieve("bar"));
+
+               } catch (CacheException e) {
+                       fail("Error exercising cache: " + e);
+               }
+       }
+
        public void testCookieCacheBasic() {
 
                try {