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

com.couchbase.client.java.CouchbaseAsyncCluster Maven / Gradle / Ivy

There is a newer version: 3.7.7
Show newest version
/*
 * Copyright (c) 2016 Couchbase, Inc.
 *
 * 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 com.couchbase.client.java;

import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import com.couchbase.client.core.ClusterFacade;
import com.couchbase.client.core.CouchbaseCore;
import com.couchbase.client.core.CouchbaseException;
import com.couchbase.client.core.annotations.InterfaceAudience;
import com.couchbase.client.core.annotations.InterfaceStability;
import com.couchbase.client.core.config.ConfigurationException;
import com.couchbase.client.core.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.core.message.CouchbaseResponse;
import com.couchbase.client.core.message.ResponseStatus;
import com.couchbase.client.core.message.cluster.DisconnectRequest;
import com.couchbase.client.core.message.cluster.DisconnectResponse;
import com.couchbase.client.core.message.cluster.OpenBucketRequest;
import com.couchbase.client.core.message.cluster.OpenBucketResponse;
import com.couchbase.client.core.message.cluster.SeedNodesRequest;
import com.couchbase.client.java.auth.Authenticator;
import com.couchbase.client.java.auth.Credential;
import com.couchbase.client.java.auth.CredentialContext;
import com.couchbase.client.java.auth.ClassicAuthenticator;
import com.couchbase.client.java.cluster.AsyncClusterManager;
import com.couchbase.client.java.cluster.DefaultAsyncClusterManager;
import com.couchbase.client.java.document.Document;
import com.couchbase.client.java.env.CouchbaseEnvironment;
import com.couchbase.client.java.env.DefaultCouchbaseEnvironment;
import com.couchbase.client.java.error.AuthenticatorException;
import com.couchbase.client.java.error.BucketDoesNotExistException;
import com.couchbase.client.java.error.InvalidPasswordException;
import com.couchbase.client.java.query.AsyncN1qlQueryResult;
import com.couchbase.client.java.query.N1qlQuery;
import com.couchbase.client.java.transcoder.Transcoder;
import com.couchbase.client.java.util.Bootstrap;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func0;
import rx.functions.Func1;

/**
 * Main asynchronous entry point to a Couchbase Cluster.
 *
 * The {@link CouchbaseAsyncCluster} object is the main entry point when connecting to a remote
 * Couchbase Server cluster. It will either create a bundled stateful environment or accept
 * one passed in, in case the application needs to connect to more clusters at the same time.
 *
 * It provides cluster level management facilities through the {@link AsyncClusterManager} class,
 * but mainly provides facilities to open {@link AsyncBucket}s where the actual CRUD and query
 * operations are performed against.
 *
 * The simplest way to initialize a {@link CouchbaseAsyncCluster} is by using the {@link #create()}
 * factory method. This is only recommended during development, since it will connect to a Cluster
 * residing on `127.0.0.1`.
 *
 * ```java
 * AsyncCluster cluster = CouchbaseAsyncCluster.create();
 * ```
 *
 * In production, it is recommended that at least two or three hosts are passed in, so in case one
 * fails the SDK is able to bootstrap through alternative options.
 *
 * ```java
 * AsyncCluster cluster = CouchbaseAsyncCluster.create(
 *   "192.168.56.101", "192.168.56.102", "192.168.56.103"
 * );
 * ```
 *
 * Please make sure that these hosts are part of the same cluster, otherwise non-deterministic
 * connecting behaviour will arise (the SDK may connect to the wrong cluster).
 *
 * If you need to customize {@link CouchbaseEnvironment} options or connect to multiple clusters,
 * it is recommended to explicitly create one and then reuse it. Keep in mind that the cluster will
 * not shut down the environment if it didn't create it, so this is up to the caller.
 *
 * ```java
 * CouchbaseEnvironment environment = DefaultCouchbaseEnvironment.builder()
 * .kvTimeout(3000) // change the default kv timeout
 * .build();
 *
 * AsyncCluster cluster = CouchbaseAsyncCluster.create(environment, "192.168.56.101",
 *   "192.168.56.102");
 * Observable<Bucket> bucket = cluster.openBucket("travel-sample");
 *
 * // Perform your work here...
 *
 * cluster.disconnect();
 * environment.shutdownAsync().toBlocking().single();
 * ```
 *
 * @since 2.0.0
 * @author Michael Nitschinger
 * @author Simon Baslé
 */
