libreccm-legacy/ccm-core/src/com/arsdigita/caching/CacheServlet.java

313 lines
11 KiB
Java
Executable File

/*
* Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package com.arsdigita.caching;
import com.arsdigita.domain.DomainCollection;
import com.arsdigita.persistence.Filter;
import com.arsdigita.persistence.Session;
import com.arsdigita.persistence.SessionManager;
import com.arsdigita.util.servlet.HttpHost;
import com.arsdigita.web.Host;
import com.arsdigita.web.ParameterMap;
import com.arsdigita.web.Web;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URLDecoder;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
/**
* <p> A simple servlet which accepts the cache notifications issued by other
* webserver to implement <em>coherent caching</em>. Since there can be any
* number of webservers attached to the same database instance, we must make
* sure that caches among different webservers contain either the same,
* consistent value for a particular cache entry, or no value at all. </p>
*
* <p> Whenever a {@link CacheTable#put(String,Object)} is invoked, a HTTP
* request is sent to all the other JVMs running this service, (except the
* current server of course).
*
* <p> The HTTP notification for newly added cache entry carries the hashcode
* of the new entry, so that the peer caches can decide whether the entry they
* already have (if at all) is outdated or not. For
* {@link CacheTable#remove(String)} invocations, no hash code is produced, and
* peer caches will remove this item unconditionally. </p>
*
* @author Matthew Booth
* @author Sebastian Skracic
*
* @version $Revision: #19 $ $DateTime: 2004/08/16 18:10:38 $
*/
public class CacheServlet extends HttpServlet {
private static final Logger s_log =
Logger.getLogger( CacheServlet.class );
private static final String ID = "id";
private static final String KEY = "key";
private static final String HASH = "hash";
private static final String REMOVEALL = "removeAll";
// If you change this, make sure that web.xml is changed as well
static final String SERVLET_URL = "/expireCache";
/**
* This is executed when foreign server asked us to drop an entry
* from our cache. Make sure that we don't end up in recursion.
*/
protected void doGet( HttpServletRequest req, HttpServletResponse res ) {
String id = req.getParameter( ID );
String key = req.getParameter( KEY );
String removeAll = req.getParameter( REMOVEALL );
if (s_log.isInfoEnabled()) {
s_log.info("Got remove request from " + req.getRemoteHost());
}
if (id != null && key != null){
//normal expire cache entry request
if (s_log.isInfoEnabled()) {
s_log.info("Got remove request from " + req.getRemoteHost());
}
String hash = req.getParameter( HASH );
expireCacheEntry(id, key, hash);
} else if (id != null && key == null && removeAll != null) {
//purge a single cache request
if (s_log.isInfoEnabled()) {
s_log.info("Got remove all entries request from " + req.getRemoteHost());
}
if(removeAll.equals("true")){
removeCacheEntries(id);
}
} else if (id == null && key == null && removeAll != null) {
//purge all caches request
if (s_log.isInfoEnabled()) {
s_log.info("Got remove all cache request from " + req.getRemoteHost());
}
if(removeAll.equals("true")){
removeAllCache();
}
} else {
s_log.error("Got an invalid cache request from " + req.getRemoteHost());
}
}
protected void expireCacheEntry(String id, String key, String hash) {
id = URLDecoder.decode(id);
key = URLDecoder.decode(key);
final CacheTable cache = CacheTable.getCache( id );
if (cache == null) {
s_log.debug("No cache with id " + id);
return;
}
s_log.debug("Removing " + key + " from cache " + id);
final Integer hashCode = getHashCode(hash);
if (hashCode == null) {
// unconditionally remove
cache.removeLocally(key);
} else {
cache.removeLocallyIfOutdated(key, hashCode.intValue());
}
}
protected void removeCacheEntries(String id) {
id = URLDecoder.decode(id);
final CacheTable cache = CacheTable.getCache( id );
if (cache == null) {
s_log.debug("No cache with id " + id);
return;
}
s_log.debug("Removing all entries from cache " + id);
// unconditionally remove
cache.removeAllEntriesLocally();
}
protected void removeAllCache() {
s_log.debug("Removing all Cache tables");
// unconditionally remove all
CacheTable.removeAllCacheTablesLocally();
}
private Integer getHashCode(final String hash) {
if (hash == null) {
return null;
}
Integer hashCode = null;
try {
hashCode = new Integer(hash);
} catch (NumberFormatException nfe) {
// just ignore and pretend that no hash value was supplied at all
s_log.warn("format exception on hash " + hash + " : " + nfe.getMessage() );
}
return hashCode;
}
/**
* Complete removal - first get rid of entry in local cache,
* then annoy other servers.
*/
static void remove( String cache_id, String key ) {
CacheTable cache = CacheTable.getCache( cache_id );
if (cache == null) {
return;
}
cache.removeLocally(key);
removeFromPeers(cache_id, key);
}
/**
* remote all entries from all purge-able tables in the peer's.
*
* The fact that there is no ID parameter tells the
* peers to purge all cache tables.
*/
static void removeAllFromPeers() {
final ParameterMap params = new ParameterMap();
params.setParameter(REMOVEALL, "true");
notifyPeers(params);
}
/**
* remote all entries from the peer's cache table with an id of cacheID.
*
* @param cacheID id of the cache table to purge
*/
static void removeAllEntriesFromPeersTable(String cacheID) {
final ParameterMap params = new ParameterMap();
params.setParameter(ID, cacheID);
params.setParameter(REMOVEALL, "true");
notifyPeers(params);
}
/**
* Sometimes we need to remove entries only from peer webservers.
*/
static void removeFromPeers(String cache_id,
String key) {
notifyPeers(cache_id, key, null);
}
/**
* Notifies peers on adding a new cache entry. Deletes the peer's cache entry
* if its contain the object with hashcode not matching <tt>newHashCode</tt>.
*/
static void removeOutdatedFromPeers(String cache_id,
String key,
int newHashCode) {
notifyPeers(cache_id, key, String.valueOf(newHashCode));
}
/**
* Sends "GET /expireCache?" + params to all peer webservers.
*/
private static void notifyPeers(final String id,
final String key,
final String hash) {
notifyPeers(makeParameterMap(id, key, hash));
}
private static void notifyPeers(ParameterMap params) {
if (!Web.getConfig().getDeactivateCacheHostNotifications()) {
s_log.debug("about to notify peers");
final Session session = SessionManager.getSession();
if (session == null) {
s_log.debug("Server is bootstrapping, disabling peer notification");
return;
}
final DomainCollection hosts = Host.retrieveAll();
final HttpHost current = Web.getConfig().getHost();
Filter f = hosts.addFilter(" not ( " + Host.SERVER_NAME + " = :currName "
+ " and " + Host.SERVER_PORT + " = :currPort )");
f.set("currName", current.getName());
f.set("currPort", new Integer(current.getPort()));
while (hosts.next()) {
final Host host = (Host) hosts.getDomainObject();
notifyPeer(host, params);
}
}
}
private static void notifyPeer(Host host, ParameterMap params) {
final String url = "http://" + host + SERVLET_URL + params;
try {
s_log.debug("sending notification to " + url);
java.net.URL netURL = new java.net.URL(url);
new Thread(new HTTPRequester(netURL)).start();
} catch(MalformedURLException e) {
s_log.error("malformed URL: " + url);
}
}
private static ParameterMap makeParameterMap(final String id,
final String key,
final String hash) {
final ParameterMap params = new ParameterMap();
params.setParameter(ID, id);
if (key != null) {
params.setParameter(KEY, key);
}
if (hash != null) {
params.setParameter(HASH, hash);
}
return params;
}
private static class HTTPRequester implements Runnable {
private static final Logger s_log =
Logger.getLogger( HTTPRequester.class );
private final java.net.URL m_url;
public HTTPRequester(java.net.URL url) {
m_url = url;
}
public void run() {
try {
if (s_log.isDebugEnabled()) {
s_log.debug("Sending invalidate to " + m_url);
}
m_url.openStream();
// XXX check status is 200, or rather, not an error code
} catch (IOException e) {
s_log.warn("Failure sending cache invalidate: " + m_url, e);
}
}
}
}