/** * 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.HorizonDBException; import io.horizondb.db.btree.KeyValueIterator; 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.Filter; import io.horizondb.model.core.Predicate; import io.horizondb.model.core.Projection; import io.horizondb.model.core.Record; import io.horizondb.model.core.ResourceIterator; import io.horizondb.model.schema.DatabaseDefinition; import io.horizondb.model.schema.TimeSeriesDefinition; import java.io.IOException; import java.util.Map.Entry; import java.util.NoSuchElementException; import com.google.common.collect.Range; import com.google.common.collect.RangeMap; import com.google.common.collect.RangeSet; import com.google.common.util.concurrent.ListenableFuture; /** * Represents a time series. */ public final class TimeSeries { /** * The database definition. */ private final DatabaseDefinition databaseDefinition; /** * The time series definition */ private final TimeSeriesDefinition definition; /** * The partition manager. */ private final TimeSeriesPartitionManager partitionManager; /** * Creates a new <code>TimeSeries</code> instance. * * @param databaseDefinition the database to which belongs this time series * @param definition the time series definition * @param partitionManager the partition manager */ public TimeSeries(DatabaseDefinition databaseDefinition, TimeSeriesDefinition definition, TimeSeriesPartitionManager partitionManager) { this.databaseDefinition = databaseDefinition; this.partitionManager = partitionManager; this.definition = definition; } /** * Returns the time series definition. * @return the time series definition. */ public TimeSeriesDefinition getDefinition() { return this.definition; } public void write(DataBlock block, ListenableFuture<ReplayPosition> future, boolean replay) throws IOException, HorizonDBException { RangeMap<Field, DataBlock> blocks = block.split(this.definition); for (Entry<Range<Field>, DataBlock> entry : blocks.asMapOfRanges().entrySet()) { writeToPartition(toPartitionId(entry.getKey()), entry.getValue(), future, replay); } } /** * Returns the records of this time series that match the specified expression. * * @param projection the data that must be returned to the user * @param predicate the predicate used to filter the data * @throws IOException if an I/O problem occurs * @throws HorizonDBException if another problem occurs */ public ResourceIterator<? extends Record> read(Projection projection, Predicate predicate) throws IOException, HorizonDBException { Filter<String> recordTypeFilter = projection.getRecordTypeFilter(this.definition); RangeSet<Field> timeRanges = predicate.getTimestampRanges(); Filter<Record> filter = predicate.toFilter(this.definition); return projection.filterFields(this.definition, read(timeRanges, recordTypeFilter, filter)); } /** * Returns the records of this time series that belong to the specified time ranges and are accepted by the * specified filter. * * @param timeRanges the time ranges for which the data must be read * @throws IOException if an I/O problem occurs * @throws HorizonDBException if another problem occurs */ public ResourceIterator<Record> read(RangeSet<Field> timeRanges, Filter<String> recordTypeFilter, Filter<Record> filter) throws IOException, HorizonDBException { Range<Field> span = timeRanges.span(); final Range<Field> from = this.definition.getPartitionTimeRange(span.lowerEndpoint()); final Range<Field> to; if (from.contains(span.upperEndpoint())) { to = from; } else { to = this.definition.getPartitionTimeRange(span.upperEndpoint()); } KeyValueIterator<PartitionId, TimeSeriesPartition> rangeForRead = this.partitionManager.getRangeForRead(toPartitionId(from), toPartitionId(to), this.definition); return new PartitionRecordIterator(timeRanges, rangeForRead, recordTypeFilter, filter); } /** * Creates the partition ID associated to the specified time range. * * @param range the partition time range * @return the partition ID associated to the specified time range. */ private PartitionId toPartitionId(Range<Field> range) { return new PartitionId(this.databaseDefinition, this.definition, range); } /** * Writes the specified set of records to the specified partition. * * @param partitionId the partition ID * @param block the block containing the records to write * @param future the commit log future * @param replay <code>true</code> if this is a commit log replay * @throws IOException if an I/O problem occurs * @throws HorizonDBException if a problem occurs */ private void writeToPartition(PartitionId partitionId, DataBlock block, ListenableFuture<ReplayPosition> future, boolean replay) throws IOException, HorizonDBException { TimeSeriesPartition partition = this.partitionManager.getPartitionForWrite(partitionId, this.definition); if (replay) { final ReplayPosition currentReplayPosition = FutureUtils.safeGet(future); final ReplayPosition partitionReplayPosition = FutureUtils.safeGet(partition.getFuture()); if (!currentReplayPosition.isAfter(partitionReplayPosition)) { return; } } partition.write(block, future); } /** * <code>RecordIterator</code> used to read records over multiple partitions. */ private final class PartitionRecordIterator implements ResourceIterator<Record> { /** * The time ranges for which data has been requested. */ private final RangeSet<Field> timeRanges; /** * The filter used to filter the records by type. */ private final Filter<String> recordTypeFilter; /** * The filter used to filter data. */ private final Filter<Record> filter; /** * The iterator over the partitions. */ private final KeyValueIterator<PartitionId, TimeSeriesPartition> partitionIterator; /** * The record iterator for the current partition been read. */ private ResourceIterator<Record> recordIterator; /** * Creates a new <code>PartitionRecordIterator</code> to read records from the specified partitions. * * @param timeRanges the time ranges for which data has been requested * @param recordTypeFilter the filter used to filter the records by type. * @param filter the filter used to filter the returned data * @param partitionIterator the partitions. */ public PartitionRecordIterator(RangeSet<Field> rangeSet, KeyValueIterator<PartitionId, TimeSeriesPartition> partitionIterator, Filter<String> recordTypeFilter, Filter<Record> filter) { this.timeRanges = rangeSet; this.recordTypeFilter = recordTypeFilter; this.filter = filter; this.partitionIterator = partitionIterator; } /** * {@inheritDoc} */ @Override public void close() throws IOException { closeRecordIteratorIfNeeded(); } /** * {@inheritDoc} */ @Override public boolean hasNext() throws IOException { if (this.recordIterator != null && this.recordIterator.hasNext()) { return true; } closeRecordIteratorIfNeeded(); while (this.partitionIterator.next()) { Range<Field> range = this.partitionIterator.getKey().getRange(); RangeSet<Field> subRangeSet = this.timeRanges.subRangeSet(range); if (!subRangeSet.isEmpty()) { TimeSeriesPartition partition = this.partitionIterator.getValue(); this.recordIterator = partition.read(subRangeSet, this.recordTypeFilter, this.filter); if (this.recordIterator.hasNext()) { return true; } } } return false; } /** * {@inheritDoc} */ @Override public Record next() throws IOException { if (!hasNext()) { throw new NoSuchElementException(); } return this.recordIterator.next(); } /** * Closes the record iterator if it is not <code>closed</code> yet. * * @throws IOException if an I/O problem occurs. */ private void closeRecordIteratorIfNeeded() throws IOException { if (this.recordIterator != null) { this.recordIterator.close(); } } } }