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

org.tickcode.broadcast.RedisMessageBroker Maven / Gradle / Ivy

/*******************************************************************************
 * Copyright (c) 2012, tickcode.org
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of tickcode, nor tickcode.org, nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
package org.tickcode.broadcast;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.apache.log4j.Logger;
import org.tickcode.trace.BreadCrumbTrail;

import redis.clients.jedis.BinaryJedisPubSub;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.util.SafeEncoder;

import com.fasterxml.jackson.databind.ObjectMapper;

import de.undercouch.bson4jackson.BsonFactory;
import de.undercouch.bson4jackson.BsonGenerator;

/**
 * Provides support for getting messages broadcasted through Redis (See http://redis.io/ for details).
 * At the moment we are using bson4jackson (See http://www.michel-kraemer.com/binary-json-with-bson4jackson for details) to move our {@link Parameters} class into a BSON data representation.
 * and broadcast to other RedisMessageBrokers.
 * @author Eyon Land
 *
 */
public class RedisMessageBroker extends VMMessageBroker {
	private static Logger logger = Logger
			.getLogger(org.tickcode.broadcast.RedisMessageBroker.class);
	private static boolean settingRedisMessageBrokerForAll;

	private String name;
	private JedisPool jedisPool;

	private ConcurrentHashMap broadcastProxyByChannel = new ConcurrentHashMap();

	private Thread thread;
	private Jedis subscriberJedis;
	private String thumbprint = UUID.randomUUID().toString();
	private volatile String methodBeingBroadcastedFromRedis;
	private long latencyFromOthers;
	private long broadcastsFromOthers;
	private long latencyFromUs;
	private long broadcastsFromUs;


	MyBinarySubscriber subscriber = new MyBinarySubscriber();

	class MyBinarySubscriber extends BinaryJedisPubSub {

		@Override
		public void onMessage(byte[] channel, byte[] message) {
		}

		@Override
		public void onSubscribe(byte[] channel, int subscribedChannels) {
		}

		@Override
		public void onUnsubscribe(byte[] channel, int subscribedChannels) {
		}

		@Override
		public void onPUnsubscribe(byte[] pattern, int subscribedChannels) {
			logger.info("RedisMessageBroker shutting down.");
		}

		@Override
		public void onPSubscribe(byte[] pattern, int subscribedChannels) {
		}

		public void onPMessage(byte[] pattern, byte[] _channel, byte[] message) {
			String channel = SafeEncoder.encode(_channel);
			Broadcast producerProxy = null;
			try {
				int firstPeriod = channel.indexOf('.');
				if (firstPeriod < 1)
					return; // we have an invalid channel
				String redisMessageBrokerName = channel.substring(0,
						firstPeriod);
				if (!getName().equals(redisMessageBrokerName))
					return; // this channel does not belong to this message
							// broker
				int lastPeriod = channel.lastIndexOf('.');
				if (lastPeriod < 1)
					return; // we have an invalid channel
				String methodName = channel.substring(lastPeriod + 1);

				producerProxy = getRedisBroadcastProxy(channel);
				if (producerProxy == null) // we don't have any Broadcast
											// consumers
					return;

				ByteArrayInputStream bais = new ByteArrayInputStream(message);
				ObjectMapper mapper = new ObjectMapper(new BsonFactory());
				
				if(latencyFromOthers > Long.MAX_VALUE/2 || latencyFromUs > Long.MAX_VALUE/2){
					latencyFromOthers = 0;
					broadcastsFromOthers = 0;
					latencyFromUs = 0;
					broadcastsFromUs = 0;
				}
				
				Parameters args = mapper.readValue(bais, Parameters.class);
				if (!thumbprint.equals(args.getThumbprint())) {
					methodBeingBroadcastedFromRedis = methodName;
					RedisMessageBroker.super.broadcast(producerProxy,
							methodName, args.getArguments());
					methodBeingBroadcastedFromRedis = null;
					latencyFromOthers = System.currentTimeMillis() - args.getTimeSent();
					broadcastsFromOthers++;
				}{
					latencyFromUs = System.currentTimeMillis() - args.getTimeSent();
					broadcastsFromUs++;
				}

			} catch (Exception ex) {
				logger.error("Unable to process the broadcast.", ex);
				for (WeakReference errorHandler : errorHandlers) {
					if (errorHandler.get() != null)
						errorHandler.get().error(RedisMessageBroker.this,
								producerProxy, ex.getCause(),
								BreadCrumbTrail.get());
					else {
						errorHandlers.remove(errorHandler);
					}
				}
			}

		}

	}