public class CouchbaseAsyncCluster implements AsyncCluster {

    private static final CouchbaseLogger LOGGER =
        CouchbaseLoggerFactory.getInstance(CouchbaseAsyncCluster.class);

    /**
     * The default bucket used when {@link #openBucket()} is called.
     *
     * Defaults to "default".
     */
    public static final String DEFAULT_BUCKET = "default";

    /**
     * The default hostname used to bootstrap then {@link #create()} is used.
     *
     * Defaults to "127.0.0.1".
     */
    public static final String DEFAULT_HOST = "127.0.0.1";

    private final ClusterFacade core;
    private final CouchbaseEnvironment environment;
    private final ConnectionString connectionString;
    private final Map bucketCache;
    private final boolean sharedEnvironment;
    private Authenticator authenticator;

    /**
     * Creates a new {@link CouchbaseAsyncCluster} reference against the {@link #DEFAULT_HOST}.
     *
     * **Note:** It is recommended to use this method only during development, since it does not
     * allow you to pass in hostnames when connecting to a remote cluster. Please use
     * {@link #create(String...)} or similar instead.
     *
     * The {@link CouchbaseEnvironment} will be automatically created and its lifecycle managed.
     *
     * @return a new {@link CouchbaseAsyncCluster} reference.
     */
    public static CouchbaseAsyncCluster create() {
        return create(DEFAULT_HOST);
    }

    /**
     * Creates a new {@link CouchbaseAsyncCluster} reference against the {@link #DEFAULT_HOST}.
     *
     * **Note:** It is recommended to use this method only during development, since it does not
     * allow you to pass in hostnames when connecting to a remote cluster. Please use
     * {@link #create(String...)} or similar instead.
     *
     * @param environment the custom environment to use for this cluster reference.
     * @return a new {@link CouchbaseAsyncCluster} reference.
     */
    public static CouchbaseAsyncCluster create(final CouchbaseEnvironment environment) {
        return create(environment, DEFAULT_HOST);
    }

    /**
     * Creates a new {@link CouchbaseAsyncCluster} reference against the nodes passed in.
     *
     * The {@link CouchbaseEnvironment} will be automatically created and its lifecycle managed.
     *
     * @param nodes the list of nodes to use when connecting to the cluster reference.
     * @return a new {@link CouchbaseAsyncCluster} reference.
     */
    public static CouchbaseAsyncCluster create(final String... nodes) {
        return create(Arrays.asList(nodes));
    }

    /**
     * Creates a new {@link CouchbaseAsyncCluster} reference against the nodes passed in.
     *
     * The {@link CouchbaseEnvironment} will be automatically created and its lifecycle managed.
     *
     * @param nodes the list of nodes to use when connecting to the cluster reference.
     * @return a new {@link CouchbaseAsyncCluster} reference.
     */
    public static CouchbaseAsyncCluster create(final List nodes) {
        return new CouchbaseAsyncCluster(
            DefaultCouchbaseEnvironment.create(),
            ConnectionString.fromHostnames(nodes),
            false
        );
    }

    /**
     * Creates a new {@link CouchbaseAsyncCluster} reference against the nodes passed in.
     *
     * @param environment the custom environment to use for this cluster reference.
     * @param nodes the list of nodes to use when connecting to the cluster reference.
     * @return a new {@link CouchbaseAsyncCluster} reference.
     */
    public static CouchbaseAsyncCluster create(final CouchbaseEnvironment environment,
        final String... nodes) {
        return create(environment, Arrays.asList(nodes));
    }

    /**
     * Creates a new {@link CouchbaseAsyncCluster} reference against the nodes passed in.
     *
     * @param environment the custom environment to use for this cluster reference.
     * @param nodes the list of nodes to use when connecting to the cluster reference.
     * @return a new {@link CouchbaseAsyncCluster} reference.
     */
    public static CouchbaseAsyncCluster create(final CouchbaseEnvironment environment,
        final List nodes) {
        return new CouchbaseAsyncCluster(environment, ConnectionString.fromHostnames(nodes), true);
    }

