All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.druid.query.RetryQueryRunner Maven / Gradle / Ivy

/*
 * 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.druid.query;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.guava.BaseSequence;
import org.apache.druid.java.util.common.guava.BaseSequence.IteratorMaker;
import org.apache.druid.java.util.common.guava.MergeSequence;
import org.apache.druid.java.util.common.guava.Sequence;
import org.apache.druid.java.util.common.guava.Yielder;
import org.apache.druid.java.util.common.guava.YieldingAccumulator;
import org.apache.druid.java.util.common.guava.YieldingSequenceBase;
import org.apache.druid.java.util.common.logger.Logger;
import org.apache.druid.query.context.ResponseContext;
import org.apache.druid.query.context.ResponseContext.Keys;
import org.apache.druid.segment.SegmentMissingException;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;

public class RetryQueryRunner implements QueryRunner
{
  private static final Logger LOG = new Logger(RetryQueryRunner.class);

  private final QueryRunner baseRunner;
  private final BiFunction, List, QueryRunner> retryRunnerCreateFn;
  private final RetryQueryRunnerConfig config;
  private final ObjectMapper jsonMapper;

  /**
   * Runnable executed after the broker creates the query distribution tree for the first attempt. This is only
   * for testing and must not be used in production code.
   */
  private final Runnable runnableAfterFirstAttempt;

  private int totalNumRetries;

  public RetryQueryRunner(
      QueryRunner baseRunner,
      BiFunction, List, QueryRunner> retryRunnerCreateFn,
      RetryQueryRunnerConfig config,
      ObjectMapper jsonMapper
  )
  {
    this(baseRunner, retryRunnerCreateFn, config, jsonMapper, () -> {});
  }

  /**
   * Constructor only for testing.
   */
  @VisibleForTesting
  RetryQueryRunner(
      QueryRunner baseRunner,
      BiFunction, List, QueryRunner> retryRunnerCreateFn,
      RetryQueryRunnerConfig config,
      ObjectMapper jsonMapper,
      Runnable runnableAfterFirstAttempt
  )
  {
    this.baseRunner = baseRunner;
    this.retryRunnerCreateFn = retryRunnerCreateFn;
    this.config = config;
    this.jsonMapper = jsonMapper;
    this.runnableAfterFirstAttempt = runnableAfterFirstAttempt;
  }

  @VisibleForTesting
  int getTotalNumRetries()
  {
    return totalNumRetries;
  }

  @Override
  public Sequence run(final QueryPlus queryPlus, final ResponseContext context)
  {
    // Calling baseRunner.run() (which is SpecificQueryRunnable.run()) in the RetryingSequenceIterator
    // could be better because we can minimize the chance that data servers report missing segments as
    // we construct the query distribution tree when the query processing is actually started.
    // However, we call baseRunner.run() here instead where it's executed while constructing a query plan.
    // This is because ResultLevelCachingQueryRunner requires to compute the cache key based on
    // the segments to query which is computed in SpecificQueryRunnable.run().
    final Sequence baseSequence = baseRunner.run(queryPlus, context);
    // runnableAfterFirstAttempt is only for testing, it must be no-op for production code.
    runnableAfterFirstAttempt.run();

    return new YieldingSequenceBase()
    {
      @Override
      public  Yielder toYielder(OutType initValue, YieldingAccumulator accumulator)
      {
        final Sequence> retryingSequence = new BaseSequence<>(
            new IteratorMaker, RetryingSequenceIterator>()
            {
              @Override
              public RetryingSequenceIterator make()
              {
                return new RetryingSequenceIterator(queryPlus, context, baseSequence);
              }

              @Override
              public void cleanup(RetryingSequenceIterator iterFromMake)
              {
                totalNumRetries = iterFromMake.retryCount;
              }
            }
        );
        return new MergeSequence<>(queryPlus.getQuery().getResultOrdering(), retryingSequence)
            .toYielder(initValue, accumulator);
      }
    };
  }

  private List getMissingSegments(QueryPlus queryPlus, final ResponseContext context)
  {
    // Sanity check before retrieving missingSegments from responseContext.
    // The missingSegments in the responseContext is only valid when all servers have responded to the broker.
    // The remainingResponses MUST be not null but 0 in the responseContext at this point.
    final ConcurrentHashMap idToRemainingResponses =
        Preconditions.checkNotNull(
            context.getRemainingResponses(),
            "%s in responseContext",
            Keys.REMAINING_RESPONSES_FROM_QUERY_SERVERS.getName()
        );

    final int remainingResponses = Preconditions.checkNotNull(
        idToRemainingResponses.get(queryPlus.getQuery().getMostSpecificId()),
        "Number of remaining responses for query[%s]",
        queryPlus.getQuery().getMostSpecificId()
    );
    if (remainingResponses > 0) {
      throw new ISE("Failed to check missing segments due to missing responses from [%d] servers", remainingResponses);
    }

    // TODO: the sender's response may contain a truncated list of missing segments.
    // Truncation is aggregated in the response context given as a parameter.
    // Check the getTruncated() value: if true, then the we don't know the full set of
    // missing segments.
    final List maybeMissingSegments = context.getMissingSegments();
    if (maybeMissingSegments == null) {
      return Collections.emptyList();
    }

    return jsonMapper.convertValue(
        maybeMissingSegments,
        new TypeReference>()
        {
        }
    );
  }

  /**
   * A lazy iterator populating a {@link Sequence} by retrying the query. The first returned sequence is always the base
   * sequence from the baseQueryRunner. Subsequent sequences are created dynamically whenever it retries the query. All
   * the sequences populated by this iterator will be merged (not combined) with the base sequence.
   *
   * The design of this iterator depends on how {@link MergeSequence} works; the MergeSequence pops an item from
   * each underlying sequence and pushes them to a {@link java.util.PriorityQueue}. Whenever it pops from the queue,
   * it pushes a new item from the sequence where the returned item was originally from. Since the first returned
   * sequence from this iterator is always the base sequence, the MergeSequence will call {@link Sequence#toYielder}
   * on the base sequence first which in turn initializes the query distribution tree. Once this tree is built, the query
   * servers (historicals and realtime tasks) will lock all segments to read and report missing segments to the broker.
   * If there are missing segments reported, this iterator will rewrite the query with those reported segments and
   * reissue the rewritten query.
   *
   * @see org.apache.druid.client.CachingClusteredClient
   * @see org.apache.druid.client.DirectDruidClient
   */
  private class RetryingSequenceIterator implements Iterator>
  {
    private final QueryPlus queryPlus;
    private final ResponseContext context;

    private Sequence sequence;
    private int retryCount = 0;

    private RetryingSequenceIterator(
        QueryPlus queryPlus,
        ResponseContext context,
        Sequence baseSequence
    )
    {
      this.queryPlus = queryPlus;
      this.context = context;
      this.sequence = baseSequence;
    }

    @Override
    public boolean hasNext()
    {
      if (sequence != null) {
        return true;
      } else {
        final QueryContext queryContext = queryPlus.getQuery().context();
        final List missingSegments = getMissingSegments(queryPlus, context);
        final int maxNumRetries = queryContext.getNumRetriesOnMissingSegments(
            config.getNumTries()
        );
        if (missingSegments.isEmpty()) {
          return false;
        } else if (retryCount >= maxNumRetries) {
          if (!queryContext.allowReturnPartialResults(config.isReturnPartialResults())) {
            throw new SegmentMissingException("No results found for segments[%s]", missingSegments);
          } else {
            return false;
          }
        } else {
          retryCount++;
          LOG.info("[%,d] missing segments found. Retry attempt [%,d]", missingSegments.size(), retryCount);

          context.initializeMissingSegments();
          final QueryPlus retryQueryPlus = queryPlus.withQuery(
              Queries.withSpecificSegments(queryPlus.getQuery(), missingSegments)
          );
          sequence = retryRunnerCreateFn.apply(retryQueryPlus.getQuery(), missingSegments).run(retryQueryPlus, context);
          return true;
        }
      }
    }

    @Override
    public Sequence next()
    {
      if (!hasNext()) {
        throw new NoSuchElementException();
      }
      final Sequence next = sequence;
      sequence = null;
      return next;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy