/*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* 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 org.jivesoftware.xmpp.workgroup.dispatcher;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import org.jivesoftware.openfire.fastpath.util.TaskEngine;
import org.jivesoftware.openfire.fastpath.util.WorkgroupUtils;
import org.jivesoftware.util.BeanUtils;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.xmpp.workgroup.AgentSession;
import org.jivesoftware.xmpp.workgroup.AgentSessionList;
import org.jivesoftware.xmpp.workgroup.AgentSessionListener;
import org.jivesoftware.xmpp.workgroup.Offer;
import org.jivesoftware.xmpp.workgroup.RequestQueue;
import org.jivesoftware.xmpp.workgroup.UnauthorizedException;
import org.jivesoftware.xmpp.workgroup.Workgroup;
import org.jivesoftware.xmpp.workgroup.WorkgroupResultFilter;
import org.jivesoftware.xmpp.workgroup.request.Request;
import org.jivesoftware.xmpp.workgroup.request.UserRequest;
import org.jivesoftware.xmpp.workgroup.spi.JiveLiveProperties;
import org.jivesoftware.xmpp.workgroup.spi.dispatcher.DbDispatcherInfoProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>Implements simple round robin dispatching of offers to agents.</p>
* <p>Agents are offered requests one at a time with no agent being offer
* the same request twice (unless their current-chats status changes).</p>
*
* @author Derek DeMoro
* @author Iain Shigeoka
*/
public class RoundRobinDispatcher implements Dispatcher, AgentSessionListener {
private static final Logger Log = LoggerFactory.getLogger(RoundRobinDispatcher.class);
/**
* <p>The circular list of agents in the pool.</p>
*/
private List<AgentSession> agentList;
private RequestQueue queue;
/**
* <p>Prop manager for the dispatcher.</p>
*/
private JiveLiveProperties properties;
private DispatcherInfo info;
private DispatcherInfoProvider infoProvider = new DbDispatcherInfoProvider();
private AgentSelector agentSelector = WorkgroupUtils.getAvailableAgentSelectors().get(0);
/**
* A set of all outstanding offers in the workgroup<p>
*
* Let's the server route offer responses to the correct offer.
*/
private Set<Offer> offers = Collections.newSetFromMap(new ConcurrentHashMap<Offer, Boolean>());
/**
* Creates a new dispatcher for the queue. The dispatcher will have a Timer with a unique task
* that will get the requests from the queue and will try to send an offer to the agents.
*
* @param queue the queue that contains the requests and the agents that may attend the
* requests.
*/
public RoundRobinDispatcher(RequestQueue queue) {
this.queue = queue;
agentList = new LinkedList<AgentSession>();
properties = new JiveLiveProperties("fpDispatcherProp", queue.getID());
try {
info = infoProvider.getDispatcherInfo(queue.getWorkgroup(), queue.getID());
}
catch (NotFoundException e) {
Log.error("Queue ID " + queue.getID(), e);
}
// Recreate the agentSelector to use for selecting the best agent to receive the offer
loadAgentSelector();
// Fill the list of AgentSessions that are active in the queue. Once the list has been
// filled this dispatcher will be notified when new AgentSessions join the queue or leave
// the queue
fillAgentsList();
TaskEngine.getInstance().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
checkForNewRequests();
}
}, 2000, 2000);
}
private void checkForNewRequests() {
for(Request request : queue.getRequests()){
// While there are requests pendings try to dispatch an offer for the request to an agent
// Skip this request if there exists an offer for this requests that is being processed
if (request.getOffer() != null && offers.contains(request.getOffer())) {
continue;
}
injectRequest(request);
}
}
public void injectRequest(Request request) {
// Create a new Offer for the request and add it to the list of active offers
final Offer offer = new Offer(request, queue, getAgentRejectionTimeout());
offer.setTimeout(info.getOfferTimeout());
offers.add(offer);
// Process this offer in another thread
Thread offerThread = new Thread("Dispatch offer - queue: " + queue.getName()) {
@Override
public void run() {
dispatch(offer);
// Remove this offer from the list of active offers
offers.remove(offer);
}
};
offerThread.start();
}
/**
* Dispatch the given request to one or more agents in the agent pool.<p>
*
* If this method returns, it is assumed that the request was properly
* dispatched.The only exception is if an agent is not in the pool for routing
* within the agent timeout period, the dispatch will throw an AgentNotFoundException
* so the request can be re-routed.
*
* @param offer the offer to send to the best agent available.
*/
public void dispatch(Offer offer) {
// The time when the request should timeout
long timeoutTime = System.currentTimeMillis() + info.getRequestTimeout();
final Request request = offer.getRequest();
boolean canBeInQueue = request instanceof UserRequest;
Map<String,List<String>> map = request.getMetaData();
String initialAgent = map.get("agent") == null || map.get("agent").isEmpty() ? null : map.get("agent").get(0);
String ignoreAgent = map.get("ignore") == null || map.get("ignore").isEmpty() ? null : map.get("ignore").get(0);
// Log debug trace
Log.debug("RR - Dispatching request: " + request + " in queue: " + queue.getAddress());
// Send the offer to the best agent. While the offer has not been accepted send it to the
// next best agent. If there aren't any agent available then skip this section and proceed
// to overflow the current request
if (!agentList.isEmpty()) {
for (long timeRemaining = timeoutTime - System.currentTimeMillis();
!offer.isAccepted() && timeRemaining > 0 && !offer.isCancelled();
timeRemaining = timeoutTime - System.currentTimeMillis()) {
try {
AgentSession session = getBestNextAgent(initialAgent, ignoreAgent, offer);
if (session == null && agentList.isEmpty()) {
// Stop looking for an agent since there are no more agent available
break;
}
else if (session == null || offer.isRejector(session)) {
initialAgent = null;
Thread.sleep(1000);
}
else {
// Recheck for changed maxchat setting
Workgroup workgroup = request.getWorkgroup();
if (session.getCurrentChats(workgroup) < session.getMaxChats(workgroup)) {
// Set the timeout of the offer based on the remaining time of the
// initial request and the default offer timeout
timeRemaining = timeoutTime - System.currentTimeMillis();
offer.setTimeout(timeRemaining < info.getOfferTimeout() ?
timeRemaining : info.getOfferTimeout());
// Make the offer and wait for a resolution to the offer
if (!request.sendOffer(session, queue)) {
// Log debug trace
Log.debug("RR - Offer for request: " + offer.getRequest() +
" FAILED TO BE SENT to agent: " +
session.getJID());
continue;
}
// Log debug trace
Log.debug("RR - Offer for request: " + offer.getRequest() + " SENT to agent: " +
session.getJID());
offer.waitForResolution();
// If the offer was accepted, we send out the invites
// and reset the offer
if (offer.isAccepted()) {
// Get the first agent that accepted the offer
AgentSession selectedAgent = offer.getAcceptedSessions().get(0);
// Log debug trace
Log.debug("RR - Agent: " + selectedAgent.getJID() +
" ACCEPTED request: " +
request);
// Create the room and send the invitations
offer.invite(selectedAgent);
// Notify the agents that accepted the offer that the offer process
// has finished
for (AgentSession agent : offer.getAcceptedSessions()) {
agent.removeOffer(offer);
}
if (canBeInQueue) {
// Remove the user from the queue since his request has
// been accepted
queue.removeRequest((UserRequest) request);
}
}
}
else {
// Log debug trace
Log.debug("RR - Selected agent: " + session.getJID() +
" has reached max number of chats");
}
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
}
if (!offer.isAccepted() && !offer.isCancelled()) {
// Calculate the maximum time limit for an unattended request before cancelling it
long limit = request.getCreationTime().getTime() +
(info.getRequestTimeout() * (getOverflowTimes() + 1));
if (limit - System.currentTimeMillis() <= 0 || !canBeInQueue) {
// Log debug trace
Log.debug("RR - Cancelling request that maxed out overflow limit or cannot be queued: " + request);
// Cancel the request if it has overflowed 'n' times
request.cancel(Request.CancelType.AGENT_NOT_FOUND);
}
else {
// Overflow if request timed out and was not dispatched and max number of overflows
// has not been reached yet
overflow(offer);
// If there is no other queue to overflow then cancel the request
if (!offer.isAccepted() && !offer.isCancelled()) {
// Log debug trace
Log.debug("RR - Cancelling request that didn't overflow: " + request);
request.cancel(Request.CancelType.AGENT_NOT_FOUND);
}
}
}
}
/**
* <p>Overflow the current request into another queue if possible.</p>
* <p/>
* <p>Future versions of the dispatcher may wish to overflow in
* more sophisticated ways. Currently we do it according to overflow
* rules: none (no overflow), backup (to a backup if it exists and is
* available, or randomly.</p>
*
* @param offer the offer to place in the overflow queue.
*/
private void overflow(Offer offer) {
RequestQueue backup = null;
if (RequestQueue.OverflowType.OVERFLOW_BACKUP.equals(queue.getOverflowType())) {
backup = queue.getBackupQueue();
// Check that the backup queue has agents available otherwise discard it
if (backup != null && !backup.getAgentSessionList().containsAvailableAgents()) {
backup = null;
}
}
else if (RequestQueue.OverflowType.OVERFLOW_RANDOM.equals(queue.getOverflowType())) {
backup = getRandomQueue();
}
// If a backup queue was found then cancel this offer, remove the request from the queue
// and add the request in the backup queue
if (backup != null) {
offer.cancel();
UserRequest request = (UserRequest) offer.getRequest();
// Remove the request from the queue since it is going to be added to another
// queue
queue.removeRequest(request);
// Log debug trace
Log.debug("RR - Overflowing request: " + request + " to queue: " +
backup.getAddress());
backup.addRequest(request);
}
}
/**
* Returns a queue that was randomly selected.
*
* @return a queue that was randomly selected.
*/
private RequestQueue getRandomQueue() {
int qCount = queue.getWorkgroup().getRequestQueueCount();
if (qCount > 1) {
// Build a list of all queues eligible for overflow
LinkedList<RequestQueue> overflowQueueList = new LinkedList<RequestQueue>();
for (RequestQueue overflowQueue : queue.getWorkgroup().getRequestQueues()) {
if (!queue.equals(overflowQueue) && overflowQueue.getAgentSessionList().containsAvailableAgents()) {
overflowQueueList.addLast(overflowQueue);
}
}
// If there are any eligible queues
if (overflowQueueList.size() > 0) {
// choose the random index of the overflow queue to use
int targetIndex = (int) Math.floor(((float) (overflowQueueList.size())) * Math.random());
if (targetIndex < overflowQueueList.size()) {
return overflowQueueList.get(targetIndex);
}
}
}
return null;
}
/**
* <p>Locate the next 'best' agent to receive an offer.</p>
* <p>Routing is based on show-status, max-chats, and who has
* already rejected the offer.
* show status is ranked from most available to least:
* chat, default (no show status), away,
* and xa. A show status of dnd indicates no offers should be routed to an agent.
* The general algorithm is:</p>
* <ul>
* <li>Mark the current position.</li>
* <li>Start iterating around the circular queue until all agents
* have been considered. For each agent:
* <ul>
* <li>Skip if session is null. Should only occur if no agents are in the list.</li>
* <li>Skip if session show state is DND. Never route to agents that are dnd.</li>
* <li>Skip if session current-chats is equal to or higher than max-chats.</li>
* <li>Replace current best if:
* <ul>
* <li>No current best. Any agent is better than none.</li>
* <li>If session hasn't rejected offer but current best has.</li>
* <li>If both session and current best have not rejected the
* offer and session show-status is higher.</li>
* <li>If both session and current best have rejected offer and
* session show-status is higher.</li>
* </ul></li>
* </ul></li>
* </li>
*
* @param initialAgent the initial agent requested by the user.
* @param ignoreAgent agent that should not be considered as available.
* @param offer the offer about to be sent to the best available agent.
* @return the best agent.
*/
private AgentSession getBestNextAgent(String initialAgent, String ignoreAgent, Offer offer) {
AgentSession bestSession;
// Look for specified agent in agent list
if (initialAgent != null) {
final AgentSessionList agentSessionList = queue.getAgentSessionList();
for (AgentSession agentSession : agentSessionList.getAgentSessions()) {
String sessionAgent = agentSession.getAgent().getAgentJID().toBareJID();
boolean match = sessionAgent.startsWith(initialAgent.toLowerCase());
Workgroup workgroup = offer.getRequest().getWorkgroup();
if (agentSession.isAvailableToChat() &&
agentSession.getCurrentChats(workgroup) < agentSession.getMaxChats(workgroup) && match) {
bestSession = agentSession;
// Log debug trace
Log.debug("RR - Initial agent: " + bestSession.getJID() +
" will receive offer for request: " +
offer.getRequest());
return bestSession;
}
}
}
// Let's iterate through each agent and check availability
final AgentSessionList agentSessionList = queue.getAgentSessionList();
final List<AgentSession> possibleSessions = new ArrayList<AgentSession>();
for (AgentSession agentSession : agentSessionList.getAgentSessions()) {
String sessionAgent = agentSession.getAgent().getAgentJID().toBareJID();
boolean ignore = ignoreAgent != null && sessionAgent.startsWith(ignoreAgent.toLowerCase());
if (!ignore && validateAgent(agentSession, offer)) {
possibleSessions.add(agentSession);
}
}
// Select the best agent from the list of possible agents
if (possibleSessions.size() > 0) {
AgentSession s = agentSelector.bestAgentFrom(possibleSessions, offer);
// Log debug trace
Log.debug("RR - Agent SELECTED: " + s.getJID() +
" for receiving offer for request: " +
offer.getRequest());
return s;
}
return null;
}
/**
* Returns true if the agent session may receive an offer. An agent session may receive new
* offers if:
*
* 1) the presence status of the agent allows to receive offers
* 2) the maximum of chats has not been reached for the agent
* 3) the agent has not rejected the offer before
* 4) the agent does not have to answer a previuos offer
*
* @param session the session to check if it may receive an offer
* @param offer the offer to send.
* @return true if the agent session may receive an offer.
*/
private boolean validateAgent(AgentSession session, Offer offer) {
if (agentSelector.validateAgent(session, offer)) {
// Log debug trace
Log.debug("RR - Agent: " + session.getJID() +
" MAY receive offer for request: " +
offer.getRequest());
return true;
}
// Log debug trace
Log.debug("RR - Agent: " + session.getJID() +
" MAY NOT receive offer for request: " +
offer.getRequest());
return false;
}
/**
* <p>Generate the agents offer list.</p>
*/
private void fillAgentsList() {
AgentSessionList agentSessionList = queue.getAgentSessionList();
agentSessionList.addAgentSessionListener(this);
for (AgentSession agentSession : agentSessionList.getAgentSessions()) {
if (!agentList.contains(agentSession)) {
agentList.add(agentSession);
}
}
}
/**
* Returns the max number of times a request may overflow. Once the request has exceded this
* number it will be cancelled. This limit avoids infinite overflow loops.
*
* @return the max number of times a request may overflow.
*/
private long getOverflowTimes() {
return JiveGlobals.getIntProperty("xmpp.live.request.overflow", 3);
}
/**
* Returns the number of milliseconds to wait until expiring an agent rejection.
*
* @return the number of milliseconds to wait until expiring an agent rejection.
*/
private long getAgentRejectionTimeout() {
return JiveGlobals.getIntProperty("xmpp.live.rejection.timeout", 20000);
}
public void notifySessionAdded(AgentSession session) {
if (!agentList.contains(session)) {
agentList.add(session);
}
}
public void notifySessionRemoved(AgentSession session) {
agentList.remove(session);
for (Offer offer : offers) {
offer.reject(session);
}
}
public DispatcherInfo getDispatcherInfo() {
return info;
}
public void setDispatcherInfo(DispatcherInfo info) throws UnauthorizedException {
try {
infoProvider.updateDispatcherInfo(queue.getID(), info);
this.info = info;
}
catch (NotFoundException e) {
Log.error(e.getMessage(), e);
}
catch (UnsupportedOperationException e) {
Log.error(e.getMessage(), e);
}
}
public int getOfferCount() {
return offers.size();
}
public Iterator<Offer> getOffers() {
return offers.iterator();
}
public Iterator<Offer> getOffers(WorkgroupResultFilter filter) {
return filter.filter(offers.iterator());
}
public String getProperty(String name) {
return properties.getProperty(name);
}
public void setProperty(String name, String value) throws UnauthorizedException {
properties.setProperty(name, value);
}
public void deleteProperty(String name) throws UnauthorizedException {
properties.deleteProperty(name);
}
public Collection<String> getPropertyNames() {
return properties.getPropertyNames();
}
public AgentSelector getAgentSelector() {
return agentSelector;
}
public void setAgentSelector(AgentSelector agentSelector) {
this.agentSelector = agentSelector;
// Delete all agentSelectorproperties.
try {
for (String property : getPropertyNames()) {
if (property.startsWith("agentSelector")) {
deleteProperty(property);
}
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
// Save the agentSelectoras a property of the dispatcher
try {
Map<String, String> propertyMap = getPropertiesMap(agentSelector, "agentSelector.");
for (Map.Entry<String, String> entry : propertyMap.entrySet()) {
setProperty(entry.getKey(), entry.getValue());
}
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
private Map<String, String> getPropertiesMap(AgentSelector agentSelector, String context) {
// Build the properties map that will be saved later
Map<String, String> propertyMap = new HashMap<String, String>();
// Write out class name
propertyMap.put(context + "className", agentSelector.getClass().getName());
// Write out all properties
Map<String, String> props = BeanUtils.getProperties(agentSelector);
for (Map.Entry<String, String> entry : props.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if (value != null && !"".equals(value)) {
propertyMap.put(context + "properties." + name, value);
}
}
return propertyMap;
}
private void loadAgentSelector() {
try {
String context = "agentSelector.";
String className = getProperty(context + "className");
if (className == null) {
// Do nothing and use the BasicAgentSelector
return;
}
Class agentSelectorClass = loadClass(className);
agentSelector = (AgentSelector) agentSelectorClass.newInstance();
// Load properties.
Collection<String> props = getChildrenPropertyNames(context + "properties", getPropertyNames());
Map<String, String> agentSelectorProps = new HashMap<String, String>();
for (String key : props) {
String value = getProperty(key);
// Get the bean property name, which is everything after the last '.' in the
// xml property name.
agentSelectorProps.put(key.substring(key.lastIndexOf(".")+1), value);
}
// Set properties on the bean
BeanUtils.setProperties(agentSelector, agentSelectorProps);
}
catch (Exception e) {
Log.error(e.getMessage(), e);
}
}
private Class loadClass(String className) throws ClassNotFoundException {
try {
return ClassUtils.forName(className);
}
catch (ClassNotFoundException e) {
return this.getClass().getClassLoader().loadClass(className);
}
}
/**
* Returns a child property names given a parent and an Iterator of property names.
*
* @param parent parent property name.
* @param properties all property names to search.
* @return an Iterator of child property names.
*/
private static Collection<String> getChildrenPropertyNames(String parent, Collection<String> properties) {
List<String> results = new ArrayList<String>();
for (String name : properties) {
if (name.startsWith(parent) && !name.equals(parent)) {
results.add(name);
}
}
return results;
}
public void shutdown() {
// Do nothing
}
}