/*
* Copyright (c) 2015 Spotify AB.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 com.spotify.heroic.analytics.bigtable;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.protobuf.ByteString;
import com.spotify.heroic.analytics.MetricAnalytics;
import com.spotify.heroic.analytics.SeriesHit;
import com.spotify.heroic.async.AsyncObservable;
import com.spotify.heroic.common.Series;
import com.spotify.heroic.lifecycle.LifeCycleRegistry;
import com.spotify.heroic.lifecycle.LifeCycles;
import com.spotify.heroic.metric.MetricBackend;
import com.spotify.heroic.metric.bigtable.BigtableConnection;
import com.spotify.heroic.metric.bigtable.api.Family;
import com.spotify.heroic.metric.bigtable.api.ReadModifyWriteRules;
import com.spotify.heroic.metric.bigtable.api.ReadRowsRequest;
import com.spotify.heroic.metric.bigtable.api.Row;
import com.spotify.heroic.metric.bigtable.api.RowRange;
import com.spotify.heroic.metric.bigtable.api.Table;
import com.spotify.heroic.statistics.AnalyticsReporter;
import eu.toolchain.async.AsyncFramework;
import eu.toolchain.async.AsyncFuture;
import eu.toolchain.async.Borrowed;
import eu.toolchain.async.Managed;
import lombok.ToString;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.LocalDate;
import java.util.Optional;
import java.util.concurrent.Semaphore;
@BigtableScope
@ToString(exclude = {"async", "mapper", "reporter"})
public class BigtableMetricAnalytics implements MetricAnalytics, LifeCycles {
final Managed<BigtableConnection> connection;
final AsyncFramework async;
final ObjectMapper mapper;
final AnalyticsReporter reporter;
final String hitsTableName;
final String hitsColumnFamily;
final Semaphore pendingReports;
final SeriesKeyEncoding fetchSeries = new SeriesKeyEncoding("fetch");
@Inject
public BigtableMetricAnalytics(
final Managed<BigtableConnection> connection, final AsyncFramework async,
@Named("application/json") final ObjectMapper mapper, final AnalyticsReporter reporter,
@Named("hitsTableName") final String hitsTableName,
@Named("hitsColumnFamily") final String hitsColumnFamily,
@Named("maxPendingReports") final int maxPendingReports
) {
this.connection = connection;
this.async = async;
this.mapper = mapper;
this.reporter = reporter;
this.hitsTableName = hitsTableName;
this.hitsColumnFamily = hitsColumnFamily;
this.pendingReports = new Semaphore(maxPendingReports);
}
@Override
public void register(LifeCycleRegistry registry) {
registry.start(this::start);
registry.stop(this::stop);
}
@Override
public AsyncFuture<Void> configure() {
return connection.doto(c -> async.call(() -> {
final Table table = c.tableAdminClient().getTable(hitsTableName).orElseGet(() -> {
return c.tableAdminClient().createTable(hitsTableName);
});
table.getColumnFamily(hitsColumnFamily).orElseGet(() -> {
return c.tableAdminClient().createColumnFamily(table, hitsColumnFamily);
});
return null;
}));
}
@Override
public MetricBackend wrap(final MetricBackend backend) {
return new BigtableAnalyticsMetricBackend(this, backend);
}
@Override
public AsyncObservable<SeriesHit> seriesHits(final LocalDate date) {
final Borrowed<BigtableConnection> b = connection.borrow();
if (!b.isValid()) {
return AsyncObservable.failed(new RuntimeException("Failed to borrow connection"));
}
final BigtableConnection c = b.get();
final ByteString start = fetchSeries.rangeKey(date);
final ByteString end = fetchSeries.rangeKey(date.plusDays(1));
final RowRange range = RowRange.rowRange(Optional.of(start), Optional.of(end));
return c
.dataClient()
.readRowsObserved(hitsTableName, ReadRowsRequest.builder().range(range).build())
.transform(row -> {
final ByteString rowKey = row.getKey();
final SeriesKeyEncoding.SeriesKey k;
try {
k = fetchSeries.decode(rowKey, s -> mapper.readValue(s, Series.class));
} catch (final Exception e) {
throw new RuntimeException(e);
}
final long value = row.getFamily(hitsColumnFamily).map(family -> {
final Family.LatestCellValueColumn col =
family.latestCellValue().iterator().next();
final ByteBuffer buf = ByteBuffer.wrap(col.getValue().toByteArray());
buf.order(ByteOrder.BIG_ENDIAN);
return buf.getLong();
}).orElse(0L);
return new SeriesHit(k.getSeries(), value);
})
.onFinished(b::release);
}
@Override
public AsyncFuture<Void> reportFetchSeries(LocalDate date, Series series) {
// limit the number of pending reports that are allowed at the same time to avoid resource
// starvation.
if (!pendingReports.tryAcquire()) {
reporter.reportDroppedFetchSeries();
return async.cancelled();
}
return connection.doto(c -> {
final ByteString key = fetchSeries.encode(new SeriesKeyEncoding.SeriesKey(date, series),
mapper::writeValueAsString);
final AsyncFuture<Row> request = c
.dataClient()
.readModifyWriteRow(hitsTableName, key, ReadModifyWriteRules
.builder()
.increment(hitsColumnFamily, ByteString.EMPTY, 1L)
.build());
return request.<Void>directTransform(d -> null).onFailed(e -> {
reporter.reportFailedFetchSeries();
});
}).onFinished(pendingReports::release);
}
private AsyncFuture<Void> start() {
return connection.start();
}
private AsyncFuture<Void> stop() {
return connection.stop();
}
}