    /**
     * Creates a new {@link CouchbaseAsyncCluster} reference using the connection string.
     *
     * The {@link CouchbaseEnvironment} will be automatically created and its lifecycle managed.
     *
     * @param connectionString the connection string to identify the remote cluster.
     * @return a new {@link CouchbaseAsyncCluster} reference.
     */
    public static CouchbaseAsyncCluster fromConnectionString(final String connectionString) {
        return new CouchbaseAsyncCluster(
            DefaultCouchbaseEnvironment.create(),
            ConnectionString.create(connectionString),
            false
        );
    }

    /**
     * Creates a new {@link CouchbaseAsyncCluster} reference using the connection string.
     *
     * @param environment the custom environment to use for this cluster reference.
     * @param connectionString the connection string to identify the remote cluster.
     * @return a new {@link CouchbaseAsyncCluster} reference.
     */
    public static CouchbaseAsyncCluster fromConnectionString(final CouchbaseEnvironment environment,
        final String connectionString) {
        return new CouchbaseAsyncCluster(
            environment,
            ConnectionString.create(connectionString),
            true
        );
    }

    /**
     * Package private constructor to create the {@link CouchbaseAsyncCluster}.
     *
     * This method should not be called directly, but rather through the many factory methods
     * available on this class.
     *
     * @param environment the custom environment to use for this cluster reference.
     * @param connectionString the connection string to identify the remote cluster.
     * @param sharedEnvironment if the environment is managed by this class or not.
     */
    CouchbaseAsyncCluster(final CouchbaseEnvironment environment,
        final ConnectionString connectionString, final boolean sharedEnvironment) {
        this.sharedEnvironment = sharedEnvironment;
        core = new CouchbaseCore(environment);
        SeedNodesRequest request = new SeedNodesRequest(
            assembleSeedNodes(connectionString, environment)
        );
        core.send(request).toBlocking().single();
        this.environment = environment;
        this.connectionString = connectionString;
        this.bucketCache = new ConcurrentHashMap();
        this.authenticator = new ClassicAuthenticator();
    }

    /**
     * Helper method to assemble list of seed nodes depending on the given input.
     *
     * If DNS SRV is enabled, see
     * {@link #seedNodesViaDnsSrv(ConnectionString, CouchbaseEnvironment, List)} for more
     * details.
     *
     * @param connectionString the connection string to check.
     * @param environment the environment for context.
     * @return a list of seed nodes ready to send.
     */
    private static List assembleSeedNodes(ConnectionString connectionString,
        CouchbaseEnvironment environment) {
        List seedNodes = new ArrayList();

        if (environment.dnsSrvEnabled()) {
            seedNodesViaDnsSrv(connectionString, environment, seedNodes);
        } else {
            for (InetSocketAddress node : connectionString.hosts()) {
                seedNodes.add(node.getHostName());
            }
        }

        if (seedNodes.isEmpty()) {
            seedNodes.add(DEFAULT_HOST);
        }

        return seedNodes;
    }

    /**
     * Helper method to assemble the list of seed nodes via DNS SRV.
     *
     * If DNS SRV is enabled on the environment and exactly one hostname is passed in (not an IP
     * address), the code performs a DNS SRV lookup, but falls back to the A record if nothing
     * suitable is found. Since the user is expected to enable it manually, a warning will be
     * issued if so.
     *
     * @param connectionString the connection string to check.
     * @param environment the environment for context.
     * @param seedNodes the assembled seed nodes.
     */
    private static void seedNodesViaDnsSrv(ConnectionString connectionString,
       CouchbaseEnvironment environment, List seedNodes) {
        if (connectionString.hosts().size() == 1) {
            InetSocketAddress lookupNode = connectionString.hosts().get(0);
            LOGGER.debug(
                "Attempting to load DNS SRV records from {}.",
                connectionString.hosts().get(0)
            );

            try {
                List foundNodes = Bootstrap.fromDnsSrv(lookupNode.getHostName(), false,
                    environment.sslEnabled());
                if (foundNodes.isEmpty()) {
                    throw new IllegalStateException("DNS SRV list is empty.");
                }
                seedNodes.addAll(foundNodes);
                LOGGER.info("Loaded seed nodes from DNS SRV {}.", foundNodes);
            } catch (Exception ex) {
                LOGGER.warn("DNS SRV lookup failed, proceeding with normal bootstrap.", ex);
                seedNodes.add(lookupNode.getHostName());
            }
        } else {
            LOGGER.info("DNS SRV enabled, but less or more than one seed node given. "
                + "Proceeding with normal bootstrap.");
            for (InetSocketAddress node : connectionString.hosts()) {
                seedNodes.add(node.getHostName());
            }
        }
    }