	public RedisMessageBroker(String name, JedisPool jedisPool) {
		this.name = name;
		this.jedisPool = jedisPool;
	}
	
	public long getLatencyFromUs(){
		if(broadcastsFromUs > 0)
			return this.latencyFromUs / this.broadcastsFromUs;
		else 
			return 0;
	}

	public long getLatencyFromOthers(){
		if(broadcastsFromOthers > 0)
			return this.latencyFromOthers / this.broadcastsFromOthers;
		else 
			return 0;
	}
	
	public void finishedBroadcasting(Broadcast producer, String methodName,
			Object[] params) {
		if (!methodName.equals(methodBeingBroadcastedFromRedis))
			broadcastToRedisServer(thumbprint, producer, methodName, params);
	}

	protected Broadcast getRedisBroadcastProxy(String channel) {
		return broadcastProxyByChannel.get(channel);
	}

	protected void broadcastToRedisServer(String thumbprint,
			Broadcast producer, String methodName, Object[] params) {
		// broadcast to Redis
		BroadcastConsumersForAGivenInterface b = interfacesByMethodName
				.get(methodName);
		String channel = this.createChannelName(b.broadcastInterface.getName(),
				methodName);
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		BsonFactory fac = new BsonFactory();
		fac.enable(BsonGenerator.Feature.ENABLE_STREAMING);
		ObjectMapper mapper = new ObjectMapper(fac);
		Parameters args = new Parameters();
		args.setTimeSent(System.currentTimeMillis());
		args.setThumbprint(thumbprint);
		args.setArguments(params);
		args.setChannel(channel.toString());
		Jedis jedis = null;
		try {
			mapper.writeValue(baos, args);
			byte[] _channel = SafeEncoder.encode(channel);
			jedis = jedisPool.getResource();
			jedis.publish(_channel, baos.toByteArray());
		} catch (Exception ex) {
			for (WeakReference errorHandler : errorHandlers) {
				if (errorHandler.get() != null)
					errorHandler.get().error(this, producer, ex.getCause(),
							BreadCrumbTrail.get());
				else {
					errorHandlers.remove(errorHandler);
				}
			}
		} finally {
			if (jedis != null){
				jedisPool.returnResource(jedis);
				jedis = null;
			}
		}
	}

	@Override
	public void add(Broadcast consumer) {
		super.add(consumer);

		// create a subscriber on Redis
		Map channels = getAllBroadcastConsumerMethodNames(name,
				consumer.getClass());
		for (String channel : channels.keySet()) {
			if (!broadcastProxyByChannel.contains(channel)) {
				Class _interface = channels.get(channel);
				Class[] broadcastInterfaces = new Class[] { _interface };
				broadcastProxyByChannel.put(channel, RedisBroadcastProxy
						.newInstance(this, broadcastInterfaces));
			}
		}
	}

	public void start() {
		if (thread != null)
			thread.interrupt();
		thread = new Thread() {
			public void run() {
				subscriberJedis = jedisPool.getResource();
				logger.info("Whatching pub/sub from " + name + ".*");
				subscriberJedis.psubscribe(subscriber,
						SafeEncoder.encodeMany(name + ".*"));
			}
		};
		thread.start();
	}

	public void stop() {
		if (subscriber.isSubscribed())
			subscriber.punsubscribe();
		if (subscriberJedis != null) {
			jedisPool.returnResource(subscriberJedis);
		}
		thread = null;
	}

	public String getName() {
		return name;
	}

	private static StringBuffer builder = new StringBuffer();

	protected String createChannelName(String interfaceName, String methodName) {
		builder.setLength(0);
		if (name != null) {
			builder.append(name);
			builder.append(".");
		}
		builder.append(interfaceName);
		builder.append(".");
		builder.append(methodName);
		return builder.toString();
	}

	private Map getAllBroadcastConsumerMethodNames(
			String appendString, Class consumer) {
		HashSet broadcastConsumerMethods = new HashSet();
		for (Method method : consumer.getMethods()) {
			if (method.isAnnotationPresent(BroadcastConsumer.class)) {
				broadcastConsumerMethods.add(method.getName());
			}
		}
		HashMap readableMethodName = new HashMap();
		for (Class _interface : consumer.getInterfaces()) {
			if (Broadcast.class.isAssignableFrom(_interface)
					&& Broadcast.class != _interface) {
				for (Method method : _interface.getMethods()) {
					if (broadcastConsumerMethods.contains(method.getName())) {
						readableMethodName.put(
								createChannelName(_interface.getName(),
										method.getName()), _interface);
					}
				}
			}
		}
		return readableMethodName;
	}

	public static boolean isSettingRedisMessageBrokerForAll() {
		return settingRedisMessageBrokerForAll;
	}

