package com.linkedin.databus.client.pub;
/*
*
* Copyright 2013 LinkedIn Corp. All rights reserved
*
* 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.
*
*/
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;
import com.linkedin.databus.core.Checkpoint;
import com.linkedin.databus.core.util.ConfigBuilder;
import com.linkedin.databus.core.util.InvalidConfigException;
import org.apache.helix.AccessOption;
import org.apache.helix.HelixException;
import org.apache.helix.HelixManager;
import org.apache.helix.HelixManagerFactory;
import org.apache.helix.InstanceType;
import org.apache.helix.ZNRecord;
import org.apache.helix.manager.zk.ZKHelixAdmin;
import org.apache.helix.manager.zk.ZNRecordSerializer;
import org.apache.helix.manager.zk.ZkClient;
import org.apache.helix.store.HelixPropertyStore;
public class ClusterCheckpointPersistenceProvider extends
CheckpointPersistenceProviderAbstract
{
protected static final Logger LOG = Logger
.getLogger(ClusterCheckpointPersistenceProvider.class);
//Internal key-name where the value of 'checkpoint' will be stored
protected static final String KEY_CHECKPOINT="c";
//Internal key-name where the value of 'sources' will be stored
protected static final String KEY_SOURCES="s";
private final String _id;
private HelixPropertyStore<ZNRecord> _propertyStore = null;
private final long _checkpointIntervalMs;
private long _numWritesSkipped = 0;
private long _lastTimeWrittenMs = 0;
private final static HelixConnectionManager _helixConnManager = new HelixConnectionManager();
public ClusterCheckpointPersistenceProvider(long id)
throws InvalidConfigException, ClusterCheckpointException
{
this(id, new Config());
}
public ClusterCheckpointPersistenceProvider(String id)
throws InvalidConfigException, ClusterCheckpointException
{
this(id, new Config().build());
}
public ClusterCheckpointPersistenceProvider(long id, Config config)
throws InvalidConfigException, ClusterCheckpointException
{
this(id, config.build());
}
public ClusterCheckpointPersistenceProvider(String id, Config config)
throws InvalidConfigException, ClusterCheckpointException
{
this(id, config.build());
}
public ClusterCheckpointPersistenceProvider(long id, StaticConfig config)
throws InvalidConfigException, ClusterCheckpointException
{
this(Long.toString(id), config);
}
/**
* Create checkpoint persistence object for a partition.
*
* @param id
* : partition id
* @param config
* : specifies zookeeper server ,cluster name, minimum frequency
* of writing checkpoints in ms
* @throws ClusterCheckpointException
* on errors like absence of a cluster; use static method
* createCluster to create a cluster
*/
public ClusterCheckpointPersistenceProvider(String id, StaticConfig config)
throws ClusterCheckpointException
{
_id = id;
_checkpointIntervalMs = config.getCheckpointIntervalMs();
try
{
HelixManager manager = _helixConnManager.open(
config.getClusterName(), config.getZkAddr(), id);
_propertyStore = manager.getHelixPropertyStore();
}
catch (Exception e)
{
LOG.error("Error creating Helix Manager! for cluster="
+ config.getClusterName() + " id=" + _id + " exception="
+ e);
throw new ClusterCheckpointException(e.toString());
}
}
/**
* Create a cluster if it doesn't exist Note: This method is not thread-safe
* as HelixAdmin.addCluster appears to fail on concurrent execution within
* threads
*
* @return true if cluster was created false otherwise
*/
static public boolean createCluster(String zkAddr, String clusterName)
{
boolean created = false;
ZkClient zkClient = null;
try
{
zkClient = new ZkClient(zkAddr,
ZkClient.DEFAULT_SESSION_TIMEOUT,
ZkClient.DEFAULT_CONNECTION_TIMEOUT, new ZNRecordSerializer());
ZKHelixAdmin admin = new ZKHelixAdmin(zkClient);
admin.addCluster(clusterName, false);
created=true;
}
catch (HelixException e)
{
LOG.warn("Warn! Cluster might already exist! " + clusterName);
created=false;
}
finally
{
// close this connection
if (zkClient != null)
{
zkClient.close();
}
}
return created;
}
/**
* Call close() when the cluster is shutdown to explicitly tear down
* connections to corresponding helix manager
*/
static public void close(String clusterName)
{
if (_helixConnManager != null)
{
_helixConnManager.close(clusterName);
}
}
/**
* new format of key; prepended "/" . construction with truncated source if possible e.g.
* e.g. input=srcs=com.linkedin.events.conns.Connections,com.linkedin.events.conns.ConnectionsCnt and _id=5
* output: /5_Connections_ConnectionsCnt
*
* @param srcs
* @return flat String source separated by _
*/
protected String makeKey(List<String> srcs)
{
StringBuilder k = new StringBuilder(50);
k.append("/");
k.append(_id);
for (String s : srcs)
{
k.append("_");
String[] list = s.split("\\.");
k.append(list[list.length - 1]);
}
return k.toString();
}
/**
* Deprecated
* old style key construction without '/' prepended
*
* @param srcs
* @return flat String source separated by _
*/
protected String makeKeyOld(List<String> srcs)
{
StringBuilder k = new StringBuilder(50);
k.append(_id);
for (String s : srcs)
{
k.append("_");
k.append(s);
}
return k.toString();
}
@Deprecated
/**
*
* @param sourceNames
* @param checkpoint
* Persist a checkpoint in legacy location
* @throws IOException
*/
public void storeCheckpointLegacy(List<String> sourceNames,
Checkpoint checkpoint) throws IOException
{
if (_propertyStore != null)
{
long curtimeMs = System.currentTimeMillis();
if ((curtimeMs - _lastTimeWrittenMs) > _checkpointIntervalMs)
{
String key = makeKeyOld(sourceNames);
ZNRecord znRecord = new ZNRecord(_id);
znRecord.setSimpleField(KEY_CHECKPOINT, checkpoint.toString());
_propertyStore.set(key, znRecord, AccessOption.PERSISTENT);
_lastTimeWrittenMs = curtimeMs;
_numWritesSkipped = 0;
}
else
{
_numWritesSkipped++;
}
}
}
/**
* Called by databus client library to persist checkpoint
*/
@Override
public void storeCheckpoint(List<String> sourceNames, Checkpoint checkpoint)
throws IOException
{
if (_propertyStore != null)
{
long curtimeMs = System.currentTimeMillis();
if ((curtimeMs - _lastTimeWrittenMs) > _checkpointIntervalMs)
{
storeZkRecord(sourceNames, checkpoint);
_lastTimeWrittenMs = curtimeMs;
_numWritesSkipped = 0;
}
else
{
_numWritesSkipped++;
}
}
}
/**
* @note This method is protected only for tests. Do NOT use it outside of this file.
* @param sourceNames List of source names
* @param checkpoint Checkpoint object to be stored in zookeeper.
*/
protected void storeZkRecord(List<String> sourceNames, Checkpoint checkpoint)
{
String key = makeKey(sourceNames);
ZNRecord znRecord = new ZNRecord(_id);
znRecord.setSimpleField(KEY_CHECKPOINT, checkpoint.toString());
znRecord.setSimpleField(KEY_SOURCES,
StringUtils.join(sourceNames.toArray(), ","));
_propertyStore.set(key, znRecord, AccessOption.PERSISTENT);
}
@Deprecated
/**
* read legacy checkpoint without migration
*
* @param sources
* @return checkpoint if found or null otherwise
*/
public Checkpoint loadCheckpointLegacy(List<String> sources)
{
String key = makeKeyOld(sources);
Checkpoint cp = getCheckpoint(key);
return cp;
}
/**
* internal function that fetches contents from Helix property store
*
* @param key
* @return checkpoint or null
*/
private Checkpoint getCheckpoint(String key)
{
ZNRecord zn = _propertyStore.get(key, null, AccessOption.PERSISTENT);
if (zn != null)
{
String v = zn.getSimpleField(KEY_CHECKPOINT);
try
{
Checkpoint cp;
cp = new Checkpoint(v);
return cp;
}
catch (JsonParseException e)
{
LOG.error("Cannot deserialize value for key=" + key + " value="
+ v + " exception=" + e);
}
catch (JsonMappingException e)
{
LOG.error("Cannot deserialize value for key=" + key + " value="
+ v + " exception=" + e);
}
catch (IOException e)
{
LOG.error("Cannot deserialize value for key=" + key + " value="
+ v + " exception=" + e);
}
}
else
{
LOG.error("No record for key = " + key);
}
return null;
}
/**
* Function called by Databus Client to load checkpoint;
*/
@Override
public Checkpoint loadCheckpoint(List<String> sourceNames)
{
if (_propertyStore != null)
{
String key = makeKey(sourceNames);
Checkpoint cp = getCheckpoint(key);
return cp;
}
return null;
}
/**
* Find unique sourceNames of checkpoints across all partitions found in the
* cluster
*
* @return set of unique sources
*/
public Set<String> getSourceNames()
{
if (_propertyStore != null)
{
// note that "/" is the root - it's prepended in 'makeKey'
List<String> keys = _propertyStore.getChildNames("/",
AccessOption.PERSISTENT);
if (keys != null)
{
HashSet<String> sources = new HashSet<String>();
for (String k : keys)
{
// add the "/" again
ZNRecord zn = _propertyStore.get("/" + k, null,
AccessOption.PERSISTENT);
if (zn != null)
{
String srcName = zn.getSimpleField(KEY_SOURCES);
if (srcName != null)
{
sources.add(srcName);
}
}
}
return sources;
}
}
return null;
}
private void removeCheckpoint(String key)
{
_propertyStore.remove(key, AccessOption.PERSISTENT);
}
public void removeCheckpointLegacy(List<String> sourceNames)
{
String keyOld = makeKeyOld(sourceNames);
removeCheckpoint(keyOld);
}
@Override
public void removeCheckpoint(List<String> sourceNames)
{
if (_propertyStore != null)
{
String key = makeKey(sourceNames);
removeCheckpoint(key);
}
}
public long getNumWritesSkipped()
{
return _numWritesSkipped;
}
@SuppressWarnings("serial")
public static class ClusterCheckpointException extends Exception
{
public ClusterCheckpointException(String msg)
{
super(msg);
}
}
/**
*
* Reuse connections to helix manager across partitions;
* one per cluster
*/
private static class HelixConnectionManager
{
final private Map<String, HelixManager> _managers;
public HelixConnectionManager()
{
_managers = new HashMap<String, HelixManager>();
}
/**
* given a cluster; return a helix connection if one exists; otherwise
* create one
*
* @param cluster
*/
public synchronized HelixManager open(String clusterName,
String zkAddr, String id) throws Exception
{
HelixManager m = _managers.get(clusterName);
if (null == m)
{
m = HelixManagerFactory.getZKHelixManager(clusterName, id,
InstanceType.SPECTATOR, zkAddr);
_managers.put(clusterName, m);
if (!m.isConnected())
{
m.connect();
}
}
return m;
}
/**
* shutdown managers explicitly
*/
public synchronized void close(String cluster)
{
HelixManager m = _managers.get(cluster);
if (m != null)
{
_managers.remove(cluster);
m.disconnect();
}
}
}
public static class StaticConfig
{
private final String _zkAddr;
private final String _clusterName;
private final long _checkpointIntervalMs;
private final int _maxNumWritesSkipped;
public StaticConfig(String zkAddr, String clusterName,
int numWritesSkipped, long checkpointIntervalMs)
{
_zkAddr = zkAddr;
_clusterName = clusterName;
_maxNumWritesSkipped = numWritesSkipped;
_checkpointIntervalMs = checkpointIntervalMs;
}
public String getZkAddr()
{
return _zkAddr;
}
public String getClusterName()
{
return _clusterName;
}
@Deprecated
public int getMaxNumWritesSkipped()
{
return _maxNumWritesSkipped;
}
public long getCheckpointIntervalMs()
{
return _checkpointIntervalMs;
}
}
public static class Config implements ConfigBuilder<StaticConfig>
{
private String _zkAddr = null;
private String _clusterName = null;
private long _checkpointIntervalMs = 5 * 60 * 1000; // 5 minutes
/** Deprecated - conf setting has no effect **/
private int _maxNumWritesSkipped = 0;
public Config()
{
}
@Override
public StaticConfig build() throws InvalidConfigException
{
if (_zkAddr == null || _clusterName == null)
{
throw new InvalidConfigException(
"zkAddr or clusterName cannot be unspecified ");
}
return new StaticConfig(_zkAddr, _clusterName,
_maxNumWritesSkipped, _checkpointIntervalMs);
}
public String getZkAddr()
{
return _zkAddr;
}
public void setZkAddr(String zkAddr)
{
_zkAddr = zkAddr;
}
public String getClusterName()
{
return _clusterName;
}
public void setClusterName(String clusterName)
{
_clusterName = clusterName;
}
@Deprecated
/** Deprecated - conf setting has no effect **/
public int getMaxNumWritesSkipped()
{
return _maxNumWritesSkipped;
}
@Deprecated
/** Deprecated - conf setting has no effect **/
public void setMaxNumWritesSkipped(int maxNumWritesSkipped)
{
_maxNumWritesSkipped = maxNumWritesSkipped;
}
public void setCheckpointIntervalMs(long checkpointIntervalMs)
{
_checkpointIntervalMs = checkpointIntervalMs;
}
public long getCheckpointIntervalMs()
{
return _checkpointIntervalMs;
}
}
}