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

com.basho.riak.client.core.RiakNode Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2013 Basho Technologies, 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.basho.riak.client.core;

import com.basho.riak.client.core.netty.*;
import com.basho.riak.client.core.util.Constants;
import com.basho.riak.client.core.util.HostAndPort;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.BlockingOperationException;
import io.netty.util.concurrent.DefaultPromise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.Security;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author Brian Roach 
 * @author Sergey Galkin 
 * @author Alex Moore 
 * @since 2.0
 */
public class RiakNode implements RiakResponseListener
{
    public enum State
    {
        CREATED, RUNNING, HEALTH_CHECKING, SHUTTING_DOWN, SHUTDOWN;
    }

    private final Logger logger = LoggerFactory.getLogger(RiakNode.class);

    private final LinkedBlockingDeque available = new LinkedBlockingDeque<>();
    private final ConcurrentLinkedQueue recentlyClosed = new ConcurrentLinkedQueue<>();
    private final List stateListeners =
        Collections.synchronizedList(new LinkedList());
    private final Map inProgressMap = new ConcurrentHashMap<>();

    private final Sync permits;
    private final String remoteAddress;
    private final int port;
    private final String username;
    private final String password;
    private final KeyStore trustStore;
    private final KeyStore keyStore;
    private final String keyPassword;
    private final AtomicLong consecutiveFailedOperations = new AtomicLong(0);
    private final AtomicLong consecutiveFailedConnectionAttempts = new AtomicLong(0);

    private volatile Bootstrap bootstrap;
    private volatile boolean ownsBootstrap;
    private volatile ScheduledExecutorService executor;
    private volatile boolean ownsExecutor;
    private volatile State state;
    private volatile ScheduledFuture idleReaperFuture;
    private volatile ScheduledFuture healthMonitorFuture;
    private volatile int minConnections;
    private volatile long idleTimeoutInNanos;
    private volatile int connectionTimeout;
    private volatile boolean blockOnMaxConnections;

    private HealthCheckFactory healthCheckFactory;

    private final ChannelFutureListener writeListener =
        new ChannelFutureListener()
        {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception
            {
                // If there's a write failure, we yank the operation, close
                // the channel, and set the exception. Returning the closed
                // channel to the pool discards it and records a disconnect
                // for the health check.
                if (!future.isSuccess())
                {
                    logger.error("Write failed on RiakNode {}:{} id: {}; cause: {}",
                                remoteAddress, port, future.channel().hashCode(),
                                future.cause());
                    FutureOperation inProgress = inProgressMap.remove(future.channel());
                    if (inProgress != null)
                    {
                        future.channel().close();
                        returnConnection(future.channel()); // to release permit
                        recentlyClosed.add(new ChannelWithIdleTime(future.channel()));
                        inProgress.setException(future.cause());
                    }
                }
                else
                {
                    // On a successful write we add the in-progress close listener
                    // and let it handle a disco during an op.
                    future.channel().closeFuture().addListener(inProgressCloseListener);
                }
            }
        };

    private final ChannelFutureListener inAvailableCloseListener =
        new ChannelFutureListener()
        {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception
            {
                // Rather than having to do an O(n) search here, we just leave
                // the channel in available. Because it's closed it'll be discarded
                // the next time it's pulled from the pool.
                // We record the disco for the health check.
                recentlyClosed.add(new ChannelWithIdleTime(future.channel()));
                logger.info("Available channel closed; id:{} {}:{}",
                             future.channel().hashCode(), remoteAddress, port);
            }
        };

    private final ChannelFutureListener inProgressCloseListener =
        new ChannelFutureListener()
        {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception
            {
                FutureOperation inProgress = inProgressMap.remove(future.channel());
                logger.error("Channel closed while operation in progress; id:{} {}:{}",
                             future.channel().hashCode(), remoteAddress, port);
                if (inProgress != null)
                {
                    returnConnection(future.channel()); // to release permit
                    recentlyClosed.add(new ChannelWithIdleTime(future.channel()));

                    // Netty seems to not bother telling you *why* the connection
                    // was closed.
                    if (future.cause() != null)
                    {
                        inProgress.setException(future.cause());
                    }
                    else
                    {
                        inProgress.setException(new Exception("Connection closed unexpectantly"));
                    }
                }
            }
        };

    private final CountDownLatch shutdownLatch = new CountDownLatch(1);

    private RiakNode(Builder builder)
    {
        this.executor = builder.executor;
        this.connectionTimeout = builder.connectionTimeout;
        this.idleTimeoutInNanos = TimeUnit.NANOSECONDS.convert(builder.idleTimeout, TimeUnit.MILLISECONDS);
        this.minConnections = builder.minConnections;
        this.port = builder.port;
        this.remoteAddress = builder.remoteAddress;
        this.blockOnMaxConnections = builder.blockOnMaxConnections;
        this.username = builder.username;
        this.password = builder.password;
        this.trustStore = builder.trustStore;
        this.keyStore = builder.keyStore;
        this.keyPassword = builder.keyPassword;
        this.healthCheckFactory = builder.healthCheckFactory;

        if (builder.bootstrap != null)
        {
            this.bootstrap = builder.bootstrap.clone();
        }

        if (builder.maxConnections < 1)
        {
            permits = new Sync(Integer.MAX_VALUE);
        }
        else
        {
            permits = new Sync(builder.maxConnections);
        }

        checkNetworkAddressCacheSettings();

        this.state = State.CREATED;
    }

