/*
* Copyright 2010 Martin Grotzke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*
*/
package de.javakaffee.web.msm;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
/**
* Provides services related to node ids.
*
* @author <a href="mailto:martin.grotzke@javakaffee.de">Martin Grotzke</a>
*/
public class NodeIdService {
private static final Log LOG = LogFactory.getLog( NodeIdService.class );
private final Random _random = new Random();
/*
* Manager.remove(session) may be called with sessionIds that already failed before (probably
* because the browser makes subsequent requests with the old sessionId -
* the exact reason needs to be verified). These failed sessionIds should If
* a session is requested that we don't have locally stored each findSession
* invocation would trigger a memcached request - this would open the door
* for DOS attacks...
*
* this solution: use a LRUCache with a timeout to store, which session had
* been requested in the last <n> millis.
*
* Updated: the node status cache holds the status of each node for the
* configured TTL.
*/
private final NodeAvailabilityCache<String> _nodeAvailabilityCache;
private final NodeIdList _nodeIds;
private final List<String> _failoverNodeIds;
/**
* Constructs a new {@link NodeIdService}.
*
* @param nodeAvailabilityCache
* @param nodeIds
* @param failoverNodeIds
*/
public NodeIdService( final NodeAvailabilityCache<String> nodeAvailabilityCache, final NodeIdList nodeIds, final List<String> failoverNodeIds ) {
_nodeAvailabilityCache = nodeAvailabilityCache;
_nodeIds = nodeIds;
_failoverNodeIds = failoverNodeIds;
}
/**
* A special constructor used for testing of {@link #getRandomNextNodeId(String, Set)}.
*
* @param nodeIds
* @param failoverNodeIds
*/
NodeIdService( final List<String> nodeIds,
final List<String> failoverNodeIds ) {
this( null, new NodeIdList( nodeIds ), failoverNodeIds );
}
/**
* Determines, if the given nodeId is available.
* @param nodeId the node to check, not <code>null</code>.
* @return <code>true</code>, if the node is marked as available
*/
public boolean isNodeAvailable( @Nonnull final String nodeId ) {
return _nodeAvailabilityCache.isNodeAvailable( nodeId );
}
/**
* Mark the given nodeId as available as specified.
* @param nodeId the nodeId to update
* @param available specifies if the node was abailable or not
*/
public void setNodeAvailable( final String nodeId, final boolean available ) {
_nodeAvailabilityCache.setNodeAvailable( nodeId, available );
}
/**
* Get an available (randomly selected) memcached node id for session backup.
* The active node ids are preferred, if no active node id is left to try,
* a failover node id is picked.
* If no failover node id is left, this method returns just null.
*
* @param nodeId the unavailable nodeId which is used to start checking other nodes.
* @return a nodeId if any available node was found, otherwise <code>null</code>.
*/
public String getAvailableNodeId( final String nodeId ) {
String result = null;
/*
* first check regular nodes
*/
result = getRandomNextNodeId( nodeId, _nodeIds );
/*
* we got no node from the first nodes list, so we must check the
* alternative node list
*/
if ( result == null && _failoverNodeIds != null && !_failoverNodeIds.isEmpty() ) {
result = getRandomNextNodeId( nodeId, _failoverNodeIds );
}
return result;
}
/**
* Gets the next node id for the given one from the list of all node ids.
* If there's only a single node known, conceptionally there's no next node
* and therefore <code>null</code> is returned.
* @param nodeId the node id for that the next one is determined.
* @return the next node id or <code>null</code>.
*
* @throws IllegalArgumentException thrown if the given nodeId is not part of this list.
*
* @see NodeIdList#getNextNodeId(String)
*/
@CheckForNull
public String getNextNodeId( @Nonnull final String nodeId ) throws IllegalArgumentException {
return _nodeIds.getNextNodeId( nodeId );
}
/**
* Determines (randomly) an available node id from the provided node ids. The
* returned node id will be different from the provided nodeId and will
* be available according to the local {@link NodeAvailabilityCache}.
*
* @param nodeId
* the original id
* @param nodeIds
* the node ids to choose from
* @return an available node or null
*/
protected String getRandomNextNodeId( final String nodeId, final Collection<String> nodeIds ) {
/* create a list of nodeIds to check randomly
*/
final List<String> otherNodeIds = new ArrayList<String>( nodeIds );
otherNodeIds.remove( nodeId );
while ( !otherNodeIds.isEmpty() ) {
final String nodeIdToCheck = otherNodeIds.get( _random.nextInt( otherNodeIds.size() ) );
if ( isNodeAvailable( nodeIdToCheck ) ) {
return nodeIdToCheck;
}
otherNodeIds.remove( nodeIdToCheck );
}
return null;
}
/**
* Get the next random, available node id. If no node is available, <code>null</code>
* is returned.
* @return a nodeId or <code>null</code>.
*/
public String getMemcachedNodeId() {
final String nodeId = _nodeIds.get( _random.nextInt( _nodeIds.size() ) );
return isNodeAvailable( nodeId ) ? nodeId : getAvailableNodeId( nodeId );
}
/* Just for testing
*/
List<String> getNodeIds() {
return new ArrayList<String>( _nodeIds );
}
/* Just for testing
*/
List<String> getFailoverNodeIds() {
return new ArrayList<String>( _failoverNodeIds );
}
/**
* Returns a new node id if the given one is <code>null</code> or not available.
* @param nodeId the node id that is checked for availability (if not <code>null</code>).
* @return a new node id if the given one is <code>null</code> or not available, otherwise <code>null</code>.
*/
public String getNewNodeIdIfUnavailable( final String nodeId ) {
final String newNodeId;
if ( nodeId == null ) {
newNodeId = getMemcachedNodeId();
}
else {
if ( !isNodeAvailable( nodeId ) ) {
newNodeId = getAvailableNodeId( nodeId );
if ( newNodeId == null ) {
LOG.warn( "The node " + nodeId + " is not available and there's no node for relocation left." );
}
}
else {
newNodeId = null;
}
}
return newNodeId;
}
}