/* * Copyright © 2015 Cask Data, Inc. * * 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 co.cask.cdap.data.stream.service; import co.cask.cdap.api.data.stream.StreamSpecification; import co.cask.cdap.api.metrics.MetricStore; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.stream.notification.StreamSizeNotification; import co.cask.cdap.data.stream.StreamCoordinatorClient; import co.cask.cdap.data.stream.StreamPropertyListener; import co.cask.cdap.data2.transaction.stream.StreamAdmin; import co.cask.cdap.data2.transaction.stream.StreamConfig; import co.cask.cdap.notifications.feeds.NotificationFeedException; import co.cask.cdap.notifications.service.NotificationService; import co.cask.cdap.proto.Id; import com.google.common.collect.Maps; import com.google.inject.Inject; import org.apache.twill.common.Cancellable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FileNotFoundException; import java.util.Map; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; /** * Stream service running in local mode. */ public class LocalStreamService extends AbstractStreamService { private static final Logger LOG = LoggerFactory.getLogger(LocalStreamService.class); private final NotificationService notificationService; private final StreamAdmin streamAdmin; private final StreamWriterSizeCollector streamWriterSizeCollector; private final StreamMetaStore streamMetaStore; private final ConcurrentMap<Id.Stream, StreamSizeAggregator> aggregators; @Inject public LocalStreamService(StreamCoordinatorClient streamCoordinatorClient, StreamFileJanitorService janitorService, StreamMetaStore streamMetaStore, StreamAdmin streamAdmin, StreamWriterSizeCollector streamWriterSizeCollector, NotificationService notificationService, MetricStore metricStore) { super(streamCoordinatorClient, janitorService, streamWriterSizeCollector, metricStore); this.streamAdmin = streamAdmin; this.streamMetaStore = streamMetaStore; this.streamWriterSizeCollector = streamWriterSizeCollector; this.notificationService = notificationService; this.aggregators = Maps.newConcurrentMap(); } @Override protected void initialize() throws Exception { for (Map.Entry<Id.Namespace, StreamSpecification> streamSpecEntry : streamMetaStore.listStreams().entries()) { Id.Stream streamId = Id.Stream.from(streamSpecEntry.getKey(), streamSpecEntry.getValue().getName()); StreamConfig config; try { config = streamAdmin.getConfig(streamId); } catch (FileNotFoundException e) { // TODO: this kind of inconsistency should not happen. [CDAP-5722] LOG.warn("Inconsistent stream state: Stream '{}' exists in meta store " + "but its configuration file does not exist", streamId); continue; } catch (Exception e) { LOG.warn("Inconsistent stream state: Stream '{}' exists in meta store " + "but its configuration cannot be read:", streamId, e); continue; } long eventsSizes = getStreamEventsSize(streamId); createSizeAggregator(streamId, eventsSizes, config.getNotificationThresholdMB()); } } @Override protected void doShutdown() throws Exception { for (StreamSizeAggregator streamSizeAggregator : aggregators.values()) { streamSizeAggregator.cancel(); } } @Override protected void runOneIteration() throws Exception { // Get stream size - which will be the entire size - and send a notification if the size is big enough for (Map.Entry<Id.Namespace, StreamSpecification> streamSpecEntry : streamMetaStore.listStreams().entries()) { Id.Stream streamId = Id.Stream.from(streamSpecEntry.getKey(), streamSpecEntry.getValue().getName()); StreamSizeAggregator streamSizeAggregator = aggregators.get(streamId); try { if (streamSizeAggregator == null) { // First time that we see this Stream here StreamConfig config; try { config = streamAdmin.getConfig(streamId); } catch (FileNotFoundException e) { // this is a stream that has no configuration: ignore it to avoid flooding the logs with exceptions continue; } streamSizeAggregator = createSizeAggregator(streamId, 0, config.getNotificationThresholdMB()); } streamSizeAggregator.checkAggregatedSize(); } catch (Exception e) { // Need to catch and not to propagate the exception, otherwise this scheduled service will be terminated // Just log the exception here as the next run iteration should have the problem fixed LOG.warn("Exception in aggregating stream size for {}", streamId, e); } } } /** * Create a new aggregator for the {@code streamId}, and add it to the existing map of {@link Cancellable} * {@code aggregators}. This method does not cancel previously existing aggregator associated to the * {@code streamId}. * * @param streamId stream name to create a new aggregator for * @param baseCount stream size from which to start aggregating * @param threshold notification threshold after which to publish a notification - in MB * @return the created {@link StreamSizeAggregator} */ private StreamSizeAggregator createSizeAggregator(Id.Stream streamId, long baseCount, int threshold) { // Handle threshold changes final Cancellable thresholdSubscription = getStreamCoordinatorClient().addListener(streamId, new StreamPropertyListener() { @Override public void thresholdChanged(Id.Stream streamId, int threshold) { StreamSizeAggregator aggregator = aggregators.get(streamId); while (aggregator == null) { Thread.yield(); aggregator = aggregators.get(streamId); } aggregator.setStreamThresholdMB(threshold); } }); StreamSizeAggregator newAggregator = new StreamSizeAggregator(streamId, baseCount, threshold, thresholdSubscription); aggregators.put(streamId, newAggregator); return newAggregator; } /** * Aggregate the sizes of all stream writers. A notification is published if the aggregated * size is higher than a threshold. */ private final class StreamSizeAggregator implements Cancellable { private final long streamInitSize; private final Id.NotificationFeed streamFeed; private final Id.Stream streamId; private final AtomicLong streamBaseCount; private final AtomicInteger streamThresholdMB; private final Cancellable cancellable; private boolean published; protected StreamSizeAggregator(Id.Stream streamId, long baseCount, int streamThresholdMB, Cancellable cancellable) { this.streamId = streamId; this.streamInitSize = baseCount; this.streamBaseCount = new AtomicLong(baseCount); this.cancellable = cancellable; this.streamFeed = new Id.NotificationFeed.Builder() .setNamespaceId(streamId.getNamespaceId()) .setCategory(Constants.Notification.Stream.STREAM_FEED_CATEGORY) .setName(String.format("%sSize", streamId.getId())) .build(); this.streamThresholdMB = new AtomicInteger(streamThresholdMB); } @Override public void cancel() { cancellable.cancel(); } /** * Set the notification threshold for the stream that this {@link StreamSizeAggregator} is linked to. * * @param newThreshold new notification threshold, in megabytes */ public void setStreamThresholdMB(int newThreshold) { streamThresholdMB.set(newThreshold); } /** * Check that the aggregated size of the heartbeats received by all Stream writers is higher than some threshold. * If it is, we publish a notification. */ public void checkAggregatedSize() { long sum = streamInitSize + streamWriterSizeCollector.getTotalCollected(streamId); if (!published || sum - streamBaseCount.get() > toBytes(streamThresholdMB.get())) { try { publishNotification(sum); } finally { streamBaseCount.set(sum); } } published = true; } private long toBytes(int mb) { return ((long) mb) * 1024 * 1024; } private void publishNotification(long absoluteSize) { try { notificationService.publish(streamFeed, new StreamSizeNotification(System.currentTimeMillis(), absoluteSize)) .get(); } catch (NotificationFeedException e) { LOG.warn("Error with notification feed {}", streamFeed, e); } catch (Throwable t) { LOG.debug("Could not publish notification on feed {}", streamFeed.getFeedId(), t); } } } }