/*
* Copyright 2017 the original author or authors.
*
* 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.glowroot.central.repo;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.PreparedStatement;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.datastax.driver.core.utils.UUIDs;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.glowroot.agent.api.Instrumentation;
import org.glowroot.central.repo.AggregateDao.NeedsRollup;
import org.glowroot.central.util.DummyResultSet;
import org.glowroot.central.util.MoreFutures;
import org.glowroot.central.util.Sessions;
import org.glowroot.common.repo.ConfigRepository;
import org.glowroot.common.repo.ConfigRepository.RollupConfig;
import org.glowroot.common.repo.ImmutableSyntheticResult;
import org.glowroot.common.repo.SyntheticResultRepository;
import org.glowroot.common.repo.Utils;
import org.glowroot.common.util.Clock;
import org.glowroot.common.util.OnlyUsedByTests;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.HOURS;
public class SyntheticResultDao implements SyntheticResultRepository {
private static final Logger logger = LoggerFactory.getLogger(SyntheticResultDao.class);
private static final String LCS = "compaction = { 'class' : 'LeveledCompactionStrategy' }";
private final Session session;
private final ConfigRepository configRepository;
private final Clock clock;
// index is rollupLevel
private final ImmutableList<PreparedStatement> insertResultPS;
private final ImmutableList<PreparedStatement> readResultPS;
private final ImmutableList<PreparedStatement> readResultForRollupPS;
private final List<PreparedStatement> insertNeedsRollup;
private final List<PreparedStatement> readNeedsRollup;
private final List<PreparedStatement> deleteNeedsRollup;
public SyntheticResultDao(Session session, ConfigRepository configRepository, Clock clock)
throws Exception {
this.session = session;
this.configRepository = configRepository;
this.clock = clock;
int count = configRepository.getRollupConfigs().size();
List<Integer> rollupExpirationHours =
configRepository.getStorageConfig().rollupExpirationHours();
List<PreparedStatement> insertResultPS = Lists.newArrayList();
List<PreparedStatement> readResultPS = Lists.newArrayList();
List<PreparedStatement> readResultForRollupPS = Lists.newArrayList();
for (int i = 0; i < count; i++) {
Sessions.createTableWithTWCS(session, "create table if not exists"
+ " synthetic_result_rollup_" + i + " (agent_rollup_id varchar,"
+ " synthetic_config_id varchar, capture_time timestamp,"
+ " total_duration_nanos double, execution_count bigint, error_count bigint,"
+ " primary key ((agent_rollup_id, synthetic_config_id), capture_time))",
rollupExpirationHours.get(i));
insertResultPS.add(session.prepare("insert into synthetic_result_rollup_" + i
+ " (agent_rollup_id, synthetic_config_id, capture_time, total_duration_nanos,"
+ " execution_count, error_count) values (?, ?, ?, ?, ?, ?) using ttl ?"));
readResultPS.add(session.prepare("select capture_time, total_duration_nanos,"
+ " execution_count, error_count from synthetic_result_rollup_" + i
+ " where agent_rollup_id = ? and synthetic_config_id = ? and capture_time >= ?"
+ " and capture_time <= ?"));
readResultForRollupPS.add(session.prepare("select total_duration_nanos,"
+ " execution_count, error_count from synthetic_result_rollup_" + i
+ " where agent_rollup_id = ? and synthetic_config_id = ? and capture_time > ?"
+ " and capture_time <= ?"));
}
this.insertResultPS = ImmutableList.copyOf(insertResultPS);
this.readResultPS = ImmutableList.copyOf(readResultPS);
this.readResultForRollupPS = ImmutableList.copyOf(readResultForRollupPS);
// since rollup operations are idempotent, any records resurrected after gc_grace_seconds
// would just create extra work, but not have any other effect
//
// 3 hours is chosen to match default max_hint_window_in_ms since hints are stored
// with a TTL of gc_grace_seconds
// (see http://www.uberobert.com/cassandra_gc_grace_disables_hinted_handoff)
long needsRollupGcGraceSeconds = HOURS.toSeconds(3);
List<PreparedStatement> insertNeedsRollup = Lists.newArrayList();
List<PreparedStatement> readNeedsRollup = Lists.newArrayList();
List<PreparedStatement> deleteNeedsRollup = Lists.newArrayList();
for (int i = 1; i < count; i++) {
session.execute("create table if not exists synthetic_needs_rollup_" + i
+ " (agent_rollup_id varchar, capture_time timestamp, uniqueness timeuuid,"
+ " synthetic_config_ids set<varchar>, primary key (agent_rollup_id,"
+ " capture_time, uniqueness)) with gc_grace_seconds = "
+ needsRollupGcGraceSeconds + " and " + LCS);
insertNeedsRollup.add(session.prepare("insert into synthetic_needs_rollup_" + i
+ " (agent_rollup_id, capture_time, uniqueness, synthetic_config_ids) values"
+ " (?, ?, ?, ?) using TTL ?"));
readNeedsRollup.add(session.prepare("select capture_time, uniqueness,"
+ " synthetic_config_ids from synthetic_needs_rollup_" + i
+ " where agent_rollup_id = ?"));
deleteNeedsRollup.add(session.prepare("delete from synthetic_needs_rollup_" + i
+ " where agent_rollup_id = ? and capture_time = ? and uniqueness = ?"));
}
this.insertNeedsRollup = insertNeedsRollup;
this.readNeedsRollup = readNeedsRollup;
this.deleteNeedsRollup = deleteNeedsRollup;
}
public void store(String agentId, String syntheticMonitorId, long captureTime,
long durationNanos, boolean error) throws Exception {
int ttl = getTTLs().get(0);
long maxCaptureTime = 0;
BoundStatement boundStatement = insertResultPS.get(0).bind();
maxCaptureTime = Math.max(captureTime, maxCaptureTime);
int adjustedTTL = AggregateDao.getAdjustedTTL(ttl, captureTime, clock);
int i = 0;
boundStatement.setString(i++, agentId);
boundStatement.setString(i++, syntheticMonitorId);
boundStatement.setTimestamp(i++, new Date(captureTime));
boundStatement.setDouble(i++, durationNanos);
boundStatement.setLong(i++, 1);
boundStatement.setLong(i++, error ? 1 : 0);
boundStatement.setInt(i++, adjustedTTL);
// wait for success before inserting "needs rollup" records
session.execute(boundStatement);
// insert into synthetic_needs_rollup_1
List<RollupConfig> rollupConfigs = configRepository.getRollupConfigs();
long intervalMillis = rollupConfigs.get(1).intervalMillis();
long rollupCaptureTime = Utils.getRollupCaptureTime(captureTime, intervalMillis);
int needsRollupAdjustedTTL = AggregateDao.getNeedsRollupAdjustedTTL(adjustedTTL,
configRepository.getRollupConfigs());
boundStatement = insertNeedsRollup.get(0).bind();
i = 0;
boundStatement.setString(i++, agentId);
boundStatement.setTimestamp(i++, new Date(rollupCaptureTime));
boundStatement.setUUID(i++, UUIDs.timeBased());
boundStatement.setSet(i++, ImmutableSet.of(syntheticMonitorId));
boundStatement.setInt(i++, needsRollupAdjustedTTL);
session.execute(boundStatement);
}
// from is INCLUSIVE
@Override
public List<SyntheticResult> readSyntheticResults(String agentRollupId,
String syntheticMonitorId, long from, long to, int rollupLevel) {
BoundStatement boundStatement = readResultPS.get(rollupLevel).bind();
int i = 0;
boundStatement.setString(i++, agentRollupId);
boundStatement.setString(i++, syntheticMonitorId);
boundStatement.setTimestamp(i++, new Date(from));
boundStatement.setTimestamp(i++, new Date(to));
ResultSet results = session.execute(boundStatement);
List<SyntheticResult> syntheticResults = Lists.newArrayList();
for (Row row : results) {
i = 0;
syntheticResults.add(ImmutableSyntheticResult.builder()
.captureTime(checkNotNull(row.getTimestamp(i++)).getTime())
.totalDurationNanos(row.getDouble(i++))
.executionCount(row.getLong(i++))
.errorCount(row.getLong(i++))
.build());
}
return syntheticResults;
}
@Instrumentation.Transaction(transactionType = "Background",
transactionName = "Rollup synthetic results",
traceHeadline = "Rollup synthetic results: {{0}}", timer = "rollup synthetic results")
public void rollup(String agentRollupId) throws Exception {
List<Integer> ttls = getTTLs();
int rollupLevel = 1;
while (rollupLevel < configRepository.getRollupConfigs().size()) {
int ttl = ttls.get(rollupLevel);
rollup(agentRollupId, rollupLevel, ttl);
rollupLevel++;
}
}
private void rollup(String agentRollupId, int rollupLevel, int ttl) throws Exception {
List<RollupConfig> rollupConfigs = configRepository.getRollupConfigs();
long rollupIntervalMillis = rollupConfigs.get(rollupLevel).intervalMillis();
List<NeedsRollup> needsRollupList = AggregateDao.getNeedsRollupList(agentRollupId,
rollupLevel, rollupIntervalMillis, readNeedsRollup, session, clock);
Long nextRollupIntervalMillis = null;
if (rollupLevel + 1 < rollupConfigs.size()) {
nextRollupIntervalMillis = rollupConfigs.get(rollupLevel + 1).intervalMillis();
}
for (NeedsRollup needsRollup : needsRollupList) {
long captureTime = needsRollup.getCaptureTime();
long from = captureTime - rollupIntervalMillis;
int adjustedTTL = AggregateDao.getAdjustedTTL(ttl, captureTime, clock);
Set<String> syntheticMonitorIds = needsRollup.getKeys();
List<ListenableFuture<ResultSet>> futures = Lists.newArrayList();
for (String syntheticMonitorId : syntheticMonitorIds) {
futures.add(rollupOne(rollupLevel, agentRollupId, syntheticMonitorId, from,
captureTime, adjustedTTL));
}
if (futures.isEmpty()) {
// no rollups occurred, warning already logged inside rollupOne() above
// this can happen there is an old "needs rollup" record that was created prior to
// TTL was introduced in 0.9.6, and when the "last needs rollup" record wasn't
// processed (also prior to 0.9.6), and when the corresponding old data has expired
AggregateDao.postRollup(agentRollupId, needsRollup.getCaptureTime(),
syntheticMonitorIds, needsRollup.getUniquenessKeysForDeletion(), null, null,
deleteNeedsRollup.get(rollupLevel - 1), -1, session);
continue;
}
// wait for above async work to ensure rollup complete before proceeding
MoreFutures.waitForAll(futures);
int needsRollupAdjustedTTL =
AggregateDao.getNeedsRollupAdjustedTTL(adjustedTTL, rollupConfigs);
PreparedStatement insertNeedsRollup = nextRollupIntervalMillis == null ? null
: this.insertNeedsRollup.get(rollupLevel);
PreparedStatement deleteNeedsRollup = this.deleteNeedsRollup.get(rollupLevel - 1);
AggregateDao.postRollup(agentRollupId, needsRollup.getCaptureTime(),
syntheticMonitorIds, needsRollup.getUniquenessKeysForDeletion(),
nextRollupIntervalMillis, insertNeedsRollup, deleteNeedsRollup,
needsRollupAdjustedTTL, session);
}
}
// from is non-inclusive
private ListenableFuture<ResultSet> rollupOne(int rollupLevel, String agentRollupId,
String syntheticMonitorId, long from, long to, int adjustedTTL) throws Exception {
BoundStatement boundStatement = readResultForRollupPS.get(rollupLevel - 1).bind();
int i = 0;
boundStatement.setString(i++, agentRollupId);
boundStatement.setString(i++, syntheticMonitorId);
boundStatement.setTimestamp(i++, new Date(from));
boundStatement.setTimestamp(i++, new Date(to));
return Futures.transformAsync(
session.executeAsync(boundStatement),
new AsyncFunction<ResultSet, ResultSet>() {
@Override
public ListenableFuture<ResultSet> apply(@Nullable ResultSet results)
throws Exception {
checkNotNull(results);
if (results.isExhausted()) {
// this is unexpected since TTL for "needs rollup" records is shorter
// than TTL for data
logger.warn("no synthetic result table records found for"
+ " agentRollupId={}, syntheticMonitorId={}, from={}, to={},"
+ " level={}", agentRollupId, syntheticMonitorId, from, to,
rollupLevel);
return Futures.immediateFuture(DummyResultSet.INSTANCE);
}
return rollupOneFromRows(rollupLevel, agentRollupId, syntheticMonitorId, to,
adjustedTTL, results);
}
});
}
private ListenableFuture<ResultSet> rollupOneFromRows(int rollupLevel, String agentRollupId,
String syntheticMonitorId, long to, int adjustedTTL, Iterable<Row> rows) {
double totalDurationNanos = 0;
long executionCount = 0;
long errorCount = 0;
for (Row row : rows) {
int i = 0;
totalDurationNanos += row.getDouble(i++);
executionCount += row.getLong(i++);
errorCount += row.getLong(i++);
}
BoundStatement boundStatement = insertResultPS.get(rollupLevel).bind();
int i = 0;
boundStatement.setString(i++, agentRollupId);
boundStatement.setString(i++, syntheticMonitorId);
boundStatement.setTimestamp(i++, new Date(to));
boundStatement.setDouble(i++, totalDurationNanos);
boundStatement.setLong(i++, executionCount);
boundStatement.setLong(i++, errorCount);
boundStatement.setInt(i++, adjustedTTL);
return session.executeAsync(boundStatement);
}
private List<Integer> getTTLs() throws Exception {
List<Integer> ttls = Lists.newArrayList();
List<Integer> rollupExpirationHours =
configRepository.getStorageConfig().rollupExpirationHours();
for (long expirationHours : rollupExpirationHours) {
ttls.add(Ints.saturatedCast(HOURS.toSeconds(expirationHours)));
}
return ttls;
}
@OnlyUsedByTests
void truncateAll() {
for (int i = 0; i < configRepository.getRollupConfigs().size(); i++) {
session.execute("truncate synthetic_result_rollup_" + i);
}
for (int i = 1; i < configRepository.getRollupConfigs().size(); i++) {
session.execute("truncate synthetic_needs_rollup_" + i);
}
}
}