    @Override
    public Observable openBucket() {
        //skip the openBucket(String) that checks the authenticator, default to empty password.
        return openBucket(DEFAULT_BUCKET, null);
    }

    @Override
    public Observable openBucket(final String name) {
        Credential cred = new Credential(name, null);
        try {
            cred = getSingleCredential(CredentialContext.BUCKET_KV, name);
        } catch (AuthenticatorException e) {
            //only propagate an error if more than one matching credentials is returned
            if (e.foundCredentials() > 1) {
                return Observable.error(e);
            }
            //otherwise, the 0 credentials found case reverts back to old behavior of a null password
            //which will get interpreted as empty string in openBucket(String, String, List) below.
        }

        return openBucket(cred.login(), cred.password());
    }

    @Override
    public Observable openBucket(final String name, final String password) {
        return openBucket(name, password, null);
    }

    @Override
    public Observable openBucket(final String name, final String password,
        final List> transcoders) {
        if (name == null || name.isEmpty()) {
            return Observable.error(
                new IllegalArgumentException("Bucket name is not allowed to be null or empty.")
            );
        }

        AsyncBucket cachedBucket = getCachedBucket(name);
        if (cachedBucket != null) {
            return Observable.just(cachedBucket);
        }

        final String pass = password == null ? "" : password;
        final List> trans = transcoders == null
            ? new ArrayList>() : transcoders;

        return Observable.defer(new Func0>() {
                @Override
                public Observable call() {
                    return core.send(new OpenBucketRequest(name, pass));
                }
            })
            .map(new Func1() {
                @Override
                public AsyncBucket call(CouchbaseResponse response) {
                    if (response.status() != ResponseStatus.SUCCESS) {
                        throw new CouchbaseException("Could not open bucket.");
                    }

                    AsyncBucket bucket = new CouchbaseAsyncBucket(core, environment, name, pass, trans);
                    bucketCache.put(name, bucket);
                    return bucket;
                }
            })
            .onErrorResumeNext(new OpenBucketErrorHandler(name));
    }

    /**
     * Helper method to get a bucket instead of opening it if it is cached already.
     *
     * @param name the name of the bucket
     * @return the cached bucket if found, null if not.
     */
    private AsyncBucket getCachedBucket(final String name) {
        AsyncBucket cachedBucket = bucketCache.get(name);

        if(cachedBucket != null) {
            if (cachedBucket.isClosed()) {
                LOGGER.debug(
                    "Not returning cached async bucket \"{}\", because it is closed.",
                    name
                );
                bucketCache.remove(name);
            } else {
                LOGGER.debug("Returning still open, cached async bucket \"{}\"", name);
                return cachedBucket;
            }
        }

        return null;
    }

    @Override
    public Observable disconnect() {
        return core
            .send(new DisconnectRequest())
            .flatMap(new Func1>() {
                @Override
                public Observable call(DisconnectResponse disconnectResponse) {
                    return sharedEnvironment ? Observable.just(true) : environment.shutdownAsync();
                }
            })
            .doOnNext(new Action1() {
                @Override
                public void call(Boolean aBoolean) {
                    bucketCache.clear();
                }
            });
    }

    @Override
    public Observable clusterManager(final String username,
        final String password) {
        return Observable.just(
            (AsyncClusterManager) DefaultAsyncClusterManager.create(
                username, password, connectionString, environment, core
            )
        );
    }

    protected Credential getSingleCredential(CredentialContext context, String specific) {
        if (this.authenticator == null || this.authenticator.isEmpty()) {
            throw new AuthenticatorException("Attempted an authenticated operation with no Authenticator, or an empty Authenticator", context, specific, 0);
        }
        List creds = this.authenticator.getCredentials(context, specific);
        if (creds == null || creds.isEmpty()) {
            throw new AuthenticatorException("Authenticator doesn't contain a credential for this operation, expected 1", context, specific, 0);
        } else if (creds.size() != 1) {
            throw new AuthenticatorException("Authenticator returned more than 1 credentials for this operation, expected 1", context, specific, creds.size());
        }

        Credential cred = creds.get(0);
        return cred;
    }

