/*
* Copyright (C) 2011 eXo Platform SAS.
*
* 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.exoplatform.services.jcr.impl.core.query.ispn;
import org.exoplatform.services.jcr.impl.core.query.ChangesFilterListsWrapper;
import org.exoplatform.services.jcr.impl.core.query.IndexerIoMode;
import org.exoplatform.services.jcr.impl.core.query.IndexerIoModeHandler;
import org.exoplatform.services.jcr.util.IdGenerator;
import org.infinispan.Cache;
import org.infinispan.container.DataContainer;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.lifecycle.ComponentStatus;
import org.infinispan.loaders.CacheLoaderConfig;
import org.infinispan.loaders.CacheLoaderException;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.marshall.StreamingMarshaller;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachemanagerlistener.annotation.CacheStarted;
import org.infinispan.notifications.cachemanagerlistener.annotation.Merged;
import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
import org.infinispan.notifications.cachemanagerlistener.event.Event;
import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
import org.infinispan.remoting.transport.Address;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Implements Cache Store for clustered environment. It gives control of Index for coordinator and
* adds failover mechanisms when it changes.
*
* @author <a href="mailto:nikolazius@gmail.com">Nikolay Zamosenchuk</a>
* @version $Id: IndexCacheLoader.java 34360 2009-07-22 23:58:59Z nzamosenchuk $
*
*/
public class IndexerCacheStore extends AbstractIndexerCacheStore
{
protected volatile IndexerIoModeHandler modeHandler;
protected CacheListener listener;
/**
* Address instance that allows SingletonStore to find out whether it became the coordinator of the cluster, or
* whether it stopped being it. This dictates whether the SingletonStore is active or not.
*/
private Address localAddress;
/**
* Whether the the current cache is the coordinator and therefore SingletonStore is active. Being active means
* delegating calls to the underlying cache loader.
*/
private volatile boolean coordinator;
protected EmbeddedCacheManager cacheManager;
@Override
public void init(CacheLoaderConfig config, Cache<?, ?> cache, StreamingMarshaller m) throws CacheLoaderException
{
super.init(config, cache, m);
this.cacheManager = cache == null ? null : cache.getCacheManager();
listener = new CacheListener();
cacheManager.addListener(listener);
}
/**
* Get the mode handler
*/
public IndexerIoModeHandler getModeHandler()
{
if (modeHandler == null)
{
if (cache.getStatus() != ComponentStatus.RUNNING)
{
throw new IllegalStateException("The cache should be started first");
}
synchronized (this)
{
if (modeHandler == null)
{
this.modeHandler =
new IndexerIoModeHandler(cacheManager.isCoordinator()
|| cache.getAdvancedCache().getRpcManager() == null ? IndexerIoMode.READ_WRITE
: IndexerIoMode.READ_ONLY);
}
}
}
return modeHandler;
}
/**
* Indicates whether the current cache is the coordinator of the cluster. This implementation assumes that the
* coordinator is the first member in the list.
*
* @param newView View instance containing the new view of the cluster
* @return whether the current cache is the coordinator or not.
*/
private boolean isCoordinator(List<Address> newView, Address currentAddress)
{
if (!currentAddress.equals(localAddress))
{
localAddress = currentAddress;
}
if (localAddress != null)
{
return !newView.isEmpty() && localAddress.equals(newView.get(0));
}
else
{
/* Invalid new view, so previous value returned */
return coordinator;
}
}
/**
* Method called when the cache either becomes the coordinator or stops being the coordinator. If it becomes the
* coordinator, it can optionally start the in-memory state transfer to the underlying cache store.
*
* @param newActiveState true if the cache just became the coordinator, false if the cache stopped being the
* coordinator.
*/
protected void activeStatusChanged(final boolean newActiveState)
{
// originally came from EXOJCR-1345.
// Deadlock occurs inside JGroups, if calling some operations inside the same thread,
// invoking ViewChanged. That's why, need to perform operation in separated async tread.
new Thread(new Runnable()
{
public void run()
{
coordinator = newActiveState;
getModeHandler().setMode(coordinator ? IndexerIoMode.READ_WRITE : IndexerIoMode.READ_ONLY);
LOG.info("Set indexer io mode to:" + (coordinator ? IndexerIoMode.READ_WRITE : IndexerIoMode.READ_ONLY));
if (coordinator)
{
doPushState();
}
}
}, "JCR Indexer ActiveStatusChanged-handler").start();
}
/**
* Flushes all cache content to underlying CacheStore
*/
@SuppressWarnings("rawtypes")
protected void doPushState()
{
final boolean debugEnabled = LOG.isDebugEnabled();
if (debugEnabled)
{
LOG.debug("start pushing in-memory state to cache cacheLoader collection");
}
Map<String, ChangesFilterListsWrapper> changesMap = new HashMap<String, ChangesFilterListsWrapper>();
List<ChangesKey> processedItemKeys = new ArrayList<ChangesKey>();
DataContainer dc = cache.getAdvancedCache().getDataContainer();
Set keys = dc.keySet();
InternalCacheEntry entry;
// collect all cache entries into the following map:
// <WS ID> : <Contracted lists of added/removed nodes>
for (Object k : keys)
{
if ((entry = dc.get(k)) != null)
{
if (entry.getValue() instanceof ChangesFilterListsWrapper && entry.getKey() instanceof ChangesKey)
{
if (debugEnabled)
{
LOG.debug("Received list wrapper, start indexing...");
}
// get stale List that was not processed
ChangesFilterListsWrapper staleListIncache = (ChangesFilterListsWrapper)entry.getValue();
ChangesKey key = (ChangesKey)entry.getKey();
// get newly created wrapper instance
ChangesFilterListsWrapper listToPush = changesMap.get(key.getWsId());
if (listToPush == null)
{
listToPush =
new ChangesFilterListsWrapper(new HashSet<String>(), new HashSet<String>(), new HashSet<String>(),
new HashSet<String>());
changesMap.put(key.getWsId(), listToPush);
}
// copying lists into the new wrapper
if (staleListIncache.getParentAddedNodes() != null)
listToPush.getParentAddedNodes().addAll(staleListIncache.getParentAddedNodes());
if (staleListIncache.getParentRemovedNodes() != null)
listToPush.getParentRemovedNodes().addAll(staleListIncache.getParentRemovedNodes());
if (staleListIncache.getAddedNodes() != null)
listToPush.getAddedNodes().addAll(staleListIncache.getAddedNodes());
if (staleListIncache.getRemovedNodes() != null)
listToPush.getRemovedNodes().addAll(staleListIncache.getRemovedNodes());
processedItemKeys.add(key);
}
}
}
// process all lists for each workspace
for (Entry<String, ChangesFilterListsWrapper> changesEntry : changesMap.entrySet())
{
// create key based on wsId and generated id
ChangesKey changesKey = new ChangesKey(changesEntry.getKey(), IdGenerator.generate());
cache.putAsync(changesKey, changesEntry.getValue());
}
for (ChangesKey key : processedItemKeys)
{
cache.removeAsync(key);
}
if (debugEnabled)
{
LOG.debug("in-memory state passed to cache cacheStore successfully");
}
}
@Listener
public class CacheListener
{
@CacheStarted
public void cacheStarted(Event e)
{
localAddress = cacheManager.getAddress();
coordinator = cacheManager.isCoordinator();
}
/**
* The cluster formation changed, so determine whether the current cache stopped being the coordinator or became
* the coordinator. This method can lead to an optional in memory to cache loader state push, if the current cache
* became the coordinator. This method will report any issues that could potentially arise from this push.
*/
@ViewChanged
@Merged
public void viewChange(ViewChangedEvent event)
{
LOG.info("The intercepted EventType is : " + event.getType());
LOG.info("The old list of members : " + event.getOldMembers());
LOG.info("The new list of members: : " + event.getNewMembers());
boolean tmp = isCoordinator(event.getNewMembers(), event.getLocalAddress());
if (coordinator != tmp || (tmp && event.isMergeView()))
{
activeStatusChanged(tmp);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void stop() throws CacheLoaderException
{
cacheManager.removeListener(listener);
super.stop();
}
}