Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.trino.plugin.pinot.client.PinotClient Maven / Gradle / Ivy
/*
* 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.trino.plugin.pinot.client;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import com.google.common.net.HostAndPort;
import com.google.inject.Inject;
import io.airlift.http.client.HttpClient;
import io.airlift.http.client.HttpUriBuilder;
import io.airlift.http.client.JsonResponseHandler;
import io.airlift.http.client.Request;
import io.airlift.http.client.StaticBodyGenerator;
import io.airlift.http.client.UnexpectedResponseException;
import io.airlift.json.JsonCodec;
import io.airlift.json.JsonCodecBinder;
import io.airlift.json.JsonCodecFactory;
import io.airlift.log.Logger;
import io.trino.cache.NonEvictableLoadingCache;
import io.trino.plugin.pinot.ForPinot;
import io.trino.plugin.pinot.PinotColumnHandle;
import io.trino.plugin.pinot.PinotConfig;
import io.trino.plugin.pinot.PinotErrorCode;
import io.trino.plugin.pinot.PinotException;
import io.trino.plugin.pinot.PinotInsufficientServerResponseException;
import io.trino.plugin.pinot.PinotSessionProperties;
import io.trino.plugin.pinot.auth.PinotBrokerAuthenticationProvider;
import io.trino.plugin.pinot.auth.PinotControllerAuthenticationProvider;
import io.trino.plugin.pinot.query.PinotQueryInfo;
import io.trino.spi.connector.ConnectorSession;
import io.trino.spi.connector.SchemaTableName;
import io.trino.spi.connector.TableNotFoundException;
import org.apache.pinot.common.response.broker.BrokerResponseNative;
import org.apache.pinot.common.response.broker.ResultTable;
import org.apache.pinot.spi.data.Schema;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.cache.CacheLoader.asyncReloading;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.common.net.HttpHeaders.ACCEPT;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static io.airlift.http.client.HttpUriBuilder.uriBuilderFrom;
import static io.airlift.http.client.JsonResponseHandler.createJsonResponseHandler;
import static io.airlift.json.JsonCodec.jsonCodec;
import static io.airlift.json.JsonCodec.listJsonCodec;
import static io.airlift.json.JsonCodec.mapJsonCodec;
import static io.trino.cache.SafeCaches.buildNonEvictableCache;
import static io.trino.plugin.pinot.PinotErrorCode.PINOT_AMBIGUOUS_TABLE_NAME;
import static io.trino.plugin.pinot.PinotErrorCode.PINOT_EXCEPTION;
import static io.trino.plugin.pinot.PinotErrorCode.PINOT_UNABLE_TO_FIND_BROKER;
import static io.trino.plugin.pinot.PinotMetadata.SCHEMA_NAME;
import static java.lang.String.format;
import static java.util.Locale.ENGLISH;
import static java.util.Objects.requireNonNull;
import static java.util.function.UnaryOperator.identity;
import static java.util.stream.Collectors.joining;
import static org.apache.pinot.spi.utils.builder.TableNameBuilder.extractRawTableName;
public class PinotClient
{
private static final Logger LOG = Logger.get(PinotClient.class);
private static final String APPLICATION_JSON = "application/json";
private static final Pattern BROKER_PATTERN = Pattern.compile("Broker_(.*)_(\\d+)");
private static final String TIME_BOUNDARY_NOT_FOUND_ERROR_CODE = "404";
private static final JsonCodec>>> ROUTING_TABLE_CODEC = mapJsonCodec(String.class, mapJsonCodec(String.class, listJsonCodec(String.class)));
private static final Object ALL_TABLES_CACHE_KEY = new Object();
private static final JsonCodec QUERY_REQUEST_JSON_CODEC = jsonCodec(QueryRequest.class);
private static final String GET_ALL_TABLES_API_TEMPLATE = "tables";
private static final String TABLE_INSTANCES_API_TEMPLATE = "tables/%s/instances";
private static final String TABLE_SCHEMA_API_TEMPLATE = "tables/%s/schema";
private static final String ROUTING_TABLE_API_TEMPLATE = "debug/routingTable/%s";
private static final String TIME_BOUNDARY_API_TEMPLATE = "debug/timeBoundary/%s";
private static final String QUERY_URL_PATH = "query/sql";
private final List controllerUrls;
private final Optional brokerHostAndPort;
private final HttpClient httpClient;
private final PinotHostMapper pinotHostMapper;
private final String scheme;
private final boolean proxyEnabled;
private final NonEvictableLoadingCache> brokersForTableCache;
private final NonEvictableLoadingCache> allTablesCache;
private final JsonCodec tablesJsonCodec;
private final JsonCodec brokersForTableJsonCodec;
private final JsonCodec timeBoundaryJsonCodec;
private final JsonCodec schemaJsonCodec;
private final JsonCodec brokerResponseCodec;
private final PinotControllerAuthenticationProvider controllerAuthenticationProvider;
private final PinotBrokerAuthenticationProvider brokerAuthenticationProvider;
@Inject
public PinotClient(
PinotConfig config,
PinotHostMapper pinotHostMapper,
@ForPinot HttpClient httpClient,
@ForPinot ExecutorService executor,
JsonCodec tablesJsonCodec,
JsonCodec brokersForTableJsonCodec,
JsonCodec timeBoundaryJsonCodec,
JsonCodec brokerResponseCodec,
PinotControllerAuthenticationProvider controllerAuthenticationProvider,
PinotBrokerAuthenticationProvider brokerAuthenticationProvider)
{
this.brokersForTableJsonCodec = requireNonNull(brokersForTableJsonCodec, "brokersForTableJsonCodec is null");
this.timeBoundaryJsonCodec = requireNonNull(timeBoundaryJsonCodec, "timeBoundaryJsonCodec is null");
this.tablesJsonCodec = requireNonNull(tablesJsonCodec, "tablesJsonCodec is null");
this.schemaJsonCodec = new JsonCodecFactory(() -> new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)).jsonCodec(Schema.class);
this.brokerResponseCodec = requireNonNull(brokerResponseCodec, "brokerResponseCodec is null");
this.pinotHostMapper = requireNonNull(pinotHostMapper, "pinotHostMapper is null");
this.scheme = config.isTlsEnabled() ? "https" : "http";
this.proxyEnabled = config.getProxyEnabled();
this.controllerUrls = config.getControllerUrls();
this.httpClient = requireNonNull(httpClient, "httpClient is null");
this.brokersForTableCache = buildNonEvictableCache(
CacheBuilder.newBuilder()
.expireAfterWrite(config.getMetadataCacheExpiry().roundTo(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS),
CacheLoader.from(this::getAllBrokersForTable));
this.allTablesCache = buildNonEvictableCache(
CacheBuilder.newBuilder()
.refreshAfterWrite(config.getMetadataCacheExpiry().roundTo(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS),
asyncReloading(CacheLoader.from(this::getAllTables), executor));
this.controllerAuthenticationProvider = controllerAuthenticationProvider;
this.brokerAuthenticationProvider = brokerAuthenticationProvider;
brokerHostAndPort = config.getBrokerUrl();
}
public static void addJsonBinders(JsonCodecBinder jsonCodecBinder)
{
jsonCodecBinder.bindJsonCodec(GetTables.class);
jsonCodecBinder.bindJsonCodec(BrokersForTable.InstancesInBroker.class);
jsonCodecBinder.bindJsonCodec(BrokersForTable.class);
jsonCodecBinder.bindJsonCodec(TimeBoundary.class);
jsonCodecBinder.bindJsonCodec(BrokerResponseNative.class);
}
protected T doHttpActionWithHeadersJson(
Request.Builder requestBuilder,
Optional requestBody,
JsonCodec codec,
Multimap additionalHeaders)
{
requestBuilder.addHeaders(additionalHeaders);
requestBuilder.setHeader(ACCEPT, APPLICATION_JSON);
if (requestBody.isPresent()) {
requestBuilder.setHeader(CONTENT_TYPE, APPLICATION_JSON);
requestBuilder.setBodyGenerator(StaticBodyGenerator.createStaticBodyGenerator(requestBody.get(), StandardCharsets.UTF_8));
}
Request request = requestBuilder.build();
JsonResponseHandler responseHandler = createJsonResponseHandler(codec);
T response = null;
try {
response = httpClient.execute(request, responseHandler);
}
catch (UnexpectedResponseException e) {
throw new PinotException(
PinotErrorCode.PINOT_HTTP_ERROR,
Optional.empty(),
format(
"Unexpected response status: %d for request %s to url %s, with headers %s, full response %s",
e.getStatusCode(),
requestBody.orElse(""),
request.getUri(),
request.getHeaders(),
response));
}
return response;
}
private T sendHttpGetToControllerJson(String path, JsonCodec codec)
{
ImmutableMultimap.Builder additionalHeadersBuilder = ImmutableMultimap.builder();
controllerAuthenticationProvider.getAuthenticationToken().ifPresent(token -> additionalHeadersBuilder.put(AUTHORIZATION, token));
URI controllerPathUri = uriBuilderFrom(getControllerUrl()).appendPath(path).scheme(scheme).build();
return doHttpActionWithHeadersJson(
Request.Builder.prepareGet().setUri(controllerPathUri),
Optional.empty(),
codec,
additionalHeadersBuilder.build());
}
private T sendHttpGetToBrokerJson(String table, String path, JsonCodec codec)
{
ImmutableMultimap.Builder additionalHeadersBuilder = ImmutableMultimap.builder();
brokerAuthenticationProvider.getAuthenticationToken().ifPresent(token -> additionalHeadersBuilder.put(AUTHORIZATION, token));
HttpUriBuilder httpUriBuilder = getBrokerHttpUriBuilder(getBrokerHost(table));
URI brokerPathUri = httpUriBuilder.scheme(scheme).appendPath(path).build();
return doHttpActionWithHeadersJson(
Request.Builder.prepareGet().setUri(brokerPathUri),
Optional.empty(),
codec,
additionalHeadersBuilder.build());
}
private HttpUriBuilder getBrokerHttpUriBuilder(String hostAndPort)
{
return proxyEnabled ?
HttpUriBuilder.uriBuilderFrom(getControllerUrl()) :
HttpUriBuilder.uriBuilder().hostAndPort(HostAndPort.fromString(hostAndPort));
}
private URI getControllerUrl()
{
return controllerUrls.get(ThreadLocalRandom.current().nextInt(controllerUrls.size()));
}
public static class GetTables
{
private final List tables;
@JsonCreator
public GetTables(@JsonProperty("tables") List tables)
{
this.tables = tables;
}
public List getTables()
{
return tables;
}
}
protected Multimap getAllTables()
{
List allTables = sendHttpGetToControllerJson(GET_ALL_TABLES_API_TEMPLATE, tablesJsonCodec).getTables();
ImmutableListMultimap.Builder builder = ImmutableListMultimap.builder();
for (String table : allTables) {
builder.put(table.toLowerCase(ENGLISH), table);
}
return builder.build();
}
public Schema getTableSchema(String table)
throws Exception
{
return sendHttpGetToControllerJson(format(TABLE_SCHEMA_API_TEMPLATE, table), schemaJsonCodec);
}
public List getPinotTableNames()
{
return ImmutableList.copyOf(getFromCache(allTablesCache, ALL_TABLES_CACHE_KEY).keySet());
}
public static V getFromCache(LoadingCache cache, K key)
{
V value = cache.getIfPresent(key);
if (value != null) {
return value;
}
try {
return cache.get(key);
}
catch (ExecutionException e) {
throw new PinotException(PinotErrorCode.PINOT_UNCLASSIFIED_ERROR, Optional.empty(), "Cannot fetch from cache " + key, e.getCause());
}
}
public String getPinotTableNameFromTrinoTableNameIfExists(String trinoTableName)
{
Collection candidates = getFromCache(allTablesCache, ALL_TABLES_CACHE_KEY).get(trinoTableName.toLowerCase(ENGLISH));
if (candidates.isEmpty()) {
return null;
}
if (candidates.size() == 1) {
return getOnlyElement(candidates);
}
throw new PinotException(PINOT_AMBIGUOUS_TABLE_NAME, Optional.empty(), format("Ambiguous table names: %s", candidates.stream().collect(joining(", "))));
}
public String getPinotTableNameFromTrinoTableName(String trinoTableName)
{
String pinotTableName = getPinotTableNameFromTrinoTableNameIfExists(trinoTableName);
if (pinotTableName == null) {
throw new TableNotFoundException(new SchemaTableName(SCHEMA_NAME, trinoTableName));
}
return pinotTableName;
}
public static class BrokersForTable
{
public static class InstancesInBroker
{
private final List instances;
@JsonCreator
public InstancesInBroker(@JsonProperty("instances") List instances)
{
this.instances = instances;
}
@JsonProperty("instances")
public List getInstances()
{
return instances;
}
}
private final List brokers;
@JsonCreator
public BrokersForTable(@JsonProperty("brokers") List brokers)
{
this.brokers = brokers;
}
@JsonProperty("brokers")
public List getBrokers()
{
return brokers;
}
}
@VisibleForTesting
public List getAllBrokersForTable(String table)
{
ArrayList brokers = sendHttpGetToControllerJson(format(TABLE_INSTANCES_API_TEMPLATE, table), brokersForTableJsonCodec)
.getBrokers().stream()
.flatMap(broker -> broker.getInstances().stream())
.distinct()
.map(brokerToParse -> {
Matcher matcher = BROKER_PATTERN.matcher(brokerToParse);
if (matcher.matches() && matcher.groupCount() == 2) {
return pinotHostMapper.getBrokerHost(matcher.group(1), matcher.group(2));
}
throw new PinotException(
PINOT_UNABLE_TO_FIND_BROKER,
Optional.empty(),
format("Cannot parse %s in the broker instance", brokerToParse));
})
.collect(Collectors.toCollection(ArrayList::new));
Collections.shuffle(brokers);
return ImmutableList.copyOf(brokers);
}
public String getBrokerHost(String table)
{
// Use global broker URI if provided explicitly via config
if (brokerHostAndPort.isPresent()) {
return brokerHostAndPort.get().toString();
}
// or fallback to broker discovery mechanism
try {
List brokers = brokersForTableCache.get(table);
if (brokers.isEmpty()) {
throw new PinotException(PINOT_UNABLE_TO_FIND_BROKER, Optional.empty(), "No valid brokers found for " + table);
}
return brokers.get(ThreadLocalRandom.current().nextInt(brokers.size()));
}
catch (ExecutionException e) {
Throwable throwable = e.getCause();
if (throwable instanceof PinotException) {
throw (PinotException) throwable;
}
throw new PinotException(PINOT_UNABLE_TO_FIND_BROKER, Optional.empty(), "Error when getting brokers for table " + table, throwable);
}
}
public Map>> getRoutingTableForTable(String tableName)
{
Map>> routingTable = sendHttpGetToBrokerJson(tableName, format(ROUTING_TABLE_API_TEMPLATE, tableName), ROUTING_TABLE_CODEC);
ImmutableMap.Builder>> routingTableMap = ImmutableMap.builder();
for (Map.Entry>> entry : routingTable.entrySet()) {
String tableNameWithType = entry.getKey();
if (!entry.getValue().isEmpty() && tableName.equals(extractRawTableName(tableNameWithType))) {
ImmutableMap.Builder> segmentBuilder = ImmutableMap.builder();
for (Map.Entry> segmentEntry : entry.getValue().entrySet()) {
if (!segmentEntry.getValue().isEmpty()) {
segmentBuilder.put(segmentEntry.getKey(), segmentEntry.getValue());
}
}
Map> segmentMap = segmentBuilder.buildOrThrow();
if (!segmentMap.isEmpty()) {
routingTableMap.put(tableNameWithType, segmentMap);
}
}
}
return routingTableMap.buildOrThrow();
}
public static class TimeBoundary
{
private final Optional onlineTimePredicate;
private final Optional offlineTimePredicate;
public TimeBoundary()
{
this(null, null);
}
@JsonCreator
public TimeBoundary(
@JsonProperty String timeColumn,
@JsonProperty String timeValue)
{
if (timeColumn != null && timeValue != null) {
// See org.apache.pinot.broker.requesthandler.BaseBrokerRequestHandler::attachTimeBoundary
offlineTimePredicate = Optional.of(format("%s <= %s", timeColumn, timeValue));
onlineTimePredicate = Optional.of(format("%s > %s", timeColumn, timeValue));
}
else {
onlineTimePredicate = Optional.empty();
offlineTimePredicate = Optional.empty();
}
}
public Optional getOnlineTimePredicate()
{
return onlineTimePredicate;
}
public Optional getOfflineTimePredicate()
{
return offlineTimePredicate;
}
}
public TimeBoundary getTimeBoundaryForTable(String table)
{
try {
return sendHttpGetToBrokerJson(table, format(TIME_BOUNDARY_API_TEMPLATE, table), timeBoundaryJsonCodec);
}
catch (Exception e) {
String[] errorMessageSplits = e.getMessage().split(" ");
if (errorMessageSplits.length >= 4 && errorMessageSplits[3].equalsIgnoreCase(TIME_BOUNDARY_NOT_FOUND_ERROR_CODE)) {
return timeBoundaryJsonCodec.fromJson("{}");
}
throw e;
}
}
public static class QueryRequest
{
private final String sql;
@JsonCreator
public QueryRequest(@JsonProperty String sql)
{
this.sql = requireNonNull(sql, "sql is null");
}
@JsonProperty
public String getSql()
{
return sql;
}
}
public interface BrokerResultRow
{
Object getField(int index);
}
private static class ResultRow
implements BrokerResultRow
{
private final Object[] row;
private final int[] indices;
public ResultRow(Object[] row, int[] indices)
{
this.row = requireNonNull(row, "row is null");
this.indices = requireNonNull(indices, "indices is null");
}
@Override
public Object getField(int index)
{
return row[indices[index]];
}
}
public static class ResultsIterator
extends AbstractIterator
{
private final List rows;
private final int[] indices;
private int rowIndex;
private ResultsIterator(List rows, int[] indices)
{
this.rows = requireNonNull(rows, "rows is null");
this.indices = requireNonNull(indices, "indices is null");
}
@Override
protected BrokerResultRow computeNext()
{
if (rowIndex == rows.size()) {
return endOfData();
}
return new ResultRow(rows.get(rowIndex++), indices);
}
}
private BrokerResponseNative submitBrokerQueryJson(ConnectorSession session, PinotQueryInfo query)
{
String queryRequest = QUERY_REQUEST_JSON_CODEC.toJson(new QueryRequest(query.query()));
return doWithRetries(PinotSessionProperties.getPinotRetryCount(session), retryNumber -> {
HttpUriBuilder httpUriBuilder = getBrokerHttpUriBuilder(getBrokerHost(query.table()));
URI queryPathUri = httpUriBuilder
.scheme(scheme)
.appendPath(QUERY_URL_PATH)
.build();
LOG.debug("Query '%s' on broker host '%s'", query.query(), queryPathUri);
Request.Builder builder = Request.Builder.preparePost().setUri(queryPathUri);
ImmutableMultimap.Builder additionalHeadersBuilder = ImmutableMultimap.builder();
brokerAuthenticationProvider.getAuthenticationToken().ifPresent(token -> additionalHeadersBuilder.put(AUTHORIZATION, token));
BrokerResponseNative response = doHttpActionWithHeadersJson(builder, Optional.of(queryRequest), brokerResponseCodec,
additionalHeadersBuilder.build());
if (response.getExceptionsSize() > 0 && response.getProcessingExceptions() != null && !response.getProcessingExceptions().isEmpty()) {
// Pinot is known to return exceptions with benign errorcodes like 200
// so we treat any exception as an error
String processingExceptionMessage = response.getProcessingExceptions().stream()
.map(e -> "code: '%s' message: '%s'".formatted(e.getErrorCode(), e.getMessage()))
.collect(joining(","));
throw new PinotException(
PINOT_EXCEPTION,
Optional.of(query.query()),
format("Query %s encountered exception %s", query.query(), processingExceptionMessage));
}
if (response.getNumServersQueried() == 0 || response.getNumServersResponded() == 0 || response.getNumServersQueried() > response.getNumServersResponded()) {
throw new PinotInsufficientServerResponseException(query, response.getNumServersResponded(), response.getNumServersQueried());
}
return response;
});
}
/**
* columnIndices: column name -> column index from column handles
* indiceToGroupByFunction<Int,String> (groupByFunctions): aggregationIndex -> groupByFunctionName(columnName)
* groupByFunctions is for values
* groupByColumnNames: from aggregationResult.groupByResult.groupByColumnNames()
* aggregationResults[GroupByColumns, GroupByResult]
* GroupByColumns: String[] // column names, i.e. group by foo, bar, baz
* GroupByResult: Row[]
* Row: {group: String[] // values of groupBy columns, value: aggregationResult}
*
* Results: aggregationResults.get(0..aggregationResults.size())
* Result: function, value means columnName -> columnValue
*/
public Iterator createResultIterator(ConnectorSession session, PinotQueryInfo query, List columnHandles)
{
BrokerResponseNative response = submitBrokerQueryJson(session, query);
return fromResultTable(response, columnHandles, query.groupByClauses());
}
@VisibleForTesting
public static ResultsIterator fromResultTable(BrokerResponseNative brokerResponse, List columnHandles, int groupByClauses)
{
requireNonNull(brokerResponse, "brokerResponse is null");
requireNonNull(columnHandles, "columnHandles is null");
ResultTable resultTable = brokerResponse.getResultTable();
String[] columnNames = resultTable.getDataSchema().getColumnNames();
Map columnIndices = IntStream.range(0, columnNames.length)
.boxed()
// Pinot lower cases column names which use aggregate functions, ex. min(my_Col) becomes min(my_col)
.collect(toImmutableMap(i -> columnNames[i].toLowerCase(ENGLISH), identity()));
int[] indices = new int[columnNames.length];
int[] inverseIndices = new int[columnNames.length];
for (int i = 0; i < columnHandles.size(); i++) {
String columnName = columnHandles.get(i).getColumnName().toLowerCase(ENGLISH);
indices[i] = requireNonNull(columnIndices.get(columnName), format("column index for '%s' was not found", columnName));
inverseIndices[indices[i]] = i;
}
List rows = resultTable.getRows();
// If returning from a global aggregation (no grouping columns) over an empty table, NULL-out all aggregation function results except for `count()`
if (groupByClauses == 0 && brokerResponse.getNumDocsScanned() == 0 && resultTable.getRows().size() == 1) {
Object[] originalRow = getOnlyElement(resultTable.getRows());
Object[] newRow = new Object[originalRow.length];
for (int i = 0; i < originalRow.length; i++) {
if (!columnHandles.get(inverseIndices[i]).isReturnNullOnEmptyGroup()) {
newRow[i] = originalRow[i];
}
}
rows = ImmutableList.of(newRow);
}
return new ResultsIterator(rows, indices);
}
public static T doWithRetries(int retries, Function caller)
{
PinotException firstError = null;
checkState(retries > 0, "Invalid num of retries %s", retries);
for (int i = 0; i < retries; ++i) {
try {
return caller.apply(i);
}
catch (PinotException e) {
if (firstError == null) {
firstError = e;
}
if (!e.isRetryable()) {
throw e;
}
}
}
throw firstError;
}
}