org.helios.redis.ts.controller.conn.RedisConnectionManager.java Source code

Java tutorial

Introduction

Here is the source code for org.helios.redis.ts.controller.conn.RedisConnectionManager.java

Source

/**
 * Helios, OpenSource Monitoring
 * Brought to you by the Helios Development Group
 *
 * Copyright 2007, Helios Development Group and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 
 *
 */
package org.helios.redis.ts.controller.conn;

import java.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.commons.pool.impl.GenericObjectPool.Config;
import org.apache.log4j.Logger;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.netty.ConnectionListener;
import redis.clients.jedis.netty.OptimizedPubSub;
import redis.clients.jedis.netty.SubListener;
import redis.clients.jedis.netty.jmx.ThreadPoolMonitor;

/**
 * <p>Title: RedisConnectionManager</p>
 * <p>Description: Manages connections and connection retry against the redis instance 
 * and publishing events to interested listeners.</p> 
 * <p>Company: Helios Development Group LLC</p>
 * @author Whitehead (nwhitehead AT heliosdev DOT org)
 * <p><code>org.helios.redis.ts.controller.conn.RedisConnectionManager</code></p>
 */
public class RedisConnectionManager implements ConnectionListener {
    /** Indicates if we have a good connection to redis */
    protected final AtomicBoolean connected = new AtomicBoolean(false);
    /** The jedis pool config */
    protected final Config poolConfig = new Config();
    /** The jedis pool instance */
    protected final JedisPool jedisPool;
    /** Instance logger */
    protected final Logger log;
    /** The RunID of the connected instance */
    protected String runId = null;
    /** The reconnect period in seconds */
    protected long reconnectPeriod = -1;
    /** The heartbeat period in seconds */
    protected long heartbeatPeriod = -1;
    /** The heartbeat publish period in seconds */
    protected long heartbeatPublishPeriod = -1;
    /** The heartbeat channel name */
    protected String heartbeatChannel;

    /** The redis host name or IP address */
    protected final String host;
    /** The redis auth password */
    protected final String auth;
    /** The redis listening port */
    protected final int port;
    /** The redis connect timeout */
    protected final int timeout;

    /** Scheduler thread pool Thread Factory Thread Serial Number */
    protected static final AtomicInteger serial = new AtomicInteger(0);
    /** Scheduler thread pool Thread Factory Thread Group */
    protected static final ThreadGroup schedulerThreadGroup = new ThreadGroup(
            RedisConnectionManager.class.getSimpleName() + "-ThreadGroup");
    /** Uncaught exception handler for scheduler */
    protected static final UncaughtExceptionHandler schedulerExceptionHandler = new UncaughtExceptionHandler() {
        public void uncaughtException(Thread t, Throwable e) {
            System.err.println("Scheduler Exception on thread [" + t + "]. Stack trace follows:");
            e.printStackTrace(System.err);
        };
    };

    /** Scheduler thread pool Thread Factory*/
    protected static final ThreadFactory threadFactory = new ThreadFactory() {
        public Thread newThread(Runnable r) {
            Thread t = new Thread(schedulerThreadGroup, r,
                    RedisConnectionManager.class.getSimpleName() + "Thread#" + serial.incrementAndGet());
            t.setDaemon(false);
            t.setUncaughtExceptionHandler(schedulerExceptionHandler);
            return t;
        }
    };
    /** Scheduler thread pool */
    protected final ScheduledThreadPoolExecutor scheduler;
    /** PubSub for publishing and subscribing to heartbeats */
    protected OptimizedPubSub heartbeatPubSub = null;
    /** Scheduler handle for reconnect loop */
    protected ScheduledFuture<?> reconnectScheduleHandle = null;
    /** Scheduler handle for sending heartbeat events */
    protected ScheduledFuture<?> heartbeatSendHandle = null;
    /** Scheduler handle for expecting heartbeat events */
    protected ScheduledFuture<?> heartbeatExpectHandle = null;
    /** Heartbeat timeout counter */
    protected final AtomicInteger totalHeartbeatTimeouts = new AtomicInteger(0);
    /** Heartbeat consecutive timeout counter */
    protected final AtomicInteger consecutiveHeartbeatTimeouts = new AtomicInteger(0);
    /** The timestamp of the last heartbeat received */
    protected final AtomicLong lastHeartbeatTime = new AtomicLong(-1);
    /** Registered connection event listeners */
    protected final Set<ConnectionManagerListener> listeners = new CopyOnWriteArraySet<ConnectionManagerListener>();
    /** A map of the pool config field types keyed by the field */
    protected static final Map<String, Field> poolConfigFieldNames = new HashMap<String, Field>(
            Config.class.getDeclaredFields().length);
    /** heartbeat Listener */
    protected SubListener heartbeatListener = new SubListener() {
        @Override
        public void onChannelMessage(String channel, String message) {
            if (log.isDebugEnabled())
                log.debug("Processing Message From Channel [" + channel + "]\n\t" + message);
            //log.info("Processing Message From Channel [" + channel + "]\n\t" + message);
            lastHeartbeatTime.set(Long.parseLong(message));
        }

        @Override
        public void onPatternMessage(String pattern, String channel, String message) {

        }

    };

