// This file is part of OpenTSDB.
// Copyright (C) 2013 The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version. This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
// General Public License for more details. You should have received a copy
// of the GNU Lesser General Public License along with this program. If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.meta;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.opentsdb.core.IncomingDataPoint;
import net.opentsdb.core.Internal;
import net.opentsdb.core.RowKey;
import net.opentsdb.core.TSDB;
import net.opentsdb.core.Tags;
import net.opentsdb.uid.NoSuchUniqueName;
import net.opentsdb.uid.UniqueId;
import net.opentsdb.uid.UniqueId.UniqueIdType;
import net.opentsdb.utils.DateTime;
import org.hbase.async.Bytes.ByteMap;
import org.hbase.async.GetRequest;
import org.hbase.async.KeyValue;
import org.hbase.async.Scanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;
/**
* Methods for querying the tsdb-meta table or finding the last data point of
* a particular time series. This can be used to figure out what
* time series are actually stored as well as fetch TSMeta objects or optimize
* queries against the data table.
* @since 2.1
*/
public class TSUIDQuery {
private static final Logger LOG = LoggerFactory.getLogger(TSUIDQuery.class);
/**
* Charset to use with our server-side row-filter.
* We use this one because it preserves every possible byte unchanged.
*/
private static final Charset CHARSET = Charset.forName("ISO-8859-1");
/** The TSUID that can be set by the caller or after processing the metric */
private byte[] tsuid;
/** The metric set by the caller */
private String metric;
/** The metric UID after lookup */
private byte[] metric_uid;
/** The tags set by the caller */
private Map<String, String> tags;
/** The tag UID list after lookup */
private ArrayList<byte[]> tag_uids;
/** Whether or not to resolve names for last data point queries */
private boolean resolve_names;
/** How far back, in hours, to scan for last data point queries */
private int back_scan;
/** The last timestamp scanned for last data point queries */
private long last_timestamp;
/** The TSDB we belong to. */
private final TSDB tsdb;
/**
* Default CTor just sets the TSDB reference
* @param tsdb The TSDB to use for storage access
* @throws IllegalArgumentException if the TSDB reference is null
* @deprecated Please use one of the other constructors. Will be removed in 2.3
*/
public TSUIDQuery(final TSDB tsdb) {
if (tsdb == null) {
throw new IllegalArgumentException("TSDB reference cannot be null");
}
this.tsdb =tsdb;
}
/**
* CTor used for a TSUID based query when we know exactly what we want
* @param tsdb The TSDB to use for storage access
* @param tsuid A TSUID to use for querying
* @throws IllegalArgumentException if the TSUID is invalid
*/
public TSUIDQuery(final TSDB tsdb, final byte[] tsuid) {
if (tsdb == null) {
throw new IllegalArgumentException("TSDB reference cannot be null");
}
if (tsuid == null || tsuid.length <
TSDB.metrics_width() + TSDB.tagk_width() + TSDB.tagv_width()) {
throw new IllegalArgumentException("TSUID must not be null and must "
+ "have a metric and at least one tag pair");
}
this.tsdb = tsdb;
this.tsuid = tsuid;
}
/**
* CTor used for a metric style query
* @param tsdb The TSDB to use for storage access
* @param metric The metric to look up
* @param tags The tags to lookup. This may be an empty map if you're scanning
* meta.
* @throws IllegalArgumentException if the metric is null, empty or the tag
* map is null.
*/
public TSUIDQuery(final TSDB tsdb, final String metric,
final Map<String, String> tags) {
if (tsdb == null) {
throw new IllegalArgumentException("TSDB reference cannot be null");
}
if (metric == null || metric.isEmpty()) {
throw new IllegalArgumentException("Metric cannot be null or empty");
}
if (tags == null) {
throw new IllegalArgumentException("Tag map cannot be null. Empty is ok");
}
this.tsdb = tsdb;
this.metric = metric;
this.tags = tags;
}
/**
* Attempts to fetch the last data point for the given metric or TSUID.
* If back_scan == 0 and meta is enabled via
* "tsd.core.meta.enable_tsuid_tracking" or
* "tsd.core.meta.enable_tsuid_incrementing" then we will look up the metric
* or TSUID in the meta table first and use the counter there to get the
* last write time.
* <p>
* However if backscan is set, then we'll start with the current time and
* iterate back "back_scan" number of hours until we find a value.
* <p>
* @param resolve_names Whether or not to resolve the UIDs back to their
* names when we find a value.
* @param back_scan The number of hours back in time to scan
* @return A data point if found, null if not. Or an exception if something
* went pear shaped.
*/
public Deferred<IncomingDataPoint> getLastPoint(final boolean resolve_names,
final int back_scan) {
if (back_scan < 0) {
throw new IllegalArgumentException(
"Backscan must be zero or a positive number");
}
this.resolve_names = resolve_names;
this.back_scan = back_scan;
final boolean meta_enabled = tsdb.getConfig().enable_tsuid_tracking() ||
tsdb.getConfig().enable_tsuid_incrementing();
class TSUIDCB implements Callback<Deferred<IncomingDataPoint>, byte[]> {
@Override
public Deferred<IncomingDataPoint> call(final byte[] incoming_tsuid)
throws Exception {
if (tsuid == null && incoming_tsuid == null) {
return Deferred.fromError(new RuntimeException("Both incoming and "
+ "supplied TSUIDs were null for " + TSUIDQuery.this));
} else if (incoming_tsuid != null) {
setTSUID(incoming_tsuid);
}
if (back_scan < 1 && meta_enabled) {
final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid);
get.family(TSMeta.FAMILY());
get.qualifier(TSMeta.COUNTER_QUALIFIER());
return tsdb.getClient().get(get).addCallbackDeferring(new MetaCB());
}
if (last_timestamp > 0) {
last_timestamp = Internal.baseTime(last_timestamp);
} else {
last_timestamp = Internal.baseTime(DateTime.currentTimeMillis());
}
final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp);
final GetRequest get = new GetRequest(tsdb.dataTable(), key);
get.family(TSDB.FAMILY());
return tsdb.getClient().get(get).addCallbackDeferring(new LastPointCB());
}
@Override
public String toString() {
return "TSUID callback";
}
}
if (tsuid == null) {
return tsuidFromMetric(tsdb, metric, tags)
.addCallbackDeferring(new TSUIDCB());
}
try {
// damn typed exceptions....
return new TSUIDCB().call(null);
} catch (Exception e) {
return Deferred.fromError(e);
}
}
/**
* Sets the query to perform
* @param metric Name of the metric to search for
* @param tags A map of tag value pairs or simply an empty map
* @throws NoSuchUniqueName if the metric or any of the tag names/values did
* not exist
* @deprecated Please use one of the constructors instead. Will be removed in 2.3
*/
public void setQuery(final String metric, final Map<String, String> tags) {
this.metric = metric;
this.tags = tags;
metric_uid = tsdb.getUID(UniqueIdType.METRIC, metric);
tag_uids = Tags.resolveAll(tsdb, tags);
}
/**
* Fetches a list of TSUIDs given the metric and optional tag pairs. The query
* format is similar to TsdbQuery but doesn't support grouping operators for
* tags. Only TSUIDs that had "ts_counter" qualifiers will be returned.
* <p>
* NOTE: If you called {@link #setQuery(String, Map)} successfully this will
* immediately scan the meta table. But if you used the CTOR to set the
* metric and tags it will attempt to resolve those and may return an exception.
* @return A map of TSUIDs to the last timestamp (in milliseconds) when the
* "ts_counter" was updated. Note that the timestamp will be the time stored
* by HBase, not the actual timestamp of the data point. If nothing was
* found, the map will be empty but not null.
* @throws IllegalArgumentException if the metric was not set or the tag map
* was null
*/
public Deferred<ByteMap<Long>> getLastWriteTimes() {
class ResolutionCB implements Callback<Deferred<ByteMap<Long>>, Object> {
@Override
public Deferred<ByteMap<Long>> call(Object arg0) throws Exception {
final Scanner scanner = getScanner();
scanner.setQualifier(TSMeta.COUNTER_QUALIFIER());
final Deferred<ByteMap<Long>> results = new Deferred<ByteMap<Long>>();
final ByteMap<Long> tsuids = new ByteMap<Long>();
final class ErrBack implements Callback<Object, Exception> {
@Override
public Object call(final Exception e) throws Exception {
results.callback(e);
return null;
}
@Override
public String toString() {
return "Error callback";
}
}
/**
* Scanner callback that will call itself while iterating through the
* tsdb-meta table
*/
final class ScannerCB implements Callback<Object,
ArrayList<ArrayList<KeyValue>>> {
/**
* Starts the scanner and is called recursively to fetch the next set of
* rows from the scanner.
* @return The map of spans if loaded successfully, null if no data was
* found
*/
public Object scan() {
return scanner.nextRows().addCallback(this).addErrback(new ErrBack());
}
/**
* Loops through each row of the scanner results and parses out data
* points and optional meta data
* @return null if no rows were found, otherwise the TreeMap with spans
*/
@Override
public Object call(final ArrayList<ArrayList<KeyValue>> rows)
throws Exception {
try {
if (rows == null) {
results.callback(tsuids);
return null;
}
for (final ArrayList<KeyValue> row : rows) {
final byte[] tsuid = row.get(0).key();
tsuids.put(tsuid, row.get(0).timestamp());
}
return scan();
} catch (Exception e) {
results.callback(e);
return null;
}
}
}
new ScannerCB().scan();
return results;
}
@Override
public String toString() {
return "Last counter time callback";
}
}
if (metric_uid == null) {
return resolveMetric().addCallbackDeferring(new ResolutionCB());
}
try {
return new ResolutionCB().call(null);
} catch (Exception e) {
return Deferred.fromError(e);
}
}
/**
* Returns all TSMeta objects stored for timeseries defined by this query. The
* query is similar to TsdbQuery without any aggregations. Returns an empty
* list, when no TSMetas are found. Only returns stored TSMetas.
* <p>
* NOTE: If you called {@link #setQuery(String, Map)} successfully this will
* immediately scan the meta table. But if you used the CTOR to set the
* metric and tags it will attempt to resolve those and may return an exception.
* @return A list of existing TSMetas for the timeseries covered by the query.
* @throws IllegalArgumentException When either no metric was specified or the
* tag map was null (Empty map is OK).
*/
public Deferred<List<TSMeta>> getTSMetas() {
class ResolutionCB implements Callback<Deferred<List<TSMeta>>, Object> {
@Override
public Deferred<List<TSMeta>> call(final Object done) throws Exception {
final Scanner scanner = getScanner();
scanner.setQualifier(TSMeta.META_QUALIFIER());
final Deferred<List<TSMeta>> results = new Deferred<List<TSMeta>>();
final List<TSMeta> tsmetas = new ArrayList<TSMeta>();
final List<Deferred<TSMeta>> tsmeta_group = new ArrayList<Deferred<TSMeta>>();
final class TSMetaGroupCB implements Callback<Object,
ArrayList<TSMeta>> {
@Override
public List<TSMeta> call(ArrayList<TSMeta> ts) throws Exception {
for (TSMeta tsm: ts) {
if (tsm != null) {
tsmetas.add(tsm);
}
}
results.callback(tsmetas);
return null;
}
@Override
public String toString() {
return "TSMeta callback";
}
}
final class ErrBack implements Callback<Object, Exception> {
@Override
public Object call(final Exception e) throws Exception {
results.callback(e);
return null;
}
@Override
public String toString() {
return "Error callback";
}
}
/**
* Scanner callback that will call itself while iterating through the
* tsdb-meta table.
*
* Keeps track of a Set of Deferred TSMeta calls. When all rows are scanned,
* will wait for all TSMeta calls to be completed and then create the result
* list.
*/
final class ScannerCB implements Callback<Object,
ArrayList<ArrayList<KeyValue>>> {
/**
* Starts the scanner and is called recursively to fetch the next set of
* rows from the scanner.
* @return The map of spans if loaded successfully, null if no data was
* found
*/
public Object scan() {
return scanner.nextRows().addCallback(this).addErrback(new ErrBack());
}
/**
* Loops through each row of the scanner results and parses out data
* points and optional meta data
* @return null if no rows were found, otherwise the TreeMap with spans
*/
@Override
public Object call(final ArrayList<ArrayList<KeyValue>> rows)
throws Exception {
try {
if (rows == null) {
Deferred.group(tsmeta_group)
.addCallback(new TSMetaGroupCB()).addErrback(new ErrBack());
return null;
}
for (final ArrayList<KeyValue> row : rows) {
tsmeta_group.add(TSMeta.parseFromColumn(tsdb, row.get(0), true));
}
return scan();
} catch (Exception e) {
results.callback(e);
return null;
}
}
}
new ScannerCB().scan();
return results;
}
@Override
public String toString() {
return "TSMeta scan callback";
}
}
if (metric_uid == null) {
return resolveMetric().addCallbackDeferring(new ResolutionCB());
}
try {
return new ResolutionCB().call(null);
} catch (Exception e) {
return Deferred.fromError(e);
}
}
public String toString() {
final StringBuilder buf = new StringBuilder();
buf.append("TSUIDQuery(metric=").append(metric)
.append(", tags=").append(tags)
.append(", tsuid=")
.append(tsuid != null ? UniqueId.uidToString(tsuid) : "null")
.append(", last_timestamp=").append(last_timestamp)
.append(", back_scan=").append(back_scan)
.append(", resolve_names=").append(resolve_names)
.append(")");
return buf.toString();
}
/**
* Converts the given metric and tags to a TSUID by resolving the strings to
* their UIDs. Note that the resulting TSUID may not exist if the combination
* was not written to TSDB
* @param tsdb The TSDB to use for storage access
* @param metric The metric name to resolve
* @param tags The tags to resolve. May not be empty.
* @return A deferred containing the TSUID when ready or an error such as
* a NoSuchUniqueName exception if the metric didn't exist or a
* DeferredGroupException if one of the tag keys or values did not exist.
* @throws IllegalArgumentException if the metric or tags were null or
* empty.
*/
public static Deferred<byte[]> tsuidFromMetric(final TSDB tsdb,
final String metric, final Map<String, String> tags) {
if (metric == null || metric.isEmpty()) {
throw new IllegalArgumentException("The metric cannot be empty");
}
if (tags == null || tags.isEmpty()) {
throw new IllegalArgumentException("Tags cannot be null or empty "
+ "when getting a TSUID");
}
final byte[] metric_uid = new byte[TSDB.metrics_width()];
class TagsCB implements Callback<byte[], ArrayList<byte[]>> {
@Override
public byte[] call(final ArrayList<byte[]> tag_list) throws Exception {
final byte[] tsuid = new byte[metric_uid.length +
((TSDB.tagk_width() + TSDB.tagv_width())
* tag_list.size())];
int idx = 0;
System.arraycopy(metric_uid, 0, tsuid, 0, metric_uid.length);
idx += metric_uid.length;
for (final byte[] t : tag_list) {
System.arraycopy(t, 0, tsuid, idx, t.length);
idx += t.length;
}
return tsuid;
}
@Override
public String toString() {
return "Tag resolution callback";
}
}
class MetricCB implements Callback<Deferred<byte[]>, byte[]> {
@Override
public Deferred<byte[]> call(final byte[] uid)
throws Exception {
System.arraycopy(uid, 0, metric_uid, 0, uid.length);
return Tags.resolveAllAsync(tsdb, tags).addCallback(new TagsCB());
}
@Override
public String toString() {
return "Metric resolution callback";
}
}
return tsdb.getUIDAsync(UniqueIdType.METRIC, metric)
.addCallbackDeferring(new MetricCB());
}
/**
* Attempts to retrieve the last data point for the given TSUID.
* This operates by checking the meta table for the {@link #COUNTER_QUALIFIER}
* and if found, parses the HBase timestamp for the counter (i.e. the time when
* the counter was written) and tries to load the row in the data table for
* the hour where that timestamp would have landed. If the counter does not
* exist or the data row doesn't exist or is empty, the results will be a null
* IncomingDataPoint.
* <b>Note:</b> This will be accurate most of the time since the counter will
* be updated at the time a data point is written within a second or so.
* However if the user is writing historical data, putting data with older
* timestamps or performing an import, then the counter timestamp will be
* inaccurate.
* @param tsdb The TSDB to which we belong
* @param tsuid TSUID to fetch
* @param resolve_names Whether or not to resolve the TSUID to names and
* return them in the IncomingDataPoint
* @param max_lookups If set to a positive integer, will look "max_lookups"
* hours into the past for a valid data point starting with the current hour.
* Setting this to 0 will only use the TSUID counter timestamp.
* @param last_timestamp Optional timestamp in milliseconds when the last data
* point was written
* @return An {@link IncomingDataPoint} if data was found, null if not
* @throws NoSuchUniqueId if one of the tag lookups failed
* @deprecated Please use {@link #getLastPoint}
*/
public static Deferred<IncomingDataPoint> getLastPoint(final TSDB tsdb,
final byte[] tsuid, final boolean resolve_names, final int max_lookups,
final long last_timestamp) {
final TSUIDQuery query = new TSUIDQuery(tsdb, tsuid);
query.last_timestamp = last_timestamp;
return query.getLastPoint(resolve_names, max_lookups);
}
/**
* Resolve the UIDs to names. If the query was for a metric and tags then we
* can just use those.
* @param dp The data point to fill in values for
* @return A deferred with the data point or an exception if something went
* wrong.
*/
private Deferred<IncomingDataPoint> resolveNames(final IncomingDataPoint dp) {
// If the caller gave us a metric and tags, save some time by NOT hitting
// our UID tables or storage.
if (metric != null) {
dp.setMetric(metric);
dp.setTags((HashMap<String, String>)tags);
return Deferred.fromResult(dp);
}
class TagsCB implements Callback<IncomingDataPoint, HashMap<String, String>> {
public IncomingDataPoint call(final HashMap<String, String> tags)
throws Exception {
dp.setTags(tags);
return dp;
}
@Override
public String toString() {
return "Tags resolution CB";
}
}
class MetricCB implements Callback<Deferred<IncomingDataPoint>, String> {
public Deferred<IncomingDataPoint> call(final String name)
throws Exception {
dp.setMetric(name);
final List<byte[]> tags = UniqueId.getTagPairsFromTSUID(tsuid);
return Tags.resolveIdsAsync(tsdb, tags).addCallback(new TagsCB());
}
@Override
public String toString() {
return "Metric resolution CB";
}
}
final byte[] metric_uid = Arrays.copyOfRange(tsuid, 0, TSDB.metrics_width());
return tsdb.getUidName(UniqueIdType.METRIC, metric_uid)
.addCallbackDeferring(new MetricCB());
}
/**
* Handles getting the results of the first GetRequest and keeps iterating
* back in time until we find a point or run out of back scans.
*/
private class LastPointCB implements Callback<Deferred<IncomingDataPoint>,
ArrayList<KeyValue>> {
int iteration = 0;
@Override
public Deferred<IncomingDataPoint> call(final ArrayList<KeyValue> row)
throws Exception {
if (row == null || row.isEmpty()) {
if (iteration >= back_scan) {
if (LOG.isDebugEnabled()) {
LOG.debug("No data points found within the time span for TSUID query "
+ TSUIDQuery.this);
}
return Deferred.fromResult(null);
}
last_timestamp -= 3600;
++iteration;
final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp);
final GetRequest get = new GetRequest(tsdb.dataTable(), key);
get.family(TSDB.FAMILY());
return tsdb.getClient().get(get).addCallbackDeferring(this);
}
final IncomingDataPoint dp =
new Internal.GetLastDataPointCB(tsdb).call(row);
dp.setTSUID(UniqueId.uidToString(tsuid));
if (!resolve_names) {
return Deferred.fromResult(dp);
}
return resolveNames(dp);
}
@Override
public String toString() {
return "LastDataPoint callback";
}
}
/**
* Callback that receives the result of a single meta table lookup to see if
* the last write counter was there or not. The input should contain just
* the counter column
*/
private class MetaCB implements Callback<Deferred<IncomingDataPoint>,
ArrayList<KeyValue>> {
@Override
public Deferred<IncomingDataPoint> call(final ArrayList<KeyValue> row)
throws Exception {
if (row == null) {
return Deferred.fromResult(null);
}
last_timestamp = Internal.baseTime(row.get(0).timestamp());
final byte[] key = RowKey.rowKeyFromTSUID(tsdb, tsuid, last_timestamp);
final GetRequest get = new GetRequest(tsdb.dataTable(), key);
get.family(TSDB.FAMILY());
return tsdb.getClient().get(get).addCallbackDeferring(new LastPointCB());
}
@Override
public String toString() {
return "Meta TSCounter lookup";
}
}
/**
* Resolves the metric and tags (if set) to their UIDs, setting the local
* arrays.
* @return A deferred to wait on or catch exceptions in
* @throws IllegalArgumentException if the metric is empty || null or the
* tag list is null.
*/
private Deferred<Object> resolveMetric() {
if (metric == null || metric.isEmpty()) {
throw new IllegalArgumentException("The metric cannot be empty");
}
if (tags == null) {
throw new IllegalArgumentException("Tags cannot be null or empty "
+ "when getting a TSUID");
}
class TagsCB implements Callback<Object, ArrayList<byte[]>> {
@Override
public Object call(final ArrayList<byte[]> tag_list) throws Exception {
setTagUIDs(tag_list);
return null;
}
@Override
public String toString() {
return "Tag resolution callback";
}
}
class MetricCB implements Callback<Deferred<Object>, byte[]> {
@Override
public Deferred<Object> call(final byte[] uid)
throws Exception {
setMetricUID(uid);
if (tags.isEmpty()) {
setTagUIDs(new ArrayList<byte[]>(0));
return null;
}
return Tags.resolveAllAsync(tsdb, tags).addCallback(new TagsCB());
}
@Override
public String toString() {
return "Metric resolution callback";
}
}
return tsdb.getUIDAsync(UniqueIdType.METRIC, metric)
.addCallbackDeferring(new MetricCB());
}
/** @param tsuid The TSUID to store */
private void setTSUID(final byte[] tsuid) {
this.tsuid = tsuid;
}
/** @param uid The metric UID to set */
private void setMetricUID(final byte[] uid) {
metric_uid = uid;
}
/** @param uids The tag UIDs to set */
private void setTagUIDs(final ArrayList<byte[]> uids) {
tag_uids = uids;
}
/**
* Configures the scanner for a specific metric and optional tags
* @return A configured scanner
*/
private Scanner getScanner() {
final Scanner scanner = tsdb.getClient().newScanner(tsdb.metaTable());
scanner.setStartKey(metric_uid);
// increment the metric UID by one so we can scan all of the rows for the
// given metric
final long stop = UniqueId.uidToLong(metric_uid, TSDB.metrics_width()) + 1;
scanner.setStopKey(UniqueId.longToUID(stop, TSDB.metrics_width()));
scanner.setFamily(TSMeta.FAMILY());
// set the filter if we have tags
if (!tags.isEmpty()) {
final short name_width = TSDB.tagk_width();
final short value_width = TSDB.tagv_width();
final short tagsize = (short) (name_width + value_width);
// Generate a regexp for our tags. Say we have 2 tags: { 0 0 1 0 0 2 }
// and { 4 5 6 9 8 7 }, the regexp will be:
// "^.{7}(?:.{6})*\\Q\000\000\001\000\000\002\\E(?:.{6})*\\Q\004\005\006\011\010\007\\E(?:.{6})*$"
final StringBuilder buf = new StringBuilder(
15 // "^.{N}" + "(?:.{M})*" + "$"
+ ((13 + tagsize) // "(?:.{M})*\\Q" + tagsize bytes + "\\E"
* (tags.size())));
// Alright, let's build this regexp. From the beginning...
buf.append("(?s)" // Ensure we use the DOTALL flag.
+ "^.{")
// ... start by skipping the metric ID.
.append(TSDB.metrics_width())
.append("}");
final Iterator<byte[]> tags = this.tag_uids.iterator();
byte[] tag = tags.hasNext() ? tags.next() : null;
// Tags and group_bys are already sorted. We need to put them in the
// regexp in order by ID, which means we just merge two sorted lists.
do {
// Skip any number of tags.
buf.append("(?:.{").append(tagsize).append("})*\\Q");
UniqueId.addIdToRegexp(buf, tag);
tag = tags.hasNext() ? tags.next() : null;
} while (tag != null); // Stop when they both become null.
// Skip any number of tags before the end.
buf.append("(?:.{").append(tagsize).append("})*$");
scanner.setKeyRegexp(buf.toString(), CHARSET);
}
return scanner;
}
}