package org.jboss.naming.remote.client; import org.jboss.logging.Logger; import org.jboss.naming.remote.client.ejb.EJBClientHandler; import org.jboss.naming.remote.protocol.IoFutureHelper; import org.jboss.naming.remote.protocol.NamingIOException; import org.jboss.remoting3.Channel; import org.jboss.remoting3.Connection; import org.jboss.remoting3.Endpoint; import org.xnio.IoFuture; import org.xnio.OptionMap; import javax.naming.AuthenticationException; import javax.naming.Binding; import javax.naming.CommunicationException; import javax.naming.Context; import javax.naming.Name; import javax.naming.NameClassPair; import javax.naming.NamingException; import javax.security.auth.callback.CallbackHandler; import javax.security.sasl.SaslException; import java.io.IOException; import java.net.ConnectException; import java.net.URI; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.concurrent.TimeUnit; /** * Remote naming store that has the ability to re-establish a connection to a destination server, * if the connection breaks at some point in time. This remote naming store also has the ability * to connect to multiple different destination hosts/servers. At a given time, the naming store will be * connected to atmost one server and it will "failover" to the "next" server if the connection with the * current server breaks. * * @author Stuart Douglas */ public class HaRemoteNamingStore implements RemoteNamingStore { private static final Logger logger = Logger.getLogger(HaRemoteNamingStore.class); private final List<RemoteNamingStoreConnectionInfo> namingStoreConnections; private volatile boolean closed = false; /** * The index of the next server to attempt to connect to */ private volatile int nextServer; private volatile RemoteNamingStore currentNamingStore; private final EJBClientHandler ejbClientHandler; //should only be accessed under lock private Connection connection; /** * @param channelCreationTimeoutInMillis The channel creation timeout in milli sec * @param channelCreationOptions The channel creation options * @param connectionTimeout The connection creation timeout in milli sec * @param callbackHandler The callback handler * @param connectOptions The connection creation options * @param connectionURIs The connection URIs * @param clientEndpoint The client Endpoint * @param randomServer True if a random connection URI has to be picked, from among the passed * <code>connectionURIs</code> for establishing the first connection */ public HaRemoteNamingStore(final long channelCreationTimeoutInMillis, final OptionMap channelCreationOptions, final long connectionTimeout, final CallbackHandler callbackHandler, final OptionMap connectOptions, final List<URI> connectionURIs, final Endpoint clientEndpoint, final boolean randomServer) { this(channelCreationTimeoutInMillis, channelCreationOptions, connectionTimeout, callbackHandler, connectOptions, connectionURIs, clientEndpoint, randomServer, null); } /** * @param channelCreationTimeoutInMillis The channel creation timeout in milli sec * @param channelCreationOptions The channel creation options * @param connectionTimeout The connection creation timeout in milli sec * @param callbackHandler The callback handler * @param connectOptions The connection creation options * @param connectionURIs The connection URIs * @param clientEndpoint The client Endpoint * @param randomServer True if a random connection URI has to be picked, from among the passed * <code>connectionURIs</code> for establishing the first connection */ HaRemoteNamingStore(final long channelCreationTimeoutInMillis, final OptionMap channelCreationOptions, final long connectionTimeout, final CallbackHandler callbackHandler, final OptionMap connectOptions, final List<URI> connectionURIs, final Endpoint clientEndpoint, final boolean randomServer, final EJBClientHandler ejbClientHandler) { if (connectionURIs.isEmpty()) { throw new IllegalArgumentException("Cannot create a HA remote naming store without any servers to connect to"); } namingStoreConnections = new ArrayList<RemoteNamingStoreConnectionInfo>(connectionURIs.size()); for (int i = 0; i < connectionURIs.size(); i++) { final RemoteNamingStoreConnectionInfo connectionInfo = new RemoteNamingStoreConnectionInfo(clientEndpoint, connectionURIs.get(i), connectOptions, connectionTimeout, callbackHandler, channelCreationTimeoutInMillis, channelCreationOptions); namingStoreConnections.add(connectionInfo); } if (randomServer) { nextServer = new Random().nextInt(namingStoreConnections.size()); } else { nextServer = 0; } this.ejbClientHandler = ejbClientHandler; } /** * @param namingStoreConnections The connection information to the destination server(s). Cannot be null or empty * @param randomServer True if a random connection URI has to be picked, from among the passed * <code>namingStoreConnections</code> for establishing the first connection */ public HaRemoteNamingStore(final List<RemoteNamingStoreConnectionInfo> namingStoreConnections, final boolean randomServer) { if (namingStoreConnections == null || namingStoreConnections.isEmpty()) { throw new IllegalArgumentException("Cannot create a HA remote naming store without any servers to connect to"); } this.namingStoreConnections = Collections.unmodifiableList(namingStoreConnections); if (randomServer) { nextServer = new Random().nextInt(namingStoreConnections.size()); } else { nextServer = 0; } this.ejbClientHandler = null; } /** * Perfoms a remoting naming operation, retrying when a server cannot be found. * * @param operation The operation * @param <T> The return type of the operation * @return The result of the operation */ private <T> T namingOperation(Operation<T> operation) throws NamingException { if (closed) { throw new NamingException("NamingStore has been closed"); } RemoteNamingStore namingStore = namingStore(); try { return operation.operation(namingStore); } catch (NamingIOException e) { synchronized (this) { namingStore = failOverSequence(namingStore); } return operation.operation(namingStore); } } /** * @return The current naming store */ private RemoteNamingStore namingStore() throws NamingException { final RemoteNamingStore namingStore = currentNamingStore; if (namingStore == null) { synchronized (this) { if (currentNamingStore == null) { return failOverSequence(null); } return currentNamingStore; } } return namingStore; } /** * Fail over to a new RemoteNamingStore * * @param attempted The remote naming store that caused the failover attempt. If it does not match the current naming * store the fail over will be aborted * @return The new remote naming store */ private RemoteNamingStore failOverSequence(RemoteNamingStore attempted) throws NamingException { assert Thread.holdsLock(this); final RemoteNamingStore currentNamingStore = this.currentNamingStore; if (attempted != null && attempted != currentNamingStore) { //a different thread has already caused a failover return currentNamingStore; } if (currentNamingStore != null) { try { //even though this probably won't work we try and close it anyway currentNamingStore.close(); connection.close(); } catch (Exception e) { //this is not unexpected, as if the naming store was in a reasonable state //we should not be failing over. logger.debug("Failed to close existing naming store on failover", e); } } final int startingNext = nextServer(); int currentServer = startingNext; RemoteNamingStore store = null; //we loop through and attempt to connect to ever server, one at a time final List<String> attemptedConnectionURIs = new ArrayList<String>(); Exception primaryException = null; do { final RemoteNamingStoreConnectionInfo connectionInfo = namingStoreConnections.get(currentServer); final URI connectionUri = connectionInfo.getConnectionURI(); Connection connection = null; try { final Endpoint clientEndpoint = connectionInfo.getEndpoint(); final IoFuture<Connection> futureConnection = clientEndpoint.connect(connectionUri, connectionInfo.getConnectionOptions(), connectionInfo.getCallbackHandler()); connection = IoFutureHelper.get(futureConnection, connectionInfo.getConnectionTimeout(), TimeUnit.MILLISECONDS); // open a channel final IoFuture<Channel> futureChannel = connection.openChannel("naming", connectionInfo.getChannelCreationOptions()); final Channel channel = IoFutureHelper.get(futureChannel, connectionInfo.getChannelCreationTimeout(), TimeUnit.MILLISECONDS); store = RemoteContextFactory.createVersionedStore(channel, ejbClientHandler); this.connection = connection; break; } catch (Exception e) { logger.debug("Failed to connect to server " + connectionUri, e); // save server attempt and cause of failure if(e instanceof SaslException) { primaryException = e; } else if (e instanceof ConnectException) { if (primaryException == null && !(primaryException instanceof AuthenticationException)) { primaryException = e; } } // add failure messages to be used in final exception if (connectionUri == null) { attemptedConnectionURIs.add("null (" + e.getMessage() + ")"); } else { attemptedConnectionURIs.add(connectionUri.toString() + " (" + e.getMessage() + ")"); } currentServer = nextServer(); if (connection != null) { try { connection.close(); } catch (IOException e1) { logger.debug("Failed to close connection " + connectionUri, e); } } } } while (currentServer != startingNext); if (store == null) { if (primaryException != null) { NamingException ne; if (primaryException instanceof SaslException) ne = new AuthenticationException("Failed to connect to any server. Servers tried: " + attemptedConnectionURIs); else ne = new CommunicationException("Failed to connect to any server. Servers tried: " + attemptedConnectionURIs); ne.initCause(primaryException); throw ne; } throw new CommunicationException("Failed to connect to any server. Servers tried: " + attemptedConnectionURIs); } this.currentNamingStore = store; // associate this connection with the EJB client context if (this.ejbClientHandler != null) { try { this.ejbClientHandler.associate(connection); } catch (Exception e) { logger.warn("Could not associate connection " + connection + " with EJB client context", e); } } return store; } private int nextServer() { assert Thread.holdsLock(this); final int next = nextServer; final int newValue = next + 1; if (newValue == namingStoreConnections.size()) { nextServer = 0; } else { nextServer = newValue; } return next; } @Override public Object lookup(final Name name) throws NamingException { return namingOperation( new Operation<Object>() { @Override public Object operation(final RemoteNamingStore store) throws NamingException { return store.lookup(name); } } ); } @Override public void bind(final Name name, final Object object) throws NamingException { namingOperation( new Operation<Void>() { @Override public Void operation(final RemoteNamingStore store) throws NamingException { store.bind(name, object); return null; } } ); } @Override public void rebind(final Name name, final Object object) throws NamingException { namingOperation( new Operation<Void>() { @Override public Void operation(final RemoteNamingStore store) throws NamingException { store.rebind(name, object); return null; } } ); } @Override public void rename(final Name name, final Name object) throws NamingException { namingOperation( new Operation<Void>() { @Override public Void operation(final RemoteNamingStore store) throws NamingException { store.rename(name, object); return null; } } ); } @Override public List<NameClassPair> list(final Name name) throws NamingException { return namingOperation( new Operation<List<NameClassPair>>() { @Override public List<NameClassPair> operation(final RemoteNamingStore store) throws NamingException { return store.list(name); } } ); } @Override public List<Binding> listBindings(final Name name) throws NamingException { return namingOperation( new Operation<List<Binding>>() { @Override public List<Binding> operation(final RemoteNamingStore store) throws NamingException { return store.listBindings(name); } } ); } @Override public void unbind(final Name name) throws NamingException { namingOperation( new Operation<Void>() { @Override public Void operation(final RemoteNamingStore store) throws NamingException { store.unbind(name); return null; } } ); } @Override public Context createSubcontext(final Name name) throws NamingException { return namingOperation( new Operation<Context>() { @Override public Context operation(final RemoteNamingStore store) throws NamingException { return store.createSubcontext(name); } } ); } @Override public void destroySubcontext(final Name name) throws NamingException { namingOperation( new Operation<Void>() { @Override public Void operation(final RemoteNamingStore store) throws NamingException { store.destroySubcontext(name); return null; } } ); } @Override public Object lookupLink(final Name name) throws NamingException { return namingOperation( new Operation<Object>() { @Override public Object operation(final RemoteNamingStore store) throws NamingException { return store.lookupLink(name); } } ); } @Override public synchronized void close() throws NamingException { closed = true; try { if (connection != null) { connection.close(); } } catch (IOException e) { NamingException exception = new NamingException("Failed to close connection"); exception.initCause(e); try { if (currentNamingStore != null) { currentNamingStore.close(); } } catch (NamingException ignored) { } throw exception; } if (currentNamingStore != null) { currentNamingStore.close(); } } @Override public void closeAsync() { closed = true; if (connection != null) { connection.closeAsync(); } if (currentNamingStore != null) { currentNamingStore.closeAsync(); } } @Override public synchronized void addEjbContext(final CurrentEjbClientConnection connection) { // no-op. CurrentEjbClientConnection is a deprecated semantic. We no longer do anything with it } @Override public synchronized void removeEjbContext(final CurrentEjbClientConnection connection) { // no-op. CurrentEjbClientConnection is a deprecated semantic. We no longer do anything with it } /** * Simple interface used to encapsulate a naming operation. * * @param <T> The return type of the operation */ private static interface Operation<T> { T operation(final RemoteNamingStore store) throws NamingException; } }