/*
* Copyright 2014-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.agent.embedded.repo;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicLongArray;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Longs;
import org.checkerframework.checker.tainting.qual.Untainted;
import org.glowroot.agent.embedded.util.DataSource;
import org.glowroot.agent.embedded.util.DataSource.JdbcQuery;
import org.glowroot.agent.embedded.util.DataSource.JdbcRowQuery;
import org.glowroot.agent.embedded.util.DataSource.JdbcUpdate;
import org.glowroot.agent.embedded.util.ImmutableColumn;
import org.glowroot.agent.embedded.util.ImmutableIndex;
import org.glowroot.agent.embedded.util.Schemas.Column;
import org.glowroot.agent.embedded.util.Schemas.ColumnType;
import org.glowroot.agent.embedded.util.Schemas.Index;
import org.glowroot.common.repo.ConfigRepository.RollupConfig;
import org.glowroot.common.repo.GaugeValueRepository;
import org.glowroot.common.repo.util.Gauges;
import org.glowroot.common.repo.util.RollupLevelService;
import org.glowroot.common.util.Clock;
import org.glowroot.wire.api.model.CollectorServiceOuterClass.GaugeValue;
import static org.glowroot.agent.util.Checkers.castUntainted;
public class GaugeValueDao implements GaugeValueRepository {
private static final ImmutableList<Column> columns = ImmutableList.<Column>of(
ImmutableColumn.of("gauge_id", ColumnType.BIGINT),
ImmutableColumn.of("capture_time", ColumnType.BIGINT),
ImmutableColumn.of("value", ColumnType.DOUBLE),
// weight is needed for rollups
// for non-counters, it is the number of recording that the (averaged) value represents
// for counters, it is the interval of time that the (averaged) value represents
ImmutableColumn.of("weight", ColumnType.BIGINT));
private final GaugeNameDao gaugeNameDao;
private final DataSource dataSource;
private final Clock clock;
private final ImmutableList<RollupConfig> rollupConfigs;
// AtomicLongArray used for visibility
private final AtomicLongArray lastRollupTimes;
private final Object rollupLock = new Object();
GaugeValueDao(DataSource dataSource, GaugeNameDao gaugeNameDao, Clock clock) throws Exception {
this.dataSource = dataSource;
this.gaugeNameDao = gaugeNameDao;
this.clock = clock;
this.rollupConfigs = ImmutableList.copyOf(RollupConfig.buildRollupConfigs());
for (int i = 0; i <= rollupConfigs.size(); i++) {
dataSource.syncTable("gauge_value_rollup_" + castUntainted(i), columns);
dataSource.syncIndexes("gauge_value_rollup_" + castUntainted(i),
ImmutableList.<Index>of(
ImmutableIndex.of(
"gauge_value_rollup_" + castUntainted(i) + "_idx",
ImmutableList.of("gauge_id", "capture_time", "value",
"weight")),
// this index is used by rollup query
ImmutableIndex.of(
"gauge_value_rollup_" + castUntainted(i)
+ "_by_capture_time_idx",
ImmutableList.of("capture_time", "gauge_id", "value",
"weight"))));
}
List<Column> columns = Lists.newArrayList();
for (int i = 1; i <= rollupConfigs.size(); i++) {
columns.add(ImmutableColumn.of("last_rollup_" + i + "_time", ColumnType.BIGINT));
}
dataSource.syncTable("gauge_value_last_rollup_times", columns);
lastRollupTimes = initData(rollupConfigs, dataSource);
// TODO initial rollup in case store is not called in a reasonable time
}
@Override
public List<Gauge> getGauges(String agentRollupId) throws Exception {
List<String> allGaugeNames = gaugeNameDao.readAllGaugeNames();
List<Gauge> gauges = Lists.newArrayList();
for (String gaugeName : allGaugeNames) {
gauges.add(Gauges.getGauge(gaugeName));
}
return gauges;
}
public void store(List<GaugeValue> gaugeValues) throws Exception {
if (gaugeValues.isEmpty()) {
return;
}
Map<GaugeValue, Long> gaugeValueIdMap = Maps.newLinkedHashMap();
for (GaugeValue gaugeValue : gaugeValues) {
long gaugeId = gaugeNameDao.updateLastCaptureTime(gaugeValue.getGaugeName(),
gaugeValue.getCaptureTime());
if (gaugeId == -1) {
// data source is closing and a new gauge id was needed, but could not insert it
// --or-- race condition with GaugeNameDao.deleteAll() in which case return is good
// option also
return;
}
gaugeValueIdMap.put(gaugeValue, gaugeId);
}
dataSource.batchUpdate(new GaugeValuesBinder(gaugeValueIdMap));
synchronized (rollupLock) {
// clock can never go backwards and future gauge captures will wait until this method
// completes since ScheduledExecutorService.scheduleAtFixedRate() guarantees that future
// invocations of GaugeCollector will wait until prior invocations complete
long safeCurrentTime = clock.currentTimeMillis() - 1;
for (int i = 0; i < rollupConfigs.size(); i++) {
long intervalMillis = rollupConfigs.get(i).intervalMillis();
long safeRollupTime =
RollupLevelService.getSafeRollupTime(safeCurrentTime, intervalMillis);
long lastRollupTime = lastRollupTimes.get(i);
if (safeRollupTime > lastRollupTime) {
rollup(lastRollupTime, safeRollupTime, intervalMillis, i + 1, i);
// JVM termination here will cause last_rollup_*_time to be out of sync, which
// will cause a re-rollup of this time after the next startup, but this is ok
// since it will just overwrite prior rollup
dataSource.update("update gauge_value_last_rollup_times set last_rollup_"
+ castUntainted(i + 1) + "_time = ?", safeRollupTime);
lastRollupTimes.set(i, safeRollupTime);
}
}
}
}
// from is INCLUSIVE
@Override
public List<GaugeValue> readGaugeValues(String agentRollupId, String gaugeName, long from,
long to, int rollupLevel) throws Exception {
Long gaugeId = gaugeNameDao.getGaugeId(gaugeName);
if (gaugeId == null) {
// not necessarily an error, gauge id not created until first store
return ImmutableList.of();
}
return dataSource.query(new GaugeValueQuery(gaugeId, from, to, rollupLevel));
}
void deleteBefore(long captureTime, int rollupLevel) throws Exception {
dataSource.deleteBefore("gauge_value_rollup_" + castUntainted(rollupLevel), captureTime);
}
void reinitAfterDeletingDatabase() throws Exception {
AtomicLongArray lastRollupTimes = initData(rollupConfigs, dataSource);
for (int i = 0; i < lastRollupTimes.length(); i++) {
this.lastRollupTimes.set(i, lastRollupTimes.get(i));
}
}
private void rollup(long lastRollupTime, long safeRollupTime, long fixedIntervalMillis,
int toRollupLevel, int fromRollupLevel) throws Exception {
// need ".0" to force double result
String captureTimeSql = castUntainted(
"ceil(capture_time / " + fixedIntervalMillis + ".0) * " + fixedIntervalMillis);
dataSource.update("merge into gauge_value_rollup_" + castUntainted(toRollupLevel)
+ " (gauge_id, capture_time, value, weight) key (gauge_id, capture_time)"
+ " select gauge_id, " + captureTimeSql + " ceil_capture_time,"
+ " sum(value * weight) / sum(weight), sum(weight) from gauge_value_rollup_"
+ castUntainted(fromRollupLevel) + " gp where gp.capture_time > ?"
+ " and gp.capture_time <= ? group by gp.gauge_id, ceil_capture_time",
lastRollupTime, safeRollupTime);
}
private static AtomicLongArray initData(ImmutableList<RollupConfig> rollupConfigs,
DataSource dataSource) throws Exception {
List<String> columnNames = Lists.newArrayList();
for (int i = 1; i <= rollupConfigs.size(); i++) {
columnNames.add("last_rollup_" + i + "_time");
}
Joiner joiner = Joiner.on(", ");
String selectClause = castUntainted(joiner.join(columnNames));
long[] lastRollupTimes = dataSource.query(new LastRollupTimesQuery(selectClause));
if (lastRollupTimes == null) {
long[] values = new long[rollupConfigs.size()];
String valueClause = castUntainted(joiner.join(Longs.asList(values)));
dataSource.update("insert into gauge_value_last_rollup_times (" + selectClause
+ ") values (" + valueClause + ")");
return new AtomicLongArray(values);
} else {
return new AtomicLongArray(lastRollupTimes);
}
}
private class GaugeValuesBinder implements JdbcUpdate {
private final Map<GaugeValue, Long> gaugeValueIdMap;
private GaugeValuesBinder(Map<GaugeValue, Long> gaugeValueIdMap) {
this.gaugeValueIdMap = gaugeValueIdMap;
}
@Override
public @Untainted String getSql() {
return "insert into gauge_value_rollup_0 (gauge_id, capture_time, value, weight)"
+ " values (?, ?, ?, ?)";
}
@Override
public void bind(PreparedStatement preparedStatement) throws SQLException {
for (Entry<GaugeValue, Long> entry : gaugeValueIdMap.entrySet()) {
GaugeValue gaugeValue = entry.getKey();
long gaugeId = entry.getValue();
int i = 1;
preparedStatement.setLong(i++, gaugeId);
preparedStatement.setLong(i++, gaugeValue.getCaptureTime());
preparedStatement.setDouble(i++, gaugeValue.getValue());
preparedStatement.setLong(i++, gaugeValue.getWeight());
preparedStatement.addBatch();
}
}
}
private static class LastRollupTimesQuery implements JdbcQuery<long/*@Nullable*/[]> {
private final @Untainted String selectClause;
public LastRollupTimesQuery(@Untainted String selectClause) {
this.selectClause = selectClause;
}
@Override
public @Untainted String getSql() {
return "select " + selectClause + " from gauge_value_last_rollup_times";
}
@Override
public void bind(PreparedStatement preparedStatement) throws Exception {}
@Override
public long/*@Nullable*/[] processResultSet(ResultSet resultSet) throws Exception {
if (!resultSet.next()) {
return null;
}
int columns = resultSet.getMetaData().getColumnCount();
long[] values = new long[columns];
for (int i = 0; i < columns; i++) {
values[i] = resultSet.getLong(i + 1);
}
return values;
}
@Override
public long/*@Nullable*/[] valueIfDataSourceClosed() {
return null;
}
}
private class GaugeValueQuery implements JdbcRowQuery<GaugeValue> {
private final long gaugeId;
private final long from;
private final long to;
private final int rollupLevel;
private GaugeValueQuery(long gaugeId, long from, long to, int rollupLevel) {
this.gaugeId = gaugeId;
this.from = from;
this.to = to;
this.rollupLevel = rollupLevel;
}
@Override
public @Untainted String getSql() {
return "select capture_time, value, weight from gauge_value_rollup_"
+ castUntainted(rollupLevel) + " where gauge_id = ? and capture_time >= ?"
+ " and capture_time <= ? order by capture_time";
}
@Override
public void bind(PreparedStatement preparedStatement) throws SQLException {
int i = 1;
preparedStatement.setLong(i++, gaugeId);
preparedStatement.setLong(i++, from);
preparedStatement.setLong(i++, to);
}
@Override
public GaugeValue mapRow(ResultSet resultSet) throws SQLException {
int i = 1;
return GaugeValue.newBuilder()
.setCaptureTime(resultSet.getLong(i++))
.setValue(resultSet.getDouble(i++))
.setWeight(resultSet.getLong(i++))
.build();
}
}
}