    @Override
    public Observable clusterManager() {
        try {
            Credential cred = getSingleCredential(CredentialContext.CLUSTER_MANAGEMENT, null);
            return clusterManager(cred.login(), cred.password());
        } catch (AuthenticatorException e) {
            return Observable.error(e);
        }
    }

    @Override
    public Observable core() {
        return Observable.just(core);
    }

    @Override
    public CouchbaseAsyncCluster authenticate(Authenticator auth) {
        if (auth == null) {
            LOGGER.trace("Authenticator was set to null, ignored");
            return this;
        }
        this.authenticator = auth;
        if (!bucketCache.isEmpty()) {
            LOGGER.warn("Authenticator was switched while {} buckets are still open. Operations on these buckets" +
                    " will continue using the old Authenticator until you close and reopen them", bucketCache.size());
        }
        return this;
    }

    /**
     * Get the {@link Authenticator} currently used when credentials are needed for an
     * operation, but no explicit credentials are provided.
     *
     * @return the Authenticator currently used for this cluster.
     */
    @InterfaceStability.Uncommitted
    @InterfaceAudience.Private
    public Authenticator authenticator() {
        return authenticator;
    }

    @Override
    public Observable query(N1qlQuery query) {
        /*
         * This method iterates over the cached buckets to choose the first open bucket it finds in
         * order to execute the query. This is done this way rather than through attempting to use
         * a cluster-dedicated N1qlQueryExecutor because the core needs a non-null bucket name to do
         * generic node dispatching (NPE otherwise), and the QueryHandler will add basic http authentication
         * information to the outgoing request, which N1QL will validate...
         *
         * That in turn means the N1qlQueryExecutor must be created with a valid bucketName and password pair,
         * which we're not guaranteed to have at this point. So the simplest path is to delegate to one of the
         * opened buckets (which has its own correctly initialized executor).
         *
         * Of course we pass all known credentials to the N1qlQuery, so they will supplement the basic HTTP
         * authentication mentioned above.
         */
        AsyncBucket delegateBucket = null;
        for (AsyncBucket asyncBucket : bucketCache.values()) {
            if (!asyncBucket.isClosed()) {
                delegateBucket = asyncBucket;
                break;
            }
        }
        if (delegateBucket == null) {
            return Observable.error(new UnsupportedOperationException("Cluster level querying is only available " +
                    "when at least 1 bucket is opened"));
        }

        //enrich with cluster-level credentials for N1QL (aka list of all bucket credentials)
        if (this.authenticator == null) {
            throw new IllegalStateException("An Authenticator is required to perform cluster level querying");
        } else {
            try {
                List creds = this.authenticator.getCredentials(CredentialContext.CLUSTER_N1QL, null);
                if (creds.isEmpty()) {
                    throw new IllegalStateException(
                            "CLUSTER_N1QL credentials are required in the Authenticator for cluster level querying");
                } else {
                    query.params().withCredentials(creds);
                    LOGGER.trace("Added {} credentials to a cluster-level N1qlQuery", creds.size());
                }
            } catch (IllegalArgumentException e) {
                throw new IllegalStateException("Couldn't retrieve credentials for cluster level querying from Authenticator", e);
            }
        }

        //note that delegating to the Bucket has the additional effect
        // of enriching the query with server side timeout if not explicitly defined
        return delegateBucket.query(query);
    }

    /**
     * The error handling logic if the open bucket process failed for some reason.
     *
     * @author Michael Nitschinger
     * @since 2.2.2
     */
    static class OpenBucketErrorHandler implements Func1> {

        private final String name;

        public OpenBucketErrorHandler(final String name) {
            this.name = name;
        }

        @Override
        public Observable call(Throwable throwable) {
            if (throwable instanceof ConfigurationException) {
                if (throwable.getCause() instanceof IllegalStateException
                    && throwable.getCause().getMessage().contains("NOT_EXISTS")) {
                    return Observable.error(new BucketDoesNotExistException("Bucket \""
                        + name + "\" does not exist."));
                } else if (throwable.getCause() instanceof IllegalStateException
                    && throwable.getCause().getMessage().contains("Unauthorized")) {
                    return Observable.error(
                        new InvalidPasswordException("Passwords for bucket \"" + name
                            + "\" do not match.")
                    );
                } else {
                    return Observable.error(throwable);
                }
            } else if (throwable instanceof CouchbaseException) {
                return Observable.error(throwable);
            } else {
                return Observable.error(new CouchbaseException(throwable));
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy