/*
* JBoss, Home of Professional Open Source
* Copyright 2010, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt 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.framework.server.ispn;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.infinispan.Cache;
import org.infinispan.CacheException;
import org.infinispan.atomic.AtomicMap;
import org.infinispan.atomic.AtomicMapLookup;
import org.infinispan.manager.CacheContainer;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent;
import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.infinispan.util.SimpleImmutableEntry;
import org.jboss.ha.core.framework.server.ChannelSource;
import org.jboss.ha.core.framework.server.ManagedDistributedState;
import org.jboss.logging.Logger;
import org.jgroups.Channel;
/**
* This class manages distributed state across the cluster.
*
* The Cache layout will have two formats. The value collection will combine the category
* + DS key for the composite key. The DS value will be the value. It will look like:
* Cache<SimpleImmutableEntry<String Category, Serializable DS_Key>, Serializable DS_Value>
*
* The other Cache format will be the DS keys per category. It will look like:
* Cache<String, AtomicMap<Serializable, null>>
*
* If it ever becomes important to eliminate the "keys per category" (e.g. to save space),
* we would have to implement the getAllCategories() + getAllKeys() sequentially.
*
* @author <a href="mailto:sacha.labourey@cogito-info.ch">Sacha Labourey</a>.
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>.
* @author Scott.Stark@jboss.org
* @author Scott Marlow
* @author Paul Ferraro
*/
@Listener
public class DistributedState implements ManagedDistributedState, ChannelSource
{
private final ConcurrentMap<String, List<DSListener>> keyListeners = new ConcurrentHashMap<String, List<DSListener>>();
private final CacheContainerSource cacheHandler;
private volatile String cacheName = "DistributedState";
private volatile Cache<Serializable, Serializable> cache;
private static final Logger LOG = Logger.getLogger(DistributedState.class);
public DistributedState(CacheContainerSource cacheHandler)
{
this.cacheHandler = cacheHandler;
}
// Public --------------------------------------------------------
@Override
public void createService() throws Exception
{
}
@Override
public void startService() throws Exception
{
CacheContainer container = this.cacheHandler.getCacheContainer();
this.cache = (this.cacheName != null) ? container.<Serializable, Serializable>getCache(this.cacheName) : container.<Serializable, Serializable>getCache();
if (!this.cache.getStatus().allowInvocations())
{
this.cache.start();
}
this.cache.addListener(this);
}
@Override
public void stopService() throws Exception
{
this.cache.removeListener(this);
this.cache.stop();
}
@Override
public void destroyService() throws Exception
{
}
public void setCacheName(String cacheName)
{
this.cacheName = cacheName;
}
/*
TODO: jmx support, delete or bring mbean interface back.
public String listContent() throws Exception
{
StringBuilder result = new StringBuilder();
Collection<String> cats = this.getAllCategories();
if (cats == null) return result.toString();
Iterator<String> catsIter = cats.iterator();
while (catsIter.hasNext())
{
String category = (String) catsIter.next();
Iterator<Serializable> keysIter = this.getAllKeys(category).iterator();
result.append("-----------------------------------------------\n");
result.append("Logger : ").append(category).append("\n\n");
result.append("KEY\t:\tVALUE\n");
while (keysIter.hasNext())
{
Serializable key = (Serializable) keysIter.next();
String value = this.get(category, key).toString();
result.append("'").append(key);
result.append("'\t:\t'");
result.append(value);
result.append("'\n");
}
result.append("\n");
}
return result.toString();
}
public String listXmlContent() throws Exception
{
StringBuilder result = new StringBuilder();
result.append("<DistributedState>\n");
Collection<String> cats = this.getAllCategories();
if (cats != null)
{
Iterator<String> catsIter = cats.iterator();
while (catsIter.hasNext())
{
String category = (String) catsIter.next();
Iterator<Serializable> keysIter = this.getAllKeys(category).iterator();
result.append("\t<Logger>\n");
result.append("\t\t<LoggerName>").append(category).append("</LoggerName>\n");
while (keysIter.hasNext())
{
Serializable key = (Serializable) keysIter.next();
String value = this.get(category, key).toString();
result.append("\t\t<Entry>\n");
result.append("\t\t\t<Key>").append(key).append("</Key>\n");
result.append("\t\t\t<Value>").append(value).append("</Value>\n");
result.append("\t\t</Entry>\n");
}
result.append("\t</Logger>\n");
}
}
result.append("</DistributedState>\n");
return result.toString();
}
*/
/**
* {@inheritDoc}
* @see org.jboss.ha.core.framework.server.ChannelSource#getChannel()
*/
@Override
public Channel getChannel()
{
if (this.cache == null) return null;
JGroupsTransport transport = (JGroupsTransport) this.cache.getAdvancedCache().getRpcManager().getTransport();
return transport.getChannel();
}
// DistributedState implementation ----------------------------------------------
@Override
public void set(String category, Serializable key, Serializable value) throws Exception
{
this.set(category, key, value, true);
}
/*
* (non-Javadoc)
*
* @see org.jboss.ha.framework.interfaces.DistributedState#set(java.lang.String,
* java.io.Serializable, java.io.Serializable, boolean) @param
* asynchronousCall is not supported yet. TreeCache cannot switch this
* on the fly. Will take value from TreeCache-config instead.
*/
@Override
public void set(String category, Serializable key, Serializable value, boolean asynchronousCall) throws Exception
{
if (LOG.isTraceEnabled())
{
LOG.trace("set: category=" + category +", key=" + key +", value=" + value);
}
//if (this.replAsync != asynchronousCall)
//{
// if (asynchronousCall)
// {
// this.cache.put(this.buildFqn(category), key, value, Flag.FORCE_ASYNCHRONOUS);
// }
// else
// {
// this.cache.put(this.buildFqn(category), key, value, Flag.FORCE_SYNCHRONOUS);
// }
//}
this.cache.startBatch();
boolean commit = false;
try
{
this.cache.put(buildValueCollectionKey(category, key), value);
AtomicMap<Serializable, Void> m = getKeysPerCategoryCollection(category);
m.put(key, null);
commit = true;
}
finally
{
this.cache.endBatch(commit);
}
}
/*
* (non-Javadoc)
*
* @see org.jboss.ha.framework.interfaces.DistributedState#remove(java.lang.String,
* java.io.Serializable) @return - returns null in case of
* CacheException
*/
@Override
public Serializable remove(String category, Serializable key) throws Exception
{
return this.remove(category, key, true);
}
/*
* (non-Javadoc)
*
* @see org.jboss.ha.framework.interfaces.DistributedState#remove(java.lang.String,
* java.io.Serializable, boolean)
*/
@Override
public Serializable remove(String category, Serializable key, boolean asynchronousCall) throws Exception
{
if (LOG.isTraceEnabled())
{
LOG.trace("remove: category=" + category +", key=" + key);
}
Serializable retVal = this.get(category, key);
if (retVal != null)
{
// if (this.replAsync != asynchronousCall)
// {
// if (asynchronousCall)
// {
// this.cache.getInvocationContext().getOptionOverrides().setForceAsynchronous(true);
// }
// else
// {
// this.cache.getInvocationContext().getOptionOverrides().setForceSynchronous(true);
// }
// }
cache.startBatch();
boolean commit = false;
try
{
cache.remove(buildValueCollectionKey(category, key));
AtomicMap<Serializable, Void> m = getKeysPerCategoryCollection(category);
m.remove(key);
commit = true;
}
finally
{
cache.endBatch(commit);
}
}
if (LOG.isTraceEnabled())
{
LOG.trace("remove: category=" + category +", key=" + key +", removed object=" + retVal);
}
return retVal;
}
/*
* (non-Javadoc)
*
* @see org.jboss.ha.framework.interfaces.DistributedState#get(java.lang.String,
* java.io.Serializable)
*/
@Override
public Serializable get(String category, Serializable key)
{
try
{
return this.cache.get(buildValueCollectionKey(category, key));
}
catch (CacheException ce)
{
return null;
}
}
@Override
public Collection<String> getAllCategories()
{
try
{
Set<Serializable> keys = this.cache.keySet();
if (keys.isEmpty()) return null;
List<String> categories = new LinkedList<String>();
for (Serializable key: keys)
{
if (key instanceof String) // only the keys that are Strings, are categories
{
categories.add((String) key);
}
}
return categories;
}
catch (CacheException ce)
{
return null;
}
}
/*
* (non-Javadoc)
*
* @see org.jboss.ha.framework.interfaces.DistributedState#getAllKeys(java.lang.String)
* @return - returns null in case of CacheException
*/
@Override
public Collection<Serializable> getAllKeys(String category)
{
try
{
AtomicMap<Serializable, Void> m = getKeysPerCategoryCollection(category);
return m.keySet(); // the keyset are the DS keys
}
catch (CacheException ce)
{
return null;
}
}
/*
* (non-Javadoc)
*
* @see org.jboss.ha.framework.interfaces.DistributedState#getAllValues(java.lang.String)
* @return - returns null in case of CacheException
*/
@Override
public Collection<Serializable> getAllValues(String category)
{
try
{
Collection<Serializable> keys = getAllKeys(category);
if (keys == null) return null;
List<Serializable> values = new ArrayList<Serializable>();
for (Serializable key: keys)
{
values.add(cache.get(buildValueCollectionKey(category, key)));
}
return values;
}
catch (CacheException ce)
{
return null;
}
}
@Override
public void registerDSListenerEx(String category, @SuppressWarnings("deprecation") DSListenerEx subscriber)
{
this.registerDSListener(category, subscriber);
}
@Override
public void unregisterDSListenerEx(String category, @SuppressWarnings("deprecation") DSListenerEx subscriber)
{
this.unregisterDSListener(category, subscriber);
}
@Override
public void registerDSListener(String category, DSListener subscriber)
{
List<DSListener> listeners = this.keyListeners.get(category);
if (listeners == null)
{
listeners = new CopyOnWriteArrayList<DSListener>();
List<DSListener> existing = this.keyListeners.putIfAbsent(category, listeners);
if (existing != null )
{
listeners = existing; // use the listeners added by other thread
}
}
listeners.add(subscriber);
}
@Override
public void unregisterDSListener(String category, DSListener subscriber)
{
List<DSListener> listeners = this.keyListeners.get(category);
if (listeners != null)
{
listeners.remove(subscriber);
this.keyListeners.remove(category, Collections.emptyList());
}
}
// ChannelSource -------------------------------------------------
// public Channel getChannel()
// {
// Channel result = null;
// if (cache != null)
// {
// result = cache.getConfiguration().getRuntimeConfig().getChannel();
// }
// return result;
// }
// Package protected ---------------------------------------------
protected void notifyKeyListeners(String category, Serializable key, Serializable value, boolean locallyModified)
{
List<DSListener> listeners = this.keyListeners.get(category);
if (listeners == null) return;
for (DSListener listener: listeners)
{
listener.valueHasChanged(category, key, value, locallyModified);
}
}
protected void notifyKeyListenersOfRemove(String category, Serializable key, Serializable oldContent,
boolean locallyModified)
{
List<DSListener> listeners = this.keyListeners.get(category);
if (listeners == null) return;
for (DSListener listener: listeners)
{
listener.keyHasBeenRemoved(category, key, oldContent, locallyModified);
}
}
protected void cleanupKeyListeners()
{
keyListeners.clear();
}
/** ExtendedTreeCacheListener methods */
// Private -------------------------------------------------------
// @CacheListener -------------------------------------------------
@CacheEntryModified
public void nodeModified(CacheEntryModifiedEvent event)
{
if (event.isPre()) return;
Object key = event.getKey();
if (key instanceof SimpleImmutableEntry)
{
@SuppressWarnings("unchecked")
SimpleImmutableEntry<String, Serializable> entry = (SimpleImmutableEntry<String, Serializable>) key;
notifyKeyListeners(entry.getKey(), entry.getValue(), (Serializable) event.getValue(), event.isOriginLocal());
}
}
@CacheEntryRemoved
public void nodeRemoved(CacheEntryRemovedEvent event)
{
if (event.isPre()) return;
Object key = event.getKey();
if (key instanceof SimpleImmutableEntry)
{
@SuppressWarnings("unchecked")
SimpleImmutableEntry<String, Serializable> entry = (SimpleImmutableEntry<String, Serializable>) key;
notifyKeyListenersOfRemove(entry.getKey(), entry.getValue(), (Serializable) event.getValue(), event.isOriginLocal());
}
}
private SimpleImmutableEntry<String, Serializable> buildValueCollectionKey(String category, Serializable dskey)
{
return new SimpleImmutableEntry<String, Serializable>(category, dskey);
}
private AtomicMap<Serializable, Void> getKeysPerCategoryCollection(String category)
{
return AtomicMapLookup.getAtomicMap(this.cache, category, true);
}
}