blob: da65c78ba8a884ee12bbde51705914974bccc45a [file] [log] [blame]
/**
* 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.hadoop.hbase.regionserver.querymatcher;
import java.io.IOException;
import java.util.NavigableSet;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellComparator;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.classification.InterfaceAudience;
import org.apache.hadoop.hbase.regionserver.querymatcher.ScanQueryMatcher.MatchCode;
/**
* This class is used for the tracking and enforcement of columns and numbers of versions during the
* course of a Get or Scan operation, when explicit column qualifiers have been asked for in the
* query. With a little magic (see {@link ScanQueryMatcher}), we can use this matcher for both scans
* and gets. The main difference is 'next' and 'done' collapse for the scan case (since we see all
* columns in order), and we only reset between rows.
* <p>
* This class is utilized by {@link ScanQueryMatcher} mainly through two methods:
* <ul>
* <li>{@link #checkColumn} is called when a Put satisfies all other conditions of the query.</li>
* <li>{@link #getNextRowOrNextColumn} is called whenever ScanQueryMatcher believes that the current
* column should be skipped (by timestamp, filter etc.)</li>
* </ul>
* <p>
* These two methods returns a
* {@link org.apache.hadoop.hbase.regionserver.querymatcher.ScanQueryMatcher.MatchCode} to define
* what action should be taken.
* <p>
* This class is NOT thread-safe as queries are never multi-threaded
*/
@InterfaceAudience.Private
public class ExplicitColumnTracker implements ColumnTracker {
private final int maxVersions;
private final int minVersions;
/**
* Contains the list of columns that the ExplicitColumnTracker is tracking. Each ColumnCount
* instance also tracks how many versions of the requested column have been returned.
*/
private final ColumnCount[] columns;
private int index;
private ColumnCount column;
/**
* Keeps track of the latest timestamp included for current column. Used to eliminate duplicates.
*/
private long latestTSOfCurrentColumn;
private long oldestStamp;
/**
* Default constructor.
* @param columns columns specified user in query
* @param minVersions minimum number of versions to keep
* @param maxVersions maximum versions to return per column
* @param oldestUnexpiredTS the oldest timestamp we are interested in, based on TTL
*/
public ExplicitColumnTracker(NavigableSet<byte[]> columns, int minVersions, int maxVersions,
long oldestUnexpiredTS) {
this.maxVersions = maxVersions;
this.minVersions = minVersions;
this.oldestStamp = oldestUnexpiredTS;
this.columns = new ColumnCount[columns.size()];
int i = 0;
for (byte[] column : columns) {
this.columns[i++] = new ColumnCount(column);
}
reset();
}
/**
* Done when there are no more columns to match against.
*/
public boolean done() {
return this.index >= columns.length;
}
public ColumnCount getColumnHint() {
return this.column;
}
/**
* {@inheritDoc}
*/
@Override
public ScanQueryMatcher.MatchCode checkColumn(Cell cell, byte type) {
// delete markers should never be passed to an
// *Explicit*ColumnTracker
assert !CellUtil.isDelete(type);
do {
// No more columns left, we are done with this query
if (done()) {
return ScanQueryMatcher.MatchCode.SEEK_NEXT_ROW; // done_row
}
// No more columns to match against, done with storefile
if (this.column == null) {
return ScanQueryMatcher.MatchCode.SEEK_NEXT_ROW; // done_row
}
// Compare specific column to current column
int ret = CellComparator.compareQualifiers(cell, column.getBuffer(), column.getOffset(),
column.getLength());
// Column Matches. Return include code. The caller would call checkVersions
// to limit the number of versions.
if (ret == 0) {
return ScanQueryMatcher.MatchCode.INCLUDE;
}
resetTS();
if (ret < 0) {
// The current KV is smaller than the column the ExplicitColumnTracker
// is interested in, so seek to that column of interest.
return ScanQueryMatcher.MatchCode.SEEK_NEXT_COL;
}
// The current KV is bigger than the column the ExplicitColumnTracker
// is interested in. That means there is no more data for the column
// of interest. Advance the ExplicitColumnTracker state to next
// column of interest, and check again.
if (ret > 0) {
++this.index;
if (done()) {
// No more to match, do not include, done with this row.
return ScanQueryMatcher.MatchCode.SEEK_NEXT_ROW; // done_row
}
// This is the recursive case.
this.column = this.columns[this.index];
}
} while (true);
}
@Override
public ScanQueryMatcher.MatchCode checkVersions(Cell cell, long timestamp, byte type,
boolean ignoreCount) throws IOException {
assert !CellUtil.isDelete(type);
if (ignoreCount) {
return ScanQueryMatcher.MatchCode.INCLUDE;
}
// Check if it is a duplicate timestamp
if (sameAsPreviousTS(timestamp)) {
// If duplicate, skip this Key
return ScanQueryMatcher.MatchCode.SKIP;
}
int count = this.column.increment();
if (count >= maxVersions || (count >= minVersions && isExpired(timestamp))) {
// Done with versions for this column
++this.index;
resetTS();
if (done()) {
// We have served all the requested columns.
this.column = null;
return ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_ROW;
}
// We are done with current column; advance to next column
// of interest.
this.column = this.columns[this.index];
return ScanQueryMatcher.MatchCode.INCLUDE_AND_SEEK_NEXT_COL;
}
setTS(timestamp);
return ScanQueryMatcher.MatchCode.INCLUDE;
}
// Called between every row.
public void reset() {
this.index = 0;
this.column = this.columns[this.index];
for (ColumnCount col : this.columns) {
col.setCount(0);
}
resetTS();
}
private void resetTS() {
latestTSOfCurrentColumn = HConstants.LATEST_TIMESTAMP;
}
private void setTS(long timestamp) {
latestTSOfCurrentColumn = timestamp;
}
private boolean sameAsPreviousTS(long timestamp) {
return timestamp == latestTSOfCurrentColumn;
}
private boolean isExpired(long timestamp) {
return timestamp < oldestStamp;
}
/**
* This method is used to inform the column tracker that we are done with this column. We may get
* this information from external filters or timestamp range and we then need to indicate this
* information to tracker. It is required only in case of ExplicitColumnTracker.
* @param cell
*/
public void doneWithColumn(Cell cell) {
while (this.column != null) {
int compare = CellComparator.compareQualifiers(cell, column.getBuffer(), column.getOffset(),
column.getLength());
resetTS();
if (compare >= 0) {
++this.index;
if (done()) {
// Will not hit any more columns in this storefile
this.column = null;
} else {
this.column = this.columns[this.index];
}
if (compare > 0) {
continue;
}
}
return;
}
}
@Override
public MatchCode getNextRowOrNextColumn(Cell cell) {
doneWithColumn(cell);
if (getColumnHint() == null) {
return MatchCode.SEEK_NEXT_ROW;
} else {
return MatchCode.SEEK_NEXT_COL;
}
}
public boolean isDone(long timestamp) {
return minVersions <= 0 && isExpired(timestamp);
}
}