/* * 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 } }