/** * 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.HorizonDBException; import io.horizondb.db.commitlog.ReplayPosition; import io.horizondb.db.util.concurrent.FutureUtils; import io.horizondb.model.core.DataBlock; import io.horizondb.model.core.Field; import io.horizondb.model.core.Record; import io.horizondb.model.core.ResourceIterator; import io.horizondb.model.core.blocks.RecordAppender; import io.horizondb.model.core.iterators.BinaryTimeSeriesRecordIterator; import io.horizondb.model.core.iterators.BlockIterators; import io.horizondb.model.core.iterators.LoggingRecordIterator; import io.horizondb.model.core.records.BinaryTimeSeriesRecord; import io.horizondb.model.core.records.TimeSeriesRecord; import io.horizondb.model.schema.TimeSeriesDefinition; import java.io.IOException; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.concurrent.Immutable; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; import com.google.common.util.concurrent.ListenableFuture; import static io.horizondb.model.core.iterators.BlockIterators.singleton; import static io.horizondb.model.core.records.BlockHeaderUtils.getUncompressedBlockSize; /** * In-memory part of a time series used to store the writes until they are * flushed to the disk. * */ @Immutable final class MemTimeSeries implements TimeSeriesElement { /** * The database configuration. */ private final Configuration configuration; /** * The time series definition. */ private final TimeSeriesDefinition definition; /** * The data blocks */ private final List<DataBlock> blocks; /** * The last records for each type. */ private final TimeSeriesRecord[] lastRecords; /** * The future associated to the first write. */ private final ListenableFuture<ReplayPosition> firstFuture; /** * The future associated to the latest write. */ private final ListenableFuture<ReplayPosition> lastFuture; /** * The range of the regions used by this <code>MemTimeSeries</code>. */ private final Range<Integer> regionRange; /** * */ public MemTimeSeries(Configuration configuration, TimeSeriesDefinition definition) { this(configuration, definition, Collections.<DataBlock>emptyList(), new TimeSeriesRecord[definition.getNumberOfRecordTypes()], null, null, null); } /** * {@inheritDoc} */ @Override public ListenableFuture<ReplayPosition> getFuture() { return this.lastFuture; } /** * Writes the specified records. * * @param allocator the slab allocator used to reduce heap fragmentation * @param block the block containing the records to write * @param future the future returning the <code>ReplayPosition</code> for this write. * @return the number of records written. * @throws IOException if an I/O problem occurs while writing the records. * @throws HorizonDBException if the one of the records is invalid */ public MemTimeSeries write(SlabAllocator allocator, DataBlock block, ListenableFuture<ReplayPosition> future) throws IOException, HorizonDBException { List<DataBlock> newBlocks = new ArrayList<>(this.blocks); TimeSeriesRecord[] previousRecords = TimeSeriesRecord.deepCopy(this.lastRecords); RecordAppender appender; if (newBlocks.isEmpty()) { appender = new RecordAppender(this.definition, allocator, previousRecords); } else { DataBlock lastBlock = newBlocks.remove(newBlocks.size() - 1); appender = new RecordAppender(this.definition, allocator, previousRecords, lastBlock); } try (BinaryTimeSeriesRecordIterator iterator = new BinaryTimeSeriesRecordIterator(this.definition, singleton(block))) { while (iterator.hasNext()) { BinaryTimeSeriesRecord next = iterator.next(); if (!appender.append(next)) { newBlocks.add(appender.getDataBlock()); appender = new RecordAppender(this.definition, allocator, this.lastRecords); appender.append(next); } } newBlocks.add(appender.getDataBlock()); } return new MemTimeSeries(this.configuration, this.definition, newBlocks, previousRecords, getFirstFuture(future), future, getNewRegionRange(allocator)); } /** * Returns <code>true</code> if this <code>MemTimeSeries</code> is full. * * @return <code>true</code> if this <code>MemTimeSeries</code> is full. * @throws IOException if an I/O problem occurs while computing the block size */ public boolean isFull() throws IOException { return this.getRecordsSize() >= this.configuration.getMemTimeSeriesSize(); } /** * Returns the range of regions used by this <code>MemTimeSeries</code>. * * @return the range of regions used by this <code>MemTimeSeries</code>. */ public Range<Integer> getRegionUsage() { return this.regionRange; } /** * Returns the ID of the first commit log segment which contains data of this <code>MemTimeSeries</code>. * * @return the ID of the first commit log segment which contains data of this <code>MemTimeSeries</code>. */ public long getFirstSegmentId() { return FutureUtils.safeGet(this.firstFuture).getSegment(); } /** * Writes the content of this <code>MemTimeSeries</code> in a readable format into the specified stream. * * @param definition the time series definition * @param stream the stream into which the record representation must be written * @throws IOException if an I/O problem occurs */ public void writePrettyPrint(TimeSeriesDefinition definition, PrintStream stream) throws IOException { try (ResourceIterator<Record> iter = new LoggingRecordIterator(definition, new BinaryTimeSeriesRecordIterator(definition, iterator()), stream)) { while (iter.hasNext()) { iter.next(); } } } /** * Returns the number of data blocks that contains this <code>MemTimeSeries</code>. * * @return the number of data blocks that contains this <code>MemTimeSeries</code>. */ public int getNumberOfBlocks() { return this.blocks.size(); } /** * {@inheritDoc} */ @Override public ResourceIterator<DataBlock> iterator() { return BlockIterators.iterator(this.blocks); } /** * {@inheritDoc} */ @Override public ResourceIterator<DataBlock> iterator(RangeSet<Field> rangeSet) throws IOException { return BlockIterators.filter(rangeSet, iterator()); } /** * Returns the greatest timestamp of this time series element. * * @return the greatest timestamp of this time series element. * @throws IOException if an I/O problem occurs while retrieving the greatest timestamp */ long getGreatestTimestamp() throws IOException { return getGreatestTimestamp(this.lastRecords); } /** * Returns the future of the first set of records written to this <code>MemTimeSeries</code>. * * @param future the future of the last record set written * @return the future of the first set of records written to this <code>MemTimeSeries</code> */ private ListenableFuture<ReplayPosition> getFirstFuture(ListenableFuture<ReplayPosition> future) { if (this.firstFuture == null) { return future; } return this.firstFuture; } /** * Returns the greatest timestamp of the specified records. * * @param records the records * @return the greatest timestamp of this time series element. * @throws IOException if an I/O problem occurs while retrieving the greatest timestamp */ private static long getGreatestTimestamp(TimeSeriesRecord[] records) throws IOException { long maxTimeStamp = 0; for (int i = 0, m = records.length; i < m; i++) { TimeSeriesRecord record = records[i]; if (record == null) { continue; } maxTimeStamp = Math.max(maxTimeStamp, record.getTimestampInNanos(0)); } return maxTimeStamp; } /** * Returns the size in bytes of the records. * * @return the size in bytes of the records. * @throws IOException if an I/O problem occurs while computing the block size */ private int getRecordsSize() throws IOException { int size = 0; for (int i = 0, m = this.blocks.size(); i < m; i++) { Record header = this.blocks.get(i).getHeader(); size += getUncompressedBlockSize(header); } return size; } /** * Returns the new range of region that is being used. * * @param allocator the slab allocator * @return the new range of region that is being used. */ private Range<Integer> getNewRegionRange(SlabAllocator allocator) { Integer regionCount = Integer.valueOf(allocator.getRegionCount()); if (this.regionRange == null) { return Range.closed(regionCount, regionCount); } if (this.regionRange.contains(regionCount)) { return this.regionRange; } return Range.closed(this.regionRange.lowerEndpoint(), regionCount); } /** * */ private MemTimeSeries(Configuration configuration, TimeSeriesDefinition definition, List<DataBlock> blocks, TimeSeriesRecord[] lastRecords, ListenableFuture<ReplayPosition> firstFuture, ListenableFuture<ReplayPosition> lastFuture, Range<Integer> regionRange) { this.configuration = configuration; this.definition = definition; this.blocks = blocks; this.lastRecords = lastRecords; this.firstFuture = firstFuture; this.lastFuture = lastFuture; this.regionRange = regionRange; } }