/** * Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.livedata.server; import java.util.Arrays; import java.util.Map; import java.util.regex.Pattern; import org.fudgemsg.FudgeField; import org.fudgemsg.FudgeMsg; import org.fudgemsg.MutableFudgeMsg; import org.fudgemsg.wire.types.FudgeWireType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.exceptions.JedisDataException; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.fudgemsg.OpenGammaFudgeContext; /** * A {@link LastKnownValueStore} backed by a Redis server. * In the case where this is not write-through, it primarily just acts * as a local cache which can be asynchronously updated from Redis * to retrieve the current values. * */ public class RedisLastKnownValueStore implements LastKnownValueStore { private static final Logger s_logger = LoggerFactory.getLogger(RedisLastKnownValueStore.class); private final FieldHistoryStore _inMemoryStore = new FieldHistoryStore(); private final JedisPool _jedisPool; //private final byte[] _jedisKey; private String _jedisKey; private final boolean _writeThrough; public RedisLastKnownValueStore(JedisPool jedisPool, String jedisKey, boolean writeThrough) { ArgumentChecker.notNull(jedisPool, "Jedis Pool"); ArgumentChecker.notNull(jedisKey, "Jedis key"); _jedisPool = jedisPool; /* try { _jedisKey = jedisKey.getBytes("UTF-8"); } catch (Exception e) { throw new OpenGammaRuntimeException("UTF-8 not a supported charset."); } */ _jedisKey = jedisKey; _writeThrough = writeThrough; updateFromRedis(true); } /** * Gets the jedisKey. * @return the jedisKey */ protected String getJedisKey() { return _jedisKey; } /** * Gets the jedisPool. * @return the jedisPool */ public JedisPool getJedisPool() { return _jedisPool; } /** * Gets the writeThrough. * @return the writeThrough */ public boolean isWriteThrough() { return _writeThrough; } // TODO kirk 2012-07-16 -- Actually implement asynchronous reading from Redis // TODO kirk 2012-07-16 -- Synchronization here is crazy restrictive. // Clearly could be done with a ReadWriteLock. @Override public synchronized void updateFields(FudgeMsg fieldValues) { // TODO kirk 2012-07-16 -- This is really only good enough as a proof of concept. // Ideally you'd want to handle more than just double-as-string ('cos really? That totally lame), // but I just want to get this working. if (isWriteThrough()) { Jedis jedis = getJedisPool().getResource(); try { for (FudgeField field : fieldValues.getAllFields()) { String redisValue = toRedisTextValue(getJedisKey(), field); if (redisValue == null) { // Signaling that we're discarding. Log message came out in toRedisTextValue. continue; } // Yep, this is ugly as hell. try { jedis.hset(getJedisKey(), field.getName(), redisValue); } catch (JedisDataException jde) { s_logger.warn("Unable to write stuff yo."); } } } catch (Exception e) { s_logger.error("Unable to write fields to Redis : " + _jedisKey, e); } finally { getJedisPool().returnResource(jedis); } } _inMemoryStore.liveDataReceived(fieldValues); } /** * Convert the contents of a {@link FudgeField} to the text that will be stored * in a Redis instance for LKV storage. * @param jedisKey key into Jedis, for debugging in log files only. * @param field the field's value to use; passed as a {@code FudgeField} for type inforamtion * @return the string to store in Redis. */ protected static String toRedisTextValue(String jedisKey, FudgeField field) { switch (field.getType().getTypeId()) { case FudgeWireType.DOUBLE_TYPE_ID: return field.getValue().toString(); case FudgeWireType.DOUBLE_ARRAY_TYPE_ID: return Arrays.toString((double[]) field.getValue()); case FudgeWireType.STRING_TYPE_ID: return (String) field.getValue(); default: s_logger.info("Redis encoding Key {} Field {} - can only handle double, double[], and String. Discarding.", jedisKey, field); return null; } } @Override public synchronized FudgeMsg getFields() { return _inMemoryStore.getLastKnownValues(); } @Override public synchronized boolean isEmpty() { return _inMemoryStore.isEmpty(); } /** * * @param failOnError Whether to propagate any exception from a failure to load. * This should be used to control whether this is resilient * in the case of Redis failures. */ public synchronized void updateFromRedis(boolean failOnError) { _inMemoryStore.clear(); Jedis jedis = getJedisPool().getResource(); try { // TODO kirk 2012-07-16 -- Give this a FudgeContext. MutableFudgeMsg fudgeMsg = OpenGammaFudgeContext.getInstance().newMessage(); Map<String, String> allFields = jedis.hgetAll(getJedisKey()); s_logger.debug("Updating {} from Jedis: {}", getJedisKey(), allFields); for (Map.Entry<String, String> fieldEntry : allFields.entrySet()) { Object parsedRedisObject = fromRedisTextValue(fieldEntry.getValue()); fudgeMsg.add(fieldEntry.getKey(), parsedRedisObject); } _inMemoryStore.liveDataReceived(fudgeMsg); } catch (Exception e) { s_logger.error("Unable to update from Redis", e); if (failOnError) { throw new OpenGammaRuntimeException("Unable to load state from underlying Redis instance on " + _jedisKey, e); } } finally { getJedisPool().returnResource(jedis); } } /** * This method is not an exemplar of proper software engineering. * For more information on the rationale, please see http://jira.opengamma.com/browse/PLAT-2536 . * <p/> * A few problems with this: * <ol> * <li>It's still not clear that storing all data as text is appropriate. See PLAT-2536 for commentary.</li> * <li>Our Redis storage (see {@link #toRedisTextValue(FudgeField)} does not have a type * prefix. Therefore, we're relying on the cascading parse attempts here. * This is clearly not optimal, and will cause us serious issues to try to support * all the various Fudge types.</li> * <li>For some reason Java provides you with a way to produce a standardized String representation * for {@code double[]}, but no way to parse that I could see. So yes, I implemented one.</li> * </ol> * @param value The text from Redis * @return a parsed object from that text */ protected static Object fromRedisTextValue(String value) { try { Double doubleValue = Double.parseDouble(value); return doubleValue.doubleValue(); } catch (Exception e) { // Not a double. Ignore. } if (value.startsWith("[") && value.endsWith("]")) { // Double array. // Is there a way to wipe my association with this code? I fear git blame in this case... try { String stripped = value.substring(1, value.length() - 1); String[] split = stripped.split(Pattern.quote(",")); double[] doubleValues = new double[split.length]; for (int i = 0; i < split.length; i++) { doubleValues[i] = Double.parseDouble(split[i]); } return doubleValues; } catch (Exception e) { // Not a double array. Ignore. s_logger.debug("String " + value + " looks like a double array but couldn't be parsed", e); } } return value; } }