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

org.jboss.naming.remote.client.HaRemoteNamingStore Maven / Gradle / Ivy

Go to download

This artifact provides a single jar that contains all classes required to use remote Jakarta Enterprise Beans and Jakarta Messaging, including all dependencies. It is intended for use by those not using maven, maven users should just import the Jakarta Enterprise Beans and Jakarta Messaging BOM's instead (shaded JAR's cause lots of problems with maven, as it is very easy to inadvertently end up with different versions on classes on the class path).

There is a newer version: 35.0.0.Beta1
Show newest version
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 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
     *                                       connectionURIs for establishing the first connection
     */
    public HaRemoteNamingStore(final long channelCreationTimeoutInMillis, final OptionMap channelCreationOptions, final long connectionTimeout, final CallbackHandler callbackHandler, final OptionMap connectOptions, final List 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
     *                                       connectionURIs for establishing the first connection
     */
    HaRemoteNamingStore(final long channelCreationTimeoutInMillis, final OptionMap channelCreationOptions, final long connectionTimeout, final CallbackHandler callbackHandler, final OptionMap connectOptions, final List 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(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
     *                               namingStoreConnections for establishing the first connection
     */
    public HaRemoteNamingStore(final List 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        The return type of the operation
     * @return The result of the operation
     */
    private  T namingOperation(Operation 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 attemptedConnectionURIs = new ArrayList();
        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 futureConnection = clientEndpoint.connect(connectionUri, connectionInfo.getConnectionOptions(), connectionInfo.getCallbackHandler());
                connection = IoFutureHelper.get(futureConnection, connectionInfo.getConnectionTimeout(), TimeUnit.MILLISECONDS);
                // open a channel
                final IoFuture 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() {
                    @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() {
                    @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() {
                    @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() {
                    @Override
                    public Void operation(final RemoteNamingStore store) throws NamingException {
                        store.rename(name, object);
                        return null;

                    }
                }
        );
    }

    @Override
    public List list(final Name name) throws NamingException {
        return namingOperation(
                new Operation>() {
                    @Override
                    public List operation(final RemoteNamingStore store) throws NamingException {
                        return store.list(name);
                    }
                }
        );
    }

    @Override
    public List listBindings(final Name name) throws NamingException {
        return namingOperation(
                new Operation>() {
                    @Override
                    public List operation(final RemoteNamingStore store) throws NamingException {
                        return store.listBindings(name);
                    }
                }
        );
    }

    @Override
    public void unbind(final Name name) throws NamingException {
        namingOperation(
                new Operation() {
                    @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() {
                    @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() {
                    @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() {
                    @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  The return type of the operation
     */
    private static interface Operation {
        T operation(final RemoteNamingStore store) throws NamingException;
    }

}