/* * Jicofo, the Jitsi Conference Focus. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package org.jitsi.jicofo; import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; import net.java.sip.communicator.util.Logger; import org.jitsi.protocol.xmpp.*; import org.jitsi.util.*; import org.jitsi.videobridge.stats.*; import org.jivesoftware.smack.packet.*; import java.util.*; /** * Class exposes methods for selecting best videobridge from all currently * available. Videobridge state is tracked through PubSub notifications and * based on feedback from Jitsi Meet conference focus. * * @author Pawel Domas */ public class BridgeSelector implements SubscriptionListener { /** * The logger. */ private final static Logger logger = Logger.getLogger(BridgeSelector.class); /** * Property used to configure mapping of videobridge JIDs to PubSub nodes. * Single mapping is defined by writing videobridge JID followed by ':' and * pub-sub node name. If multiple mapping are to be appended then ';' must * be used to separate each mapping. * * org.jitsi.focus.BRIDGE_PUBSUB_MAPPING * =jvb1.server.net:pubsub1;jvb2.server.net:pubsub2;jvb3.server.net:pubsub3 * * PubSub service node is discovered automatically for now and the first one * that offer PubSub feature is selected. Then this selector class * subscribes for all mapped PubSub nodes on that service for notifications. * * FIXME: we do not unsubscribe from pubsub notifications on shutdown */ public static final String BRIDGE_TO_PUBSUB_PNAME = "org.jitsi.focus.BRIDGE_PUBSUB_MAPPING"; /** * Operation set used to subscribe to PubSub nodes notifications. */ private final OperationSetSubscription subscriptionOpSet; /** * The map of bridge JID to <tt>BridgeState</tt>. */ private Map<String, BridgeState> bridges = new HashMap<String, BridgeState>(); /** * Pre-configured JVB used as last chance option even if no bridge has been * auto-detected on startup. */ private String preConfiguredBridge; /** * The map of Pub-Sub nodes to videobridge JIDs. */ private Map<String, String> pubSubToBridge = new HashMap<String, String>(); /** * Creates new instance of {@link BridgeSelector}. * * @param subscriptionOpSet the operations set that will be used by this * instance to subscribe to pub-sub notifications. */ public BridgeSelector(OperationSetSubscription subscriptionOpSet) { this.subscriptionOpSet = subscriptionOpSet; String mappingPropertyValue = FocusBundleActivator.getConfigService() .getString(BRIDGE_TO_PUBSUB_PNAME); if (StringUtils.isNullOrEmpty(mappingPropertyValue)) { return; } String[] pairs = mappingPropertyValue.split(";"); for (String pair : pairs) { String[] bridgeAndNode = pair.split(":"); String bridge = bridgeAndNode[0]; String pubSubNode = bridgeAndNode[1]; pubSubToBridge.put(pubSubNode, bridge); logger.info("Pub-sub mapping: " + pubSubNode + " -> " + bridge); } } /** * Adds next Jitsi Videobridge XMPP address to be observed by this selected * and taken into account in best bridge selection process. * * @param bridgeJid the JID of videobridge to be added to this selector's * set of videobridges. */ public void addJvbAddress(String bridgeJid) { String pubSubNode = findNodeForBridge(bridgeJid); if (pubSubNode != null) { logger.info( "Subscribing to pubsub notfications to " + pubSubNode + " for " + bridgeJid); subscriptionOpSet.subscribe(pubSubNode, this); } else { logger.warn("No pub-sub node mapped for " + bridgeJid + " statistics will not be tracked for this instance."); } bridges.put(bridgeJid, new BridgeState(bridgeJid)); } /** * Returns least loaded and *operational* videobridge. By operational it * means that it was not reported by any of conference focuses to fail while * allocating channels. * * @return the JID of least loaded videobridge or <tt>null</tt> if there are * not any operational bridges currently. */ public String selectVideobridge() { // FIXME: Consider caching elected bridge and reset on stats // or is operational change if (bridges.size() == 0) { // No bridges registered return null; } // Elect best bridge Iterator<BridgeState> bridgesIter = bridges.values().iterator(); BridgeState bestChoice = bridgesIter.next(); while (bridgesIter.hasNext()) { BridgeState candidate = bridgesIter.next(); if (candidate.compareTo(bestChoice) < 0) { bestChoice = candidate; } } return bestChoice.isOperational ? bestChoice.jid : null; } /** * Returns the list of all known videobridges JIDs ordered by load and * *operational* status. Not operational bridges are at the end of the list. */ public List<String> getPrioritizedBridgesList() { ArrayList<BridgeState> bridgeList = new ArrayList<BridgeState>(bridges.values()); Collections.sort(bridgeList); boolean isAnyBridgeUp = false; ArrayList<String> bridgeJidList = new ArrayList<String>(); for (BridgeState bridgeState : bridgeList) { bridgeJidList.add(bridgeState.jid); if (bridgeState.isOperational) { isAnyBridgeUp = true; } } // Check if we have pre-configured bridge to include in the list if (!StringUtils.isNullOrEmpty(preConfiguredBridge) && !bridgeJidList.contains(preConfiguredBridge)) { // If no auto-detected bridge is up then put pre-configured up front if (!isAnyBridgeUp) { bridgeJidList.add(0, preConfiguredBridge); } else { bridgeJidList.add(preConfiguredBridge); } } return bridgeJidList; } /** * Updates given *operational* status of the videobridge identified by given * <tt>bridgeJid</tt> address. * * @param bridgeJid the XMPP address of the bridge. * @param isWorking <tt>true</tt> if bridge successfully allocated * the channels which means it is in *operational* state. */ public void updateBridgeOperationalStatus(String bridgeJid, boolean isWorking) { BridgeState bridge = bridges.get(bridgeJid); if (bridge != null) { bridge.setIsOperational(isWorking); } else { logger.warn("No bridge registered for jid: " + bridgeJid); } } /** * Returns videobridge JID for given pub-sub node, but only if it has been * added using {@link #addJvbAddress(String)} method. * * @param pubSubNode the pub-sub node name. * * @return videobridge JID for given pub-sub node. */ public String getBridgeForPubSubNode(String pubSubNode) { BridgeState bridge = findBridgeForNode(pubSubNode); return bridge != null ? bridge.jid : null; } /** * Finds <tt>BridgeState</tt> for given pub-sub node. * * @param pubSubNode the name of pub-sub node to match with the bridge. * * @return <tt>BridgeState</tt> for given pub-sub node name. */ private BridgeState findBridgeForNode(String pubSubNode) { String bridgeJid = pubSubToBridge.get(pubSubNode); if (bridgeJid != null) { return bridges.get(bridgeJid); } return null; } /** * Finds pub-sub node name for given videobridge JID. * * @param bridgeJid the JID of videobridge to be matched with * pub-sub node name. * * @return name of pub-sub node mapped for given videobridge JID. */ private String findNodeForBridge(String bridgeJid) { for (Map.Entry<String, String> psNodeToBridge : pubSubToBridge.entrySet()) { if (psNodeToBridge.getValue().equals(bridgeJid)) { return psNodeToBridge.getKey(); } } return null; } /** * Pub-sub notification processing logic. * * {@inheritDoc} */ @Override public void onSubscriptionUpdate(String node, PacketExtension payload) { if (!(payload instanceof ColibriStatsExtension)) { logger.error( "Unexpected pub-sub notification payload: " + payload.getClass().getName()); return; } BridgeState bridgeState = findBridgeForNode(node); if (bridgeState == null) { logger.warn( "No bridge registered or missing mapping for node: " + node); return; } ColibriStatsExtension stats = (ColibriStatsExtension) payload; for (PacketExtension child : stats.getChildExtensions()) { if (!(child instanceof ColibriStatsExtension.Stat)) { continue; } ColibriStatsExtension.Stat stat = (ColibriStatsExtension.Stat) child; if (VideobridgeStatistics.CONFERENCES.equals(stat.getName())) { Object statValue = stat.getValue(); if (statValue == null) { return; } String stringStatValue = String.valueOf(statValue); try { bridgeState.setConferenceCount( Integer.parseInt(stringStatValue)); } catch(NumberFormatException e) { logger.error( "Error parsing conference count stat: " + stringStatValue); } } } } /** * Returns the JID of pre-configured Jitsi Videobridge instance. */ public String getPreConfiguredBridge() { return preConfiguredBridge; } /** * Sets the JID of pre-configured JVB instance which will be used when all * auto-detected bridges are down. * @param preConfiguredBridge XMPP address of pre-configured JVB component. */ public void setPreConfiguredBridge(String preConfiguredBridge) { this.preConfiguredBridge = preConfiguredBridge; } /** * Class holds videobridge state and implements {@link java.lang.Comparable} * interface to find least loaded bridge. */ class BridgeState implements Comparable<BridgeState> { /** * Videobridge XMPP address. */ private final String jid; // If not set we consider it highly occupied, // because no stats we have been fetched so far. private int conferenceCount = Integer.MAX_VALUE; /** * Stores *operational* status which means it has been successfully used * by the focus to allocate the channels. It is reset to false when * focus fails to allocate channels, but it gets another chance when all * currently working bridges go down and might eventually get elevated * back to *operational* state. */ private boolean isOperational = true /* we assume it is operational */; BridgeState(String bridgeJid) { if (StringUtils.isNullOrEmpty(bridgeJid)) throw new NullPointerException("bridgeJid"); this.jid = bridgeJid; } public void setConferenceCount(int conferenceCount) { if (this.conferenceCount != conferenceCount) { logger.info( "Conference count for: " + jid + ": " + conferenceCount); } this.conferenceCount = conferenceCount; } public int getConferenceCount() { return this.conferenceCount; } public void setIsOperational(boolean isOperational) { this.isOperational = isOperational; } /** * The least value is returned the least the bridge is loaded. * * {@inheritDoc} */ @Override public int compareTo(BridgeState o) { if (this.isOperational && !o.isOperational) return -1; else if (!this.isOperational && o.isOperational) return 1; return conferenceCount - o.conferenceCount; } } }