/*
* #%L
* Nazgul Project: nazgul-core-cache-impl-hazelcast
* %%
* Copyright (C) 2010 - 2017 jGuru Europe AB
* %%
* Licensed under the jGuru Europe AB license (the "License"), based
* on Apache License, Version 2.0; you may not use this file except
* in compliance with the License.
*
* You may obtain a copy of the License at
*
* http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*
*/
package se.jguru.nazgul.core.cache.impl.hazelcast;
import com.hazelcast.core.DistributedObject;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.ICollection;
import com.hazelcast.core.IMap;
import com.hazelcast.core.ISet;
import com.hazelcast.core.ITopic;
import com.hazelcast.core.Message;
import com.hazelcast.core.MessageListener;
import com.hazelcast.transaction.TransactionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jguru.nazgul.core.cache.api.CacheListener;
import se.jguru.nazgul.core.cache.api.ReadOnlyIterator;
import se.jguru.nazgul.core.cache.api.distributed.DistributedCache;
import se.jguru.nazgul.core.cache.api.distributed.UnsupportedDistributionException;
import se.jguru.nazgul.core.cache.api.distributed.async.DistributedExecutor;
import se.jguru.nazgul.core.cache.api.distributed.async.LightweightTopic;
import se.jguru.nazgul.core.cache.api.transaction.AbstractTransactedAction;
import se.jguru.nazgul.core.cache.api.transaction.TransactedAction;
import se.jguru.nazgul.core.cache.impl.hazelcast.grid.AdminMessage;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
/**
* Abstract implementation managing registration and de-registration of HazelcastCacheListenerAdapter instances.
*
* @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
*/
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public abstract class AbstractHazelcastInstanceWrapper extends AbstractHazelcastCacheListenerManager
implements MessageListener<AdminMessage>, DistributedExecutor<String, Object> {
// Our log
private static final Logger log = LoggerFactory.getLogger(AbstractHazelcastInstanceWrapper.class);
// Constants
private static final String EXECUTOR_NAME = "NazulCoreCacheImplHazelcast_Executor";
// Internal state
private HazelcastInstance cacheInstance;
private static final int MESSAGE_WAIT_MILLIS = 100;
/**
* Creates a new AbstractHazelcastInstanceWrapper instance wrapping the provided HazelcastInstance
* (which can be a HazelcastInstance [part of the cluster] or a HazelcastClient [not part of the cluster]).
*
* @param internalInstance The HazelcastInstance wrapped by this AbstractHazelcastInstanceWrapper.
*/
protected AbstractHazelcastInstanceWrapper(final HazelcastInstance internalInstance) {
super(internalInstance.getName());
cacheInstance = internalInstance;
// Create or acquire the cluster-wide shared collections.
getAdminMessageTopic().addMessageListener(this);
getOrCreateInClusterIMap(CLUSTER_SHARED_CACHE_MAP);
getOrCreateInClusterIMap(CLUSTER_KNOWN_LISTENERIDS);
}
/**
* Adds an instanceListener to the wrapped HazelcastInstance.
*
* @param listener The listener to add.
* @return the HazelCast ID of the listener added.
*/
public final String addInstanceListener(final CacheListener<String, Object> listener) {
if (listener == null || isLocallyRegistered(listener)) {
throw new IllegalArgumentException("Cannot add null or already registered listener");
}
synchronized (lock) {
final StringKeyedHazelcastListenerAdapter<Object> wrapper
= new StringKeyedHazelcastListenerAdapter<Object>(listener);
final String listenerId = cacheInstance.addDistributedObjectListener(wrapper);
if (listenerId != null) {
getLocallyRegisteredListeners().put(listener.getClusterId(), wrapper);
} else {
log.warn("Could not add DistributedObjectListener [" + wrapper.toString() + "]");
}
return listenerId;
}
}
/**
* Removes an instanceListener to the wrapped HazelcastInstance.
*
* @param listenerID The id of the listener to remove.
* @return true if the listener was removed, and false otherwise.
*/
public final boolean removeInstanceListener(final String listenerID) {
if (listenerID == null || !getLocallyRegisteredListeners().keySet().contains(listenerID)) {
return false;
}
synchronized (lock) {
final boolean successfullyRemovedListener = cacheInstance.removeDistributedObjectListener(listenerID);
if (successfullyRemovedListener) {
getLocallyRegisteredListeners().remove(listenerID);
}
return successfullyRemovedListener;
}
}
/**
* Shuts down the local cacheInstance.
* If you need to shut down all Hazelcast activities, simply use
* <code>Hazelcast.shutdownAll()</code>.
*/
public final void stopCache() {
cacheInstance.getLifecycleService().shutdown();
cacheInstance = null;
}
/**
* Acquires a Transactional context from this Cache, and Executes the
* TransactedAction::doInTransaction method within it.
*
* @param action The TransactedAction to be executed within a Cache Transactional context.
* @throws UnsupportedOperationException if the underlying Cache implementation does not
* support Transactions.
*/
@Override
public final void performTransactedAction(final TransactedAction action)
throws UnsupportedOperationException {
final TransactionContext trans = cacheInstance.newTransactionContext();
trans.beginTransaction();
try {
// Perform the action, and commit.
action.doInTransaction();
trans.commitTransaction();
} catch (final Exception ex) {
// Whoops.
trans.rollbackTransaction();
// Perform custom rollback, if applicable.
if (action instanceof AbstractTransactedAction) {
((AbstractTransactedAction) action).onRollback();
}
// Re-throw
throw new IllegalStateException(action.getRollbackErrorDescription(), ex);
}
}
/**
* Returns true if this cache contains a mapping for the specified key
*
* @param key The <code>key</code> whose presence in this map is to be tested.
* @return <code>true</code> if this map contains a mapping for the specified key.
*/
@Override
public final boolean containsKey(final String key) {
return getSharedMap().containsKey(key);
}
/**
* @return The ExecutorService of the underlying cache implementation.
*/
@Override
public final ExecutorService getExecutorService() {
return cacheInstance.getExecutorService(EXECUTOR_NAME);
}
/**
* @return An identifier unique to the active cache cluster.
*/
@Override
public final String getClusterUniqueID() {
return "" + cacheInstance.getIdGenerator("nazgul_hazelcast").newId();
}
/**
* Retrieves an object from this Cache.
*
* @param key The key of the instance to retrieve.
* @return The value corresponding to the provided key, or <code>null</code> if no object was found.
*/
@Override
public final Object get(final String key) {
return getSharedMap().get(key);
}
/**
* Stores the provided object in this Cache, associated with the provided key. Will overwrite existing objects with
* identical key.
*
* @param key The key under which to cache the provided value. This parameter should not be <code>null</code>.
* @param value The value to cache.
* @return The previous value associated with <code>key</code>, or <code>null</code> if no such object exists.
*/
@Override
public final Object put(final String key, final Object value) {
return getSharedMap().put(key, getSerializable(value));
}
/**
* Removes the object with the given key from the underlying cache implementation, returning the value held before
* the object was removed.
*
* @param key The cache key for which the value should be removed.
* @return The object to remove.
*/
@Override
public final Object remove(final String key) {
return getSharedMap().remove(key);
}
/**
* Gets a distributed collection with the given type and provided key from the cache. Note that the distributed
* Collection will be created on the provided key if it does not already exist.
* A typical usage example would be
* <pre>
* List cachedList = getDistributedCollection(List.class, "someListKey");
* </pre>
*
* @param type The type of Collection desired. This should be an abstract type, such as <code>List, Map, Set</code>
* etc.
* @param key The key where the distributed collection resides or will be created to.
* @return The created (empty) or acquired (potentially not empty) distributed collection cached at the given key.
* @throws ClassCastException if the cache key [key] maps to an object not of the given type.
* @throws UnsupportedDistributionException if the underlying cache implementation could not support creating
* a distributed collection of the given type.
*/
@Override
public final Collection<? extends Serializable> getDistributedCollection(final DistributedCollectionType type,
final String key)
throws ClassCastException, UnsupportedDistributionException {
switch (type) {
case COLLECTION:
return cacheInstance.getList(key);
case SET:
return cacheInstance.getSet(key);
case QUEUE:
return cacheInstance.getQueue(key);
}
// This should never happen.
throw new UnsupportedDistributionException("Could not create collection of type [" + type + "]");
}
/**
* Retrieves a LightweightTopic with the provided topicId from
* the DestinationProvider. Depending on the capabilities of the underlying
* implementation, the topic can be dynamically
*
* @param <MessageType> The type of message transmitted by this LightweightTopic.
* @param topicId The ID of the LightweightTopic to retrieve.
* @return The LightweightTopic with the provided topicId.
*/
@Override
public final <MessageType extends Serializable> LightweightTopic<MessageType> getTopic(final String topicId) {
final ITopic<MessageType> topic = cacheInstance.getTopic(topicId);
return new HazelcastLightweightTopic<MessageType>(topic);
}
/**
* @return The shared Map holding the default (direct-level) cached instances.
*/
@Override
public final IMap<String, Object> getSharedMap() {
return cacheInstance.getMap(CLUSTER_SHARED_CACHE_MAP);
}
/**
* @return The shared Map relating ids for distributed objects [String::key] to a List of ids for all registered
* listeners to the given distributed object [List[String]::value].
*/
@Override
public final IMap<String, TreeSet<String>> getCacheListenersIDMap() {
return cacheInstance.getMap(CLUSTER_KNOWN_LISTENERIDS);
}
/**
* Sends the provided AdminMessage to all members of the Cluster.
*
* @param message The message to send.
*/
@Override
public final void sendAdminMessage(final AdminMessage message) {
getAdminMessageTopic().publish(message);
// Wait 0.1 seconds for the message to be echoed
// to the cluster nodes before proceeding.
//
// This will provide a cushion for permitting sequential
// messages interacting with the same distributed Instance
// without too much of a race condition ...
try {
Thread.sleep(MESSAGE_WAIT_MILLIS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* @return The shared topic transmitting AdminMessage instances.
*/
protected final ITopic<AdminMessage> getAdminMessageTopic() {
return cacheInstance.getTopic(CLUSTER_ADMIN_TOPIC);
}
/**
* Gets a distributed Map with the provided key from the cache. Note that the distributed Map
* will be created on the provided key if it does not already exist.
*
* @param <K> The key type of the distributed Map returned.
* @param <V> The value type of the distributed Map returned.
* @param key The key where the distributed Map resides or will be created to.
* @return The created (empty) or acquired (potentially not empty) distributed Map cached at the given key.
* @throws UnsupportedDistributionException if the underlying cache implementation could not support creating a distributed Map.
*/
@Override
public <K, V> Map<K, V> getDistributedMap(final String key) throws UnsupportedDistributionException {
return cacheInstance.getMap(key);
}
/**
* Adds the given listenerID to the listenerIdSet for the provided distributedObject.
*
* @param distributedObject The DistributedObject for which a listener ID should be registered.
* @param listenerId The id of the Listener to register to the provided distributedObject.
* @return <code>true</code> if the registration was successful, and false otherwise.
*/
@Override
public boolean addListenerIdFor(final DistributedObject distributedObject, final String listenerId) {
TreeSet<String> listenerIDs = getCacheListenersIDMap().get("" + distributedObject.getName());
if (listenerIDs == null) {
listenerIDs = new TreeSet<String>();
}
// Add the new listenerId, and update the listenersIdMap.
// Remember, this is the lifecycle required for non-proxy stored collections in HC.
listenerIDs.add(listenerId);
getCacheListenersIDMap().put(distributedObject.getName(), listenerIDs);
return true;
}
/**
* Invoked when a message is received for the added topic.
*
* @param adminMessageMessage received message
*/
@Override
@SuppressWarnings(value = {"PMD.UnusedLocalVariable", "unchecked", "rawtypes"})
public void onMessage(final Message<AdminMessage> adminMessageMessage) {
final AdminMessage message = adminMessageMessage.getMessageObject();
switch (message.getCommand()) {
case REMOVE_LISTENER:
final String distributedObjectiD = message.getArguments().get(0);
final String toRemoveId = message.getArguments().get(1);
// Take no action if we do not own the listener.
// Only the member that owns the listener should remove it.
if (!getLocallyRegisteredListeners().containsKey(toRemoveId)) {
log.debug("(CacheID: " + getClusterId() + "): No local registered listener with id [" + toRemoveId
+ "] found. Ignoring remove request.");
return;
}
final String rollbackMessage =
"(CacheID: " + getClusterId() + "): Could not remove listener with id [" + toRemoveId
+ "] from distributedObject [" + distributedObjectiD + "]";
performTransactedAction(new AbstractTransactedAction(rollbackMessage) {
@SuppressWarnings({"incomplete-switch", "unused"})
@Override
public void doInTransaction() throws RuntimeException {
DistributedObject distributedObject = null;
// Find the distributed object from which to remove the listener
for (final DistributedObject current : getInstances()) {
if (distributedObjectiD.equals("" + current.getName())) {
distributedObject = current;
break;
}
}
// Remove the listener locally.
final AbstractHazelcastCacheListenerAdapter removed
= getLocallyRegisteredListeners().remove(toRemoveId);
// Remove the listener ID from the listenersIdMap,
// and update the TreeSet within the distributedListenersIdMap.
final TreeSet<String> idSet = getCacheListenersIDMap().get(distributedObjectiD);
final boolean removedOKFromIdMap = idSet.remove(toRemoveId);
getCacheListenersIDMap().put(distributedObjectiD, idSet);
// Remove the listener from the distributedObject.
if (distributedObject instanceof IMap) {
((IMap) distributedObject).removeEntryListener(toRemoveId);
} else if (distributedObject instanceof ICollection) {
((ICollection) distributedObject).removeItemListener(toRemoveId);
} else {
// We can't handle this type of distObject...
final Class<?>[] handleableTypes = {IMap.class, ICollection.class};
final String distObjectType = distributedObject == null
? "<null>"
: distributedObject.getClass().getName();
final StringBuilder permitted = new StringBuilder("[");
for (final Class<?> current : handleableTypes) {
permitted.append(current.getName()).append(", ");
}
throw new IllegalArgumentException("Will not add listener to an instance of type ["
+ distObjectType + "]. Supported types are "
+ permitted.substring(0, permitted.length() - 2) + "].");
}
}
});
break;
case SHUTDOWN_INSTANCE:
// Is it *this* instance that should be shut down?
final String shutdownInstanceID = message.getArguments().get(0);
if (!getClusterId().equals(shutdownInstanceID)) {
return;
}
// Unregister all keys for listeners that we own.
for (final DistributedObject current : getInstances()) {
final Set<String> listenerIDs = getListenerIDsFor(current);
for (final String currentID : getLocallyRegisteredListeners().keySet()) {
// Just remove to save the extra processing in checking if the key exists.
listenerIDs.remove(currentID);
}
}
// Now perform shutdown.
// This will automatically remove all local listeners from their instances.
AbstractHazelcastInstanceWrapper.this.stopCache();
break;
case CREATE_INCACHE_INSTANCE:
final AdminMessage.TypeDefinition toCreateType =
AdminMessage.TypeDefinition.valueOf(message.getArguments().get(0));
final String clusterUniqueID = message.getArguments().get(1);
switch (toCreateType) {
case SET:
getDistributedCollection(DistributedCache.DistributedCollectionType.SET, clusterUniqueID);
break;
case COLLECTION:
getDistributedCollection(DistributedCache.DistributedCollectionType.COLLECTION,
clusterUniqueID);
break;
case QUEUE:
getDistributedCollection(DistributedCache.DistributedCollectionType.QUEUE, clusterUniqueID);
break;
case TOPIC:
getTopic(clusterUniqueID);
break;
case MAP:
getDistributedMap(clusterUniqueID);
break;
}
break;
default:
throw new UnsupportedOperationException("AdminMessage command [" + message.getCommand()
+ "] not yet supported.");
}
}
/**
* Retrieves a ReadOnlyIterator for the shared cache map of this AbstractHazelcastInstanceWrapper instance.
*/
@Override
public Iterator<String> iterator() {
return new ReadOnlyIterator<String>(getSharedMap().keySet().iterator());
}
/**
* Creates a serializable ISet instance within the cluster.
*
* @param clusterWideUniqueID The cluster-wide unique ID of the ISet to be created.
* @return a serializable ISet instance, which is created by a HazelcastInstance member.
*/
protected ISet<String> getOrCreateInClusterISet(final String clusterWideUniqueID) {
return cacheInstance.getSet(clusterWideUniqueID);
}
/**
* Creates a serializable IMap instance within the cluster.
*
* @param clusterWideUniqueID The cluster-wide unique ID of the ISet to be created.
* @return a serializable ISet instance, which is created by a HazelcastInstance member.
*/
protected IMap<String, Serializable> getOrCreateInClusterIMap(final String clusterWideUniqueID) {
return cacheInstance.getMap(clusterWideUniqueID);
}
/**
* @return A Collection holding the DistributedObjects known to the wrapped HazelcastInstance.
*/
protected final Collection<DistributedObject> getInstances() {
return cacheInstance.getDistributedObjects();
}
//
// Private helpers
//
private Serializable getSerializable(final Object object) {
if (object == null || object instanceof Serializable) {
return (Serializable) object;
}
// This is a non-null object which is not Serializable.
throw new IllegalArgumentException("Could not convert [" + object.getClass().getName() + "] to Serializable.");
}
}