/** * Copyright 2013 Benjamin Lerer * * 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 io.horizondb.db.series; import io.horizondb.db.Configuration; import io.horizondb.db.cache.AbstractMultilevelCache; import io.horizondb.io.files.FileUtils; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import com.codahale.metrics.Gauge; import com.codahale.metrics.MetricRegistry; import com.google.common.cache.CacheBuilder; import com.google.common.cache.RemovalCause; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.cache.Weigher; import com.google.common.collect.TreeMultimap; /** * Decorator that add caching functionalities to a <code>PartitionManager</code> * * @author Benjamin * */ @ThreadSafe final class TimeSeriesPartitionWriteCache extends AbstractMultilevelCache<PartitionId, TimeSeriesPartition> { /** * The listener used to track the memory usage change. */ private final TimeSeriesPartitionListener listener; /** * The counter used to keep track of the memory usage. */ private final AtomicLong memTimeSeriesMemoryUsage = new AtomicLong(); /** * The partitions per first segment containing non persisted data. */ @GuardedBy("partitionsPerSegment") private final TreeMultimap<Long, TimeSeriesPartition> partitionsPerSegment = TreeMultimap.create(); public TimeSeriesPartitionWriteCache(Configuration configuration, TimeSeriesPartitionSecondLevelCache cache) { super(configuration, cache); this.listener = new TimeSeriesPartitionListener() { @Override public void memoryUsageChanged(TimeSeriesPartition partition, int previousMemoryUsage, int newMemoryUsage) { updateMemoryUsage(previousMemoryUsage, newMemoryUsage); if (newMemoryUsage == 0 && TimeSeriesPartitionWriteCache.this.getIfPresent(partition.getId()) == null) { return; } TimeSeriesPartitionWriteCache.this.put(partition.getId(), partition); } @Override public void firstSegmentContainingNonPersistedDataChanged(TimeSeriesPartition partition, Long previousSegment, Long newSegment) { updatePartitionsPerSegment(partition, previousSegment, newSegment); } }; } /** * {@inheritDoc} */ @Override protected CacheBuilder<PartitionId, TimeSeriesPartition> newBuilder(Configuration configuration) { return CacheBuilder.newBuilder() .maximumWeight(configuration.getMaximumMemoryUsageByMemTimeSeries()) .expireAfterAccess(configuration.getMemTimeSeriesIdleTimeInSeconds(), TimeUnit.SECONDS) .weigher(new Weigher<PartitionId, TimeSeriesPartition>() { /** * {@inheritDoc} */ @Override public int weigh(PartitionId id, TimeSeriesPartition partition) { return partition.getMemoryUsage(); } }) .removalListener(new RemovalListener<PartitionId, TimeSeriesPartition>() { /** * {@inheritDoc} */ @Override public void onRemoval(RemovalNotification<PartitionId, TimeSeriesPartition> notification) { if (RemovalCause.REPLACED.equals(notification.getCause())) { return; } final TimeSeriesPartition partition = notification.getValue(); partition.scheduleForceFlush(new FlushListener() { @Override public void afterFlush() { partition.removeListener(TimeSeriesPartitionWriteCache.this.listener); } }); } }) .recordStats().concurrencyLevel(configuration.getCachesConcurrencyLevel()); } /** * @param id * @return */ public List<TimeSeriesPartition> getPartitionsWithNonPersistedDataWithin(long id) { synchronized(this.partitionsPerSegment) { List<TimeSeriesPartition> partitions = new ArrayList<>(); for (Long segment : this.partitionsPerSegment.keySet()) { if (id < segment.longValue()){ break; } partitions.addAll(this.partitionsPerSegment.get(segment)); } return partitions; } } /** * {@inheritDoc} */ @Override protected void afterLoad(TimeSeriesPartition partition) { partition.addListener(TimeSeriesPartitionWriteCache.this.listener); } /** * {@inheritDoc} */ @Override protected void onRegister(MetricRegistry registry) { registry.register(MetricRegistry.name(getName(), "memTimeSeriesMemoryUsage"), new Gauge<Long>() { /** * {@inheritDoc} */ @Override public Long getValue() { return Long.valueOf(TimeSeriesPartitionWriteCache.this.memTimeSeriesMemoryUsage.get()); } }); } /** * Updates the total memory usage by the time series. * * @param previousMemoryUsage the previous amount of memory being used by the partition * @param newMemoryUsage the new amount of memory being used by the partition * @return the new total memory usage */ private long updateMemoryUsage(int previousMemoryUsage, int newMemoryUsage) { int delta = newMemoryUsage - previousMemoryUsage; long newMemTimeSeriesMemoryUsage = this.memTimeSeriesMemoryUsage.addAndGet(delta); this.logger.debug("memory usage by all memTimeSeries: {}", FileUtils.printNumberOfBytes(newMemTimeSeriesMemoryUsage)); return newMemTimeSeriesMemoryUsage; } /** * Updates the mapping between partitions an the first segment that contains non persisted data. * * @param partition the partition * @param previousSegment the previous first segment for which the partition was containing non persisted data * @param newSegment the new first segment for which the partition contains non persisted data */ private void updatePartitionsPerSegment(TimeSeriesPartition partition, Long previousSegment, Long newSegment) { synchronized(this.partitionsPerSegment) { if (previousSegment != null) { this.partitionsPerSegment.remove(previousSegment, partition); } if (newSegment != null) { this.partitionsPerSegment.put(newSegment, partition); this.logger.debug("first segment containing non persisted data for partition {}: {}", partition.getId(), newSegment); } else { this.logger.debug("partition {} does not contains anymore non persisted data", partition.getId()); } } } }