Java tutorial
/* * Copyright 2012 Janrain, 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.janrain.backplane2.server; import com.janrain.backplane.DateTimeUtils; import com.janrain.backplane2.server.config.Backplane2Config; import com.janrain.backplane2.server.dao.DAOFactory; import com.janrain.backplane2.server.dao.redis.RedisBackplaneMessageDAO; import com.janrain.commons.util.Pair; import com.janrain.redis.Redis; import com.janrain.utils.BackplaneSystemProps; import com.netflix.curator.framework.CuratorFramework; import com.netflix.curator.framework.recipes.leader.LeaderSelectorListener; import com.netflix.curator.framework.state.ConnectionState; import com.yammer.metrics.Metrics; import com.yammer.metrics.core.Histogram; import com.yammer.metrics.core.MetricName; import org.apache.commons.lang.SerializationUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** * @author Tom Raney */ public class V2MessageProcessor implements LeaderSelectorListener { // - PUBLIC public V2MessageProcessor(Backplane2Config backplane2Config, final DAOFactory daoFactory) { this.config = backplane2Config; cleanupRunnable = new Runnable() { @Override public void run() { try { daoFactory.getBackplaneMessageDAO().deleteExpiredMessages(); } catch (Exception e) { logger.warn(e); } } }; } @Override public void takeLeadership(CuratorFramework curatorFramework) throws Exception { setLeader(true); logger.info("[" + BackplaneSystemProps.getMachineName() + "] v2 leader elected for message processing"); ScheduledFuture<?> cleanupTask = scheduledExecutor.scheduleAtFixedRate(cleanupRunnable, 1, 2, TimeUnit.HOURS); insertMessages(); cleanupTask.cancel(false); logger.info("[" + BackplaneSystemProps.getMachineName() + "] v2 leader ended message processing"); } @Override public void stateChanged(CuratorFramework curatorFramework, ConnectionState connectionState) { logger.info("v2 leader selector state changed to " + connectionState); if (isLeader() && (ConnectionState.LOST == connectionState || ConnectionState.SUSPENDED == connectionState)) { setLeader(false); logger.info("v2 leader lost connection, giving up leadership"); } } // - PRIVATE private static final Logger logger = Logger.getLogger(V2MessageProcessor.class); private static final String V2_LAST_ID = "v2_last_id"; private static ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1); static { Backplane2Config.addToBackgroundServices(scheduledExecutor); } private final Runnable cleanupRunnable; private final Histogram timeInQueue = Metrics .newHistogram(new MetricName("v2", this.getClass().getName().replace(".", "_"), "time_in_queue")); private final Backplane2Config config; private synchronized void setLeader(boolean leader) { this.leader = leader; } private synchronized boolean isLeader() { return leader && !config.isLeaderDisabled(); } private boolean leader = false; /** * Processor to pull messages off queue and make them available * */ private void insertMessages() { try { logger.info("v2 message processor started"); while (isLeader()) { try { processSingleBatchOfPendingMessages(); Thread.sleep(150); } catch (Exception e) { logger.warn(e); try { Thread.sleep(2000); } catch (InterruptedException e1) { // ignore } } } } finally { // very bad if we get here... logger.error("method exited but it should NEVER do so"); } } private void processSingleBatchOfPendingMessages() throws Exception { Jedis jedis = null; try { jedis = Redis.getInstance().getWriteJedis(); List<String> insertionTimes = new ArrayList<String>(); // set watch on V1_LAST_ID // needs to be set before retrieving the value stored at this key jedis.watch(V2_LAST_ID); Pair<String, Date> lastIdAndDate = getLastMessageId(jedis); String newId = lastIdAndDate.getLeft(); // retrieve a handful of messages (ten) off the queue for processing List<byte[]> messagesToProcess = jedis.lrange(RedisBackplaneMessageDAO.V2_MESSAGE_QUEUE.getBytes(), 0, 9); // only enter the next block if we have messages to process if (messagesToProcess.size() > 0) { Transaction transaction = jedis.multi(); insertionTimes.clear(); // <ATOMIC> - redis transaction for (byte[] messageBytes : messagesToProcess) { if (messageBytes != null) { BackplaneMessage backplaneMessage = (BackplaneMessage) SerializationUtils .deserialize(messageBytes); if (backplaneMessage != null) { newId = processSingleMessage(backplaneMessage, transaction, insertionTimes, lastIdAndDate); } } } transaction.set(V2_LAST_ID, newId); logger.info("processing transaction with " + insertionTimes.size() + " v2 message(s)"); List<Object> results = transaction.exec(); if (results == null || results.size() == 0) { logger.warn("transaction failed! - halting work for now"); return; } // </ATOMIC> - redis transaction logger.info("flushed " + insertionTimes.size() + " v2 messages"); long now = System.currentTimeMillis(); for (String insertionId : insertionTimes) { long diff = now - com.janrain.backplane.server.BackplaneMessage.getDateFromId(insertionId).getTime(); timeInQueue.update(diff); if (diff < 0 || diff > 2880000) { logger.warn("time diff is bizarre at: " + diff); } } } } catch (Exception e) { // if we get here, something bonked, like a connection to the redis server logger.warn("an error occurred while trying to process v2 message batch: " + e.getMessage()); Redis.getInstance().releaseBrokenResourceToPool(jedis); jedis = null; throw e; } finally { try { if (jedis != null) { jedis.unwatch(); Redis.getInstance().releaseToPool(jedis); } } catch (Exception e) { // ignore } } } private Pair<String, Date> getLastMessageId(Jedis jedis) { // retrieve the latest 'live' message ID String latestMessageId = jedis.get(V2_LAST_ID); Date dateFromId = BackplaneMessage.getDateFromId(latestMessageId); return StringUtils.isEmpty(latestMessageId) || null == dateFromId ? getLastMessageIdLegacy(jedis) : new Pair<String, Date>(latestMessageId, dateFromId); } private Pair<String, Date> getLastMessageIdLegacy(Jedis jedis) { // retrieve the latest 'live' message ID // old/legacy method, used as fallback with the deployment of the replacement method // todo: remove after transition is completed String latestMessageId = null; Set<String> latestMessageMetaSet = jedis.zrange(RedisBackplaneMessageDAO.V2_MESSAGES, -1, -1); if (latestMessageMetaSet != null && !latestMessageMetaSet.isEmpty()) { String[] segs = latestMessageMetaSet.iterator().next().split(" "); if (segs.length == 3) { latestMessageId = segs[2]; } } Pair<String, Date> lastIdAndDate = new Pair<String, Date>("", new Date(0)); try { lastIdAndDate = StringUtils.isEmpty(latestMessageId) ? new Pair<String, Date>("", new Date(0)) : new Pair<String, Date>(latestMessageId, DateTimeUtils.ISO8601.get() .parse(latestMessageId.substring(0, latestMessageId.indexOf("Z") + 1))); } catch (Exception e) { logger.warn("error retrieving last message ID and date from V1_MESSAGES: " + e.getMessage(), e); lastIdAndDate = new Pair<String, Date>("", new Date(0)); } return lastIdAndDate; } private String processSingleMessage(BackplaneMessage backplaneMessage, Transaction transaction, List<String> insertionTimes, Pair<String, Date> lastIdAndDate) throws Exception { try { String oldId = backplaneMessage.getIdValue(); insertionTimes.add(oldId); // TOTAL ORDER GUARANTEE // verify that the date portion of the new message ID is greater than all existing message ID dates // if not, uptick id by 1 ms and insert // this means that all message ids have unique time stamps, even if they // arrived at the same time. backplaneMessage.updateId(lastIdAndDate); String newId = backplaneMessage.getIdValue(); // messageTime is guaranteed to be a unique identifier of the message // because of the TOTAL ORDER mechanism above long messageTime = BackplaneMessage.getDateFromId(newId).getTime(); // <ATOMIC> // save the individual message by key & TTL transaction.setex(RedisBackplaneMessageDAO.getKey(newId), DateTimeUtils.getExpireSeconds(backplaneMessage.getIdValue(), backplaneMessage.get(BackplaneMessage.Field.EXPIRE), backplaneMessage.isSticky()), SerializationUtils.serialize(backplaneMessage)); // channel and bus sorted set index transaction.zadd(RedisBackplaneMessageDAO.getChannelKey(backplaneMessage.getChannel()), messageTime, backplaneMessage.getIdValue().getBytes()); transaction.zadd(RedisBackplaneMessageDAO.getBusKey(backplaneMessage.getBus()), messageTime, backplaneMessage.getIdValue().getBytes()); // add message id to sorted set of all message ids as an index String metaData = backplaneMessage.getBus() + " " + backplaneMessage.getChannel() + " " + backplaneMessage.getIdValue(); transaction.zadd(RedisBackplaneMessageDAO.V2_MESSAGES.getBytes(), messageTime, metaData.getBytes()); // add message id to sorted set keyed by bus as an index // make sure all subscribers get the update transaction.publish("alerts", newId); // pop one message off the queue - which will only happen if this transaction is successful transaction.lpop(RedisBackplaneMessageDAO.V2_MESSAGE_QUEUE); // </ATOMIC> logger.info("pipelined v2 message " + oldId + " -> " + newId); return newId; } catch (Exception e) { throw e; } } }