All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cometd.oort.Oort Maven / Gradle / Ivy

/*
 * Copyright (c) 2010 the original author or authors.
 *
 * 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.cometd.oort;

import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.cometd.bayeux.Channel;
import org.cometd.bayeux.ChannelId;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.BayeuxServer.Extension;
import org.cometd.bayeux.server.ConfigurableServerChannel;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerMessage.Mutable;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.common.HashMapMessage;
import org.cometd.server.BayeuxServerImpl;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.component.AggregateLifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.eclipse.jetty.websocket.WebSocketClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

Oort is the cluster manager that links one CometD server to a set of other CometD servers.

*

The Oort instance is created and configured by either {@link OortMulticastConfigServlet} or * {@link OortStaticConfigServlet}.

*

This class maintains a collection of {@link OortComet} instances to each * CometD server, created by calls to {@link #observeComet(String)}.

*

The key configuration parameter is the Oort URL, which is * full public URL of the CometD servlet to which the Oort instance is bound, * for example: http://myserver:8080/context/cometd.

*

Oort instances can be configured with a shared {@link #setSecret(String) secret}, which allows * the Oort instance to distinguish handshakes coming from remote clients from handshakes coming from * other Oort comets: the firsts may be subject to a stricter authentication policy than the seconds.

* * @see OortMulticastConfigServlet * @see OortStaticConfigServlet */ public class Oort extends AggregateLifeCycle { public final static String OORT_ATTRIBUTE = Oort.class.getName(); public static final String EXT_OORT_FIELD = "org.cometd.oort"; public static final String EXT_OORT_URL_FIELD = "oortURL"; public static final String EXT_OORT_ID_FIELD = "oortId"; public static final String EXT_OORT_SECRET_FIELD = "oortSecret"; public static final String EXT_COMET_URL_FIELD = "cometURL"; public static final String EXT_OORT_ALIAS_URL_FIELD = "oortAliasURL"; public static final String OORT_CLOUD_CHANNEL = "/oort/cloud"; private static final String COMET_URL_ATTRIBUTE = EXT_OORT_FIELD + "." + EXT_COMET_URL_FIELD; private final ConcurrentMap _pendingComets = new ConcurrentHashMap(); private final ConcurrentMap _clientComets = new ConcurrentHashMap(); private final ConcurrentMap _serverComets = new ConcurrentHashMap(); private final ConcurrentMap _channels = new ConcurrentHashMap(); private final CopyOnWriteArrayList _cometListeners = new CopyOnWriteArrayList(); private final Extension _oortExtension = new OortExtension(); private final ServerChannel.MessageListener _cloudListener = new CloudListener(); private final BayeuxServer _bayeux; private final String _url; private final String _id; private final Logger _logger; private ThreadPool _threadPool; private HttpClient _httpClient; private WebSocketClientFactory _wsFactory; private final LocalSession _oortSession; private String _secret; private boolean _debug; private boolean _clientDebug; public Oort(BayeuxServer bayeux, String url) { _bayeux = bayeux; _url = url; _id = UUID.randomUUID().toString(); _logger = LoggerFactory.getLogger(getClass().getName() + "." + _url); _debug = String.valueOf(BayeuxServerImpl.DEBUG_LOG_LEVEL).equals(bayeux.getOption(BayeuxServerImpl.LOG_LEVEL)); _oortSession = bayeux.newLocalSession("oort"); _secret = Long.toHexString(new SecureRandom().nextLong()); } @Override protected void doStart() throws Exception { if (_threadPool == null) _threadPool = new QueuedThreadPool(); addBean(_threadPool); if (_httpClient == null) { _httpClient = new HttpClient(); _httpClient.setThreadPool(_threadPool); } addBean(_httpClient); if (_wsFactory == null) _wsFactory = new WebSocketClientFactory(_threadPool); addBean(_wsFactory); super.doStart(); _bayeux.addExtension(_oortExtension); _bayeux.createIfAbsent(OORT_CLOUD_CHANNEL, new ConfigurableServerChannel.Initializer() { public void configureChannel(ConfigurableServerChannel channel) { channel.addAuthorizer(GrantAuthorizer.GRANT_ALL); channel.addListener(_cloudListener); } }); _oortSession.handshake(); } @Override protected void doStop() throws Exception { _oortSession.disconnect(); for (OortComet comet : _pendingComets.values()) comet.disconnect(1000); _pendingComets.clear(); for (ClientCometInfo cometInfo : _clientComets.values()) cometInfo.comet.disconnect(1000); _clientComets.clear(); _serverComets.clear(); _channels.clear(); ServerChannel oortCloudChannel = _bayeux.getChannel(OORT_CLOUD_CHANNEL); if (oortCloudChannel != null) { oortCloudChannel.removeListener(_cloudListener); oortCloudChannel.removeAuthorizer(GrantAuthorizer.GRANT_ALL); } _bayeux.removeExtension(_oortExtension); super.doStop(); } public BayeuxServer getBayeuxServer() { return _bayeux; } /** * @return the public absolute URL of the Oort CometD server */ public String getURL() { return _url; } public String getId() { return _id; } public String getSecret() { return _secret; } public void setSecret(String secret) { this._secret = secret; } public boolean isDebugEnabled() { return _debug; } public void setDebugEnabled(boolean debug) { _debug = debug; } private void debug(String message, Object... args) { if (isDebugEnabled()) _logger.info(message, args); else _logger.debug(message, args); } public boolean isClientDebugEnabled() { return _clientDebug; } public void setClientDebugEnabled(boolean clientDebugEnabled) { _clientDebug = clientDebugEnabled; for (ClientCometInfo cometInfo : _clientComets.values()) cometInfo.comet.setDebugEnabled(clientDebugEnabled); } /** *

Connects (if not already connected) and observes another Oort instance * (identified by the given URL) via a {@link OortComet} instance.

* * @param cometURL the Oort URL to observe * @return The {@link OortComet} instance associated to the Oort instance identified by the URL * or null if the given Oort URL represent this Oort instance */ public OortComet observeComet(String cometURL) { return observeComet(cometURL, null); } protected OortComet observeComet(String cometURL, String cometAliasURL) { try { URI uri = new URI(cometURL); if (uri.getScheme() == null) throw new IllegalArgumentException("Missing protocol in comet URL " + cometURL); if (uri.getHost() == null) throw new IllegalArgumentException("Missing host in comet URL " + cometURL); } catch (URISyntaxException x) { throw new IllegalArgumentException(x); } if (_url.equals(cometURL)) return null; OortComet comet = getComet(cometURL); if (comet != null) { debug("Comet {} is already connected", cometURL); return comet; } comet = newOortComet(cometURL); OortComet existing = _pendingComets.putIfAbsent(cometURL, comet); if (existing != null) { debug("Comet {} is already connecting", cometURL); return existing; } comet.getChannel(Channel.META_HANDSHAKE).addListener(new HandshakeListener(cometURL, comet)); debug("Connecting to comet {}", cometURL); String b64Secret = encodeSecret(getSecret()); Message.Mutable fields = new HashMapMessage(); Map ext = fields.getExt(true); Map oortExt = new HashMap(4); ext.put(EXT_OORT_FIELD, oortExt); oortExt.put(EXT_OORT_URL_FIELD, getURL()); oortExt.put(EXT_OORT_ID_FIELD, getId()); oortExt.put(EXT_OORT_SECRET_FIELD, b64Secret); oortExt.put(EXT_COMET_URL_FIELD, cometURL); if (cometAliasURL != null) oortExt.put(EXT_OORT_ALIAS_URL_FIELD, cometAliasURL); connectComet(comet, fields); return comet; } protected OortComet newOortComet(String cometURL) { return new OortComet(this, cometURL); } protected String encodeSecret(String secret) { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); return new String(B64Code.encode(digest.digest(secret.getBytes("UTF-8")))); } catch (Exception x) { throw new IllegalArgumentException(x); } } protected void connectComet(OortComet comet, Message.Mutable fields) { comet.handshake(fields); } public OortComet deobserveComet(String cometURL) { if (_url.equals(cometURL)) return null; OortComet comet = _pendingComets.remove(cometURL); if (comet != null) { debug("Disconnecting pending comet {}", cometURL); comet.disconnect(); } Iterator cometInfos = _clientComets.values().iterator(); while (cometInfos.hasNext()) { ClientCometInfo cometInfo = cometInfos.next(); if (cometInfo.matchesURL(cometURL)) { debug("Disconnecting comet {}", cometURL); comet = cometInfo.getOortComet(); comet.disconnect(); cometInfos.remove(); break; } } return comet; } /** * @return the set of known Oort comet servers URLs. */ public Set getKnownComets() { Set result = new HashSet(); for (ClientCometInfo cometInfo : _clientComets.values()) result.add(cometInfo.getURL()); return result; } /** * @param cometURL the URL of a Oort comet * @return the OortComet instance connected with the Oort comet with the given URL */ public OortComet getComet(String cometURL) { for (ClientCometInfo cometInfo : _clientComets.values()) { if (cometInfo.matchesURL(cometURL)) return cometInfo.getOortComet(); } return null; } /** * * @param cometURL the URL of a Oort comet * @return the OortComet instance connecting or connected with the Oort comet with the given URL */ protected OortComet findComet(String cometURL) { OortComet result = getComet(cometURL); if (result == null) result = _pendingComets.get(cometURL); return result; } /** *

Observes the given channel, registering to receive messages from * the Oort comets connected to this Oort instance.

*

Once observed, all {@link OortComet} instances subscribe * to the channel and will repeat any messages published to * the local channel (with loop prevention), so that the * messages are distributed to all Oort comet servers.

* * @param channelName the channel to observe */ public void observeChannel(String channelName) { if (!ChannelId.isBroadcast(channelName)) throw new IllegalArgumentException("Channel " + channelName + " cannot be observed because is not a broadcast channel"); if (_channels.putIfAbsent(channelName, Boolean.TRUE) == null) { Set observedChannels = getObservedChannels(); for (ClientCometInfo cometInfo : _clientComets.values()) cometInfo.getOortComet().subscribe(observedChannels); } } public void deobserveChannel(String channelId) { if (_channels.remove(channelId) != null) { for (ClientCometInfo cometInfo : _clientComets.values()) cometInfo.getOortComet().unsubscribe(channelId); } } /** * @param session the server session to test * @return whether the given server session is one of those created by the Oort internal working * @see #isOortHandshake(Message) */ public boolean isOort(ServerSession session) { String id = session.getId(); if (id.equals(_oortSession.getId())) return true; for (ServerCometInfo cometInfo : _serverComets.values()) { if (cometInfo.getServerSession().getId().equals(session.getId())) return true; } return false; } /** * @param handshake the handshake message to test * @return whether the given handshake message is coming from another Oort comet * that has been configured with the same {@link #setSecret(String) secret} * @see #isOort(ServerSession) */ public boolean isOortHandshake(Message handshake) { if (!Channel.META_HANDSHAKE.equals(handshake.getChannel())) return false; Map ext = handshake.getExt(); if (ext == null) return false; Object oortExtObject = ext.get(EXT_OORT_FIELD); if (!(oortExtObject instanceof Map)) return false; @SuppressWarnings("unchecked") Map oortExt = (Map)oortExtObject; String cometURL = (String)oortExt.get(EXT_COMET_URL_FIELD); if (!getURL().equals(cometURL)) return false; String b64RemoteSecret = (String)oortExt.get(EXT_OORT_SECRET_FIELD); String b64LocalSecret = encodeSecret(getSecret()); return b64LocalSecret.equals(b64RemoteSecret); } public String toString() { return _url; } /** *

Called to register the details of a successful handshake from another Oort comet.

* * @param oortExt the remote Oort information * @param session the server session that represent the connection with the remote Oort comet * @return false if a connection from a remote Oort has already been established, true otherwise */ protected boolean incomingCometHandshake(Map oortExt, ServerSession session) { String remoteOortURL = (String)oortExt.get(EXT_OORT_URL_FIELD); debug("Incoming comet handshake from comet {} with {}", remoteOortURL, session); String remoteOortId = (String)oortExt.get(EXT_OORT_ID_FIELD); ServerCometInfo serverCometInfo = new ServerCometInfo(remoteOortId, remoteOortURL, session); ServerCometInfo existing = _serverComets.putIfAbsent(remoteOortId, serverCometInfo); if (existing != null) { debug("Comet {} is already known with {}", remoteOortURL, existing.getServerSession()); return false; } session.setAttribute(COMET_URL_ATTRIBUTE, remoteOortURL); // Be notified when the remote comet stops session.addListener(new OortCometDisconnectListener(remoteOortURL, remoteOortId)); // Prevent loops in sending/receiving messages session.addListener(new OortCometLoopListener()); return true; } /** * Registers the given listener to be notified of comet events. * @param listener the listener to add */ public void addCometListener(CometListener listener) { _cometListeners.add(listener); } /** * Deregisters the given listener from being notified of comet events. * @param listener the listener to remove */ public void removeCometListener(CometListener listener) { _cometListeners.remove(listener); } private void notifyCometJoined(String remoteURL) { CometListener.Event event = new CometListener.Event(this, remoteURL); for (CometListener cometListener : _cometListeners) { try { cometListener.cometJoined(event); } catch (Exception x) { _logger.info("Exception while invoking listener " + cometListener, x); } } } private void notifyCometLeft(String remoteURL) { CometListener.Event event = new CometListener.Event(this, remoteURL); for (CometListener cometListener : _cometListeners) { try { cometListener.cometLeft(event); } catch (Exception x) { _logger.info("Exception while invoking listener " + cometListener, x); } } } /** *

Extension that detects incoming handshakes from other Oort servers.

* * @see Oort#incomingCometHandshake(Map, ServerSession) */ protected class OortExtension implements Extension { public boolean rcv(ServerSession from, Mutable message) { return true; } public boolean rcvMeta(ServerSession from, Mutable message) { return true; } public boolean send(ServerSession from, ServerSession to, Mutable message) { return true; } public boolean sendMeta(ServerSession to, Mutable message) { // Skip local sessions if (to == null) return true; if (!Channel.META_HANDSHAKE.equals(message.getChannel())) return true; // Process only successful responses if (!message.isSuccessful()) return true; Map associatedExt = message.getAssociated().getExt(); if (associatedExt == null) return true; Object associatedOortExtObject = associatedExt.get(EXT_OORT_FIELD); if (associatedOortExtObject instanceof Map) { @SuppressWarnings("unchecked") Map associatedOortExt = (Map)associatedOortExtObject; String remoteOortURL = (String)associatedOortExt.get(EXT_OORT_URL_FIELD); String cometURL = (String)associatedOortExt.get(EXT_COMET_URL_FIELD); String remoteOortId = (String)associatedOortExt.get(EXT_OORT_ID_FIELD); if (_id.equals(remoteOortId)) { // Connecting to myself: disconnect _logger.warn("Detected self connect from {} to {}, disconnecting", remoteOortURL, cometURL); disconnect(message); } else { // Add the extension information even in case we're then disconnecting. // The presence of the extension information will inform the client // that the connection "succeeded" from the Oort point of view, but // we add the advice information to drop it because if it already exists. Map ext = message.getExt(true); Map oortExt = new HashMap(2); ext.put(EXT_OORT_FIELD, oortExt); oortExt.put(EXT_OORT_URL_FIELD, getURL()); oortExt.put(EXT_OORT_ID_FIELD, getId()); boolean connectBack = incomingCometHandshake(Collections.unmodifiableMap(associatedOortExt), to); if (connectBack) { String cometAliasURL = (String)associatedOortExt.get(EXT_OORT_ALIAS_URL_FIELD); if (cometAliasURL != null && _pendingComets.containsKey(cometAliasURL)) { // We are connecting to a comet that it is connecting back to us // so there is not need to connect back again (just to be disconnected) debug("Comet {} is pending with alias {}, avoiding to establish connection", remoteOortURL, cometAliasURL); } else { debug("Comet {} is unknown, establishing connection", remoteOortURL); observeComet(remoteOortURL, cometURL); } // Must notify after an eventual call to observeComet() so that // a pending comet is present and may be returned to the application notifyCometJoined(remoteOortURL); } else { disconnect(message); } } } return true; } private void disconnect(Mutable message) { message.setSuccessful(false); Map advice = message.getAdvice(true); advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE); } } protected void joinComets(Message message) { Object data = message.getData(); Object[] array = data instanceof List ? ((List)data).toArray() : (Object[])data; for (Object element : array) observeComet((String)element); } /** *

This listener handles messages sent to /oort/cloud that contains the list of comets * connected to the Oort that just joined the cloud.

*

For example, if comets A and B are connected, and if comets C and D are connected, when connecting * A and C, a message is sent from A to C on /oort/cloud containing the comets connected * to A (in this case B). When C receives this message, it knows it has to connect to B also.

*/ protected class CloudListener implements ServerChannel.MessageListener { public boolean onMessage(ServerSession from, ServerChannel channel, Mutable message) { if (!from.isLocalSession()) joinComets(message); return true; } } public void setThreadPool(ThreadPool threadPool) { _threadPool = threadPool; } public ThreadPool getThreadPool() { return _threadPool; } public WebSocketClientFactory getWebSocketClientFactory() { return _wsFactory; } public void setWebSocketClientFactory(WebSocketClientFactory wsFactory) { this._wsFactory = wsFactory; } public HttpClient getHttpClient() { return _httpClient; } public void setHttpClient(HttpClient httpClient) { this._httpClient = httpClient; } protected Logger getLogger() { return _logger; } public Set getObservedChannels() { return new HashSet(_channels.keySet()); } /** * @return the oortSession */ public LocalSession getOortSession() { return _oortSession; } /** *

Listener interface that gets notified of comet events, that is when a new * comet joins the cloud or when a comet leaves the cloud.

*

If oortA and oortB form a cloud, when oortC joins to the cloud, both * oortA and oortB receive one event of type "comet joined", signaling that oortC * joined the cloud.

*

If, later, oortB leaves the cloud, then both oortA and oortC receive one * event of type "comet left" signaling that oortB left the cloud.

*/ public interface CometListener extends EventListener { /** * Callback method invoked when a new comet joins the cloud * @param event the comet event */ public void cometJoined(Event event); /** * Callback method invoked when a comet leaves the cloud * @param event the comet event */ public void cometLeft(Event event); /** * Comet event object delivered to {@link CometListener} methods. */ public static class Event extends EventObject { private final String cometURL; public Event(Oort source, String cometURL) { super(source); this.cometURL = cometURL; } /** * @return the URL of the comet that generated the event */ public String getCometURL() { return cometURL; } } } /** *

Listener that detect when a server session is removed (means that the remote * comet disconnected), and disconnects the OortComet associated.

*/ private class OortCometDisconnectListener implements ServerSession.RemoveListener { private final String cometURL; private final String remoteOortId; public OortCometDisconnectListener(String cometURL, String remoteOortId) { this.cometURL = cometURL; this.remoteOortId = remoteOortId; } public void removed(ServerSession session, boolean timeout) { Iterator cometInfos = _serverComets.values().iterator(); while (cometInfos.hasNext()) { ServerCometInfo serverCometInfo = cometInfos.next(); if (serverCometInfo.getServerSession().getId().equals(session.getId())) { _logger.info("Disconnected from comet {} with session {}", cometURL, session); assert remoteOortId.equals(serverCometInfo.getId()); cometInfos.remove(); ClientCometInfo clientCometInfo = _clientComets.remove(remoteOortId); if (clientCometInfo != null) clientCometInfo.getOortComet().disconnect(); notifyCometLeft(serverCometInfo.getURL()); break; } } } } private class OortCometLoopListener implements ServerSession.MessageListener { public boolean onMessage(ServerSession to, ServerSession from, ServerMessage message) { // Prevent loops by not delivering a message from self or Oort session to remote Oort comets if (to.getId().equals(from.getId()) || isOort(from)) { debug("{} --| {} {}", from, to, message); return false; } debug("{} --> {} {}", from, to, message); return true; } } protected static abstract class CometInfo { private final String id; private final String url; protected CometInfo(String id, String url) { this.id = id; this.url = url; } public String getId() { return id; } public String getURL() { return url; } } protected static class ServerCometInfo extends CometInfo { private final ServerSession session; protected ServerCometInfo(String id, String url, ServerSession session) { super(id, url); this.session = session; } public ServerSession getServerSession() { return session; } } protected static class ClientCometInfo extends CometInfo { private final OortComet comet; private final Map urls = new ConcurrentHashMap(); protected ClientCometInfo(String id, String url, OortComet comet) { super(id, url); this.comet = comet; } public OortComet getOortComet() { return comet; } public void addAliasURL(String url) { urls.put(url, Boolean.TRUE); } public boolean matchesURL(String url) { return getURL().equals(url) || urls.containsKey(url); } } private class HandshakeListener implements ClientSessionChannel.MessageListener { private final String cometURL; private final OortComet oortComet; private HandshakeListener(String cometURL, OortComet oortComet) { this.cometURL = cometURL; this.oortComet = oortComet; } public void onMessage(ClientSessionChannel channel, Message message) { OortComet comet = _pendingComets.get(cometURL); if (comet != null) { if (!message.isSuccessful()) { getLogger().warn("Failed to connect to comet {}, message {}", cometURL, message); Map advice = message.getAdvice(); if (advice != null && Message.RECONNECT_NONE_VALUE.equals(advice.get(Message.RECONNECT_FIELD))) { debug("Disconnecting pending comet {}", cometURL); comet.disconnect(); // Fall through: if it was an alias URL the message will // have the extension and we can map it, otherwise there // will be no extension and we return } } } Map ext = message.getExt(); if (ext != null) { Object oortExtObject = ext.get(Oort.EXT_OORT_FIELD); if (oortExtObject instanceof Map) { @SuppressWarnings("unchecked") Map oortExt = (Map)oortExtObject; String url = (String)oortExt.get(Oort.EXT_OORT_URL_FIELD); String id = (String)oortExt.get(Oort.EXT_OORT_ID_FIELD); ClientCometInfo cometInfo = new ClientCometInfo(id, url, oortComet); ClientCometInfo existing = _clientComets.putIfAbsent(id, cometInfo); if (existing != null) cometInfo = existing; if (!cometURL.equals(url)) { cometInfo.addAliasURL(cometURL); debug("Adding alias to {}: {}", url, cometURL); } if (message.isSuccessful()) getLogger().info("Connected to comet {} as {} with {}/{}", new Object[]{url, cometURL, message.getClientId(), oortComet.getTransport()}); } } // Remove the pending comet as last step, so that if there is a concurrent // call to observeComet() we are sure that we always return either the // pending OortComet, or the connected one from the _clientComets field _pendingComets.remove(cometURL); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy