/** * 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 org.apache.ambari.server.controller.metrics.timeline.cache; import java.io.IOException; import java.util.Date; import java.util.TreeMap; import org.apache.ambari.server.configuration.ComponentSSLConfiguration; import org.apache.ambari.server.configuration.Configuration; import org.apache.ambari.server.controller.internal.URLStreamProvider; import org.apache.ambari.server.controller.metrics.timeline.MetricsRequestHelper; import org.apache.ambari.server.controller.spi.TemporalInfo; import org.apache.hadoop.metrics2.sink.timeline.Precision; import org.apache.hadoop.metrics2.sink.timeline.TimelineMetric; import org.apache.hadoop.metrics2.sink.timeline.TimelineMetrics; import org.apache.http.client.utils.URIBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.Singleton; import net.sf.ehcache.constructs.blocking.UpdatingCacheEntryFactory; @Singleton public class TimelineMetricCacheEntryFactory implements UpdatingCacheEntryFactory { private final static Logger LOG = LoggerFactory.getLogger(TimelineMetricCacheEntryFactory.class); // Not declared final to ease unit test code and allow streamProvider // injection private MetricsRequestHelper requestHelperForGets; private MetricsRequestHelper requestHelperForUpdates; private final Long BUFFER_TIME_DIFF_CATCHUP_INTERVAL; @Inject public TimelineMetricCacheEntryFactory(Configuration configuration) { // Longer timeout for first cache miss requestHelperForGets = new MetricsRequestHelper(new URLStreamProvider( configuration.getMetricsRequestConnectTimeoutMillis(), configuration.getMetricsRequestReadTimeoutMillis(), ComponentSSLConfiguration.instance())); // Timeout setting different from first request timeout // Allows stale data to be returned at the behest of performance. requestHelperForUpdates = new MetricsRequestHelper(new URLStreamProvider( configuration.getMetricsRequestConnectTimeoutMillis(), configuration.getMetricsRequestIntervalReadTimeoutMillis(), ComponentSSLConfiguration.instance())); BUFFER_TIME_DIFF_CATCHUP_INTERVAL = configuration.getMetricRequestBufferTimeCatchupInterval(); } /** * This method is called on a get element from cache call when key is not * found in cache, returns a value for the key to be cached. * * @param key @org.apache.ambari.server.controller.metrics.timeline.cache.TimelineAppMetricCacheKey * @return @org.apache.hadoop.metrics2.sink.timeline.TimelineMetrics * @throws Exception */ @Override public Object createEntry(Object key) throws Exception { LOG.debug("Creating cache entry since none exists, key = " + key); TimelineAppMetricCacheKey metricCacheKey = (TimelineAppMetricCacheKey) key; TimelineMetrics timelineMetrics = null; try { URIBuilder uriBuilder = new URIBuilder(metricCacheKey.getSpec()); timelineMetrics = requestHelperForGets.fetchTimelineMetrics(uriBuilder, metricCacheKey.getTemporalInfo().getStartTimeMillis(), metricCacheKey.getTemporalInfo().getEndTimeMillis()); } catch (IOException io) { LOG.debug("Caught IOException on fetching metrics. " + io.getMessage()); throw io; } TimelineMetricsCacheValue value = null; if (timelineMetrics != null && !timelineMetrics.getMetrics().isEmpty()) { value = new TimelineMetricsCacheValue( metricCacheKey.getTemporalInfo().getStartTime(), metricCacheKey.getTemporalInfo().getEndTime(), timelineMetrics, // Null or empty should prompt a refresh Precision.getPrecision(metricCacheKey.getTemporalInfo().getStartTimeMillis(), metricCacheKey.getTemporalInfo().getEndTimeMillis()) //Initial Precision ); LOG.debug("Created cache entry: " + value); } return value; } /** * Called on a get call for existing values in the cache, * the necessary locking code is present in the get call and this call * should update the value of the cache entry before returning. * * @param key @org.apache.ambari.server.controller.metrics.timeline.cache.TimelineAppMetricCacheKey * @param value @org.apache.hadoop.metrics2.sink.timeline.TimelineMetrics * @throws Exception */ @Override public void updateEntryValue(Object key, Object value) throws Exception { TimelineAppMetricCacheKey metricCacheKey = (TimelineAppMetricCacheKey) key; TimelineMetricsCacheValue existingMetrics = (TimelineMetricsCacheValue) value; LOG.debug("Updating cache entry, key: " + key + ", with value = " + value); Long existingSeriesStartTime = existingMetrics.getStartTime(); Long existingSeriesEndTime = existingMetrics.getEndTime(); TemporalInfo newTemporalInfo = metricCacheKey.getTemporalInfo(); Long requestedStartTime = newTemporalInfo.getStartTimeMillis(); Long requestedEndTime = newTemporalInfo.getEndTimeMillis(); // Calculate new start and end times URIBuilder uriBuilder = new URIBuilder(metricCacheKey.getSpec()); Precision requestedPrecision = Precision.getPrecision(requestedStartTime, requestedEndTime); Precision currentPrecision = existingMetrics.getPrecision(); Long newStartTime = null; Long newEndTime = null; if(!requestedPrecision.equals(currentPrecision)) { // Ignore cache entry. Get the entire data from the AMS and update the cache. LOG.debug("Precision changed from " + currentPrecision + " to " + requestedPrecision); newStartTime = requestedStartTime; newEndTime = requestedEndTime; } else { //Get only the metric values for the delta period from the cache. LOG.debug("No change in precision " + currentPrecision); newStartTime = getRefreshRequestStartTime(existingSeriesStartTime, existingSeriesEndTime, requestedStartTime); newEndTime = getRefreshRequestEndTime(existingSeriesStartTime, existingSeriesEndTime, requestedEndTime); } // Cover complete overlap scenario // time axis: |-------- exSt ----- reqSt ------ reqEnd ----- exEnd ---------| if (newEndTime > newStartTime && !((newStartTime.equals(existingSeriesStartTime) && newEndTime.equals(existingSeriesEndTime)) && requestedPrecision.equals(currentPrecision)) ) { LOG.debug("Existing cached timeseries startTime = " + new Date(getMillisecondsTime(existingSeriesStartTime)) + ", endTime = " + new Date(getMillisecondsTime(existingSeriesEndTime))); LOG.debug("Requested timeseries startTime = " + new Date(getMillisecondsTime(newStartTime)) + ", endTime = " + new Date(getMillisecondsTime(newEndTime))); // Update spec with new start and end time uriBuilder.setParameter("startTime", String.valueOf(newStartTime)); uriBuilder.setParameter("endTime", String.valueOf(newEndTime)); uriBuilder.setParameter("precision",requestedPrecision.toString()); try { TimelineMetrics newTimeSeries = requestHelperForUpdates.fetchTimelineMetrics(uriBuilder, newStartTime, newEndTime); // Update existing time series with new values updateTimelineMetricsInCache(newTimeSeries, existingMetrics, getMillisecondsTime(requestedStartTime), getMillisecondsTime(requestedEndTime), !currentPrecision.equals(requestedPrecision)); // Replace old boundary values existingMetrics.setStartTime(requestedStartTime); existingMetrics.setEndTime(requestedEndTime); existingMetrics.setPrecision(requestedPrecision); } catch (IOException io) { if (LOG.isDebugEnabled()) { LOG.debug("Exception retrieving metrics.", io); } throw io; } } else { LOG.debug("Skip updating cache with new startTime = " + new Date(getMillisecondsTime(newStartTime)) + ", new endTime = " + new Date(getMillisecondsTime(newEndTime))); } } /** * Update cache with new timeseries data */ protected void updateTimelineMetricsInCache(TimelineMetrics newMetrics, TimelineMetricsCacheValue timelineMetricsCacheValue, Long requestedStartTime, Long requestedEndTime, boolean removeAll) { TimelineMetrics existingTimelineMetrics = timelineMetricsCacheValue.getTimelineMetrics(); // Remove values that do not fit before adding new data updateExistingMetricValues(existingTimelineMetrics, requestedStartTime, requestedEndTime, removeAll); if (newMetrics != null && !newMetrics.getMetrics().isEmpty()) { for (TimelineMetric timelineMetric : newMetrics.getMetrics()) { if (LOG.isTraceEnabled()) { TreeMap<Long, Double> sortedMetrics = new TreeMap<>(timelineMetric.getMetricValues()); LOG.trace("New metric: " + timelineMetric.getMetricName() + " # " + timelineMetric.getMetricValues().size() + ", startTime = " + sortedMetrics.firstKey() + ", endTime = " + sortedMetrics.lastKey()); } TimelineMetric existingMetric = null; for (TimelineMetric metric : existingTimelineMetrics.getMetrics()) { if (metric.equalsExceptTime(timelineMetric)) { existingMetric = metric; } } if (existingMetric != null) { // Add new ones existingMetric.getMetricValues().putAll(timelineMetric.getMetricValues()); if (LOG.isTraceEnabled()) { TreeMap<Long, Double> sortedMetrics = new TreeMap<>(existingMetric.getMetricValues()); LOG.trace("Merged metric: " + timelineMetric.getMetricName() + ", " + "Final size: " + existingMetric.getMetricValues().size() + ", startTime = " + sortedMetrics.firstKey() + ", endTime = " + sortedMetrics.lastKey()); } } else { existingTimelineMetrics.getMetrics().add(timelineMetric); } } } } // Remove out of band data from the cache private void updateExistingMetricValues(TimelineMetrics existingMetrics, Long requestedStartTime, Long requestedEndTime, boolean removeAll) { for (TimelineMetric existingMetric : existingMetrics.getMetrics()) { if (removeAll) { existingMetric.setMetricValues(new TreeMap<Long, Double>()); } else { TreeMap<Long, Double> existingMetricValues = existingMetric.getMetricValues(); LOG.trace("Existing metric: " + existingMetric.getMetricName() + " # " + existingMetricValues.size()); // Retain only the values that are within the [requestStartTime, requestedEndTime] window existingMetricValues.headMap(requestedStartTime,false).clear(); existingMetricValues.tailMap(requestedEndTime, false).clear(); } } } // Scenario: Regular graph updates // time axis: |-------- exSt ----- reqSt ------ exEnd ----- reqEnd ---------| // Scenario: Selective graph updates // time axis: |-------- exSt ----- exEnd ------ reqSt ----- reqEnd ---------| // Scenario: Extended time window // time axis: |-------- reSt ----- exSt ------- extEnd ---- reqEnd ---------| protected Long getRefreshRequestStartTime(Long existingSeriesStartTime, Long existingSeriesEndTime, Long requestedStartTime) { Long diff = requestedStartTime - existingSeriesEndTime; Long startTime = requestedStartTime; if (diff < 0 && requestedStartTime > existingSeriesStartTime) { // Regular graph updates // Overlapping timeseries data refresh only new part // Account for missing data on the trailing edge due to buffering startTime = getTimeShiftedStartTime(existingSeriesEndTime); } LOG.trace("Requesting timeseries data with new startTime = " + new Date(getMillisecondsTime(startTime))); return startTime; } // Scenario: Regular graph updates // time axis: |-------- exSt ----- reqSt ------ exEnd ----- reqEnd ---------| // Scenario: Old data request /w overlap // time axis: |-------- reqSt ----- exSt ------ reqEnd ----- extEnd --------| // Scenario: Very Old data request /wo overlap // time axis: |-------- reqSt ----- reqEnd ------ exSt ----- extEnd --------| protected Long getRefreshRequestEndTime(Long existingSeriesStartTime, Long existingSeriesEndTime, Long requestedEndTime) { Long endTime = requestedEndTime; Long diff = requestedEndTime - existingSeriesEndTime; if (diff < 0 && requestedEndTime > existingSeriesStartTime) { // End time overlaps existing timeseries // Get only older data that might not be in the cache endTime = existingSeriesStartTime; } LOG.trace("Requesting timeseries data with new endTime = " + new Date(getMillisecondsTime(endTime))); return endTime; } /** * Time shift by a constant taking into account Epoch vs millis */ private long getTimeShiftedStartTime(long startTime) { if (startTime < 9999999999l) { // Epoch time return startTime - (BUFFER_TIME_DIFF_CATCHUP_INTERVAL / 1000); } else { return startTime - BUFFER_TIME_DIFF_CATCHUP_INTERVAL; } } private long getMillisecondsTime(long time) { if (time < 9999999999l) { return time * 1000; } else { return time; } } }