/*
* Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
package org.opendaylight.tsdr.persistence.cassandra;
import com.datastax.driver.core.BatchStatement;
import com.datastax.driver.core.Cluster;
import com.datastax.driver.core.RegularStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.SimpleStatement;
import com.datastax.driver.core.exceptions.InvalidQueryException;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.opendaylight.tsdr.spi.util.FormatUtil;
import org.opendaylight.tsdr.spi.util.TSDRKeyCache;
import org.opendaylight.tsdr.spi.util.TSDRKeyCache.TSDRCacheEntry;
import org.opendaylight.yang.gen.v1.opendaylight.tsdr.binary.data.rev160325.storetsdrbinaryrecord.input.TSDRBinaryRecord;
import org.opendaylight.yang.gen.v1.opendaylight.tsdr.binary.data.rev160325.storetsdrbinaryrecord.input.TSDRBinaryRecordBuilder;
import org.opendaylight.yang.gen.v1.opendaylight.tsdr.log.data.rev160325.storetsdrlogrecord.input.TSDRLogRecord;
import org.opendaylight.yang.gen.v1.opendaylight.tsdr.log.data.rev160325.storetsdrlogrecord.input.TSDRLogRecordBuilder;
import org.opendaylight.yang.gen.v1.opendaylight.tsdr.log.data.rev160325.tsdrlog.RecordAttributes;
import org.opendaylight.yang.gen.v1.opendaylight.tsdr.metric.data.rev160325.storetsdrmetricrecord.input.TSDRMetricRecord;
import org.opendaylight.yang.gen.v1.opendaylight.tsdr.metric.data.rev160325.storetsdrmetricrecord.input.TSDRMetricRecordBuilder;
import org.opendaylight.yang.gen.v1.opendaylight.tsdr.rev150219.DataCategory;
import org.opendaylight.yang.gen.v1.opendaylight.tsdr.rev150219.tsdrrecord.RecordKeys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Sharon Aicler(saichler@gmail.com)
**/
public class CassandraStore {
private static final int MAX_BATCH_SIZE = 500;
private static final String confFile = "./etc/tsdr-persistence-cassandra.properties";
private Session session = null;
private Cluster cluster = null;
private boolean isMaster = true;
private String host = null;
private int replication_factor = 1;
private Logger log = LoggerFactory.getLogger(CassandraStore.class);
private TSDRKeyCache cache = new TSDRKeyCache();
private BatchStatement batch = null;
public CassandraStore(){
log.info("Connecting to Cassandra...");
try {
getSession();
} catch (Exception e) {
log.error("Failed to connect to Cassandra",e);
}
}
public CassandraStore(Session s,Cluster c){
log.info("Connecting to Cassandra...");
this.session = s;
this.cluster = c;
try {
getSession();
} catch (Exception e) {
log.error("Failed to connect to Cassandra",e);
}
}
private Map<String,String> loadConfig() throws IOException{
HashMap<String, String> result = new HashMap<String,String>();
BufferedReader in = null;
in = new BufferedReader(new InputStreamReader(new FileInputStream(confFile)));
String line = in.readLine();
while(line!=null){
int index = line.indexOf("=");
String key = line.substring(0,index).trim();
String value = line.substring(index+1).trim();
result.put(key, value);
line = in.readLine();
}
in.close();
return result;
}
public BatchStatement getBatch(){
return this.batch;
}
public Session getSession() throws Exception {
if (session == null) {
synchronized (this) {
Map<String,String> config = loadConfig();
this.host = config.get("host");
this.isMaster = Boolean.parseBoolean(config.get("master"));
this.replication_factor = Integer.parseInt(config.get("replication_factor"));
log.info("Trying to work with " + this.host+ ", Which cassandra master is set to=" + this.isMaster);
Cluster cluster = Cluster.builder().addContactPoint(host).build();
// Try 5 times to connect to cassandra with a 5 seconds delay
// between each try
for (int index = 0; index < 5; index++) {
try {
session = cluster.connect("tsdr");
return session;
} catch (InvalidQueryException err) {
try {
log.info("Failed to get tsdr keyspace...");
if (this.isMaster) {
log.info("This is the main node, trying to create keyspace and tables...");
session = cluster.connect();
session.execute("CREATE KEYSPACE tsdr WITH replication "
+ "= {'class':'SimpleStrategy', 'replication_factor':"+replication_factor+"};");
session = cluster.connect("tsdr");
createTSDRTables();
return session;
}
} catch (Exception err2) {
log.error("Failed to create keyspace & tables, will retry in 5 seconds...",err2);
}
}
log.info("Sleeping for 5 seconds...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
log.error("Interrupted",e);
}
}
}
}
return session;
}
public void createTSDRTables(){
String cql = "CREATE TABLE MetricVal ("+
"KeyA bigint, "+
"KeyB bigint, "+
"Time bigint, "+
"value double,"+
"PRIMARY KEY (KeyA,KeyB,Time))";
this.session.execute(cql);
cql = "CREATE TABLE MetricLog ("+
"KeyA bigint, "+
"KeyB bigint, "+
"Time bigint, "+
"xIndex int,"+
"value text,"+
"PRIMARY KEY (KeyA,KeyB,Time,xIndex))";
this.session.execute(cql);
cql = "CREATE TABLE MetricBlob ("+
"KeyA bigint, "+
"KeyB bigint, "+
"Time bigint, "+
"xIndex int,"+
"value blob,"+
"PRIMARY KEY (KeyA,KeyB,Time,xIndex))";
this.session.execute(cql);
}
public void startBatch(){
this.batch = new BatchStatement();
}
public void executeBatch(){
try {
this.session.execute(this.batch);
}catch(Exception err){
log.error("Failed to run batch",err);
}
}
public void store(TSDRMetricRecord mr){
//create metric key
String tsdrKey = FormatUtil.getTSDRMetricKey(mr);
TSDRCacheEntry cacheEntry = cache.getCacheEntry(tsdrKey);
//if it does not exist, create it
if(cacheEntry==null){
cacheEntry = cache.addTSDRCacheEntry(tsdrKey);
}
RegularStatement st = QueryBuilder.insertInto("tsdr","MetricVal").
value("KeyA",cacheEntry.getMd5ID().getMd5Long1()).
value("KeyB",cacheEntry.getMd5ID().getMd5Long2()).
value("Time",mr.getTimeStamp()).
value("value",mr.getMetricValue().doubleValue());
this.batch.add(st);
if(this.batch.size()>=MAX_BATCH_SIZE){
this.executeBatch();
this.startBatch();
}
}
public void store(TSDRLogRecord lr){
//create log key
String tsdrKey = FormatUtil.getTSDRLogKey(lr);
TSDRCacheEntry cacheEntry = cache.getCacheEntry(tsdrKey);
//if it does not exist, create it
if(cacheEntry==null){
cacheEntry = cache.addTSDRCacheEntry(tsdrKey);
}
RegularStatement st = QueryBuilder.insertInto("tsdr","MetricLog").
value("KeyA",cacheEntry.getMd5ID().getMd5Long1()).
value("KeyB",cacheEntry.getMd5ID().getMd5Long2()).
value("Time",lr.getTimeStamp()).
value("xIndex",lr.getIndex()).
value("value",lr.getRecordFullText());
this.batch.add(st);
if(this.batch.size()>=MAX_BATCH_SIZE){
this.executeBatch();
this.startBatch();
}
}
public void store(TSDRBinaryRecord lr){
//create log key
String tsdrKey = FormatUtil.getTSDRBinaryKey(lr);
TSDRCacheEntry cacheEntry = cache.getCacheEntry(tsdrKey);
//if it does not exist, create it
if(cacheEntry==null){
cacheEntry = cache.addTSDRCacheEntry(tsdrKey);
}
RegularStatement st = QueryBuilder.insertInto("tsdr","MetricBlob").
value("KeyA",cacheEntry.getMd5ID().getMd5Long1()).
value("KeyB",cacheEntry.getMd5ID().getMd5Long2()).
value("Time",lr.getTimeStamp()).
value("xIndex",lr.getIndex()).
value("value",lr.getData());
this.batch.add(st);
if(this.batch.size()>=MAX_BATCH_SIZE){
this.executeBatch();
this.startBatch();
}
}
public List<TSDRMetricRecord> getTSDRMetricRecords(String tsdrMetricKey, long startDateTime, long endDateTime,int recordLimit) {
TSDRCacheEntry entry = this.cache.getCacheEntry(tsdrMetricKey);
//Exact match was found
if(entry!=null){
final List<TSDRMetricRecord> result = new LinkedList<TSDRMetricRecord>();
String cql = "select * from MetricVal where KeyA=" + entry.getMd5ID().getMd5Long1()
+ " and KeyB=" + entry.getMd5ID().getMd5Long2() + " and Time>=" + startDateTime
+ " and Time<=" + endDateTime + " limit "+recordLimit;
ResultSet rs = session.execute(cql);
for (Row r : rs.all()) {
result.add(getTSDRMetricRecord(r.getLong("Time"), r.getDouble("value"), entry));
}
return result;
}else{
final TSDRKeyCache.TSDRMetricCollectJob job = new TSDRKeyCache.TSDRMetricCollectJob() {
@Override
public void collectMetricRecords(TSDRCacheEntry entry, long startDateTime, long endDateTime, int recordLimit, List<TSDRMetricRecord> globalResult) {
String cql = "select * from MetricVal where KeyA=" + entry.getMd5ID().getMd5Long1()
+ " and KeyB=" + entry.getMd5ID().getMd5Long2() + " and Time>=" + startDateTime
+ " and Time<=" + endDateTime + " limit "+(recordLimit-globalResult.size());
ResultSet rs = session.execute(cql);
for (Row r : rs.all()) {
globalResult.add(getTSDRMetricRecord(r.getLong("Time"), r.getDouble("value"), entry));
}
}
};
return this.cache.getTSDRMetricRecords(tsdrMetricKey,startDateTime,endDateTime,recordLimit,job);
}
}
public List<TSDRLogRecord> getTSDRLogRecords(String tsdrLogKey, long startDateTime, long endDateTime, int recordLimit) {
TSDRCacheEntry entry = this.cache.getCacheEntry(tsdrLogKey);
//Exact match was found
if(entry!=null){
final List<TSDRLogRecord> result = new LinkedList<TSDRLogRecord>();
String cql = "select * from MetricLog where KeyA=" + entry.getMd5ID().getMd5Long1()
+ " and KeyB=" + entry.getMd5ID().getMd5Long2() + " and Time>="
+ startDateTime + " and Time<=" + endDateTime + " limit "+recordLimit;
ResultSet rs = session.execute(cql);
for (Row r : rs.all()) {
result.add(getTSDRLogRecord(r.getLong("Time"), r.getString("value"), r.getInt("xIndex"), entry));
}
return result;
}else{
TSDRKeyCache.TSDRLogCollectJob job = new TSDRKeyCache.TSDRLogCollectJob() {
@Override
public void collectLogRecords(TSDRCacheEntry entry, long startDateTime, long endDateTime, int recordLimit, List<TSDRLogRecord> globalResult) {
String cql = "select * from MetricLog where KeyA=" + entry.getMd5ID().getMd5Long1()
+ " and KeyB=" + entry.getMd5ID().getMd5Long2() + " and Time>="
+ startDateTime + " and Time<=" + endDateTime + " limit "+(recordLimit-globalResult.size());
ResultSet rs = session.execute(cql);
for (Row r : rs.all()) {
globalResult.add(getTSDRLogRecord(r.getLong("Time"), r.getString("value"), r.getInt("xIndex"), entry));
}
}
};
return this.cache.getTSDRLogRecords(tsdrLogKey,startDateTime,endDateTime,recordLimit,job);
}
}
public List<TSDRBinaryRecord> getTSDRBinaryRecords(String tsdrBinaryKey, long startDateTime, long endDateTime, int recordLimit) {
TSDRCacheEntry entry = this.cache.getCacheEntry(tsdrBinaryKey);
//Exact match was found
if(entry!=null){
final List<TSDRBinaryRecord> result = new LinkedList<TSDRBinaryRecord>();
String cql = "select * from MetricBlob where KeyA=" + entry.getMd5ID().getMd5Long1()
+ " and KeyB=" + entry.getMd5ID().getMd5Long2() + " and Time>="
+ startDateTime + " and Time<=" + endDateTime + " limit "+recordLimit;
ResultSet rs = session.execute(cql);
for (Row r : rs.all()) {
result.add(getTSDRBinaryRecord(r.getLong("Time"), r.getBytes("value").array(), r.getInt("xIndex"), entry));
}
return result;
}else{
TSDRKeyCache.TSDRBinaryCollectJob job = new TSDRKeyCache.TSDRBinaryCollectJob() {
@Override
public void collectBinaryRecords(TSDRCacheEntry entry, long startDateTime, long endDateTime, int recordLimit, List<TSDRBinaryRecord> globalResult) {
String cql = "select * from MetricBlob where KeyA=" + entry.getMd5ID().getMd5Long1()
+ " and KeyB=" + entry.getMd5ID().getMd5Long2() + " and Time>="
+ startDateTime + " and Time<=" + endDateTime + " limit "+(recordLimit-globalResult.size());
ResultSet rs = session.execute(cql);
for (Row r : rs.all()) {
globalResult.add(getTSDRBinaryRecord(r.getLong("Time"), r.getBytes("value").array(), r.getInt("xIndex"), entry));
}
}
};
return this.cache.getTSDRBinaryRecords(tsdrBinaryKey,startDateTime,endDateTime,recordLimit,job);
}
}
private static final List<RecordKeys> EMPTY_RECORD_KEYS = new ArrayList<>();
private static final List<RecordAttributes> EMPTY_RECORD_ATTRIBUTES = new ArrayList<>();
private static final TSDRMetricRecord getTSDRMetricRecord(long time, double value, TSDRCacheEntry entry){
TSDRMetricRecordBuilder rb = new TSDRMetricRecordBuilder();
rb.setMetricName(entry.getMetricName());
rb.setMetricValue(new BigDecimal(value));
rb.setNodeID(entry.getNodeID());
rb.setRecordKeys(FormatUtil.getRecordKeysFromTSDRKey(entry.getTsdrKey()));
rb.setTimeStamp(time);
rb.setTSDRDataCategory(entry.getDataCategory());
return rb.build();
}
private static final TSDRLogRecord getTSDRLogRecord(long time, String value, int index, TSDRCacheEntry entry){
TSDRLogRecordBuilder lb = new TSDRLogRecordBuilder();
lb.setTSDRDataCategory(entry.getDataCategory());
lb.setTimeStamp(time);
lb.setRecordKeys(FormatUtil.getRecordKeysFromTSDRKey(entry.getTsdrKey()));
lb.setNodeID(entry.getNodeID());
lb.setIndex(index);
lb.setRecordAttributes(null);
lb.setRecordFullText(value);
return lb.build();
}
private static final TSDRBinaryRecord getTSDRBinaryRecord(long time, byte[] value, int index, TSDRCacheEntry entry){
TSDRBinaryRecordBuilder lb = new TSDRBinaryRecordBuilder();
lb.setTSDRDataCategory(entry.getDataCategory());
lb.setTimeStamp(time);
lb.setRecordKeys(FormatUtil.getRecordKeysFromTSDRKey(entry.getTsdrKey()));
lb.setNodeID(entry.getNodeID());
lb.setIndex(index);
lb.setRecordAttributes(null);
lb.setData(value);
return lb.build();
}
public void shutdown(){
if(this.session!=null){
try{this.session.close();}catch(Exception err){log.error("Failed to close the cassandra session",err);}
}
this.cache.shutdown();
}
public void purge(DataCategory category, long retentionTime){
purgeMetrics(category,retentionTime);
purgeLogs(category,retentionTime);
purgeBinary(category,retentionTime);
}
private void purgeMetrics(DataCategory category, long retentionTime){
//Cassandra does not support range delete prior to verison 3.
//To overcome, we need to do a "select" and then batch delete one by one.
String cql1 = "Select * from MetricVal where keyA = ";
String dcql1 = "delete from MetricVal where keyA = ";
String cql2 = " and keyB = ";
String cql3 = " and time < "+retentionTime;
String dcql3 = " and time =";
this.startBatch();
for(TSDRCacheEntry entry:this.cache.getAll()){
if(entry.getDataCategory()==category){
String cql = cql1 + entry.getMd5ID().getMd5Long1()+cql2+entry.getMd5ID().getMd5Long2()+cql3;
final ResultSet rs = session.execute(cql);
for(Row row:rs.all()){
String deleteCql = dcql1+row.getLong("keyA")+cql2+row.getLong("keyB")+dcql3+row.getLong("time");
batch.add(new SimpleStatement(deleteCql));
if(this.batch.size()>=MAX_BATCH_SIZE){
this.executeBatch();
this.startBatch();
}
}
}
}
if(this.batch.size()>0) {
this.executeBatch();
this.startBatch();
}
}
private void purgeLogs(DataCategory category, long retentionTime){
//Cassandra does not support range delete prior to verison 3.
//To overcome, we need to do a "select" and then batch delete one by one.
String cql1 = "Select * from MetricLog where keyA = ";
String dcql1 = "delete from MetricLog where keyA = ";
String cql2 = " and keyB = ";
String cql3 = " and time < "+retentionTime;
String dcql3 = " and time = ";
String dcql4 = " and xIndex = ";
this.startBatch();
for(TSDRCacheEntry entry:this.cache.getAll()){
if(entry.getDataCategory()==category){
String cql = cql1 + entry.getMd5ID().getMd5Long1()+cql2+entry.getMd5ID().getMd5Long2()+cql3;
final ResultSet rs = session.execute(cql);
for(Row row:rs.all()){
String deleteCql = dcql1+row.getLong("keyA")+cql2+row.getLong("keyB")+dcql3+row.getLong("time")+dcql4+row.getInt("xIndex");
batch.add(new SimpleStatement(deleteCql));
if(this.batch.size()>=MAX_BATCH_SIZE){
this.executeBatch();
this.startBatch();
}
}
}
}
if(this.batch.size()>0) {
this.executeBatch();
this.startBatch();
}
}
private void purgeBinary(DataCategory category, long retentionTime){
//Cassandra does not support range delete prior to verison 3.
//To overcome, we need to do a "select" and then batch delete one by one.
String cql1 = "Select * from MetricBlob where keyA = ";
String dcql1 = "delete from MetricBlov where keyA = ";
String cql2 = " and keyB = ";
String cql3 = " and time < "+retentionTime;
String dcql3 = " and time = ";
String dcql4 = " and xIndex = ";
this.startBatch();
for(TSDRCacheEntry entry:this.cache.getAll()){
if(entry.getDataCategory()==category){
String cql = cql1 + entry.getMd5ID().getMd5Long1()+cql2+entry.getMd5ID().getMd5Long2()+cql3;
final ResultSet rs = session.execute(cql);
for(Row row:rs.all()){
String deleteCql = dcql1+row.getLong("keyA")+cql2+row.getLong("keyB")+dcql3+row.getLong("time")+dcql4+row.getInt("xIndex");
batch.add(new SimpleStatement(deleteCql));
if(this.batch.size()>=MAX_BATCH_SIZE){
this.executeBatch();
this.startBatch();
}
}
}
}
if(this.batch.size()>0) {
this.executeBatch();
this.startBatch();
}
}
}