org.cometd.oort.Oort Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2018 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.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
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.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.ChannelId;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSession;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.BayeuxServer.Extension;
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.client.ext.AckExtension;
import org.cometd.client.transport.ClientTransport;
import org.cometd.client.transport.LongPollingTransport;
import org.cometd.common.JSONContext;
import org.cometd.server.authorizer.GrantAuthorizer;
import org.cometd.server.ext.AcknowledgedMessagesExtension;
import org.cometd.server.ext.BinaryExtension;
import org.cometd.websocket.client.WebSocketTransport;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.B64Code;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.annotation.ManagedOperation;
import org.eclipse.jetty.util.annotation.Name;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
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: {@code 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
*/
@ManagedObject("CometD cloud node")
public class Oort extends ContainerLifeCycle {
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";
public static final String OORT_SERVICE_CHANNEL = "/service/oort";
private static final String COMET_URL_ATTRIBUTE = EXT_OORT_FIELD + "." + EXT_COMET_URL_FIELD;
private static final String JOIN_MESSAGE_ATTRIBUTE = Oort.class.getName() + ".joinMessage";
private final Map _pendingComets = new HashMap<>();
private final Map _clientComets = new HashMap<>();
private final Map _serverComets = new HashMap<>();
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 ServerChannel.MessageListener _joinListener = new JoinListener();
private final List _transportFactories = new ArrayList<>();
private final BayeuxServer _bayeux;
private final String _url;
private final String _id;
private final Logger _logger;
private final LocalSession _oortSession;
private final Object _lock = this;
private ScheduledExecutorService _scheduler;
private String _secret;
private boolean _ackExtensionEnabled;
private Extension _ackExtension;
private boolean _binaryExtensionEnabled;
private Extension _serverBinaryExtension;
private ClientSession.Extension _binaryExtension;
private JSONContext.Client _jsonContext;
public Oort(BayeuxServer bayeux, String url) {
_bayeux = bayeux;
_url = url;
_id = UUID.randomUUID().toString();
_logger = LoggerFactory.getLogger(getClass().getName() + "." + replacePunctuation(_url, '_'));
_oortSession = bayeux.newLocalSession("oort");
_secret = Long.toHexString(new SecureRandom().nextLong());
}
@Override
protected void doStart() throws Exception {
ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1);
scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
scheduler.setRemoveOnCancelPolicy(true);
_scheduler = scheduler;
if (_transportFactories.isEmpty()) {
_transportFactories.add(new WebSocketTransport.Factory());
_transportFactories.add(new LongPollingTransport.Factory(new HttpClient()));
}
for (ClientTransport.Factory factory : _transportFactories) {
addBean(factory);
}
super.doStart();
if (isAckExtensionEnabled()) {
boolean present = false;
for (Extension extension : _bayeux.getExtensions()) {
if (extension instanceof AcknowledgedMessagesExtension) {
present = true;
break;
}
}
if (!present) {
_bayeux.addExtension(_ackExtension = new AcknowledgedMessagesExtension());
}
}
if (isBinaryExtensionEnabled()) {
_oortSession.addExtension(_binaryExtension = new org.cometd.client.ext.BinaryExtension());
boolean present = false;
for (Extension extension : _bayeux.getExtensions()) {
if (extension instanceof BinaryExtension) {
present = true;
break;
}
}
if (!present) {
_bayeux.addExtension(_serverBinaryExtension = new BinaryExtension());
}
}
_bayeux.addExtension(_oortExtension);
ServerChannel oortCloudChannel = _bayeux.createChannelIfAbsent(OORT_CLOUD_CHANNEL).getReference();
oortCloudChannel.addAuthorizer(GrantAuthorizer.GRANT_ALL);
oortCloudChannel.addListener(_cloudListener);
ServerChannel oortServiceChannel = _bayeux.createChannelIfAbsent(OORT_SERVICE_CHANNEL).getReference();
oortServiceChannel.addListener(_joinListener);
_oortSession.handshake();
}
@Override
protected void doStop() throws Exception {
_oortSession.disconnect();
_oortSession.removeExtension(_binaryExtension);
List comets = new ArrayList<>();
synchronized (_lock) {
comets.addAll(_pendingComets.values());
_pendingComets.clear();
for (ClientCometInfo cometInfo : _clientComets.values()) {
comets.add(cometInfo.getOortComet());
}
_clientComets.clear();
_serverComets.clear();
}
for (OortComet comet : comets) {
comet.disconnect(1000);
}
_channels.clear();
ServerChannel channel = _bayeux.getChannel(OORT_SERVICE_CHANNEL);
if (channel != null) {
channel.removeListener(_joinListener);
}
channel = _bayeux.getChannel(OORT_CLOUD_CHANNEL);
if (channel != null) {
channel.removeListener(_cloudListener);
channel.removeAuthorizer(GrantAuthorizer.GRANT_ALL);
}
Extension ackExtension = _ackExtension;
_ackExtension = null;
if (ackExtension != null) {
_bayeux.removeExtension(ackExtension);
}
Extension binaryExtension = _serverBinaryExtension;
_serverBinaryExtension = null;
if (binaryExtension != null) {
_bayeux.removeExtension(binaryExtension);
}
_bayeux.removeExtension(_oortExtension);
_scheduler.shutdown();
super.doStop();
for (ClientTransport.Factory factory : _transportFactories) {
removeBean(factory);
}
}
@ManagedAttribute(value = "The BayeuxServer of this Oort", readonly = true)
public BayeuxServer getBayeuxServer() {
return _bayeux;
}
/**
* @return the public absolute URL of the Oort CometD server
*/
@ManagedAttribute(value = "The URL of this Oort", readonly = true)
public String getURL() {
return _url;
}
@ManagedAttribute(value = "The unique ID of this Oort", readonly = true)
public String getId() {
return _id;
}
@ManagedAttribute("The secret of this Oort")
public String getSecret() {
return _secret;
}
public void setSecret(String secret) {
this._secret = secret;
}
@ManagedAttribute("Whether the acknowledgement extension is enabled")
public boolean isAckExtensionEnabled() {
return _ackExtensionEnabled;
}
public void setAckExtensionEnabled(boolean value) {
_ackExtensionEnabled = value;
}
@ManagedAttribute("Whether the binary extension is enabled")
public boolean isBinaryExtensionEnabled() {
return _binaryExtensionEnabled;
}
public void setBinaryExtensionEnabled(boolean value) {
_binaryExtensionEnabled = value;
}
public JSONContext.Client getJSONContextClient() {
return _jsonContext;
}
public void setJSONContextClient(JSONContext.Client jsonContext) {
_jsonContext = jsonContext;
}
public List getClientTransportFactories() {
return _transportFactories;
}
public void setClientTransportFactories(List factories) {
_transportFactories.clear();
_transportFactories.addAll(factories);
}
/**
* 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 oortAliasURL) {
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;
}
if (_logger.isDebugEnabled()) {
_logger.debug("Observing comet {}", cometURL);
}
OortComet oortComet;
synchronized (_lock) {
oortComet = getComet(cometURL);
if (oortComet != null) {
if (_logger.isDebugEnabled()) {
_logger.debug("Comet {} is already connected with {}", cometURL, oortComet);
}
return oortComet;
}
oortComet = _pendingComets.get(cometURL);
if (oortComet != null) {
if (_logger.isDebugEnabled()) {
_logger.debug("Comet {} is already connecting with {}", cometURL, oortComet);
}
return oortComet;
}
oortComet = newOortComet(cometURL);
configureOortComet(oortComet);
_pendingComets.put(cometURL, oortComet);
}
oortComet.getChannel(Channel.META_HANDSHAKE).addListener(new HandshakeListener(cometURL, oortComet));
if (_logger.isDebugEnabled()) {
_logger.debug("Connecting to comet {} with {}", cometURL, oortComet);
}
Map fields = newOortHandshakeFields(cometURL, oortAliasURL);
connectComet(oortComet, fields);
return oortComet;
}
protected OortComet newOortComet(String cometURL) {
Map options = new HashMap<>(2);
options.put(ClientTransport.SCHEDULER_OPTION, _scheduler);
JSONContext.Client jsonContext = getJSONContextClient();
if (jsonContext != null) {
options.put(ClientTransport.JSON_CONTEXT_OPTION, jsonContext);
}
String maxMessageSizeOption = ClientTransport.MAX_MESSAGE_SIZE_OPTION;
Object option = _bayeux.getOption(maxMessageSizeOption);
if (option != null) {
options.put(maxMessageSizeOption, option);
}
maxMessageSizeOption = WebSocketTransport.PREFIX + "." + ClientTransport.MAX_MESSAGE_SIZE_OPTION;
option = _bayeux.getOption(maxMessageSizeOption);
if (option != null) {
options.put(maxMessageSizeOption, option);
}
String idleTimeoutOption = WebSocketTransport.PREFIX + "." + WebSocketTransport.IDLE_TIMEOUT_OPTION;
option = _bayeux.getOption(idleTimeoutOption);
if (option != null) {
options.put(idleTimeoutOption, option);
}
List transports = new ArrayList<>();
for (ClientTransport.Factory factory : getClientTransportFactories()) {
transports.add(factory.newClientTransport(cometURL, options));
}
ClientTransport transport = transports.get(0);
int size = transports.size();
ClientTransport[] otherTransports = transports.subList(1, size).toArray(new ClientTransport[size - 1]);
return new OortComet(this, cometURL, _scheduler, transport, otherTransports);
}
protected void configureOortComet(OortComet oortComet) {
if (isAckExtensionEnabled()) {
boolean present = false;
for (ClientSession.Extension extension : oortComet.getExtensions()) {
if (extension instanceof AckExtension) {
present = true;
break;
}
}
if (!present) {
oortComet.addExtension(new AckExtension());
}
}
if (isBinaryExtensionEnabled()) {
boolean present = false;
for (ClientSession.Extension extension : oortComet.getExtensions()) {
if (extension instanceof org.cometd.client.ext.BinaryExtension) {
present = true;
break;
}
}
if (!present) {
oortComet.addExtension(new org.cometd.client.ext.BinaryExtension());
}
}
}
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, Map fields) {
comet.handshake(fields);
}
public OortComet deobserveComet(String cometURL) {
if (_url.equals(cometURL)) {
return null;
}
OortComet comet;
synchronized (_lock) {
comet = _pendingComets.remove(cometURL);
if (comet != null) {
if (_logger.isDebugEnabled()) {
_logger.debug("Disconnecting pending comet {} with {}", cometURL, comet);
}
} else {
Iterator cometInfos = _clientComets.values().iterator();
while (cometInfos.hasNext()) {
ClientCometInfo cometInfo = cometInfos.next();
if (cometInfo.matchesURL(cometURL)) {
if (_logger.isDebugEnabled()) {
_logger.debug("Disconnecting comet {}", cometInfo);
}
comet = cometInfo.getOortComet();
cometInfos.remove();
break;
}
}
}
}
if (comet != null) {
comet.disconnect();
}
return comet;
}
/**
* @return the set of known Oort comet servers URLs.
*/
@ManagedAttribute(value = "URLs of known Oorts in the cluster", readonly = true)
public Set getKnownComets() {
Set result = new HashSet<>();
synchronized (_lock) {
for (ClientCometInfo cometInfo : _clientComets.values()) {
result.add(cometInfo.getOortURL());
}
}
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) {
synchronized (_lock) {
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) {
synchronized (_lock) {
OortComet result = _pendingComets.get(cometURL);
if (result == null) {
result = getComet(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
*/
@ManagedOperation(value = "Observes the given channel", impact = "ACTION")
public void observeChannel(@Name(value = "channel", description = "The channel to observe") String channelName) {
if (_logger.isDebugEnabled()) {
_logger.debug("Observing channel {}", 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();
List oortComets = new ArrayList<>();
synchronized (_lock) {
for (ClientCometInfo cometInfo : _clientComets.values()) {
oortComets.add(cometInfo.getOortComet());
}
}
for (OortComet oortComet : oortComets) {
oortComet.subscribe(observedChannels);
}
}
}
@ManagedOperation(value = "Deobserves the given channel", impact = "ACTION")
public void deobserveChannel(@Name(value = "channel", description = "The channel to deobserve") String channelId) {
if (_channels.remove(channelId) != null) {
List oortComets = new ArrayList<>();
synchronized (_lock) {
for (ClientCometInfo cometInfo : _clientComets.values()) {
oortComets.add(cometInfo.getOortComet());
}
}
for (OortComet oortComet : oortComets) {
oortComet.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;
}
synchronized (_lock) {
for (ServerCometInfo cometInfo : _serverComets.values()) {
if (cometInfo.getServerSession().getId().equals(session.getId())) {
return true;
}
}
}
return session.getAttribute(COMET_URL_ATTRIBUTE) != null;
}
/**
* @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);
}
protected Map newOortHandshakeFields(String cometURL, String oortAliasURL) {
Map fields = new HashMap<>(1);
Map ext = new HashMap<>(1);
fields.put(Message.EXT_FIELD, ext);
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());
String b64Secret = encodeSecret(getSecret());
oortExt.put(EXT_OORT_SECRET_FIELD, b64Secret);
oortExt.put(EXT_COMET_URL_FIELD, cometURL);
if (oortAliasURL != null) {
oortExt.put(EXT_OORT_ALIAS_URL_FIELD, oortAliasURL);
}
return fields;
}
/**
* @param oortURL the comet URL to check for connection
* @return whether the given comet is connected to this comet
*/
protected boolean isCometConnected(String oortURL) {
synchronized (_lock) {
for (ServerCometInfo serverCometInfo : _serverComets.values()) {
if (serverCometInfo.getOortURL().equals(oortURL)) {
return true;
}
}
return false;
}
}
/**
* 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);
String remoteOortId = (String)oortExt.get(EXT_OORT_ID_FIELD);
ServerCometInfo serverCometInfo = new ServerCometInfo(remoteOortId, remoteOortURL, session);
ClientCometInfo clientCometInfo;
synchronized (_lock) {
ServerCometInfo existing = _serverComets.get(remoteOortId);
if (existing != null) {
if (_logger.isDebugEnabled()) {
_logger.debug("Comet already known {}", existing);
}
return false;
}
_serverComets.put(remoteOortId, serverCometInfo);
if (_logger.isDebugEnabled()) {
_logger.debug("Registered server comet {}", serverCometInfo);
}
clientCometInfo = _clientComets.get(remoteOortId);
}
// Be notified when the remote comet stops.
session.addListener(new OortCometDisconnectListener());
// Prevent loops in sending/receiving messages.
session.addListener(new OortCometLoopListener());
if (clientCometInfo != null) {
if (_logger.isDebugEnabled()) {
_logger.debug("Client comet present {}", clientCometInfo);
}
clientCometInfo.getOortComet().open();
} else {
if (_logger.isDebugEnabled()) {
_logger.debug("Client comet not yet present");
}
}
return true;
}
/**
* Registers the given listener to be notified of comet events.
*
* @param listener the listener to add
* @see #removeCometListener(CometListener)
*/
public void addCometListener(CometListener listener) {
_cometListeners.add(listener);
}
/**
* Deregisters the given listener from being notified of comet events.
*
* @param listener the listener to remove
* @see #addCometListener(CometListener)
*/
public void removeCometListener(CometListener listener) {
_cometListeners.remove(listener);
}
/**
* Deregisters all comet listeners.
*
* @see #addCometListener(CometListener)
* @see #removeCometListener(CometListener)
*/
public void removeCometListeners() {
_cometListeners.clear();
}
private void notifyCometJoined(String remoteOortId, String remoteOortURL) {
if (_logger.isDebugEnabled()) {
_logger.debug("Comet joined: {}|{}", remoteOortId, remoteOortURL);
}
CometListener.Event event = new CometListener.Event(this, remoteOortId, remoteOortURL);
for (CometListener cometListener : _cometListeners) {
try {
cometListener.cometJoined(event);
} catch (Throwable x) {
_logger.info("Exception while invoking listener " + cometListener, x);
}
}
}
private void notifyCometLeft(String remoteOortId, String remoteOortURL) {
if (_logger.isDebugEnabled()) {
_logger.debug("Comet left: {}|{}", remoteOortId, remoteOortURL);
}
CometListener.Event event = new CometListener.Event(this, remoteOortId, remoteOortURL);
for (CometListener cometListener : _cometListeners) {
try {
cometListener.cometLeft(event);
} catch (Throwable x) {
_logger.info("Exception while invoking listener " + cometListener, x);
}
}
}
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);
}
}
public Set getObservedChannels() {
return new HashSet<>(_channels.keySet());
}
List knownOortIds() {
List result = new ArrayList<>();
synchronized (_lock) {
result.addAll(_clientComets.keySet());
}
return result;
}
/**
* @return the oortSession
*/
public LocalSession getOortSession() {
return _oortSession;
}
protected static String replacePunctuation(String source, char replacement) {
String replaced = source.replaceAll("[^\\p{Alnum}]", String.valueOf(replacement));
// Compact multiple consecutive replacement chars
return replaced.replaceAll("(" + replacement + ")\\1+", "$1");
}
@Override
public void dump(Appendable out, String indent) throws IOException {
super.dump(out, indent);
List children = new ArrayList<>();
Set observedChannels = getObservedChannels();
children.add(new DumpableCollection("observed channels: " + observedChannels.size(), observedChannels));
List clientInfos;
synchronized (_lock) {
clientInfos = new ArrayList<>(_clientComets.values());
}
children.add(new DumpableCollection("connected comets: " + clientInfos.size(), clientInfos));
ContainerLifeCycle.dump(out, indent, children);
}
@Override
public String toString() {
return String.format("%s[%s]", getClass().getSimpleName(), getURL());
}
/**
* Extension that detects incoming handshakes from other Oort servers.
*
* @see Oort#incomingCometHandshake(Map, ServerSession)
*/
protected class OortExtension implements Extension {
@Override
public boolean sendMeta(ServerSession session, Mutable reply) {
if (!Channel.META_HANDSHAKE.equals(reply.getChannel())) {
return true;
}
// Process only successful responses.
if (!reply.isSuccessful()) {
return true;
}
// Skip local sessions.
if (session == null || session.isLocalSession()) {
return true;
}
Map messageExt = reply.getAssociated().getExt();
if (messageExt == null) {
return true;
}
Object messageOortExtObject = messageExt.get(EXT_OORT_FIELD);
if (messageOortExtObject instanceof Map) {
@SuppressWarnings("unchecked")
Map messageOortExt = (Map)messageOortExtObject;
String remoteOortURL = (String)messageOortExt.get(EXT_OORT_URL_FIELD);
String cometURL = (String)messageOortExt.get(EXT_COMET_URL_FIELD);
String remoteOortId = (String)messageOortExt.get(EXT_OORT_ID_FIELD);
session.setAttribute(COMET_URL_ATTRIBUTE, remoteOortURL);
if (_id.equals(remoteOortId)) {
// Connecting to myself: disconnect.
if (_logger.isDebugEnabled()) {
_logger.debug("Detected self connect from {} to {}, disconnecting", remoteOortURL, cometURL);
}
disconnect(session, reply);
} 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 extension information to drop it if it already exists.
Map replyExt = reply.getExt(true);
Map replyOortExt = new HashMap<>(2);
replyExt.put(EXT_OORT_FIELD, replyOortExt);
replyOortExt.put(EXT_OORT_URL_FIELD, getURL());
replyOortExt.put(EXT_OORT_ID_FIELD, getId());
boolean connectBack = incomingCometHandshake(messageOortExt, session);
if (connectBack) {
String cometAliasURL = (String)messageOortExt.get(EXT_OORT_ALIAS_URL_FIELD);
if (cometAliasURL != null && findComet(cometAliasURL) != null) {
// We are connecting to a comet that it is connecting back to us
// so there is no need to connect back again (just to be disconnected)
if (_logger.isDebugEnabled()) {
_logger.debug("Comet {} exists with alias {}, avoiding to establish connection", remoteOortURL, cometAliasURL);
}
} else {
if (_logger.isDebugEnabled()) {
_logger.debug("Comet {} is unknown, establishing connection", remoteOortURL);
}
observeComet(remoteOortURL, cometURL);
}
} else {
disconnect(session, reply);
}
}
}
return true;
}
private void disconnect(ServerSession session, Mutable message) {
_bayeux.removeSession(session);
message.setSuccessful(false);
Map advice = message.getAdvice(true);
advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE);
}
}
/**
* This listener handles messages sent to {@code /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 {@code /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 {
@Override
public boolean onMessage(ServerSession from, ServerChannel channel, Mutable message) {
if (!from.isLocalSession()) {
joinComets(message);
}
return true;
}
}
protected class JoinListener implements ServerChannel.MessageListener {
@Override
public boolean onMessage(ServerSession from, ServerChannel channel, Mutable message) {
Map data = message.getDataAsMap();
String remoteOortId = (String)data.get(EXT_OORT_ID_FIELD);
String remoteOortURL = (String)data.get(EXT_OORT_URL_FIELD);
if (remoteOortURL != null && remoteOortId != null) {
boolean ready = false;
Set staleComets = null;
synchronized (_lock) {
Iterator iterator = _serverComets.values().iterator();
while (iterator.hasNext()) {
ServerCometInfo serverCometInfo = iterator.next();
if (remoteOortURL.equals(serverCometInfo.getOortURL())) {
String oortId = serverCometInfo.getOortId();
if (remoteOortId.equals(oortId)) {
if (_clientComets.containsKey(remoteOortId)) {
ready = true;
} else {
serverCometInfo.getServerSession().setAttribute(JOIN_MESSAGE_ATTRIBUTE, message);
}
} else {
// We found a stale entry for a crashed node.
iterator.remove();
if (staleComets == null) {
staleComets = new HashSet<>(1);
}
staleComets.add(oortId);
}
}
}
}
if (staleComets != null) {
for (String oortId : staleComets) {
notifyCometLeft(oortId, remoteOortURL);
}
}
if (ready) {
notifyCometJoined(remoteOortId, remoteOortURL);
} else {
if (_logger.isDebugEnabled()) {
_logger.debug("Delaying comet joined: {}|{}", remoteOortId, remoteOortURL);
}
}
}
return true;
}
}
/**
* 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);
/**
* Empty implementation of {@link CometListener}
*/
public static class Adapter implements CometListener {
@Override
public void cometJoined(Event event) {
}
@Override
public void cometLeft(Event event) {
}
}
/**
* Comet event object delivered to {@link CometListener} methods.
*/
public static class Event extends EventObject {
private final String cometId;
private final String cometURL;
public Event(Oort source, String cometId, String cometURL) {
super(source);
this.cometId = cometId;
this.cometURL = cometURL;
}
/**
* @return the local Oort object
*/
public Oort getOort() {
return (Oort)getSource();
}
/**
* @return the ID of the comet that generated the event
*/
public String getCometId() {
return cometId;
}
/**
* @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 {
@Override
public void removed(ServerSession session, boolean timeout) {
ServerCometInfo serverCometInfo = null;
synchronized (_lock) {
Iterator cometInfos = _serverComets.values().iterator();
while (cometInfos.hasNext()) {
ServerCometInfo info = cometInfos.next();
if (info.getServerSession().getId().equals(session.getId())) {
cometInfos.remove();
serverCometInfo = info;
break;
}
}
}
if (serverCometInfo != null) {
if (_logger.isDebugEnabled()) {
_logger.debug("Disconnected from {}", serverCometInfo);
}
String remoteOortId = serverCometInfo.getOortId();
String remoteOortURL = serverCometInfo.getOortURL();
if (!timeout) {
OortComet oortComet;
synchronized (_lock) {
oortComet = _pendingComets.remove(remoteOortURL);
if (oortComet == null) {
ClientCometInfo clientCometInfo = _clientComets.remove(remoteOortId);
if (clientCometInfo != null) {
oortComet = clientCometInfo.getOortComet();
}
}
}
if (oortComet != null) {
if (_logger.isDebugEnabled()) {
_logger.debug("Disconnecting from comet {} with {}", remoteOortURL, oortComet);
}
oortComet.disconnect();
}
}
// Do not notify if we are stopping.
if (isRunning()) {
notifyCometLeft(remoteOortId, remoteOortURL);
}
}
}
}
private class OortCometLoopListener implements ServerSession.MessageListener {
@Override
public boolean onMessage(ServerSession session, ServerSession sender, ServerMessage message) {
// Prevent loops by not delivering a message from self or Oort session to remote Oort comets
if (session.getId().equals(sender.getId()) || isOort(sender)) {
if (_logger.isDebugEnabled()) {
_logger.debug("{} --| {} {}", sender, session, message);
}
return false;
}
if (_logger.isDebugEnabled()) {
_logger.debug("{} --> {} {}", sender, session, message);
}
return true;
}
}
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;
}
@Override
public void onMessage(ClientSessionChannel channel, Message message) {
Map ext = message.getExt();
if (ext == null) {
return;
}
Object oortExtObject = ext.get(Oort.EXT_OORT_FIELD);
if (!(oortExtObject instanceof Map)) {
return;
}
@SuppressWarnings("unchecked")
Map oortExt = (Map)oortExtObject;
String oortId = (String)oortExt.get(Oort.EXT_OORT_ID_FIELD);
String oortURL = (String)oortExt.get(Oort.EXT_OORT_URL_FIELD);
ClientCometInfo clientCometInfo;
ServerCometInfo serverCometInfo;
boolean ready = false;
synchronized (_lock) {
_pendingComets.remove(cometURL);
Iterator iterator = _clientComets.values().iterator();
while (iterator.hasNext()) {
clientCometInfo = iterator.next();
if (!clientCometInfo.getOortId().equals(oortId)) {
if (clientCometInfo.matchesURL(cometURL) || clientCometInfo.matchesURL(oortURL)) {
iterator.remove();
if (_logger.isDebugEnabled()) {
_logger.debug("Unregistered client comet {}", clientCometInfo);
}
}
}
}
clientCometInfo = _clientComets.get(oortId);
if (clientCometInfo == null) {
clientCometInfo = new ClientCometInfo(oortId, oortURL, oortComet);
_clientComets.put(oortId, clientCometInfo);
if (_logger.isDebugEnabled()) {
_logger.debug("Registered client comet {}", clientCometInfo);
}
}
serverCometInfo = _serverComets.get(oortId);
if (serverCometInfo != null) {
if (serverCometInfo.getServerSession().removeAttribute(JOIN_MESSAGE_ATTRIBUTE) != null) {
ready = true;
}
}
}
if (!cometURL.equals(oortURL)) {
clientCometInfo.addAliasURL(cometURL);
if (_logger.isDebugEnabled()) {
_logger.debug("Added comet alias {}", clientCometInfo);
}
}
if (message.isSuccessful()) {
if (serverCometInfo != null) {
if (_logger.isDebugEnabled()) {
_logger.debug("Server comet present {}", serverCometInfo);
}
oortComet.open();
if (ready) {
notifyCometJoined(oortId, oortURL);
}
} else {
if (_logger.isDebugEnabled()) {
_logger.debug("Server comet not yet present");
}
}
} else {
if (_logger.isDebugEnabled()) {
_logger.debug("Handshake failed to comet {}, message {}", cometURL, message);
}
oortComet.disconnect();
}
}
}
private static abstract class CometInfo {
private final String oortId;
private final String oortURL;
protected CometInfo(String oortId, String oortURL) {
this.oortId = oortId;
this.oortURL = oortURL;
}
protected String getOortId() {
return oortId;
}
protected String getOortURL() {
return oortURL;
}
@Override
public String toString() {
return String.format("%s@%x[%s|%s]", getClass().getSimpleName(), hashCode(), oortId, oortURL);
}
}
private static class ServerCometInfo extends CometInfo {
private final ServerSession session;
private ServerCometInfo(String oortId, String oortURL, ServerSession session) {
super(oortId, oortURL);
this.session = session;
}
private ServerSession getServerSession() {
return session;
}
@Override
public String toString() {
return String.format("%s[%s]", super.toString(), session);
}
}
private static class ClientCometInfo extends CometInfo {
private final OortComet oortComet;
private Set urls;
private ClientCometInfo(String oortId, String oortURL, OortComet oortComet) {
super(oortId, oortURL);
this.oortComet = oortComet;
}
private OortComet getOortComet() {
return oortComet;
}
private void addAliasURL(String url) {
synchronized (this) {
if (urls == null) {
urls = new HashSet<>();
}
urls.add(url);
}
}
private boolean matchesURL(String url) {
if (getOortURL().equals(url)) {
return true;
}
synchronized (this) {
return urls != null && urls.contains(url);
}
}
@Override
public String toString() {
return String.format("%s[%s]%s", super.toString(), oortComet, Objects.toString(urls, ""));
}
}
private static class DumpableCollection implements Dumpable {
private final String name;
private final Collection> collection;
private DumpableCollection(String name, Collection> collection) {
this.name = name;
this.collection = collection;
}
public String dump() {
return ContainerLifeCycle.dump(this);
}
public void dump(Appendable out, String indent) throws IOException {
out.append(name).append(System.lineSeparator());
if (collection != null) {
ContainerLifeCycle.dump(out, indent, collection);
}
}
}
}