/*
* Copyright 2015, The OpenNMS Group
*
* 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 org.opennms.newts.persistence.cassandra;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.querybuilder.Batch;
import com.datastax.driver.core.querybuilder.Delete;
import com.datastax.driver.core.querybuilder.Insert;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import org.opennms.newts.aggregate.IntervalGenerator;
import org.opennms.newts.aggregate.ResultProcessor;
import org.opennms.newts.api.Context;
import org.opennms.newts.api.Duration;
import org.opennms.newts.api.Measurement;
import org.opennms.newts.api.Resource;
import org.opennms.newts.api.Results;
import org.opennms.newts.api.Results.Row;
import org.opennms.newts.api.Sample;
import org.opennms.newts.api.SampleProcessorService;
import org.opennms.newts.api.SampleRepository;
import org.opennms.newts.api.SampleSelectCallback;
import org.opennms.newts.api.Timestamp;
import org.opennms.newts.api.ValueType;
import org.opennms.newts.api.query.ResultDescriptor;
import org.opennms.newts.cassandra.CassandraSession;
import org.opennms.newts.cassandra.ContextConfigurations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import static com.datastax.driver.core.querybuilder.QueryBuilder.bindMarker;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.gte;
import static com.datastax.driver.core.querybuilder.QueryBuilder.insertInto;
import static com.datastax.driver.core.querybuilder.QueryBuilder.lte;
import static com.datastax.driver.core.querybuilder.QueryBuilder.ttl;
import static com.datastax.driver.core.querybuilder.QueryBuilder.unloggedBatch;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
public class CassandraSampleRepository implements SampleRepository {
private static final Logger LOG = LoggerFactory.getLogger(CassandraSampleRepository.class);
// Used to calculate the duration when the duration is not specified
private static final int TARGET_NUMBER_OF_STEPS = 10;
private static final int DELETION_INTERVAL = 360;
private final CassandraSession m_session;
private final int m_ttl;
private final SampleProcessorService m_processorService;
private final PreparedStatement m_selectStatement;
private final PreparedStatement m_deleteStatement;
private final Timer m_sampleSelectTimer;
private final Timer m_measurementSelectTimer;
private final Timer m_insertTimer;
private final Meter m_samplesInserted;
private final Meter m_samplesSelected;
private final ContextConfigurations m_contextConfigurations;
@Inject
public CassandraSampleRepository(CassandraSession session, @Named("samples.cassandra.time-to-live") int ttl, MetricRegistry registry, SampleProcessorService processorService, ContextConfigurations contextConfigurations) {
m_session = checkNotNull(session, "session argument");
checkArgument(ttl >= 0, "Negative Cassandra column TTL");
m_ttl = ttl;
checkNotNull(registry, "metric registry argument");
m_processorService = processorService;
m_contextConfigurations = checkNotNull(contextConfigurations, "contextConfigurations argument");
Select select = QueryBuilder.select().from(SchemaConstants.T_SAMPLES);
select.where(eq(SchemaConstants.F_CONTEXT, bindMarker(SchemaConstants.F_CONTEXT)));
select.where(eq(SchemaConstants.F_PARTITION, bindMarker(SchemaConstants.F_PARTITION)));
select.where(eq(SchemaConstants.F_RESOURCE, bindMarker(SchemaConstants.F_RESOURCE)));
select.where(gte(SchemaConstants.F_COLLECTED, bindMarker("start")));
select.where(lte(SchemaConstants.F_COLLECTED, bindMarker("end")));
m_selectStatement = m_session.prepare(select.toString());
Delete delete = QueryBuilder.delete().from(SchemaConstants.T_SAMPLES);
delete.where(eq(SchemaConstants.F_CONTEXT, bindMarker(SchemaConstants.F_CONTEXT)));
delete.where(eq(SchemaConstants.F_PARTITION, bindMarker(SchemaConstants.F_PARTITION)));
delete.where(eq(SchemaConstants.F_RESOURCE, bindMarker(SchemaConstants.F_RESOURCE)));
m_deleteStatement = m_session.prepare(delete.toString());
m_sampleSelectTimer = registry.timer(metricName("sample-select-timer"));
m_measurementSelectTimer = registry.timer(metricName("measurement-select-timer"));
m_insertTimer = registry.timer(metricName("insert-timer"));
m_samplesInserted = registry.meter(metricName("samples-inserted"));
m_samplesSelected = registry.meter(metricName("samples-selected"));
}
public Iterable<Results.Row<Sample>> select(Context context, Resource resource, Timestamp start, Timestamp end, ResultDescriptor descriptor, Duration step) {
return new DriverAdapter(cassandraSelect(context, resource, start.minus(step), end),
descriptor.getSourceNames());
}
@Override
public Results<Measurement> select(Context context, Resource resource, Optional<Timestamp> start, Optional<Timestamp> end, ResultDescriptor descriptor, Optional<Duration> resolution) {
return select(context, resource, start, end, descriptor, resolution, noopSampleSelectCallback);
}
@Override
public Results<Measurement> select(Context context, Resource resource, Optional<Timestamp> start, Optional<Timestamp> end, ResultDescriptor descriptor, Optional<Duration> resolution, SampleSelectCallback callback) {
Timer.Context timer = m_measurementSelectTimer.time();
validateSelect(start, end);
Timestamp upper = end.isPresent() ? end.get() : Timestamp.now();
Timestamp lower = start.isPresent() ? start.get() : upper.minus(Duration.seconds(86400));
Duration step;
if (resolution.isPresent()) {
step = resolution.get();
} else {
// Determine the ideal step size, splitting the interval evenly into N slices
long stepMillis = upper.minus(lower).asMillis() / TARGET_NUMBER_OF_STEPS;
// But every step must be a multiple of the interval
long intervalMillis = descriptor.getInterval().asMillis();
// If the interval is greater than the target step, use the 2 * interval as the step
if (intervalMillis >= stepMillis) {
step = descriptor.getInterval().times(2);
} else {
// Otherwise, round stepMillkeyis up to the closest multiple of intervalMillis
long remainderMillis = stepMillis % intervalMillis;
if (remainderMillis != 0) {
stepMillis = stepMillis + intervalMillis - remainderMillis;
}
step = Duration.millis(stepMillis);
}
}
LOG.debug("Querying database for resource {}, from {} to {}", resource, lower.minus(step), upper);
DriverAdapter driverAdapter = new DriverAdapter(cassandraSelect(context, resource, lower.minus(step), upper),
descriptor.getSourceNames());
Results<Measurement> results;
callback.beforeProcess();
try {
results = new ResultProcessor(resource, lower, upper, descriptor, step).process(driverAdapter);
} finally {
callback.afterProcess();
}
LOG.debug("{} results returned from database", driverAdapter.getResultCount());
m_samplesSelected.mark(driverAdapter.getResultCount());
try {
return results;
} finally {
timer.stop();
}
}
@Override
public Results<Sample> select(Context context, Resource resource, Optional<Timestamp> start, Optional<Timestamp> end) {
Timer.Context timer = m_sampleSelectTimer.time();
validateSelect(start, end);
Timestamp upper = end.isPresent() ? end.get() : Timestamp.now();
Timestamp lower = start.isPresent() ? start.get() : upper.minus(Duration.seconds(86400));
LOG.debug("Querying database for resource {}, from {} to {}", resource, lower, upper);
Results<Sample> samples = new Results<>();
DriverAdapter driverAdapter = new DriverAdapter(cassandraSelect(context, resource, lower, upper));
for (Row<Sample> row : driverAdapter) {
samples.addRow(row);
}
LOG.debug("{} results returned from database", driverAdapter.getResultCount());
m_samplesSelected.mark(driverAdapter.getResultCount());
try {
return samples;
} finally {
timer.stop();
}
}
@Override
public void insert(Collection<Sample> samples) {
insert(samples, false);
}
@Override
public void insert(Collection<Sample> samples, boolean calculateTimeToLive) {
Timer.Context timer = m_insertTimer.time();
Timestamp now = Timestamp.now();
Batch batch = unloggedBatch();
for (Sample m : samples) {
int ttl = m_ttl;
if (calculateTimeToLive) {
ttl -= (int) (now.asSeconds() - m.getTimestamp().asSeconds());
if (ttl <= 0) {
LOG.debug("Skipping expired sample: {}", m);
continue;
}
}
Duration resourceShard = m_contextConfigurations.getResourceShard(m.getContext());
Insert insert = insertInto(SchemaConstants.T_SAMPLES)
.value(SchemaConstants.F_CONTEXT, m.getContext().getId())
.value(SchemaConstants.F_PARTITION, m.getTimestamp().stepFloor(resourceShard).asSeconds())
.value(SchemaConstants.F_RESOURCE, m.getResource().getId())
.value(SchemaConstants.F_COLLECTED, m.getTimestamp().asMillis())
.value(SchemaConstants.F_METRIC_NAME, m.getName())
.value(SchemaConstants.F_VALUE, ValueType.decompose(m.getValue()));
// Inserting a column with a null value inserts a tombstone (a deletion marker); Skip the attributes
// for any sample that has not specified them.
if (m.getAttributes() != null) {
insert.value(SchemaConstants.F_ATTRIBUTES, m.getAttributes());
}
// Use the context specific consistency level
insert.setConsistencyLevel(m_contextConfigurations.getWriteConsistency(m.getContext()));
batch.add(insert.using(ttl(ttl)));
}
try {
m_session.execute(batch);
if (m_processorService != null) {
m_processorService.submit(samples);
}
m_samplesInserted.mark(samples.size());
} finally {
timer.stop();
}
}
@Override
public void delete(Context context, Resource resource) {
/**
* Check for ttl value > 0
*/
if (m_ttl > 0) {
/**
* Delete exactly from (now - ttl) till now
*/
final Timestamp start = Timestamp.now().minus(m_ttl, TimeUnit.SECONDS);
final Timestamp end = Timestamp.now();
final Duration resourceShard = m_contextConfigurations.getResourceShard(context);
final List<Future<ResultSet>> futures = Lists.newArrayList();
for (Timestamp partition : new IntervalGenerator(start.stepFloor(resourceShard),
end.stepFloor(resourceShard),
resourceShard)) {
BoundStatement bindStatement = m_deleteStatement.bind();
bindStatement.setString(SchemaConstants.F_CONTEXT, context.getId());
bindStatement.setInt(SchemaConstants.F_PARTITION, (int) partition.asSeconds());
bindStatement.setString(SchemaConstants.F_RESOURCE, resource.getId());
futures.add(m_session.executeAsync(bindStatement));
}
for (final Future<ResultSet> future : futures) {
try {
future.get();
} catch (final InterruptedException | ExecutionException e) {
throw Throwables.propagate(e);
}
}
} else {
// Choose (now - one year) till now...
Timestamp end = Timestamp.now();
Timestamp start = end.minus(DELETION_INTERVAL, TimeUnit.DAYS);
// ... and check whether samples exist for this period of time.
while (cassandraSelect(context, resource, start, end).hasNext()) {
// Now delete the samples...
final Duration resourceShard = m_contextConfigurations.getResourceShard(context);
final List<Future<ResultSet>> futures = Lists.newArrayList();
for (Timestamp partition : new IntervalGenerator(start.stepFloor(resourceShard),
end.stepFloor(resourceShard),
resourceShard)) {
BoundStatement bindStatement = m_deleteStatement.bind();
bindStatement.setString(SchemaConstants.F_CONTEXT, context.getId());
bindStatement.setInt(SchemaConstants.F_PARTITION, (int) partition.asSeconds());
bindStatement.setString(SchemaConstants.F_RESOURCE, resource.getId());
futures.add(m_session.executeAsync(bindStatement));
}
for (final Future<ResultSet> future : futures) {
try {
future.get();
} catch (final InterruptedException | ExecutionException e) {
throw Throwables.propagate(e);
}
}
// ...set end to start and start to (end - one year)
end = start;
start = end.minus(DELETION_INTERVAL, TimeUnit.DAYS);
// and start over again until no more samples are found
}
}
}
private Iterator<com.datastax.driver.core.Row> cassandraSelect(Context context, Resource resource,
Timestamp start, Timestamp end) {
List<Future<ResultSet>> futures = Lists.newArrayList();
Duration resourceShard = m_contextConfigurations.getResourceShard(context);
Timestamp lower = start.stepFloor(resourceShard);
Timestamp upper = end.stepFloor(resourceShard);
for (Timestamp partition : new IntervalGenerator(lower, upper, resourceShard)) {
BoundStatement bindStatement = m_selectStatement.bind();
bindStatement.setString(SchemaConstants.F_CONTEXT, context.getId());
bindStatement.setInt(SchemaConstants.F_PARTITION, (int) partition.asSeconds());
bindStatement.setString(SchemaConstants.F_RESOURCE, resource.getId());
bindStatement.setTimestamp("start", start.asDate());
bindStatement.setTimestamp("end", end.asDate());
// Use the context specific consistency level
bindStatement.setConsistencyLevel(m_contextConfigurations.getReadConsistency(context));
futures.add(m_session.executeAsync(bindStatement));
}
return new ConcurrentResultWrapper(futures);
}
private void validateSelect(Optional<Timestamp> start, Optional<Timestamp> end) {
if ((start.isPresent() && end.isPresent()) && start.get().gt(end.get())) {
throw new IllegalArgumentException("start time must be less than end time");
}
}
private String metricName(String suffix) {
return name("repository", suffix);
}
private static final SampleSelectCallback noopSampleSelectCallback = new SampleSelectCallback() {
@Override
public void beforeProcess() {
// pass
}
@Override
public void afterProcess() {
// pass
}
};
}