/*
* 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.ingestion;
import com.google.common.collect.ImmutableList;
import com.spotify.heroic.common.Collected;
import com.spotify.heroic.common.DateRange;
import com.spotify.heroic.common.Grouped;
import com.spotify.heroic.common.Groups;
import com.spotify.heroic.filter.Filter;
import com.spotify.heroic.metadata.MetadataBackend;
import com.spotify.heroic.metadata.WriteMetadata;
import com.spotify.heroic.metric.Metric;
import com.spotify.heroic.metric.MetricBackend;
import com.spotify.heroic.metric.WriteMetric;
import com.spotify.heroic.statistics.IngestionManagerReporter;
import com.spotify.heroic.suggest.SuggestBackend;
import com.spotify.heroic.suggest.WriteSuggest;
import eu.toolchain.async.AsyncFramework;
import eu.toolchain.async.AsyncFuture;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class CoreIngestionGroup implements IngestionGroup {
private final AsyncFramework async;
private final Supplier<Filter> filter;
private final Semaphore writePermits;
private final IngestionManagerReporter reporter;
private final LongAdder ingested;
private final Optional<MetricBackend> metric;
private final Optional<MetadataBackend> metadata;
private final Optional<SuggestBackend> suggest;
@Override
public Groups groups() {
return Groups.combine(metric.map(Grouped::groups).orElseGet(Groups::empty),
metadata.map(Grouped::groups).orElseGet(Groups::empty),
suggest.map(Grouped::groups).orElseGet(Groups::empty));
}
@Override
public AsyncFuture<Ingestion> write(final Ingestion.Request request) {
ingested.increment();
return syncWrite(request);
}
@Override
public boolean isEmpty() {
return metric.map(Collected::isEmpty).orElse(true) &&
metadata.map(Collected::isEmpty).orElse(true) &&
suggest.map(Collected::isEmpty).orElse(true);
}
protected AsyncFuture<Ingestion> syncWrite(final Ingestion.Request request) {
if (!filter.get().apply(request.getSeries())) {
reporter.reportDroppedByFilter();
return async.resolved(Ingestion.of(ImmutableList.of()));
}
try {
writePermits.acquire();
} catch (final InterruptedException e) {
return async.failed(
new Exception("Failed to acquire semaphore for bounded request", e));
}
reporter.incrementConcurrentWrites();
return doWrite(request).onFinished(() -> {
writePermits.release();
reporter.decrementConcurrentWrites();
});
}
protected AsyncFuture<Ingestion> doWrite(final Ingestion.Request request) {
final List<AsyncFuture<Ingestion>> futures = new ArrayList<>();
final Supplier<DateRange> range = rangeSupplier(request);
metric.map(m -> doMetricWrite(m, request)).ifPresent(futures::add);
metadata.map(m -> doMetadataWrite(m, request, range.get())).ifPresent(futures::add);
suggest.map(s -> doSuggestWrite(s, request, range.get())).ifPresent(futures::add);
return async.collect(futures, Ingestion.reduce());
}
protected AsyncFuture<Ingestion> doMetricWrite(
final MetricBackend metric, final Ingestion.Request write
) {
return metric
.write(new WriteMetric.Request(write.getSeries(), write.getData()))
.directTransform(Ingestion::fromWriteMetric);
}
protected AsyncFuture<Ingestion> doMetadataWrite(
final MetadataBackend metadata, final Ingestion.Request write, final DateRange range
) {
return metadata
.write(new WriteMetadata.Request(write.getSeries(), range))
.directTransform(Ingestion::fromWriteMetadata);
}
protected AsyncFuture<Ingestion> doSuggestWrite(
final SuggestBackend suggest, final Ingestion.Request write, final DateRange range
) {
return suggest
.write(new WriteSuggest.Request(write.getSeries(), range))
.directTransform(Ingestion::fromWriteSuggest);
}
/**
* Setup a range supplier that memoizes the result.
*/
protected Supplier<DateRange> rangeSupplier(final Ingestion.Request request) {
return new Supplier<DateRange>() {
// memoized value.
private DateRange calculated = null;
@Override
public DateRange get() {
if (calculated != null) {
return calculated;
}
calculated = rangeFrom(request);
return calculated;
}
};
}
protected DateRange rangeFrom(final Ingestion.Request request) {
final Iterator<? extends Metric> it = request.all();
if (!it.hasNext()) {
throw new IllegalArgumentException("write batch must not be empty");
}
final Metric first = it.next();
long start = first.getTimestamp();
long end = first.getTimestamp();
while (it.hasNext()) {
final Metric d = it.next();
start = Math.min(d.getTimestamp(), start);
end = Math.max(d.getTimestamp(), end);
}
return new DateRange(start, end);
}
}