/*
* JBoss, Home of Professional Open Source.
* Copyright 2000 - 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.cache;
import net.jcip.annotations.ThreadSafe;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import static org.jboss.cache.Region.Type.ANY;
import static org.jboss.cache.Region.Type.EVICTION;
import static org.jboss.cache.Region.Type.MARSHALLING;
import org.jboss.cache.buddyreplication.BuddyFqnTransformer;
import org.jboss.cache.buddyreplication.BuddyManager;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.config.ConfigurationException;
import org.jboss.cache.config.EvictionConfig;
import org.jboss.cache.config.EvictionRegionConfig;
import org.jboss.cache.eviction.EvictionTimerTask;
import org.jboss.cache.factories.annotations.Destroy;
import org.jboss.cache.factories.annotations.Inject;
import org.jboss.cache.factories.annotations.Start;
import org.jboss.cache.factories.annotations.Stop;
import org.jboss.cache.jmx.annotations.MBean;
import org.jboss.cache.jmx.annotations.ManagedAttribute;
import org.jboss.cache.jmx.annotations.ManagedOperation;
import org.jboss.cache.lock.LockManager;
import org.jboss.cache.util.concurrent.locks.LockContainer;
import org.jboss.cache.util.concurrent.locks.ReentrantSharedLockContainer;
import org.jgroups.Address;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* The default region manager, used with MVCC locking.
*
* @author Manik Surtani
* @since 3.0.0
*/
@ThreadSafe
@MBean(objectName = "RegionManager", description = "Manages eviction and marshalling regions")
public class RegionManagerImpl implements RegionManager
{
/**
* The default region used in XML configuration files when defining eviction policies. Any
* eviction settings bound under this 'default' Fqn is appplied to {@link org.jboss.cache.Fqn#ROOT} internally so
* any region that is not explicitly defined comes under the settings defined for this default.
*/
public static final Fqn DEFAULT_REGION = Fqn.fromString("/_default_");
private RegionRegistry regionsRegistry;
private boolean defaultInactive;
protected static final Log log = LogFactory.getLog(RegionManagerImpl.class);
protected static final boolean trace = log.isTraceEnabled();
CacheSPI, ?> cache;
private boolean usingEvictions;
private EvictionConfig evictionConfig;
private final EvictionTimerTask evictionTimerTask = new EvictionTimerTask();
private final LockContainer regionLocks = new ReentrantSharedLockContainer(4);
protected Configuration configuration;
protected RPCManager rpcManager;
protected LockManager lockManager;
protected BuddyFqnTransformer buddyFqnTransformer;
private boolean isUsingBR;
// -------- region lock helpers
protected final boolean isRegionLocked(Fqn fqn)
{
return regionLocks.isLocked(fqn);
}
protected final void lock(Fqn fqn)
{
regionLocks.acquireLock(fqn);
}
protected final void unlock(Fqn fqn)
{
regionLocks.releaseLock(fqn);
}
@Inject
public void injectDependencies(CacheSPI cache, Configuration configuration, RPCManager rpcManager, LockManager lockManager,
BuddyFqnTransformer transformer, RegionRegistry regionsRegistry)
{
this.cache = cache;
this.rpcManager = rpcManager;
this.configuration = configuration;
this.lockManager = lockManager;
this.buddyFqnTransformer = transformer;
this.regionsRegistry = regionsRegistry;
}
@Start
public void start()
{
if (trace) log.trace("Starting region manager");
isUsingBR = configuration.getBuddyReplicationConfig() != null && configuration.getBuddyReplicationConfig().isEnabled();
evictionConfig = configuration.getEvictionConfig();
if (evictionConfig != null && evictionConfig.isValidConfig())
{
this.evictionConfig = configuration.getEvictionConfig();
// start with the default region
EvictionRegionConfig defaultRegion = configuration.getEvictionConfig().getDefaultEvictionRegionConfig();
if (defaultRegion.getEvictionAlgorithmConfig() != null) defaultRegion.getEvictionAlgorithmConfig().validate();
// validate individual region configs now
for (EvictionRegionConfig erc : configuration.getEvictionConfig().getEvictionRegionConfigs())
{
evictionConfig.applyDefaults(erc);
erc.validate();
}
setEvictionConfig(configuration.getEvictionConfig());
setUsingEvictions(true);
}
else
{
setUsingEvictions(false);
log.debug("Not using an EvictionPolicy");
}
setDefaultInactive(configuration.isInactiveOnStartup());
if (isUsingEvictions())
{
evictionTimerTask.init(evictionConfig.getWakeupInterval(), configuration.getRuntimeConfig().getEvictionTimerThreadFactory(), regionsRegistry);
}
}
@Stop
protected void stop()
{
if (isUsingEvictions()) evictionTimerTask.stop();
}
@Destroy
protected void destroy()
{
regionsRegistry.clear();
regionLocks.reset();
}
public boolean isUsingEvictions()
{
return usingEvictions;
}
public boolean isDefaultInactive()
{
return defaultInactive;
}
public void setDefaultInactive(boolean defaultInactive)
{
this.defaultInactive = defaultInactive;
Region defaultRegion = regionsRegistry.get(Fqn.ROOT);
if (defaultRegion != null) defaultRegion.setActive(!defaultInactive);
}
public void setContextClassLoaderAsCurrent(Fqn fqn)
{
if (fqn.isChildOf(BuddyManager.BUDDY_BACKUP_SUBTREE_FQN))
{
if (fqn.size() <= 2)
{
fqn = Fqn.ROOT;
}
else
{
fqn = fqn.getSubFqn(2, fqn.size());
}
}
Region region = getRegion(fqn, false);
ClassLoader regionCL = (region == null) ? null : region.getClassLoader();
if (regionCL != null)
{
Thread.currentThread().setContextClassLoader(regionCL);
}
}
public Region getRegion(Fqn fqn, boolean createIfAbsent)
{
return getRegion(fqn, ANY, createIfAbsent);
}
public Region getValidMarshallingRegion(Fqn fqn)
{
if (fqn == null) return null;
return getRegion(fqn, Region.Type.MARSHALLING, false);
}
public Region getRegion(Fqn fqn, Region.Type type, boolean createIfAbsent)
{
if (isUsingBR && fqn != null && buddyFqnTransformer.isBackupFqn(fqn))
{
fqn = buddyFqnTransformer.getActualFqn(fqn);
}
if (trace) log.trace("Contents of RegionsRegistry: " + regionsRegistry);
Fqn fqnToUse = fqn;
if (DEFAULT_REGION.equals(fqnToUse)) fqnToUse = Fqn.ROOT;
// first see if a region for this specific Fqn exists
if (regionsRegistry.containsKey(fqnToUse))
{
Region r = regionsRegistry.get(fqnToUse);
// this is a very poor way of telling whether a region is a marshalling one or an eviction one. :-(
// mandates that class loaders be registered for marshalling regions.
if (type == ANY
|| (type == MARSHALLING && r.getClassLoader() != null)
|| (type == EVICTION && r.getEvictionRegionConfig() != null))
{
return r;
}
}
// if not, attempt to create one ...
if (createIfAbsent)
{
Region r = new RegionImpl(fqnToUse, this);
// could be created concurrently; so make sure we use appropriate methods on regionsRegistry for this.
Region previous = regionsRegistry.putIfAbsent(fqnToUse, r);
if (previous != null) r = previous;
if (type == MARSHALLING)
{
// insert current class loader into region so at least it is recognised as a marshalling region
r.registerContextClassLoader(getClass().getClassLoader());
}
return r;
}
// else try and find a parent which has a defined region, may return null if nothing is defined.
Region nextBestThing = null;
Fqn nextFqn = fqnToUse;
while (nextBestThing == null)
{
nextFqn = nextFqn.getParent();
if (regionsRegistry.containsKey(nextFqn))
{
Region r = regionsRegistry.get(nextFqn);
if (trace) log.trace("Trying next region " + nextFqn + " and got " + r);
// this is a very poor way of telling whether a region is a marshalling one or an eviction one. :-(
// mandates that class loaders be registered for marshalling regions.
if (type == ANY
|| (type == MARSHALLING && r.getClassLoader() != null)
|| (type == EVICTION && r.getEvictionRegionConfig() != null))
{
nextBestThing = r;
}
}
if (nextFqn.isRoot()) break;
}
// test if the default region has been defined. If not, and if the request
// is for an eviction region, return null
if (type == EVICTION && nextBestThing != null && nextBestThing.getFqn().isRoot() && !regionsRegistry.containsKey(Fqn.ROOT))
{
log.trace("No default eviction region; returning null");
nextBestThing = null;
}
return nextBestThing;
}
public Region getRegion(String fqn, boolean createIfAbsent)
{
return getRegion(Fqn.fromString(fqn), createIfAbsent);
}
public boolean removeRegion(Fqn fqn)
{
Region r = regionsRegistry.remove(fqn);
if (r == null) return false;
return true;
}
public EvictionTimerTask getEvictionTimerTask()
{
return evictionTimerTask;
}
public Configuration getConfiguration()
{
return configuration;
}
public void activate(Fqn fqn) throws RegionNotEmptyException
{
activate(fqn, false);
}
public void activateIfEmpty(Fqn fqn)
{
activate(fqn, true);
}
private void activate(Fqn fqn, boolean suppressRegionNotEmptyException)
{
try
{
if (trace) log.trace("Activating region " + fqn);
Region r = getRegion(fqn, false);
if (r != null)
{
if (!defaultInactive && r.getClassLoader() == null)
{
// This region's state will no match that of a non-existent one
// So, there is no reason to keep this region any more
// (Brian) We shouldn't do this anymore; now outside code
// can have a ref to the region!!
removeRegion(fqn);
}
else
{
//r.activate();
r.setStatus(Region.Status.ACTIVATING);
if (configuration.isFetchInMemoryState())
{
activateRegion(r.getFqn(), suppressRegionNotEmptyException);
}
r.setActive(true);
}
}
else if (defaultInactive)
{
// "Active" region is not the default, so create a region
r = getRegion(fqn, true);
// FIXME - persistent state transfer counts too!
r.setStatus(Region.Status.ACTIVATING);
if (configuration.isFetchInMemoryState())
{
activateRegion(r.getFqn(), suppressRegionNotEmptyException);
}
r.setActive(true);
}
}
catch (RuntimeException re)
{
throw re;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Causes the cache to transfer state for the subtree rooted at
* subtreeFqn
and to begin accepting replication messages
* for that subtree.
*
* NOTE: This method will cause the creation of a node
* in the local cache at subtreeFqn
whether or not that
* node exists anywhere else in the cluster. If the node does not exist
* elsewhere, the local node will be empty. The creation of this node will
* not be replicated.
*
* @param fqn Fqn string indicating the uppermost node in the
* portion of the cache that should be activated.
* @throws RegionNotEmptyException if the node subtreeFqn
* exists and has either data or children
*/
private void activateRegion(Fqn fqn, boolean suppressRegionNotEmptyException)
{
// Check whether the node already exists and has data
Node subtreeRoot = cache.getNode(fqn); // NOTE this used to be a peek!
if (log.isDebugEnabled())
{
log.debug("activating " + fqn);
}
try
{
// Add this fqn to the set of those we are activating
// so calls to _getState for the fqn can return quickly
lock(fqn);
BuddyManager buddyManager = cache.getBuddyManager();
// Request partial state from the cluster and integrate it
if (buddyManager == null)
{
// Get the state from any node that has it and put it
// in the main cache
if (subtreeRoot == null)
{
// We'll update this node with the state we receive
// need to obtain all necessary locks.
Node root = cache.getRoot();
cache.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
subtreeRoot = root.addChild(fqn);
cache.getInvocationContext().getOptionOverrides().setCacheModeLocal(false);
}
List members = cache.getMembers();
// Don't bother trying to fetch state if we are in LOCAL mode
if (members != null && !members.isEmpty())
{
rpcManager.fetchPartialState(members, subtreeRoot.getFqn());
}
}
else if (!buddyFqnTransformer.isBackupFqn(fqn))
{
// Get the state from each DataOwner and integrate in their
// respective buddy backup cache
List buddies = buddyManager.getBackupDataOwners();
for (Address buddy : buddies)
{
List sources = new ArrayList(1);
if (!cache.getMembers().contains(buddy))
{
continue;
}
sources.add(buddy);
Fqn buddyRoot = buddyFqnTransformer.getBackupFqn(buddy, fqn);
subtreeRoot = cache.peek(buddyRoot, false, false);
if (subtreeRoot == null)
{
// We'll update this node with the state we receive
// need to obtain all necessary locks.
// needs to be a LOCAL call!
NodeSPI root = cache.getRoot();
cache.getInvocationContext().getOptionOverrides().setCacheModeLocal(true);
subtreeRoot = root.addChild(buddyRoot);
cache.getInvocationContext().getOptionOverrides().setCacheModeLocal(false);
}
rpcManager.fetchPartialState(sources, fqn, subtreeRoot.getFqn());
}
}
else
{
log.info("Attempting to activate a backup region. Not attempting to retrieve any state as this will be pushed.");
}
}
catch (Throwable t)
{
log.error("failed to activate " + fqn, t);
// "Re-deactivate" the region
try
{
inactivateRegion(fqn);
}
catch (Exception e)
{
log.error("failed inactivating " + fqn, e);
// just swallow this one and throw the first one
}
// Throw the exception on, wrapping if necessary
if (t instanceof RegionNotEmptyException)
{
if (!suppressRegionNotEmptyException) throw (RegionNotEmptyException) t;
}
else if (t instanceof CacheException)
{
throw (CacheException) t;
}
else
{
throw new CacheException(t.getClass().getName() + " " +
t.getLocalizedMessage(), t);
}
}
finally
{
unlock(fqn);
}
}
public boolean isInactive(Fqn fqn)
{
Region region = getRegion(fqn, false);
return region == null ? defaultInactive : !region.isActive();
}
/**
* Causes the cache to stop accepting replication events for the subtree
* rooted at subtreeFqn
and evict all nodes in that subtree.
*
* This is legacy code and should not be called directly. This is a private method for now and will be refactored out.
* You should be using {@link #activate(Fqn)} and {@link #deactivate(Fqn)}
*
*
* @param fqn Fqn string indicating the uppermost node in the
* portion of the cache that should be activated.
* @throws CacheException if there is a problem evicting nodes
* @throws IllegalStateException if {@link org.jboss.cache.config.Configuration#isUseRegionBasedMarshalling()} is false
*/
protected void inactivateRegion(Fqn fqn) throws CacheException
{
NodeSPI subtreeRoot = null;
InvocationContext ctx = cache.getInvocationContext();
ctx.getOptionOverrides().setLockAcquisitionTimeout((int) (cache.getConfiguration().getLockAcquisitionTimeout() + 5000));
try
{
// Record that this fqn is in status change, so can't provide state
lock(fqn);
if (!isInactive(fqn))
{
deactivate(fqn);
}
// Create a list with the Fqn in the main cache and any buddy backup trees
BuddyManager buddyManager = cache.getBuddyManager();
ArrayList list = new ArrayList();
list.add(fqn);
if (buddyManager != null)
{
Set buddies = cache.peek(BuddyManager.BUDDY_BACKUP_SUBTREE_FQN, false, false).getChildrenNames();
if (buddies != null)
{
for (Object buddy : buddies)
{
list.add(buddyFqnTransformer.getBackupFqn((String) buddy, fqn));
}
}
}
// Remove the subtree from the main cache and any buddy backup trees
for (Fqn subtree : list)
{
subtreeRoot = cache.peek(subtree, false);
if (subtreeRoot != null)
{
// Remove the subtree
cache.evict(subtree, true);
}
}
}
finally
{
unlock(fqn);
}
}
public boolean hasRegion(Fqn fqn, Region.Type type)
{
Region r = regionsRegistry.get(fqn);
if (r == null) return false;
switch (type)
{
case ANY:
return true;
case EVICTION:
return r.getEvictionRegionConfig() != null;
case MARSHALLING:
return r.isActive() && r.getClassLoader() != null;
}
// should never reach here?
return false;
}
public void deactivate(Fqn fqn)
{
try
{
Region region = getRegion(fqn, false);
if (region != null)
{
if (defaultInactive && region.getClassLoader() == null)
{
// This region's state will no match that of a non-existent one
// So, there is no reason to keep this region any more
// FIXME (Brian) We shouldn't do this anymore; now outside code can have a ref to the region!!
removeRegion(fqn);
}
else
{
//region.deactivate();
region.setActive(false);
// FIXME - we should always clean up; otherwise stale data is in memory!
if (cache.getConfiguration().isFetchInMemoryState())
{
inactivateRegion(fqn);
}
}
}
else if (!defaultInactive)
{
region = getRegion(fqn, true);
region.setActive(false);
// FIXME - we should always clean up; otherwise stale data is in memory!
if (cache.getConfiguration().isFetchInMemoryState())
{
inactivateRegion(fqn);
}
}
}
catch (RuntimeException re)
{
throw re;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
public void reset()
{
regionsRegistry.clear();
}
public List getAllRegions(Region.Type type)
{
List regions;
if (type != ANY)
{
regions = new ArrayList();
// we need to loop thru the regions and only select specific regions to rtn.
for (Region r : regionsRegistry.values())
{
if ((type == EVICTION && r.getEvictionRegionConfig() != null) ||
(type == MARSHALLING && r.isActive() && r.getClassLoader() != null))
{
regions.add(r);
}
}
}
else
{
// put all regions
regions = new ArrayList(regionsRegistry.values());
}
Collections.sort(regions);
return regions;
}
public void setUsingEvictions(boolean usingEvictions)
{
this.usingEvictions = usingEvictions;
}
public void setEvictionConfig(EvictionConfig evictionConfig)
{
// JBAS-1288
// Try to establish a default region if there isn't one already
// boolean needDefault;
List ercs = evictionConfig.getEvictionRegionConfigs();
// Only add a default region if there are no regions. This is
// contrary to the idea that there *must* be a default region, but some
// unit tests fail w/ APPROACH 1, so for now we go with this approach.
// needDefault = ercs.size() == 0;
if (evictionConfig.getDefaultEvictionRegionConfig().getEvictionAlgorithmConfig() != null &&
!ercs.contains(evictionConfig.getDefaultEvictionRegionConfig())) // then the default is a real region too; not just a template for others
{
ercs.add(0, evictionConfig.getDefaultEvictionRegionConfig());
}
// create regions for the regions defined in the evictionConfig.
// scan to be sure the _default_ region isn't added twice
boolean setDefault = false;
for (EvictionRegionConfig erc : ercs)
{
Fqn fqn = erc.getRegionFqn();
if (fqn == null) throw new ConfigurationException("Regions cannot be configured with a null region fqn. If you configured this region programmatically, ensure that you set the region fqn in EvictionRegionConfig");
if (trace) log.trace("Creating eviction region " + fqn);
if (fqn.equals(DEFAULT_REGION) || fqn.isRoot())
{
if (setDefault)
{
throw new ConfigurationException("A default region for evictions has already been set for this cache");
}
if (trace) log.trace("Applying settings for default region to Fqn.ROOT");
fqn = Fqn.ROOT;
setDefault = true;
}
Region r = getRegion(fqn, true);
evictionConfig.applyDefaults(erc);
r.setEvictionRegionConfig(erc);
}
}
@ManagedOperation(description = "A String representation of all registered regions")
public String dumpRegions()
{
StringBuilder sb = new StringBuilder();
if (regionsRegistry != null)
{
for (Region r : regionsRegistry.values())
{
sb.append("\tRegion ").append(r);
sb.append("\n");
}
}
return sb.toString();
}
/**
* Returns a string containing debug information on every region.
*/
@Override
public String toString()
{
return "RegionManager " + dumpRegions();
}
public CacheSPI getCache()
{
return cache;
}
// --------- for backward compat --------------
/**
* Starts the eviction processing thread.
*/
public void startEvictionThread()
{
evictionTimerTask.init(evictionConfig.getWakeupInterval(), configuration.getRuntimeConfig().getEvictionTimerThreadFactory(), regionsRegistry);
}
/**
* Stops the eviction processing thread
*/
public void stopEvictionThread()
{
evictionTimerTask.stop();
}
@ManagedAttribute(name = "numRegions", description = "A count of all regions")
public int getNumRegions()
{
return regionsRegistry.size();
}
}