    static {
        try {
            for (Field f : Config.class.getDeclaredFields()) {
                f.setAccessible(true);
                poolConfigFieldNames.put(f.getName(), f);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to initialize Pool Config Fields", e);
        }
    }

    /**
     * Creates a new RedisConnectionManager
     * @param configProps The redis-ts.config specified properties
     */
    public RedisConnectionManager(Properties configProps) {
        reconnectPeriod = Integer.parseInt(configProps.getProperty("redis.reconnect.period", "5"));
        heartbeatPeriod = Integer.parseInt(configProps.getProperty("redis.heartbeat.period", "3"));
        heartbeatPublishPeriod = Integer.parseInt(configProps.getProperty("redis.heartbeat.period.publish", "2"));
        int poolSize = Integer.parseInt(configProps.getProperty("redis.scheduler.pool.size", "5"));
        scheduler = new ScheduledThreadPoolExecutor(poolSize, threadFactory);
        ThreadPoolMonitor.registerMonitor(scheduler, new StringBuilder(getClass().getPackage().getName())
                .append(":service=Scheduler,name=RedisConnectionManager"));
        scheduler.prestartCoreThread();
        initPoolConfig(configProps);
        host = configProps.getProperty("redis.connect.host");
        auth = configProps.getProperty("redis.connect.auth");
        port = Integer.parseInt(configProps.getProperty("redis.connect.port", "6379"));
        timeout = Integer.parseInt(configProps.getProperty("redis.connect.timeout", "2000"));
        heartbeatChannel = configProps.getProperty("redis.ts.hearbeat.channel", "redis-ts.heartbeat");
        log = Logger.getLogger(getClass().getName() + "-" + host + ":" + port);
        heartbeatPubSub = OptimizedPubSub.getInstance(host, port, auth, timeout);
        if (auth == null || auth.trim().isEmpty()) {
            jedisPool = new JedisPool(poolConfig, host, port, timeout);
        } else {
            jedisPool = new JedisPool(poolConfig, host, port, timeout, auth);
        }
    }

    /**
     * Starts the connection manager
     */
    public void start() {
        try {
            runId = getInfo("run_id");
            onSuccessfulConnect();
            connected.set(true);
            log.info("Connection Manager Initialized");
            log.info("Redis Run ID:" + runId);
            fireConnectNewInstance();
        } catch (Exception e) {
            log.warn("Failed initial connection. Initiating reconnect loop");
        }
    }

    protected void onSuccessfulConnect() {
        heartbeatSendHandle = scheduler.scheduleAtFixedRate(new Runnable() {
            public void run() {
                heartbeatPubSub.publish(heartbeatChannel, "" + System.currentTimeMillis());
            }
        }, 0, heartbeatPublishPeriod, TimeUnit.SECONDS);
        heartbeatPubSub.subscribe(heartbeatChannel);
        heartbeatPubSub.registerListener(heartbeatListener);

        scheduler.scheduleAtFixedRate(new Runnable() {
            protected final long MAX_HB_ELAPSED = TimeUnit.MILLISECONDS.convert(heartbeatPeriod, TimeUnit.SECONDS);

            public void run() {
                long now = System.currentTimeMillis();
                long lastHb = lastHeartbeatTime.get();
                long diff = now - lastHb;
                if (log.isDebugEnabled())
                    log.debug("\n\t****\n\tHB Info:" + "\n\tNow:" + now + "\n\tLast HB:" + lastHb + "\n\tDiff:"
                            + diff + "\n\tMax Diff:" + MAX_HB_ELAPSED + "\n\t****\n");

                if (System.currentTimeMillis() - lastHeartbeatTime.get() > MAX_HB_ELAPSED) {
                    processHeartbeatTimeout();
                } else {
                    consecutiveHeartbeatTimeouts.set(0);
                }
            }
        }, heartbeatPeriod, heartbeatPeriod, TimeUnit.SECONDS);
    }

    /**
     * {@inheritDoc}
     * @see redis.clients.jedis.netty.ConnectionListener#onConnect(redis.clients.jedis.netty.OptimizedPubSub)
     */
    @Override
    public void onConnect(OptimizedPubSub pubSub) {

    }

    /**
     * {@inheritDoc}
     * @see redis.clients.jedis.netty.ConnectionListener#onDisconnect(redis.clients.jedis.netty.OptimizedPubSub, java.lang.Throwable)
     */
    @Override
    public void onDisconnect(OptimizedPubSub pubSub, Throwable cause) {

    }

    /**
     * Closes the connection manager and deallocates all associated resources
     */
    public void stop() {

    }

    /**
     * Returns true if the connection manager is connected, false otherwise
     * @return true if the connection manager is connected, false otherwise
     */
    public boolean isConnected() {
        return connected.get();
    }

    /**
     * Called when a heartbeat timeout occurs
     */
    protected void processHeartbeatTimeout() {
        consecutiveHeartbeatTimeouts.incrementAndGet();
        totalHeartbeatTimeouts.incrementAndGet();
        log.warn("\n\t=======================\n\tHeartbeat Failure\n\tConsecutive:"
                + consecutiveHeartbeatTimeouts.get() + "\n\tTotal:" + totalHeartbeatTimeouts.get()
                + "\n\t=======================\n");
        fireHeartbeatFailed();
    }

    /**
     * Returns a jedis instance from the pool
     * @return a jedis instance 
     */
    public Jedis getJedis() {
        return jedisPool.getResource();
    }

    /**
     * Returns a jedis instance to the pool
     * @param jedis the instance to return to the pool
     */
    public void returnJedis(Jedis jedis) {
        jedisPool.returnResource(jedis);
    }

    /**
     * Registers a new connection manager event listener
     * @param listener the new connection manager event listener to register
     */
    public void addListener(ConnectionManagerListener listener) {
        if (listener != null) {
            listeners.add(listener);
        }
    }

    /**
     * Unregisters a connection manager event listener
     * @param listener the new connection manager event listener to unregister
     */
    public void removeListener(ConnectionManagerListener listener) {
        if (listener != null) {
            listeners.remove(listener);
        }
    }

    /**
     * Returns a map of entries from redis INFO
     * @return a map of entries from redis INFO
     */
    public Map<String, String> getInfo() {
        Map<String, String> infoMap = new HashMap<String, String>(52);
        Jedis jedis = null;
        String infoString = null;
        try {
            jedis = jedisPool.getResource();
            infoString = jedis.info();
        } finally {
            jedisPool.returnResource(jedis);
        }
        if (infoString == null) {
            throw new RuntimeException("Failed to get INFO from redis", new Throwable());
        }
        for (String info : infoString.split("\r\n")) {
            info = info.replace(" ", "");
            if (!info.isEmpty() && !info.startsWith("#")) {
                String[] frags = info.split(":");
                infoMap.put(frags[0], frags[1]);
            }
        }
        return infoMap;
    }

    /**
     * Returns the value for the specified key from the redis INFO
     * @param key The key of the INFO field requested
     * @return The value of the key or nulll if it was not found 
     */
    public String getInfo(String key) {
        return getInfo().get(key);
    }

    /**
     * Initializes the pool config from the pool properties
     * @param configProps the pool properties
     */
    protected void initPoolConfig(Properties configProps) {
        String fName = null;
        try {
            for (String s : configProps.stringPropertyNames()) {
                if (s.startsWith("redis.pool.")) {
                    fName = s.replace("redis.pool.", "");
                    Field f = poolConfigFieldNames.get(fName);
                    if (f != null) {
                        String value = configProps.getProperty(s);
                        if (value == null || value.trim().isEmpty())
                            continue;
                        value = value.trim();
                        PropertyEditor pe = PropertyEditorManager.findEditor(f.getType());
                        pe.setAsText(value);
                        f.set(poolConfig, pe.getValue());
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to configure Jedis Pool Item [" + fName + "]", e);
        }
    }

    /**
     * Fired when the connection manager connects 
     * or reconnects to an instance with different run_id from the prior connection. 
     */
    protected void fireConnectNewInstance() {
        for (ConnectionManagerListener listener : listeners) {
            listener.onConnectNewInstance();
        }
    }

    /**
     * Fired when the connection manager connects to the 
     * same run_id as was connected to in the prior connection 
     */
    protected void fireConnect() {
        for (ConnectionManagerListener listener : listeners) {
            listener.onConnect();
        }
    }

    /**
     * Fired when connectivity is lost
     */
    protected void fireDisconnect() {
        for (ConnectionManagerListener listener : listeners) {
            listener.onDisconnect();
        }
    }

    /**
     * Fired when a heatbeat is not received in a timely manner. 
     * Does not, by itself, indicate a disconnect.
     */
    protected void fireHeartbeatFailed() {
        for (ConnectionManagerListener listener : listeners) {
            listener.onHeartbeatFailed();
        }
    }

}