    private void stateCheck(State... allowedStates)
    {
        if (Arrays.binarySearch(allowedStates, state) < 0)
        {
            logger.debug("IllegalStateException; RiakNode: {}:{} required: {} current: {} ",
                remoteAddress, port, Arrays.toString(allowedStates), state);
            throw new IllegalStateException("required: "
                + Arrays.toString(allowedStates)
                + " current: " + state);
        }
    }

    /**
     * exposed for testing only
     *
     * @return number of inprogress tasks
     */
    int getNumInProgress()
    {
        return inProgressMap.size();
    }

    public synchronized RiakNode start() throws UnknownHostException
    {
        stateCheck(State.CREATED);

        if (executor == null)
        {
            executor = Executors.newSingleThreadScheduledExecutor();
            ownsExecutor = true;
        }

        if (bootstrap == null)
        {
            bootstrap = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class);
            ownsBootstrap = true;
        }

        bootstrap.handler(new RiakChannelInitializer(this));

        refreshBootstrapRemoteAddress();

        if (connectionTimeout > 0)
        {
            bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout);
        }

        if (minConnections > 0)
        {
            List minChannels = new LinkedList<>();
            for (int i = 0; i < minConnections; i++)
            {
                Channel channel;
                try
                {
                    channel = doGetConnection(false);
                    minChannels.add(channel);
                }
                catch (ConnectionFailedException ex)
                {
                    // no-op, we don't care right now
                }
            }

            for (Channel c : minChannels)
            {
                available.offerFirst(new ChannelWithIdleTime(c));
                c.closeFuture().addListener(inAvailableCloseListener);
            }
        }

        idleReaperFuture = executor.scheduleWithFixedDelay(new IdleReaper(), 1, 5, TimeUnit.SECONDS);
        healthMonitorFuture = executor.scheduleWithFixedDelay(new HealthMonitorTask(), 1000, 1000, TimeUnit.MILLISECONDS);

        state = State.RUNNING;
        logger.info("RiakNode started; {}:{}", remoteAddress, port);
        notifyStateListeners();
        return this;
    }

    private void refreshBootstrapRemoteAddress() throws UnknownHostException
    {
        // Refresh the address, hope their DNS TTL settings allow this.
        InetSocketAddress socketAddress = new InetSocketAddress(remoteAddress, port);

        if (socketAddress.isUnresolved())
        {
            throw new UnknownHostException("RiakNode:start - Failed resolving host " + remoteAddress);
        }

        bootstrap.remoteAddress(socketAddress);
    }

    public synchronized Future shutdown()
    {
        stateCheck(State.RUNNING, State.HEALTH_CHECKING);
        state = State.SHUTTING_DOWN;
        logger.info("RiakNode shutting down; {}:{}", remoteAddress, port);
        notifyStateListeners();
        idleReaperFuture.cancel(true);
        healthMonitorFuture.cancel(true);
        ChannelWithIdleTime cwi = available.poll();
        while (cwi != null)
        {
            Channel c = cwi.getChannel();
            closeConnection(c);
            cwi = available.poll();
        }

        executor.schedule(new ShutdownTask(), 0, TimeUnit.SECONDS);

        return new Future()
        {
            @Override
            public boolean cancel(boolean mayInterruptIfRunning)
            {
                return false;
            }
            @Override
            public Boolean get() throws InterruptedException
            {
                shutdownLatch.await();
                return true;
            }
            @Override
            public Boolean get(long timeout, TimeUnit unit) throws InterruptedException
            {
                return shutdownLatch.await(timeout, unit);
            }
            @Override
            public boolean isCancelled()
            {
                return false;
            }
            @Override
            public boolean isDone()
            {
                return shutdownLatch.getCount() <= 0;
            }
        };
    }

    /**
     * Sets the Netty {@link Bootstrap} for this Node's connections.
     * {@link Bootstrap#clone()} is called to clone the bootstrap.
     *
     * @param bootstrap - the Netty Bootstrap to use
     * @return a reference to this RiakNode
     * @throws IllegalArgumentException if it was already set via the builder.
     * @throws IllegalStateException    if the node has already been started.
     * @see Builder#withBootstrap(io.netty.bootstrap.Bootstrap)
     */
    public RiakNode setBootstrap(Bootstrap bootstrap)
    {
        stateCheck(State.CREATED);
        if (this.bootstrap != null)
        {
            throw new IllegalArgumentException("Bootstrap already set");
        }

        this.bootstrap = bootstrap.clone();
        return this;
    }

    /**
     * Sets the {@link ScheduledExecutorService} for this Node and its pool(s).
     *
     * @param executor - the ScheduledExecutorService to use.
     * @return a reference to this RiakNode
     * @throws IllegalArgumentException if it was already set via the builder.
     * @throws IllegalStateException    if the node has already been started.
     * @see Builder#withExecutor(java.util.concurrent.ScheduledExecutorService)
     */
    public RiakNode setExecutor(ScheduledExecutorService executor)
    {
        stateCheck(State.CREATED);
        if (this.executor != null)
        {
            throw new IllegalArgumentException("Executor already set");
        }
        this.executor = executor;
        return this;
    }

    /**
     * Sets the maximum number of connections allowed.
     *
     * @param maxConnections the maxConnections to set.
     * @return a reference to this RiakNode.
     * @see Builder#withMaxConnections(int)
     */
    public RiakNode setMaxConnections(int maxConnections)
    {
        stateCheck(State.CREATED, State.RUNNING, State.HEALTH_CHECKING);
        if (maxConnections >= getMinConnections())
        {
            permits.setMaxPermits(maxConnections);
        }
        else
        {
            throw new IllegalArgumentException("Max connections less than min connections");
        }
        // TODO: reap delta?
        return this;
    }

    /**
     * Returns the maximum number of connections allowed.
     *
     * @return the maxConnections
     * @see Builder#withMaxConnections(int)
     */
    public int getMaxConnections()
    {
        stateCheck(State.CREATED, State.RUNNING, State.HEALTH_CHECKING);
        return permits.getMaxPermits();
    }

    /**
     * Sets the minimum number of active connections to be maintained.
     *
     * @param minConnections the minConnections to set
     * @return a reference to this RiakNode
     * @see Builder#withMinConnections(int)
     */
    public RiakNode setMinConnections(int minConnections)
    {
        stateCheck(State.CREATED, State.RUNNING, State.HEALTH_CHECKING);
        if (minConnections <= getMaxConnections())
        {
            this.minConnections = minConnections;
        }
        else
        {
            throw new IllegalArgumentException("Min connections greater than max connections");
        }
        // TODO: Start / reap delta?
        return this;
    }

    /**
     * Returns the current minimum number of active connections to be maintained.
     *
     * @return the minConnections
     * @see Builder#withMinConnections(int)
     */
    public int getMinConnections()
    {
        stateCheck(State.CREATED, State.RUNNING, State.HEALTH_CHECKING);
        return minConnections;
    }

    /**
     * Set whether to block when all connections are in use.
     * @param block true to block.
     * @see Builder#withBlockOnMaxConnections(boolean)
     */
    public void setBlockOnMaxConnections(boolean block)
    {
        this.blockOnMaxConnections = block;
    }

    /**
     * Returns if this node is set to block when all connections are in use.
     * @return true if set to block, false otherwise.
     * @see Builder#withBlockOnMaxConnections(boolean)
     */
    public boolean getBlockOnMaxConnections()
    {
        return blockOnMaxConnections;
    }

    /**
     * Sets the connection idle timeout for connections.
     *
     * @param idleTimeoutInMillis the idleTimeout to set
     * @return a reference to this RiakNode
     * @see Builder#withIdleTimeout(int)
     */
    public RiakNode setIdleTimeout(int idleTimeoutInMillis)
    {
        stateCheck(State.CREATED, State.RUNNING, State.HEALTH_CHECKING);
        this.idleTimeoutInNanos = TimeUnit.NANOSECONDS.convert(idleTimeoutInMillis, TimeUnit.MILLISECONDS);
        return this;
    }

    /**
     * Returns the connection idle timeout for connections in milliseconds.
     *
     * @return the idleTimeout in milliseconds
     * @see Builder#withIdleTimeout(int)
     */
    public int getIdleTimeout()
    {
        stateCheck(State.CREATED, State.RUNNING, State.HEALTH_CHECKING);
        return (int) TimeUnit.MILLISECONDS.convert(idleTimeoutInNanos, TimeUnit.NANOSECONDS);
    }

    /**
     * Sets the connection timeout for new connections.
     *
     * @param connectionTimeoutInMillis the connectionTimeout to set
     * @return a reference to this RiakNode
     * @see Builder#withConnectionTimeout(int)
     */
    public RiakNode setConnectionTimeout(int connectionTimeoutInMillis)
    {
        stateCheck(State.CREATED, State.RUNNING, State.HEALTH_CHECKING);
        this.connectionTimeout = connectionTimeoutInMillis;
        bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectionTimeout);
        return this;
    }

    /**
     * Returns the connection timeout in milliseconds.
     *
     * @return the connectionTimeout
     * @see Builder#withConnectionTimeout(int)
     */
    public int getConnectionTimeout()
    {
        stateCheck(State.CREATED, State.RUNNING, State.HEALTH_CHECKING);
        return connectionTimeout;
    }

    /**
     * Returns the number of permits currently available.
     * The number of available permits indicates how many additional
     * connections can be made without blocking.
     *
     * @return the number of available permits.
     * @see Builder#withMaxConnections(int)
     */
    public int availablePermits()
    {
        stateCheck(State.CREATED, State.RUNNING, State.HEALTH_CHECKING);
        return permits.availablePermits();
    }

    public void addStateListener(NodeStateListener listener)
    {
        stateListeners.add(listener);
    }

    public boolean removeStateListener(NodeStateListener listener)
    {
        return stateListeners.remove(listener);
    }

    private void notifyStateListeners()
    {
        synchronized (stateListeners)
        {
            for (NodeStateListener listener : stateListeners)
            {
                listener.nodeStateChanged(this, state);
            }
        }
    }

    /**
     * Submits the operation to be executed on this node.
     *
     * @param operation The operation to perform
     * @return {@code true} if this operation was accepted, {@code false} if there
     *         were no available connections.
     * @throws IllegalStateException    if this node is not in the {@code RUNNING} or {@code HEALTH_CHECKING} state
     * @throws IllegalArgumentException if the protocol required for the operation is not supported by this node
     */
    public boolean execute(FutureOperation operation)
    {
        stateCheck(State.RUNNING, State.HEALTH_CHECKING);

        operation.setLastNode(this);
        Channel channel = getConnection();
        if (channel != null)
        {
            inProgressMap.put(channel, operation);
            ChannelFuture writeFuture = channel.writeAndFlush(operation);
            writeFuture.addListener(writeListener);
            logger.debug("Operation {} being executed on RiakNode {}:{}",
                         System.identityHashCode(operation), remoteAddress, port);
            return true;
        }
        else
        {
            logger.debug("Operation {} not being executed Riaknode {}:{}; no connections available",
                         System.identityHashCode(operation), remoteAddress, port);
            return false;
        }
    }

    // ConnectionPool Stuff

    /**
     * Get a Netty channel from the pool.
     * 

* The first thing this method does is attempt to acquire a permit from the * Semaphore that controls the pool's behavior. Depending on whether * {@code blockOnMaxConnections} is set, this will either block until one * becomes available or return null. *

*

* Once a permit has been acquired, a channel from the pool or a newly * created one will be returned. If an attempt to create a new connection * fails, null will then be returned. *

* @return a connected channel or {@code null} * @see Builder#withBlockOnMaxConnections(boolean) */ private Channel getConnection() { stateCheck(State.RUNNING, State.HEALTH_CHECKING); boolean acquired = false; if (blockOnMaxConnections) { try { logger.debug("Attempting to acquire channel permit"); if (!permits.tryAcquire()) { logger.info("All connections in use for {}; had to wait for one.", remoteAddress); permits.acquire(); } acquired = true; } catch (InterruptedException ex) { // no-op, don't care } catch (BlockingOperationException ex) { logger.error("Netty interrupted waiting for connection permit to be available; {}", remoteAddress); } } else { logger.debug("Attempting to acquire channel permit"); acquired = permits.tryAcquire(); } Channel channel = null; if (acquired) { try { channel = doGetConnection(true); channel.closeFuture().removeListener(inAvailableCloseListener); } catch (ConnectionFailedException ex) { permits.release(); } catch (UnknownHostException ex) { permits.release(); logger.error("Unknown host encountered while trying to open connection; {}", ex); } } return channel; } private Channel doGetConnection(boolean forceAddressRefresh) throws ConnectionFailedException, UnknownHostException { ChannelWithIdleTime cwi; while ((cwi = available.poll()) != null) { Channel channel = cwi.getChannel(); // If the channel from available is closed, try again. This will result in // the caller always getting a connection or an exception. If closed // the channel is simply discarded so this also acts as a purge // for dead channels during a health check. if (channel.isOpen()) { return channel; } } if (forceAddressRefresh) { refreshBootstrapRemoteAddress(); } ChannelFuture f = bootstrap.connect(); try { f.await(); } catch (InterruptedException ex) { logger.error("Thread interrupted waiting for new connection to be made; {}", remoteAddress); Thread.currentThread().interrupt(); throw new ConnectionFailedException(ex); } catch (BlockingOperationException ex) { logger.error("Netty interrupted waiting for new connection to be made; {}", remoteAddress); throw new ConnectionFailedException(ex); } if (!f.isSuccess()) { logger.error("Connection attempt failed: {}:{}; {}", remoteAddress, port, f.cause()); consecutiveFailedConnectionAttempts.incrementAndGet(); throw new ConnectionFailedException(f.cause()); } consecutiveFailedConnectionAttempts.set(0); Channel c = f.channel(); if (trustStore != null) { setupTLSAndAuthenticate(c); } return c; } private void setupTLSAndAuthenticate(Channel c) throws ConnectionFailedException { SSLContext context; try { context = SSLContext.getInstance("TLS"); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustStore); if (keyStore!=null) { KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, keyPassword==null?"".toCharArray():keyPassword.toCharArray()); context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); } else { context.init(null, tmf.getTrustManagers(), null); } } catch (Exception ex) { c.close(); logger.error("Failure configuring SSL; {}:{} {}", remoteAddress, port, ex); throw new ConnectionFailedException(ex); } SSLEngine engine = context.createSSLEngine(); Set protocols = new HashSet<>(Arrays.asList(engine.getSupportedProtocols())); if (protocols.contains("TLSv1.2")) { engine.setEnabledProtocols(new String[] {"TLSv1.2"}); logger.debug("Using TLSv1.2"); } else if (protocols.contains("TLSv1.1")) { engine.setEnabledProtocols(new String[] {"TLSv1.1"}); logger.debug("Using TLSv1.1"); } engine.setUseClientMode(true); RiakSecurityDecoder decoder = new RiakSecurityDecoder(engine, username, password); c.pipeline().addFirst(decoder); try { DefaultPromise promise = decoder.getPromise(); logger.debug("Waiting on SSL Promise"); promise.await(); if (promise.isSuccess()) { logger.debug("Auth succeeded; {}:{}", remoteAddress, port); } else { c.close(); logger.error("Failure during Auth; {}:{} {}",remoteAddress, port, promise.cause()); throw new ConnectionFailedException(promise.cause()); } } catch (InterruptedException e) { c.close(); logger.error("Thread interrupted during Auth; {}:{}", remoteAddress, port); Thread.currentThread().interrupt(); throw new ConnectionFailedException(e); } } /** * Return a Netty channel. * * @param c The Netty channel to return to the pool */ private void returnConnection(Channel c) { switch (state) { case SHUTTING_DOWN: case SHUTDOWN: closeConnection(c); break; case RUNNING: case HEALTH_CHECKING: default: if (inProgressMap.containsKey(c)) { logger.error("Channel returned to pool while still in use. id: {}", c.hashCode()); } else { if (c.isOpen()) { logger.debug("Channel id:{} returned to pool", c.hashCode()); c.closeFuture().removeListener(inProgressCloseListener); c.closeFuture().addListener(inAvailableCloseListener); available.offerFirst(new ChannelWithIdleTime(c)); } else { logger.debug("Closed channel id:{} returned to pool; discarding", c.hashCode()); } logger.debug("Released pool permit"); permits.release(); } } } private void closeConnection(Channel c) { // If we are explicitly closing the connection we don't want to hear // about it. c.closeFuture().removeListener(inProgressCloseListener); c.closeFuture().removeListener(inAvailableCloseListener); c.close(); } // End ConnectionPool stuff @Override public void onSuccess(Channel channel, final RiakMessage response) { logger.debug("Operation onSuccess() channel: id:{} {}:{}", channel.hashCode(), remoteAddress, port); consecutiveFailedOperations.set(0); final FutureOperation inProgress = inProgressMap.get(channel); // Especially with a streaming op, the close listener may trigger causing // a race. This check guards that. if (inProgress != null) { inProgress.setResponse(response); if (inProgress.isDone()) { try { inProgressMap.remove(channel); returnConnection(channel); // return permit } finally { inProgress.setComplete(); } } } } @Override public void onRiakErrorResponse(Channel channel, RiakResponseException ex) { logger.debug("Riak replied with error; {}:{}", ex.getCode(), ex.getMessage()); final FutureOperation inProgress = inProgressMap.remove(channel); consecutiveFailedOperations.incrementAndGet(); if (inProgress != null) { returnConnection(channel); // release permit inProgress.setException(ex); } } @Override public void onException(Channel channel, final Throwable t) { logger.error("Operation onException() channel: id:{} {}:{} {}", channel.hashCode(), remoteAddress, port, t); final FutureOperation inProgress = inProgressMap.remove(channel); // There are fail cases where multiple exceptions are thrown from // the pipeline. In that case we'll get an exception from the // handler but will not have an entry in inProgress because it's // already been handled. if (inProgress != null) { returnConnection(channel); // release permit inProgress.setException(t); } } private void checkNetworkAddressCacheSettings() { final String property = Security.getProperty("networkaddress.cache.ttl"); final boolean usingSecurityMgr = System.getSecurityManager() != null; final boolean propertyUndefined = property == null; boolean logWarning = false; if (propertyUndefined && usingSecurityMgr) { logWarning = true; } else if (!propertyUndefined) { final int cacheTTL = Integer.parseInt(property); logWarning = (cacheTTL == -1); } if (logWarning) { logger.warn( "The Java Security \"networkaddress.cache.ttl\" property may be set to cache DNS lookups forever. " + "Using domain names for Riak nodes or an intermediate load balancer could result in stale IP " + "addresses being used for new connections, causing connection errors. " + "If you use domain names for Riak nodes, please set this property to a value greater than zero."); } } /** * Returns the {@code remoteAddress} for this RiakNode * * @return The IP address or FQDN as a {@code String} */ public String getRemoteAddress() { return remoteAddress; } /** * returns the remote port for this RiakNode * * @return the port number */ public int getPort() { return port; } /** * Returns the current state of this node. * * @return The state */ public State getNodeState() { return this.state; } private class ChannelWithIdleTime { private Channel channel; private long idleStart; public ChannelWithIdleTime(Channel channel) { this.channel = channel; idleStart = System.nanoTime(); } public Channel getChannel() { return channel; } public long getIdleStart() { return idleStart; } } static class Sync extends Semaphore { private static final long serialVersionUID = -5118488872281021072L; private volatile int maxPermits; public Sync(int numPermits) { super(numPermits); this.maxPermits = numPermits; } public Sync(int numPermits, boolean fair) { super(numPermits, fair); this.maxPermits = numPermits; } public int getMaxPermits() { return maxPermits; } // Synchronized because we're (potentially) changing this.maxPermits synchronized void setMaxPermits(int maxPermits) { int diff = maxPermits - this.maxPermits; if (diff == 0) { return; } else if (diff > 0) { release(diff); } else if (diff < 0) { reducePermits(diff); } this.maxPermits = maxPermits; } } private class IdleReaper implements Runnable { @Override public void run() { reapIdleConnections(); } } private void reapIdleConnections() { // with all the concurrency there's really no reason to keep // checking the sizes. This is really just a "best guess" int currentNum = inProgressMap.size() + available.size(); if (currentNum > minConnections) { // Note this will not throw a ConncurrentModificationException // and if hasNext() returns true you are guaranteed that // the next() will return a value (even if it has already // been removed from the Deque between those calls). Iterator i = available.descendingIterator(); while (i.hasNext() && currentNum > minConnections) { ChannelWithIdleTime cwi = i.next(); if (cwi.getIdleStart() + idleTimeoutInNanos < System.nanoTime()) { boolean removed = available.remove(cwi); if (removed) { Channel c = cwi.getChannel(); logger.debug("Idle channel closed; {}:{}", remoteAddress, port); closeConnection(c); currentNum--; } } else { // Since we are descending and this is a LIFO, // if the current connection hasn't been idle beyond // the threshold, there's no reason to descend further break; } } } } // TODO: Revisit if we ever support multiple protocols or change protocols. // As-is the parameters work well for protocol buffers. /** * Task to see if a criteria should trigger a health check. *

* We keep a list of connections that triggered the closeListener. We also * track the number of consecutive failed connection attempts, and the * number of consecutive error responses from Riak. *

*/ private class HealthMonitorTask implements Runnable { @Override public void run() { // Purge recentlyClosed past a certain age // sliding window should be larger than the // frequency of this task long current = System.nanoTime(); long window = 3000000000L; // 3 seconds for (ChannelWithIdleTime cwi = recentlyClosed.peek(); cwi != null && current - cwi.getIdleStart() > window; cwi = recentlyClosed.peek()) { recentlyClosed.poll(); } // If we more than 5 recently closed in 3 seconds, more than 1 consecutive failed // connection attempts, more than 5 consecutive error responses from Riak, // or we failed a healthcheck if ((state == State.RUNNING && (recentlyClosed.size() > 5 || consecutiveFailedConnectionAttempts.get() > 1 || consecutiveFailedOperations.get() > 5) ) || state == State.HEALTH_CHECKING) { checkHealth(); } } } private void checkHealth() { try { HealthCheckDecoder healthCheck = healthCheckFactory.makeDecoder(); RiakFuture future = healthCheck.getFuture(); // See: doGetConnection() - this will purge closed // connections from the available queue and either // return/create a new one (meaning the node is up) or throw // an exception if a connection can't be made. Channel c = doGetConnection(true); logger.debug("Healthcheck channel: {} isOpen: {} handlers:{}", c.hashCode(), c.isOpen(), c.pipeline().names()); // If the channel closes between when we got it and now, the pipeline is emptied. If the handlers // aren't there we fail the healthcheck try { if (c.pipeline().names().contains(Constants.SSL_HANDLER)) { c.pipeline().addAfter(Constants.SSL_HANDLER, Constants.HEALTHCHECK_CODEC, healthCheck); } else { c.pipeline().addBefore(Constants.MESSAGE_CODEC, Constants.HEALTHCHECK_CODEC, healthCheck); } logger.debug("healthCheck added to pipeline."); //future.await(5, TimeUnit.SECONDS); future.await(); if (future.isSuccess()) { healthCheckSucceeded(); } else { healthCheckFailed(future.cause()); } } catch (InterruptedException ex) { logger.error("Thread interrupted performing healthcheck."); } catch (NoSuchElementException e) { healthCheckFailed(new IOException("Channel closed during health check")); } finally { closeConnection(c); } } catch (ConnectionFailedException | UnknownHostException ex) { healthCheckFailed(ex); } catch (IllegalStateException ex) { // no-op; there's a race condition where the bootstrap is shutting down // right when a healthcheck occurs and netty will throw this logger.debug("Illegal state exception during healthcheck."); logger.debug("Stack: {}", ex); } catch (RuntimeException ex) { logger.error("Runtime exception during healthcheck: {}", ex); } } private void healthCheckFailed(Throwable cause) { if (state == State.RUNNING) { logger.error("RiakNode failed healthcheck operation; health checking; {}:{} {}", remoteAddress, port, cause); state = State.HEALTH_CHECKING; notifyStateListeners(); } else { logger.error("RiakNode failed healthcheck operation; {}:{} {}", remoteAddress, port, cause); } } private void healthCheckSucceeded() { if (state == State.HEALTH_CHECKING) { logger.info("RiakNode recovered; {}:{}", remoteAddress, port); state = State.RUNNING; notifyStateListeners(); } } private class ShutdownTask implements Runnable { @Override public void run() { if (inProgressMap.isEmpty()) { state = State.SHUTDOWN; notifyStateListeners(); if (ownsExecutor) { executor.shutdown(); } if (ownsBootstrap) { bootstrap.config().group().shutdownGracefully(); } logger.debug("RiakNode shut down {}:{}", remoteAddress, port); shutdownLatch.countDown(); } } } /** * Builder used to construct a RiakNode. */ public static class Builder { /** * The default remote address to be used if not specified: {@value #DEFAULT_REMOTE_ADDRESS} * * @see #withRemoteAddress(java.lang.String) */ public final static String DEFAULT_REMOTE_ADDRESS = "127.0.0.1"; /** * The default port number to be used if not specified: {@value #DEFAULT_REMOTE_PORT} * * @see #withRemotePort(int) */ public final static int DEFAULT_REMOTE_PORT = 8087; /** * The default minimum number of connections to maintain if not specified: {@value #DEFAULT_MIN_CONNECTIONS} * * @see #withMinConnections(int) */ public final static int DEFAULT_MIN_CONNECTIONS = 1; /** * The default maximum number of connections allowed if not specified: {@value #DEFAULT_MAX_CONNECTIONS} * * @see #withMaxConnections(int) */ public final static int DEFAULT_MAX_CONNECTIONS = 0; /** * The default idle timeout in milliseconds for connections if not specified: {@value #DEFAULT_IDLE_TIMEOUT} * * @see #withIdleTimeout(int) */ public final static int DEFAULT_IDLE_TIMEOUT = 1000; /** * The default connection timeout in milliseconds if not specified: {@value #DEFAULT_CONNECTION_TIMEOUT} * * @see #withConnectionTimeout(int) */ public final static int DEFAULT_CONNECTION_TIMEOUT = 0; /** * The default HealthCheckFactory. *

* By default this is the {@link PingHealthCheck} *

* @see HealthCheckFactory * @see HealthCheckDecoder */ public final static HealthCheckFactory DEFAULT_HEALTHCHECK_FACTORY = new PingHealthCheck(); private int port = DEFAULT_REMOTE_PORT; private String remoteAddress = DEFAULT_REMOTE_ADDRESS; private int minConnections = DEFAULT_MIN_CONNECTIONS; private int maxConnections = DEFAULT_MAX_CONNECTIONS; private int idleTimeout = DEFAULT_IDLE_TIMEOUT; private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; private HealthCheckFactory healthCheckFactory = DEFAULT_HEALTHCHECK_FACTORY; private Bootstrap bootstrap; private ScheduledExecutorService executor; private boolean blockOnMaxConnections; private String username; private String password; private KeyStore trustStore; private KeyStore keyStore; private String keyPassword; /** * Default constructor. Returns a new builder for a RiakNode with * default values set. */ public Builder() { } /** * Sets the remote host and port for this RiakNode. * * @param hostAndPOrt * @return this */ public Builder withRemoteHost(HostAndPort hostAndPOrt) { this.withRemoteAddress(hostAndPOrt.getHost()); this.withRemotePort(hostAndPOrt.getPort()); return this; } /** * Sets the remote address for this RiakNode. * * @param remoteAddress Can either be a FQDN or IP address. Since 2.0.3 it may contain port delimited by ':'. * @return this * @see #DEFAULT_REMOTE_ADDRESS */ public Builder withRemoteAddress(String remoteAddress) { withRemoteAddress(HostAndPort.fromString(remoteAddress, this.port)); return this; } /** * Specifies the remote port for this RiakNode. * * @param port - the port * @return this * @see #DEFAULT_REMOTE_PORT */ public Builder withRemotePort(int port) { this.port = port; return this; } /** * Specifies the remote host and remote port for this RiakNode. * * @param hp - host and port * @return this * @see #DEFAULT_REMOTE_PORT * * @since 2.0.3 * @see HostAndPort */ public Builder withRemoteAddress(HostAndPort hp) { this.port = hp.getPortOrDefault(DEFAULT_REMOTE_PORT); this.remoteAddress = hp.getHost(); return this; } /** * Set the minimum number of active connections to maintain. * These connections are exempt from the idle timeout. * * @param minConnections - number of connections to maintain. * @return this * @see #DEFAULT_MIN_CONNECTIONS */ public Builder withMinConnections(int minConnections) { if (maxConnections == DEFAULT_MAX_CONNECTIONS || minConnections <= maxConnections) { this.minConnections = minConnections; } else { throw new IllegalArgumentException("Min connections greater than max connections"); } return this; } /** * Set the maximum number of connections allowed. * A value of 0 sets this to unlimited. * * @param maxConnections - maximum number of connections to allow * @return this * @see #DEFAULT_MAX_CONNECTIONS */ public Builder withMaxConnections(int maxConnections) { if (maxConnections == DEFAULT_MAX_CONNECTIONS || maxConnections >= minConnections) { this.maxConnections = maxConnections; } else { throw new IllegalArgumentException("Max connections less than min connections"); } return this; } /** * Set the idle timeout used to reap inactive connections. * Any connection that has been idle for this amount of time * becomes eligible to be closed and discarded unless {@code minConnections} * has been set via {@link #withMinConnections(int) } * * @param idleTimeoutInMillis - idle timeout in milliseconds * @return this * @see #DEFAULT_IDLE_TIMEOUT */ public Builder withIdleTimeout(int idleTimeoutInMillis) { this.idleTimeout = idleTimeoutInMillis; return this; } /** * Set the connection timeout used when making new connections * * @param connectionTimeoutInMillis * @return this * @see #DEFAULT_CONNECTION_TIMEOUT */ public Builder withConnectionTimeout(int connectionTimeoutInMillis) { this.connectionTimeout = connectionTimeoutInMillis; return this; } /** * Provides an executor for this node to use for internal maintenance tasks. * If not provided one will be created via * {@link Executors#newSingleThreadScheduledExecutor()} * * @param executor the ScheduledExecutorService to use. * @return this */ public Builder withExecutor(ScheduledExecutorService executor) { this.executor = executor; return this; } /** * Provides a Netty Bootstrap for this node to use. * If not provided one * will be created with its own {@code NioEventLoopGroup}. * * @param bootstrap * @return this */ public Builder withBootstrap(Bootstrap bootstrap) { this.bootstrap = bootstrap; return this; } /** * Set whether to block if all connections are in use. *

* If a maximum number of connections is specified and all those * connections are in use, the default * behavior when an operation is submitted to a node is to * fail-fast and return. Setting this to true will cause the * call to block (fair-scheduled, FIFO) until a connection becomes * available. *

* @param block whether to block when an operation is submitted and * all connections are in use. * @return this */ public Builder withBlockOnMaxConnections(boolean block) { this.blockOnMaxConnections = block; return this; } /** * Set the credentials for Riak security and authentication. *

* Riak supports authentication and authorization features. * These credentials will be used for all connections. *

*

* Note this requires Riak to have been configured with security enabled. *

* * @param username the riak user name. * @param password the password for this user. * @param trustStore A Java KeyStore loaded with the CA certificate required for TLS/SSL * @return a reference to this object. */ public Builder withAuth(String username, String password, KeyStore trustStore) { this.username = username; this.password = password; this.trustStore = trustStore; return this; } /** * Set the credentials for Riak security and authentication. *

* Riak supports authentication and authorization features. * These credentials will be used for all connections. *

*

* Note this requires Riak to have been configured with security enabled. *

* * @param username the riak user name. * @param password the password for this user. * @param trustStore A Java KeyStore loaded with the CA certificate required for TLS/SSL * @param keyStore A Java KeyStore loaded with User's Private certificate required for TLS/SSL * @param keyPassword the password for User's Private certificate. * @return a reference to this object. */ public Builder withAuth(String username, String password, KeyStore trustStore, KeyStore keyStore, String keyPassword) { this.username = username; this.password = password; this.trustStore = trustStore; this.keyStore = keyStore; this.keyPassword = keyPassword; return this; } /** * Set the HealthCheckFactory used to determine if this RiakNode is healthy. *

* If not set the {@link PingHealthCheck} is used. *

* @param factory a HealthCheckFactory instance that produces HealthCheckDecoders * @return a reference to this object. * @see HealthCheckDecoder */ public Builder withHealthCheck(HealthCheckFactory factory) { this.healthCheckFactory = factory; return this; } /** * Builds a RiakNode. * If a Netty {@code Bootstrap} and/or a {@code ScheduledExecutorService} has not been provided they * will be created. * * @return a new Riaknode */ public RiakNode build() { return new RiakNode(this); } /** * Build a set of RiakNodes. * The provided builder will be used to construct a set of RiakNodes * using the supplied addresses. * * @param builder a configured builder, used for common properties among the nodes * @param remoteAddresses a list of IP addresses or FQDN. * Since 2.0.3 each of list item is treated as a comma separated list * of FQDN or IP addresses with the optional remote port delimited by ':'. * @return a list of constructed RiakNodes */ public static List buildNodes(Builder builder, List remoteAddresses) { final Set hps = new HashSet<>(); for (String remoteAddress: remoteAddresses) { hps.addAll( HostAndPort.hostsFromString(remoteAddress, builder.port) ); } return buildNodes(hps, builder); } /** * Build a set of RiakNodes. * The provided builder will be used to construct a set of RiakNodes * using the supplied hosts. * * @param remoteHosts a list of hosts w/wo ports. * @param builder a configured builder, used for common properties among the nodes * * @return a list of constructed RiakNodes * @since 2.0.6 */ public static List buildNodes(Collection remoteHosts, Builder builder) { final Set hps; if (!(remoteHosts instanceof Set)) { hps = new HashSet<>(remoteHosts); } else { hps = (Set) remoteHosts; } final List nodes = new ArrayList<>(hps.size()); for (HostAndPort hp : hps) { builder.withRemoteAddress(hp); nodes.add(builder.build()); } return nodes; } /** * Build a set of RiakNodes. * The provided builder will be used to construct a set of RiakNodes using the supplied addresses. * * @since 2.0.3 * @see #buildNodes(RiakNode.Builder, List) */ public static List buildNodes(Builder builder, String... remoteAddresses) throws UnknownHostException { return buildNodes(builder, Arrays.asList(remoteAddresses)); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy