/* * 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.atmosphere.plugin.rmi; import org.atmosphere.cpr.AtmosphereConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.rmi.Naming; import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; /** * <p> * This manager is in charge of sending all broadcast messages to a set of known peers (thanks to its * {@link RMIPeerManager#sendAll(String, Object)} method) and to bind a {@link RMIBroadcastService} (thanks to * its {@link RMIPeerManager#server(String, RMIBroadcastService, AtmosphereConfig) method}). * </p> * * <p> * The manager is a singleton. To be instantiated successfully the first time its {@link RMIPeerManager#getInstance()} * method is called, a property file at {@link RMIPeerManager#RMI_PROPERTIES_LOCATION this} location must exists * in the classpath. Both RMI server port and peers location must be declared. The port is associated to the key * {@link RMIPeerManager#RMI_SERVER_PORT_PROPERTY} and each peer location will be associated to a key starting by * {@link RMIPeerManager#PEER_PROPERTY_PREFIX this} prefix. Sample of properties file content : * <pre> * rmi.server.port=4000 * rmi.peer.server1=com.my.company.server1:4000 * rmi.peer.server2=com.my.company.server2:4000 * </pre> * </p> * * <p> * NOTE : the properties file should not contains the peer's URL of the server this manager is running on. * </p> * * <p> * Moreover, any value declared in the properties file could be overridden with a system property. Each system property * must starts with {@link RMIPeerManager#SYSTEM_PROPERTY_PREFIX this} prefix followed by the property key as declared in * the properties file. For instance, if I want to override the value of the property 'rmi.peer.server1' declared in the * properties file, I will run my server with '-D org.atmosphere.rmi.peer.server1=my.overridden.host:port' in the command * line. * </p> * * <p> * TODO : Should be enhanced to discover peers automatically thanks to multicast * </p> * * @author Guillaume DROUET * @version 1.0 * @since 1.1.1 */ public class RMIPeerManager { /** * Singleton. */ private static RMIPeerManager instance; /** * Expected path for the properties file defining all the peers. */ private static final String RMI_PROPERTIES_LOCATION = "/org/atmosphere/plugin/rmi/rmi.properties"; /** * Required server port. */ private static final String RMI_SERVER_PORT_PROPERTY = "rmi.server.port"; /** * Required prefix for all keys referencing a peer's URL in the properties file. */ private static final String PEER_PROPERTY_PREFIX = "rmi.peer."; /** * Required prefix for properties to be overridden through system properties. */ private static final String SYSTEM_PROPERTY_PREFIX = "org.atmosphere."; /** * Server port system property. */ private static final String RMI_SERVER_PORT_SYSTEM_PROPERTY = SYSTEM_PROPERTY_PREFIX + RMI_SERVER_PORT_PROPERTY; /** * Prefix for all system properties referencing a peer's URL. */ private static final String PEER_SYSTEM_PROPERTY_PREFIX = SYSTEM_PROPERTY_PREFIX + PEER_PROPERTY_PREFIX; /** * Logger. */ private final Logger logger = LoggerFactory.getLogger(RMIPeerManager.class); /** * All the discovered peers. */ private List<Peer> peers; /** * Registry to use when creating the server. */ private Registry registry; /** * The port to use when creating the registry. */ private int serverPort; /** * <p> * Builds a new instance by loading properties from {@link RMIPeerManager#RMI_PROPERTIES_LOCATION}. * </p> * * <p> * If the properties file is not found in the classpath or could not be read successfully, * then an {@code IllegalStateException} will be thrown. * </p> * * <p> * If one property does not refer to a valid URL, then an {@code IllegalArgumentException} will be thrown. * </p> */ private RMIPeerManager() { final Properties properties = new Properties(); logger.info("Looking for '{}' file in the classpath", RMI_PROPERTIES_LOCATION); final InputStream peerProperties = getClass().getResourceAsStream(RMI_PROPERTIES_LOCATION); if (peerProperties != null) { try { logger.info("Loading '{}' file from classpath", RMI_PROPERTIES_LOCATION); properties.load(peerProperties); } catch (final IOException ioe) { logger.error("Unable to load '" + RMI_PROPERTIES_LOCATION + "' file from the classpath", ioe); } } peers = new ArrayList<Peer>(); discoverServerPort(properties); discoverPeers(properties); } /** * <p> * Discovers the port to bind when creating the RMI server in the given {@code Properties} object. * </p> * * <p> * If the given port is not a valid integer, then an {@code IllegalArgumentException} will be thrown. * </p> * * @param properties the properties containing the RMI port. */ private void discoverServerPort(final Properties properties) { String portValue = properties.getProperty(RMI_SERVER_PORT_PROPERTY); // Looking for system property final String sysPropertyValue = System.getProperty(RMI_SERVER_PORT_SYSTEM_PROPERTY); if (sysPropertyValue != null) { logger.info("System property '{}' set. Overriding value '{}' with '{}'", new Object[] { RMI_SERVER_PORT_SYSTEM_PROPERTY, portValue, sysPropertyValue, }); portValue = sysPropertyValue; } if (portValue == null) { throw new IllegalArgumentException(RMI_SERVER_PORT_PROPERTY + " property's value is null. Must be a valid integer"); } try { serverPort = Integer.parseInt(portValue); } catch (NumberFormatException nfe) { throw new IllegalArgumentException(RMI_SERVER_PORT_PROPERTY + " property's value is not an integer : " + portValue, nfe); } } /** * <p> * Discovers all the peer name to authority (host:port) mappings defined in the given {@code Properties} object and/or the system properties. * </p> * * If the same peer name occurs in both the given {@code Properties} object and the system properties, * the system properties authority overrides the given {@code Properties} authority. * * @param properties Properties containing peer name to authority mappings * * @throws IllegalArgumentException If any peer authority (host:port) is malformed */ private void discoverPeers(final Properties properties) { logger.info("Discovering RMI peers"); final Map<String, String> sysPropPeerAuthorityByPeerName = new HashMap<String, String>(); // Add peers from system properties for (final Entry<Object, Object> sysProp : System.getProperties().entrySet()) { final String sysPropKey = sysProp.getKey().toString(); if (sysPropKey.startsWith(PEER_SYSTEM_PROPERTY_PREFIX)) { final String peerName = sysPropKey.substring(PEER_SYSTEM_PROPERTY_PREFIX.length()); final String peerAuthority = sysProp.getValue().toString(); addPeer(peerName, peerAuthority); sysPropPeerAuthorityByPeerName.put(peerName, peerAuthority); logger.info("Added peer '{}' with authority '{}' from system properties", peerName, peerAuthority); } } // Add peers from properties that weren't overridden by system properties for (final Entry<Object, Object> property : properties.entrySet()) { final String propertyKey = property.getKey().toString(); if (propertyKey.startsWith(PEER_PROPERTY_PREFIX)) { final String peerName = propertyKey.substring(PEER_PROPERTY_PREFIX.length()); final String peerAuthority = property.getValue().toString(); final String sysPropPeerAuthority = sysPropPeerAuthorityByPeerName.get(peerName); if (sysPropPeerAuthority == null) { addPeer(peerName, peerAuthority); logger.info("Added peer '{}' with authority '{}' from properties", peerName, peerAuthority); } else if (! sysPropPeerAuthority.equals(peerAuthority)) { logger.info( "Peer '{}' with authority '{}' from system properties overrode authority '{}'", new Object[] {peerName, sysPropPeerAuthority, peerAuthority} ); } } } } private void addPeer(final String peerName, final String peerAuthority) { try { peers.add(new Peer("rmi://" + peerAuthority + '/' + RMIBroadcastService.class.getSimpleName() + '/')); } catch (final MalformedURLException mue) { throw new IllegalArgumentException( "Value for peer '" + peerName + "' must be a valid host name and port (e.g., foo:40001). Invalid value: " + peerAuthority ); } } /** * <p> * Gets the unique instance of {@link RMIPeerManager}. * </p> * * <p> * If this is the first time the method is called, the singleton will be instantiated here. * </p> * * @return the unique instance */ public synchronized static RMIPeerManager getInstance() { if (instance == null) { instance = new RMIPeerManager(); } return instance; } /** * <p> * Sends the given message to the broadcaster identified by the given ID belonging to all the registered peers. * </p> * * @param broadcasterId the broadcaster ID * @param message the message to be sent */ public synchronized void sendAll(final String broadcasterId, final Object message) { logger.info("Sending message to {} known RMI peers", peers.size()); for (Peer peer : peers) { peer.send(broadcasterId, message, 1); } } /** * <p> * Creates a service by binding the given service for the given broadcaster ID. * </p> * @param broadcasterId the broadcaster ID * @param service the service to be bound * @param config the atmosphere config */ public synchronized void server(final String broadcasterId, final RMIBroadcastService service, AtmosphereConfig config) { try { if (registry == null) { logger.info("Creating registry with port {}", serverPort); registry = LocateRegistry.createRegistry(serverPort); if (config != null) { config.shutdownHook(new AtmosphereConfig.ShutdownHook() { @Override public void shutdown() { for (Thread t : Thread.getAllStackTraces().keySet()) { if ("RMI Reaper".equals(t.getName())) { t.interrupt(); } } } }); } } logger.info("Rebinding {}", RMIBroadcastService.class.getSimpleName()); final String url = RMIBroadcastService.class.getSimpleName() + "/" + broadcasterId; logger.info("URL : {}", url); registry.rebind(url, service); } catch (RemoteException re) { logger.error("Unable to create the RMI server. Won't receive message to broadcast from other peers", re); } } /** * <p> * Internal class which represents a peer handling connections where messages should be broadcast. * </p> * * <p> * Ths class encapsulates a {@link RMIBroadcastService} retrieved remotely to send the messages. If the * connection could not be established because the service is not already reachable, the messages are lost. * When a new message is sent, the {@link Peer} tries to reconnect and if it succeeds, it sends it. * </p> * */ private class Peer { /** * The remote URL. */ String url; /** * <p> * Creates a new instance. If the connection could not be established because of a non reachable remote * service, the object will try to reconnect when a message will be sent. * </p> * * @param peerUrl the remote service URL * @throws MalformedURLException if the URL is not correct */ Peer(final String peerUrl) throws MalformedURLException { logger.info("Connecting to peer at {}", peerUrl); url = peerUrl; // Just connect to detect a MalformedURLException exception connect(""); } /** * <p> * Tries to establish a connection by looking up the remote service dedicated to the specified broadcaster ID. * </p> * * @param broadcasterId the broadcaster ID * @return the remote interface, {@code } * @throws MalformedURLException if the {@link Peer#url} is not correct */ RMIBroadcastService connect(final String broadcasterId) throws MalformedURLException { try { logger.info("Trying to connect to {}", url); return (RMIBroadcastService) Naming.lookup(url + broadcasterId); } catch (RemoteException re) { logger.warn("Could not reach the remote host with the url {}. Reason is '{}'. Will try later", new Object[] { url, re.getMessage() }, re); } catch (NotBoundException nbe) { logger.warn("{} for url {} not currently bound. Reason is {}. Will try later", new Object[] { RMIBroadcastService.class.getSimpleName(), url, nbe.getMessage() }, nbe); } return null; } /** * <p> * Sends the given message to the broadcaster identified by the specified ID. * </p> * * <p> * Its first tries to connect. Once the connection is established, the given message is sent. * If the connection fails or if an error occurs, the method will retries a specified number of times. * </p> * * @param broadcasterId the broadcaster ID * @param message the message to send * @param retry how many times the method will retry if an error occurs */ synchronized void send(final String broadcasterId, final Object message, final int retry) { try { RMIBroadcastService service; if ((service = connect(broadcasterId)) != null) { logger.debug("Sending message '{}' to peer at url {}", new Object[] { message, url, }); if (retry > 0) { try { service.send(message); } catch(Exception e) { logger.warn("Send operation failed {}. Retrying...", e.getMessage()); send(broadcasterId, message, retry - 1); } } else { service.send(message); } } } catch (MalformedURLException mue) { // Should never occurs since the URL has been validated when the class was instantiated throw new IllegalStateException(mue); } catch (RemoteException re) { logger.warn("Failed to send message to peer '{}'", url, re); } } } }