	public static void setSettingRedisMessageBrokerForAll(
			boolean settingRedisMessageBrokerForAll) {
		RedisMessageBroker.settingRedisMessageBrokerForAll = settingRedisMessageBrokerForAll;
	}

	
//  From here down we are providing a way to check a live Redis system.	
	
	
	protected interface PingRedisMessageBroker extends Broadcast {
		public void ping(String message, long timeSent);
	}

	protected static class WatchPingMessages implements PingRedisMessageBroker {
		String message;
		CountDownLatch latch;
		long timeSent;
		long latency;
		long count;

		public WatchPingMessages(CountDownLatch latch) {
			this.latch = latch;
		}

		@Override
		@BroadcastConsumer
		@BroadcastProducer
		public void ping(String message, long timeSent) {
			this.message = message;
			this.timeSent = timeSent;
			latency += System.currentTimeMillis() - timeSent;
			count++;
			latch.countDown();
		}

		public String getMessage() {
			return message;
		}

		public long getAverageLatency() {
			return latency / count;
		}

		public long getCount() {
			return count;
		}
	}

	public static void main(String[] args) throws Exception {

		JedisPoolConfig poolConfig = new JedisPoolConfig();
		poolConfig.maxActive = 10;
		poolConfig.maxIdle = 5;
		poolConfig.minIdle = 2;
		poolConfig.testOnBorrow = true;
		poolConfig.numTestsPerEvictionRun = 10;
		poolConfig.timeBetweenEvictionRunsMillis = 60000;
		poolConfig.maxWait = 3000;
		poolConfig.whenExhaustedAction = org.apache.commons.pool.impl.GenericObjectPool.WHEN_EXHAUSTED_FAIL;
		JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379, 0);
		RedisMessageBroker broker = new RedisMessageBroker("LocalTest",
				jedisPool);

		try {
			broker.start();

			int totalPings = 1000;
			CountDownLatch latch = new CountDownLatch(totalPings);
			WatchPingMessages consumer = new WatchPingMessages(latch);
			broker.add(consumer);

			if (RedisMessageBroker.isUsingAspectJ())
				logger.info("We are using AspectJ");
			else
				logger.warn("Where is AspectJ?");

			String channel = broker.createChannelName(
					PingRedisMessageBroker.class.getName(), "ping");
			Broadcast broadcastProxy = broker.getRedisBroadcastProxy(channel);

			// simulate getting a broadcast from another virtual machine through
			// Redis
			String thumbprint = UUID.randomUUID().toString();
			for (int i = 0; i < totalPings; i++){
				broker.broadcastToRedisServer(thumbprint, broadcastProxy,
						"ping",
						new Object[] { "Pong", System.currentTimeMillis() });
			}
			latch.await(5, TimeUnit.SECONDS);
			if (latch.getCount() > 0) {
				logger.error("Never received ping response from Redis server.");
			}

			boolean redisServerWorking = false;
			String pingMessage = consumer.getMessage();
			if ("Pong".equals(pingMessage)) {
				logger.info("Redis server appears working. Average response time was "
						+ consumer.getAverageLatency() + " microseconds for " + totalPings + " pings.");
				logger.info("Average response time from others was "
						+ broker.getLatencyFromOthers() + " microseconds.");
				redisServerWorking = true;
			} else {
				logger.error("Redis server does not appear to be working.");
			}

			if (redisServerWorking)
				checkInternalPing(broker);

		} catch (Exception ex) {
			ex.printStackTrace();
		} finally {
			broker.stop();
		}
	}

	private static void checkInternalPing(RedisMessageBroker broker)
			throws Exception {
		CountDownLatch latch = new CountDownLatch(2);
		WatchPingMessages producer = new WatchPingMessages(latch);
		WatchPingMessages consumer = new WatchPingMessages(latch);
		broker.add(producer);
		broker.add(consumer);
		producer.ping("Sending out a ping message", System.currentTimeMillis());
		latch.await(105, TimeUnit.SECONDS);
		if (latch.getCount() > 0) {
			logger.error("Never received ping response internally.");
		} else {
			if (producer.getCount() > 1 || consumer.getCount() > 1) {
				logger.error("We are getting too many ping messages internally.");
				throw new Exception(
						"We are getting too many ping messages internally.");
			} else if (producer.getCount() == 1 && consumer.getCount() == 1) {
				logger.info("Internal broadcasting looks OK.");
				logger.info("Average response time from us was "
						+ broker.getLatencyFromUs() + " microseconds.");
			} else {
				logger.error("There's something wrong with the internal broadcasting.");
			}
		}

	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy