/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This 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 software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.ha.cachemanager;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.management.JMException;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Name;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import org.jboss.cache.Cache;
import org.jboss.cache.CacheFactory;
import org.jboss.cache.CacheStatus;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.ConfigurationRegistry;
import org.jboss.cache.config.RuntimeConfig;
import org.jboss.cache.config.XmlParsingConfigurationRegistry;
import org.jboss.cache.notifications.annotation.CacheListener;
import org.jboss.cache.notifications.annotation.CacheStarted;
import org.jboss.cache.notifications.annotation.CacheStopped;
import org.jboss.cache.notifications.event.CacheStartedEvent;
import org.jboss.cache.notifications.event.CacheStoppedEvent;
import org.jboss.logging.Logger;
import org.jboss.util.naming.NonSerializableFactory;
import org.jgroups.Channel;
import org.jgroups.ChannelFactory;
import org.jgroups.JChannel;
/**
* JBoss AS specific implementation of {@link CacheManager}. Extends the core JBoss Cache
* cache manager by also registering created caches
* in JMX, and by registering itself in JNDI.
*
* @author <a href="brian.stansberry@jboss.com">Brian Stansberry</a>
* @version $Revision: 1.1 $
*/
public class CacheManager
implements org.jboss.cache.CacheManager, MBeanRegistration, CacheManagerMBean
{
private static final Logger log = Logger.getLogger(CacheManager.class);
public static final String DEFAULT_CORE_CACHE_JMX_ATTRIBUTES = "service=Cache,config=";
private static final ThreadLocal<Boolean> starting = new ThreadLocal<Boolean>();
private MBeanServer mbeanServer;
private String jmxDomain;
private String coreCacheJmxAttributes = DEFAULT_CORE_CACHE_JMX_ATTRIBUTES;
private ConfigurationRegistry configRegistry;
private boolean configRegistryInjected;
private ChannelFactory channelFactory;
private final Map<String, Cache<Object, Object>> plainCaches = new HashMap<String, Cache<Object, Object>>();
private final Map<String, Integer> plainCacheCheckouts = new HashMap<String, Integer>();
private Map<String, String> configAliases = new HashMap<String, String>();
private boolean registerCachesInJmx = true;
private Map<String, Boolean> startupCaches = new HashMap<String, Boolean>();
private String jndiName;
private boolean started;
/**
* Create a new CacheManager.
*
*/
public CacheManager()
{
}
/**
* Create a new CacheManager.
*
* @param configRegistry
* @param factory
*/
public CacheManager(ConfigurationRegistry configRegistry, ChannelFactory factory)
{
this.configRegistry = configRegistry;
this.configRegistryInjected = true;
this.channelFactory = factory;
}
/**
* Create a new CacheManager.
*
* @param configFileName
* @param factory
*/
public CacheManager(String configFileName, ChannelFactory factory)
{
configRegistry = new XmlParsingConfigurationRegistry(configFileName);
this.channelFactory = factory;
}
// ------------------------------------------------------------ CacheManager
public ChannelFactory getChannelFactory()
{
return channelFactory;
}
public Set<String> getConfigurationNames()
{
Set<String> configNames = getPlainCacheConfigurationNames();
synchronized(configAliases)
{
configNames.addAll(configAliases.keySet());
}
return configNames;
}
public Set<String> getCacheNames()
{
synchronized (plainCaches)
{
return new HashSet<String>(plainCaches.keySet());
}
}
public Cache<Object, Object> getCache(String configName, boolean create) throws Exception
{
if (create)
{
checkStarted();
}
// Check if there's an alias involved
configName = resolveAlias(configName);
synchronized (plainCaches)
{
return wrapCache(getPlainCache(configName, create));
}
}
public void registerCache(Cache<Object, Object> cache, String configName)
{
checkStarted();
synchronized (plainCaches)
{
registerPlainCache(cache, configName);
if (registerCachesInJmx && mbeanServer != null)
{
String oName = getObjectName(getCoreCacheJmxAttributes(), configName);
org.jboss.cache.jmx.CacheJmxWrapper<Object, Object> wrapper =
new org.jboss.cache.jmx.CacheJmxWrapper<Object, Object>(cache);
try
{
mbeanServer.registerMBean(wrapper, new ObjectName(oName));
}
catch (JMException e)
{
throw new RuntimeException("Cannot register cache under name " + oName, e);
}
// Synchronize the start/stop of the cache
cache.addCacheListener(new StartStopListener(wrapper));
}
}
}
public void releaseCache(String configName)
{
// Check if there's an alias involved
configName = resolveAlias(configName);
synchronized (plainCaches)
{
if (!plainCaches.containsKey(configName))
throw new IllegalStateException(configName + " not registered");
if (decrementPlainCacheCheckout(configName) == 0)
{
Cache<Object, Object> cache = plainCaches.remove(configName);
destroyPlainCache(cache, configName);
}
}
}
// ----------------------------------------------------------------- Public
public void start() throws Exception
{
if (!started)
{
if (configRegistry == null)
throw new IllegalStateException("Must configure a ConfigurationRegistry before calling start()");
if (channelFactory == null)
throw new IllegalStateException("Must provide a ChannelFactory before calling start()");
try
{
starting.set(Boolean.TRUE);
if (!configRegistryInjected)
{
((XmlParsingConfigurationRegistry) configRegistry).start();
}
startEagerStartCaches();
CacheManagerLocator locator = CacheManagerLocator.getCacheManagerLocator();
if (locator.getDirectlyRegisteredManager() == null)
locator.registerCacheManager(this);
// Bind ourself in the public JNDI space if configured to do so
if (jndiName != null)
{
Context ctx = new InitialContext();
this.bind(jndiName, this, CacheManager.class, ctx);
log.debug("Bound in JNDI under " + jndiName);
}
}
finally
{
starting.remove();
}
started = true;
}
}
public void stop()
{
if (started)
{
releaseEagerStartCaches();
// synchronized (plainCaches)
// {
// for (Iterator<Map.Entry<String, Cache<Object, Object>>> it = plainCaches.entrySet().iterator(); it.hasNext();)
// {
// Map.Entry<String, Cache<Object, Object>> entry = it.next();
// destroyPlainCache(entry.getValue(), entry.getKey());
// it.remove();
// }
// plainCaches.clear();
// plainCacheCheckouts.clear();
// }
if (!configRegistryInjected)
{
((XmlParsingConfigurationRegistry) configRegistry).stop();
}
if (jndiName != null)
{
InitialContext ctx = null;
try
{
// the following statement fails when the server is being shut down (07/19/2007)
ctx = new InitialContext();
ctx.unbind(jndiName);
}
catch (Exception e) {
log.error("partition unbind operation failed", e);
}
finally
{
if (ctx != null)
{
try
{
ctx.close();
}
catch (NamingException e)
{
log.error("Caught exception closing naming context", e);
}
}
}
try
{
NonSerializableFactory.unbind (jndiName);
}
catch (NameNotFoundException e)
{
log.error("Caught exception unbinding from NonSerializableFactory", e);
}
}
CacheManagerLocator locator = CacheManagerLocator.getCacheManagerLocator();
if (locator.getDirectlyRegisteredManager() == this)
locator.deregisterCacheManager();
started = false;
}
}
// ------------------------------------------------------------- Properties
public ConfigurationRegistry getConfigurationRegistry()
{
return configRegistry;
}
public void setConfigurationRegistry(ConfigurationRegistry configRegistry)
{
this.configRegistry = configRegistry;
this.configRegistryInjected = true;
}
public void setChannelFactory(ChannelFactory channelFactory)
{
this.channelFactory = channelFactory;
}
public String getJmxDomain()
{
return jmxDomain;
}
public void setJmxDomain(String jmxDomain)
{
this.jmxDomain = jmxDomain;
}
public String getCoreCacheJmxAttributes()
{
return coreCacheJmxAttributes;
}
public void setCoreCacheJmxAttributes(String coreCacheJmxAttributes)
{
this.coreCacheJmxAttributes = coreCacheJmxAttributes;
}
public boolean getRegisterCachesInJmx()
{
return registerCachesInJmx;
}
public void setRegisterCachesInJmx(boolean register)
{
this.registerCachesInJmx = register;
}
public String getJndiName()
{
return jndiName;
}
public void setJndiName(String jndiName)
{
this.jndiName = jndiName;
}
public Map<String, String> getConfigAliases()
{
synchronized (configAliases)
{
return new HashMap<String, String>(configAliases);
}
}
public void setConfigAliases(Map<String, String> aliases)
{
synchronized (configAliases)
{
configAliases.clear();
if (aliases != null)
configAliases.putAll(aliases);
}
}
public void setEagerStartCaches(Set<String> configNames)
{
if (configNames != null)
{
for (String name : configNames)
{
startupCaches.put(name, Boolean.FALSE);
}
}
}
// ------------------------------------------------------ MBeanRegistration
public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception
{
this.mbeanServer = server;
if (jmxDomain == null)
{
jmxDomain = name.getDomain();
}
return name;
}
public void postDeregister()
{
// no-op
}
public void preDeregister() throws Exception
{
// no-op
}
public void postRegister(Boolean registrationDone)
{
// no-op
}
// -------------------------------------------------------------- Protected
/**
* Extension point for subclasses, where we actually use a
* {@link CacheFactory} to create a cache. This default implementation
* uses {@link DefaultCacheFactory}.
*
* @param config the Configuration for the cache
* @return the Cache
*/
protected Cache<Object, Object> createCache(Configuration config) throws Exception
{
Cache<Object, Object> cache = new DefaultCacheFactory<Object, Object>().createCache(config, false);
configureMuxUpHandlerChannel(cache);
return cache;
}
// ---------------------------------------------------------------- Private
private void checkStarted()
{
if (!started && Boolean.TRUE.equals(starting.get()) == false)
{
throw new IllegalStateException(getClass().getSimpleName() + " is not started.");
}
}
private int incrementCheckout(String configName)
{
synchronized (plainCacheCheckouts)
{
Integer count = plainCacheCheckouts.get(configName);
if (count == null)
count = Integer.valueOf(0);
Integer newVal = Integer.valueOf(count.intValue() + 1);
plainCacheCheckouts.put(configName, newVal);
return newVal;
}
}
private int decrementPlainCacheCheckout(String configName)
{
synchronized (plainCacheCheckouts)
{
Integer count = plainCacheCheckouts.get(configName);
if (count == null || count.intValue() < 1)
throw new IllegalStateException("invalid count of " + count + " for " + configName);
Integer newVal = Integer.valueOf(count.intValue() - 1);
plainCacheCheckouts.put(configName, newVal);
return newVal;
}
}
private void destroyPlainCache(Cache<Object, Object> cache, String configName)
{
if (cache.getCacheStatus() == CacheStatus.STARTED)
{
cache.stop();
}
if (cache.getCacheStatus() != CacheStatus.DESTROYED && cache.getCacheStatus() != CacheStatus.INSTANTIATED)
{
cache.destroy();
}
if (registerCachesInJmx && mbeanServer != null && !getCacheNames().contains(configName))
{
String oNameStr = getObjectName(getCoreCacheJmxAttributes(), configName);
try
{
ObjectName oName = new ObjectName(oNameStr);
if (mbeanServer.isRegistered(oName))
{
mbeanServer.unregisterMBean(oName);
}
}
catch (JMException e)
{
log.error("Problem unregistering CacheJmxWrapper " + oNameStr, e);
}
}
}
private void registerPlainCache(Cache<Object, Object> cache, String configName)
{
synchronized (plainCaches)
{
if (plainCaches.containsKey(configName))
throw new IllegalStateException(configName + " already registered");
plainCaches.put(configName, cache);
incrementCheckout(configName);
}
}
private Cache<Object, Object> getPlainCache(String configName, boolean create) throws Exception
{
Cache<Object, Object> cache;
synchronized (plainCaches)
{
cache = plainCaches.get(configName);
if (cache == null && create)
{
Configuration config = configRegistry.getConfiguration(configName);
if (channelFactory != null && config.getMultiplexerStack() != null)
{
config.getRuntimeConfig().setMuxChannelFactory(channelFactory);
}
cache = createCache(config);
registerCache(cache, configName);
}
else if (cache != null)
{
incrementCheckout(configName);
}
}
return cache;
}
private Set<String> getPlainCacheConfigurationNames()
{
synchronized (plainCaches)
{
Set<String> configNames = configRegistry == null ? new HashSet<String>()
: configRegistry.getConfigurationNames();
configNames.addAll(getCacheNames());
return configNames;
}
}
/**
* Wrap the cache to control classloading and disable stop/destroy
*/
private Cache<Object, Object> wrapCache(Cache<Object, Object> toWrap)
{
return toWrap == null ? null : new CacheManagerManagedCache<Object, Object>(toWrap);
}
private String getObjectName(String attributesBase, String configName)
{
String base = getJmxDomain() == null ? "" : getJmxDomain();
return base + ":" + attributesBase + configName;
}
private void configureMuxUpHandlerChannel(Cache<?, ?> cache) throws Exception
{
RuntimeConfig rc = cache.getConfiguration().getRuntimeConfig();
Channel channel = rc.getChannel();
if (channel == null)
{
// TODO we could deal with other JBC mechanisms of configuring Channels, but in
// reality the AS use cases that want MuxUpHandler shouldn't configure their
// JBC that way
ChannelFactory cf = rc.getMuxChannelFactory();
if (cf == null)
{
log.debug("Cache " + cache.getConfiguration().getClusterName() +
" does not have a ChannelFactory injected so MuxUpHandler cannot be integrated");
}
String stack = cache.getConfiguration().getMuxStackName();
if (stack == null)
{
log.debug("Cache " + cache.getConfiguration().getClusterName() +
" does not have a MuxStackName configured so MuxUpHandler cannot be integrated");
}
if (cf != null && stack != null)
{
// This doesn't result in JMX reg of channel
//channel = cf.createChannel(stack);
channel = cf.createMultiplexerChannel(stack, cache.getConfiguration().getClusterName());
rc.setChannel(new MuxHandlerChannel((JChannel) channel));
}
}
else if (channel.getUpHandler() == null)
{
// replace
rc.setChannel(new MuxHandlerChannel((JChannel) channel));
}
// else the Channel was injected and already had a handler -- shouldn't happen
}
@CacheListener
public static class StartStopListener
{
private final org.jboss.cache.jmx.CacheJmxWrapper<Object, Object> plainWrapper;
private StartStopListener(org.jboss.cache.jmx.CacheJmxWrapper<Object, Object> wrapper)
{
assert wrapper != null : "wrapper is null";
this.plainWrapper = wrapper;
}
@CacheStarted
public void cacheStarted(CacheStartedEvent event)
{
plainWrapper.start();
}
@CacheStopped
public void cacheStopped(CacheStoppedEvent event)
{
plainWrapper.stop();
}
}
/**
* Helper method that binds the partition in the JNDI tree.
* @param jndiName Name under which the object must be bound
* @param who Object to bind in JNDI
* @param classType Class type under which should appear the bound object
* @param ctx Naming context under which we bind the object
* @throws Exception Thrown if a naming exception occurs during binding
*/
private void bind(String jndiName, Object who, Class<?> classType, Context ctx) throws Exception
{
// Ah ! This service isn't serializable, so we use a helper class
//
NonSerializableFactory.bind(jndiName, who);
Name n = ctx.getNameParser("").parse(jndiName);
while (n.size () > 1)
{
String ctxName = n.get (0);
try
{
ctx = (Context)ctx.lookup (ctxName);
}
catch (NameNotFoundException e)
{
log.debug ("creating Subcontext " + ctxName);
ctx = ctx.createSubcontext (ctxName);
}
n = n.getSuffix (1);
}
// The helper class NonSerializableFactory uses address type nns, we go on to
// use the helper class to bind the service object in JNDI
//
StringRefAddr addr = new StringRefAddr("nns", jndiName);
Reference ref = new Reference(classType.getName (), addr, NonSerializableFactory.class.getName (), null);
ctx.rebind (n.get (0), ref);
}
private String resolveAlias(String configName)
{
String alias = configAliases.get(configName);
return alias == null ? configName : alias;
}
private void startEagerStartCaches() throws Exception
{
for (Map.Entry<String, Boolean> entry : startupCaches.entrySet())
{
Cache<Object, Object> cache = null;
cache = getCache(entry.getKey(), true);
if (cache.getCacheStatus() != CacheStatus.STARTED)
{
if (cache.getCacheStatus() != CacheStatus.CREATED)
{
cache.create();
}
cache.start();
}
}
}
private void releaseEagerStartCaches()
{
for (String name : startupCaches.keySet())
{
releaseCache(name);
}
}
}