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

io.camunda.tasklist.os.RetryOpenSearchClient Maven / Gradle / Ivy

/*
 * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
 * one or more contributor license agreements. See the NOTICE file distributed
 * with this work for additional information regarding copyright ownership.
 * Licensed under the Camunda License 1.0. You may not use this file
 * except in compliance with the Camunda License 1.0.
 */
package io.camunda.tasklist.os;

import static io.camunda.tasklist.util.CollectionUtil.getOrDefaultForNullValue;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.camunda.tasklist.data.conditionals.OpenSearchCondition;
import io.camunda.tasklist.exceptions.TasklistRuntimeException;
import io.camunda.tasklist.property.TasklistProperties;
import io.camunda.tasklist.util.CollectionUtil;
import io.camunda.tasklist.util.OpenSearchUtil;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;
import jakarta.json.stream.JsonParser;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
import net.jodah.failsafe.function.CheckedSupplier;
import org.json.JSONObject;
import org.opensearch.client.Request;
import org.opensearch.client.Response;
import org.opensearch.client.ResponseException;
import org.opensearch.client.RestClient;
import org.opensearch.client.json.JsonpMapper;
import org.opensearch.client.opensearch.OpenSearchClient;
import org.opensearch.client.opensearch._types.HealthStatus;
import org.opensearch.client.opensearch._types.OpenSearchException;
import org.opensearch.client.opensearch._types.Result;
import org.opensearch.client.opensearch._types.Time;
import org.opensearch.client.opensearch._types.query_dsl.Query;
import org.opensearch.client.opensearch.cluster.GetComponentTemplateResponse;
import org.opensearch.client.opensearch.cluster.HealthResponse;
import org.opensearch.client.opensearch.cluster.PutComponentTemplateRequest;
import org.opensearch.client.opensearch.core.*;
import org.opensearch.client.opensearch.core.search.Hit;
import org.opensearch.client.opensearch.indices.*;
import org.opensearch.client.opensearch.ingest.Processor;
import org.opensearch.client.opensearch.tasks.GetTasksResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

@Component
@Conditional(OpenSearchCondition.class)
public class RetryOpenSearchClient {

  public static final String REFRESH_INTERVAL = "index.refresh_interval";
  public static final String NO_REFRESH = "-1";
  public static final String NUMBERS_OF_REPLICA = "index.number_of_replicas";
  public static final String NO_REPLICA = "0";
  public static final String SCROLL_KEEP_ALIVE_MS = "60000ms";
  public static final int DEFAULT_NUMBER_OF_RETRIES =
      30 * 10; // 30*10 with 2 seconds = 10 minutes retry loop
  public static final int DEFAULT_DELAY_INTERVAL_IN_SECONDS = 2;
  private static final Logger LOGGER = LoggerFactory.getLogger(RetryOpenSearchClient.class);
  @Autowired protected RestClient opensearchRestClient;
  @Autowired private OpenSearchClient openSearchClient;
  private int numberOfRetries = DEFAULT_NUMBER_OF_RETRIES;
  private int delayIntervalInSeconds = DEFAULT_DELAY_INTERVAL_IN_SECONDS;
  @Autowired private OpenSearchInternalTask openSearchInternalTask;
  @Autowired private TasklistProperties tasklistProperties;

  public boolean isHealthy() {
    try {
      final HealthResponse response =
          openSearchClient.cluster().health(h -> h.timeout(t -> t.time("500ms")));
      final HealthStatus status = response.status();
      return !response.timedOut() && !status.equals(HealthStatus.Red);
    } catch (final IOException | OpenSearchException e) {
      LOGGER.error(
          String.format(
              "Couldn't connect to OpenSearch due to %s. Return unhealthy state.", e.getMessage()),
          e);
      return false;
    }
  }

  public int getNumberOfRetries() {
    return numberOfRetries;
  }

  public RetryOpenSearchClient setNumberOfRetries(final int numberOfRetries) {
    this.numberOfRetries = numberOfRetries;
    return this;
  }

  public int getDelayIntervalInSeconds() {
    return delayIntervalInSeconds;
  }

  public RetryOpenSearchClient setDelayIntervalInSeconds(final int delayIntervalInSeconds) {
    this.delayIntervalInSeconds = delayIntervalInSeconds;
    return this;
  }

  public void refresh(final String indexPattern) {
    executeWithRetries(
        "Refresh " + indexPattern,
        () -> {
          try {
            for (final var index : getFilteredIndices(indexPattern)) {
              openSearchClient.indices().refresh(r -> r.index(List.of(index)));
            }
          } catch (final IOException e) {
            throw new RuntimeException(e);
          }
          return true;
        });
  }

  public long getNumberOfDocumentsFor(final String... indexPatterns) {
    final CountResponse countResponse =
        executeWithRetries(
            "Count number of documents in " + Arrays.asList(indexPatterns),
            () -> openSearchClient.count(c -> c.index(List.of(indexPatterns))),
            (c) -> c.shards().failures().size() > 0);
    return countResponse.count();
  }

  public Set getIndexNames(final String namePattern) {
    return executeWithRetries(
        "Get indices for " + namePattern,
        () -> {
          try {
            final GetIndexResponse response =
                openSearchClient.indices().get(i -> i.index(List.of(namePattern)));
            return response.result().keySet();
          } catch (final OpenSearchException e) {
            if (e.status() == 404) {
              return Set.of();
            }
            throw e;
          }
        });
  }

  public boolean createIndex(final CreateIndexRequest createIndexRequest) {
    return executeWithRetries(
        "CreateIndex " + createIndexRequest.index(),
        () -> {
          if (!indicesExist(createIndexRequest.index())) {
            return openSearchClient.indices().create(createIndexRequest).acknowledged();
          }

          final String replicas =
              getOrDefaultNumbersOfReplica(createIndexRequest.index(), NO_REPLICA);
          if (!replicas.equals(
              String.valueOf(tasklistProperties.getOpenSearch().getNumberOfReplicas()))) {
            final IndexSettings indexSettings =
                new IndexSettings.Builder()
                    .settings(
                        IndexSettings.of(
                            s ->
                                s.numberOfReplicas(
                                    String.valueOf(
                                        tasklistProperties.getOpenSearch().getNumberOfReplicas()))))
                    .build();
            setIndexSettingsFor(indexSettings, createIndexRequest.index());
          }
          return true;
        });
  }

  public boolean createOrUpdateDocument(final String name, final String id, final Map source) {
    return executeWithRetries(
        () -> {
          final IndexResponse response =
              openSearchClient.index(i -> i.index(name).id(id).document(source));
          final Result result = response.result();
          return result.equals(Result.Created) || result.equals(Result.Updated);
        });
  }

  public boolean documentExists(final String name, final String id) {
    return executeWithGivenRetries(
        10,
        String.format("Exists document from %s with id %s", name, id),
        () -> openSearchClient.exists(e -> e.index(name).id(id)).value(),
        null);
  }

  public Map getDocument(final String name, final String id) {
    return (Map)
        executeWithGivenRetries(
            10,
            String.format("Get document from %s with id %s", name, id),
            () -> {
              final GetRequest request = new GetRequest.Builder().index(name).id(id).build();
              final GetResponse response2 = openSearchClient.get(request, Object.class);
              if (response2.found()) {
                return response2.source();
              } else {
                return null;
              }
            },
            null);
  }

  public boolean deleteDocumentsByQuery(final String indexName, final Query query) {
    return executeWithRetries(
        () -> {
          final DeleteByQueryRequest request =
              new DeleteByQueryRequest.Builder().index(List.of(indexName)).query(query).build();
          final DeleteByQueryResponse response = openSearchClient.deleteByQuery(request);
          return response.failures().isEmpty() && response.deleted() > 0;
        });
  }

  public boolean deleteDocument(final String name, final String id) {
    return executeWithRetries(
        () -> {
          final DeleteResponse response =
              openSearchClient.delete(new DeleteRequest.Builder().index(name).id(id).build());
          final Result result = response.result();
          return result.equals(Result.Deleted);
        });
  }

  private boolean templatesExist(final String templatePattern) throws IOException {
    return openSearchClient.indices().existsIndexTemplate(it -> it.name(templatePattern)).value();
  }

  public boolean createTemplate(final PutIndexTemplateRequest request) {
    return executeWithRetries(
        "CreateTemplate " + request.name(),
        () -> {
          if (!templatesExist(request.name())) {
            return openSearchClient.indices().putIndexTemplate(request).acknowledged();
          }
          return true;
        });
  }

  public boolean deleteTemplatesFor(final String templateNamePattern) {
    return executeWithRetries(
        "DeleteTemplate " + templateNamePattern,
        () -> {
          if (templatesExist(templateNamePattern)) {
            return openSearchClient
                .indices()
                .deleteIndexTemplate(it -> it.name(templateNamePattern))
                .acknowledged();
          }
          return true;
        });
  }

  private boolean indicesExist(final String indexPattern) throws IOException {
    return openSearchClient
        .indices()
        .exists(e -> e.index(List.of(indexPattern)).ignoreUnavailable(true).allowNoIndices(false))
        .value();
  }

  private Set getFilteredIndices(final String indexPattern) throws IOException {
    return openSearchClient.indices().get(i -> i.index(List.of(indexPattern))).result().keySet();
  }

  public boolean deleteIndicesFor(final String indexPattern) {
    return executeWithRetries(
        "DeleteIndices " + indexPattern,
        () -> {
          for (final var index : getFilteredIndices(indexPattern)) {
            openSearchClient.indices().delete(d -> d.index(List.of(indexPattern)));
          }
          return true;
        });
  }

  public IndexSettings getIndexSettingsFor(final String indexName, final String... fields) {
    return executeWithRetries(
        "GetIndexSettings " + indexName,
        () -> {
          final GetIndicesSettingsResponse response =
              openSearchClient.indices().getSettings(s -> s.index(indexName).flatSettings(true));

          return response.result().get(indexName).settings();
        });
  }

  public String getOrDefaultRefreshInterval(final String indexName, final String defaultValue) {
    final IndexSettings settings = getIndexSettingsFor(indexName, REFRESH_INTERVAL);
    String refreshInterval;
    if (settings.refreshInterval() == null) {
      refreshInterval = defaultValue;
    } else {
      refreshInterval = settings.refreshInterval().time();
    }
    if (refreshInterval.trim().equals(NO_REFRESH)) {
      refreshInterval = defaultValue;
    }
    return refreshInterval;
  }

  public String getOrDefaultNumbersOfReplica(final String indexName, final String defaultValue) {
    final IndexSettings settings = getIndexSettingsFor(indexName, NUMBERS_OF_REPLICA);

    String numbersOfReplica;
    if (settings.numberOfReplicas() == null) {
      numbersOfReplica = defaultValue;
    } else {
      numbersOfReplica = settings.numberOfReplicas();
    }
    if (numbersOfReplica.trim().equals(NO_REPLICA)) {
      numbersOfReplica = defaultValue;
    }
    return numbersOfReplica;
  }

  public boolean setIndexSettingsFor(final IndexSettings settings, final String indexPattern) {
    return executeWithRetries(
        "SetIndexSettings " + indexPattern,
        () ->
            openSearchClient
                .indices()
                .putSettings(s -> s.index(indexPattern).settings(settings))
                .acknowledged());
  }

  public boolean addPipeline(final String name, final List processorDefinitions) {
    return executeWithRetries(
        "AddPipeline " + name,
        () -> {
          final List processors =
              processorDefinitions.stream()
                  .map(
                      definition -> {
                        final JsonpMapper mapper = openSearchClient._transport().jsonpMapper();
                        final JsonParser parser =
                            mapper
                                .jsonProvider()
                                .createParser(new ByteArrayInputStream(definition.getBytes()));
                        return Processor._DESERIALIZER.deserialize(parser, mapper);
                      })
                  .collect(Collectors.toList());

          return openSearchClient
              .ingest()
              .putPipeline(i -> i.id(name).processors(processors))
              .acknowledged();
        });
  }

  public boolean removePipeline(final String name) {
    return executeWithRetries(
        "RemovePipeline " + name,
        () -> openSearchClient.ingest().deletePipeline(dp -> dp.id(name)).acknowledged());
  }

  public void reindex(final ReindexRequest reindexRequest) {
    reindex(reindexRequest, true);
  }

  private void refreshAndRetryOnShardFailures(final String indexPattern) {
    executeWithRetries(
        "Refresh " + indexPattern,
        () -> openSearchClient.indices().refresh(r -> r.index(indexPattern)),
        (r) -> r.shards().failures().size() > 0);
  }

  public void reindex(final ReindexRequest reindexRequest, final boolean checkDocumentCount) {
    executeWithRetries(
        "Reindex "
            + Arrays.asList(reindexRequest.source().index())
            + " -> "
            + reindexRequest.dest().index(),
        () -> {
          final var srcIndices = reindexRequest.source().index().get(0);
          final var dstIndex = reindexRequest.dest().index();
          final var srcCount = getNumberOfDocumentsFor(srcIndices);

          final var taskIds =
              openSearchInternalTask.getRunningReindexTasksIdsFor(srcIndices, dstIndex);
          final String taskId;
          if (taskIds == null || taskIds.isEmpty()) {
            if (checkDocumentCount) {
              refreshAndRetryOnShardFailures(dstIndex + "*");
              final var dstCount = getNumberOfDocumentsFor(dstIndex + "*");
              if (srcCount == dstCount) {
                LOGGER.info("Reindex of {} -> {} is already done.", srcIndices, dstIndex);
                return true;
              }
            }
            taskId = openSearchClient.reindex(reindexRequest).task();
          } else {
            LOGGER.info(
                "There is an already running reindex task for [{}] -> [{}]. Will not submit another reindex task but wait for completion of this task",
                srcIndices,
                dstIndex);
            taskId = taskIds.get(0);
          }
          TimeUnit.of(ChronoUnit.MILLIS).sleep(2_000);
          if (checkDocumentCount) {
            return waitUntilTaskIsCompleted(taskId, srcCount);
          } else {
            return waitUntilTaskIsCompleted(taskId);
          }
        },
        done -> !done);
  }

  private boolean waitUntilTaskIsCompleted(final String taskId) {
    return waitUntilTaskIsCompleted(taskId, null);
  }

  // Returns if task is completed under this conditions:
  // - If the response is empty we can immediately return false to force a new reindex in outer
  // retry loop
  // - If the response has a status with uncompleted flag and a sum of changed documents
  // (created,updated and deleted documents) not equal to to total documents
  //   we need to wait and poll again the task status
  private boolean waitUntilTaskIsCompleted(final String taskId, final Long srcCount) {
    final GetTasksResponse taskResponse =
        executeWithGivenRetries(
            Integer.MAX_VALUE,
            "GetTaskInfo{" + taskId + "}",
            () -> {
              final GetTasksResponse tasksResponse =
                  openSearchClient.tasks().get(t -> t.taskId(taskId));
              openSearchInternalTask.checkForErrorsOrFailures(tasksResponse);
              return tasksResponse;
            },
            openSearchInternalTask::needsToPollAgain);
    if (taskResponse != null) {
      final long total = openSearchInternalTask.getTotal(taskResponse);
      LOGGER.info("Source docs: {}, Migrated docs: {}", srcCount, total);
      return total == srcCount;
    } else {
      // need to reindex again
      return false;
    }
  }

  public  List searchWithScroll(
      final SearchRequest searchRequest,
      final Class resultClass,
      final ObjectMapper objectMapper) {
    final long totalHits =
        executeWithRetries(
            "Count search results",
            () -> openSearchClient.search(searchRequest, resultClass).hits().total().value());
    return executeWithRetries(
        "Search with scroll",
        () -> scroll(searchRequest, resultClass, objectMapper),
        resultList -> resultList.size() != totalHits);
  }

  private  List scroll(
      final SearchRequest searchRequest, final Class clazz, final ObjectMapper objectMapper)
      throws IOException {
    final List results = new ArrayList<>();
    SearchResponse response = openSearchClient.search(searchRequest, clazz);

    String scrollId = null;
    while (response.hits().hits().size() > 0) {
      results.addAll(CollectionUtil.map(response.hits().hits(), Hit::source));

      scrollId = response.scrollId();
      final ScrollRequest scrollRequest =
          new ScrollRequest.Builder()
              .scrollId(scrollId)
              .scroll(s -> s.time(SCROLL_KEEP_ALIVE_MS))
              .build();
      response = openSearchClient.scroll(scrollRequest, clazz);
    }
    OpenSearchUtil.clearScroll(scrollId, openSearchClient);
    return results;
  }

  // ------------------- Retry part ------------------
  private  T executeWithRetries(final CheckedSupplier supplier) {
    return executeWithRetries("", supplier, null);
  }

  private  T executeWithRetries(final String operationName, final CheckedSupplier supplier) {
    return executeWithRetries(operationName, supplier, null);
  }

  private  T executeWithRetries(
      final String operationName,
      final CheckedSupplier supplier,
      final Predicate retryPredicate) {
    return executeWithGivenRetries(numberOfRetries, operationName, supplier, retryPredicate);
  }

  private  T executeWithGivenRetries(
      final int retries,
      final String operationName,
      final CheckedSupplier operation,
      final Predicate predicate) {
    try {
      final RetryPolicy retryPolicy =
          new RetryPolicy()
              .handle(IOException.class, OpenSearchException.class)
              .withDelay(Duration.ofSeconds(delayIntervalInSeconds))
              .withMaxAttempts(retries)
              .onRetry(
                  e ->
                      LOGGER.info(
                          "Retrying #{} {} due to {}",
                          e.getAttemptCount(),
                          operationName,
                          e.getLastFailure()))
              .onAbort(e -> LOGGER.error("Abort {} by {}", operationName, e.getFailure()))
              .onRetriesExceeded(
                  e ->
                      LOGGER.error(
                          "Retries {} exceeded for {}", e.getAttemptCount(), operationName));
      if (predicate != null) {
        retryPolicy.handleResultIf(predicate);
      }
      return Failsafe.with(retryPolicy).get(operation);
    } catch (final Exception e) {
      throw new TasklistRuntimeException(
          "Couldn't execute operation "
              + operationName
              + " on opensearch for "
              + numberOfRetries
              + " attempts with "
              + delayIntervalInSeconds
              + " seconds waiting.",
          e);
    }
  }

  public boolean createComponentTemplate(final PutComponentTemplateRequest request) {
    return executeWithRetries(
        "CreateComponentTemplate " + request.name(),
        () -> {
          if (!templatesExist(request.name())
              || !getOrDefaultComponentTemplateNumbersOfReplica(request.name(), NO_REPLICA)
                  .equals(
                      String.valueOf(tasklistProperties.getOpenSearch().getNumberOfReplicas()))) {
            return openSearchClient.cluster().putComponentTemplate(request).acknowledged();
          }
          return false;
        });
  }

  protected Map getComponentTemplateProperties(
      final String templatePattern, final String... fields) {
    return executeWithRetries(
        "GetComponentTemplateSettings " + templatePattern,
        () -> {
          final Map settings = new HashMap<>();
          final GetComponentTemplateResponse response =
              openSearchClient.cluster().getComponentTemplate(ct -> ct.name(templatePattern));
          if (response.componentTemplates().size() > 0) {
            for (final String field : fields) {
              settings.put(
                  field,
                  response
                      .componentTemplates()
                      .get(0)
                      .componentTemplate()
                      .template()
                      .settings()
                      .get(templatePattern)
                      .numberOfReplicas());
            }
          }
          return settings;
        });
  }

  public String getOrDefaultComponentTemplateNumbersOfReplica(
      final String templatePattern, final String defaultValue) {
    final Map settings =
        getComponentTemplateProperties(templatePattern, NUMBERS_OF_REPLICA);
    String numbersOfReplica = getOrDefaultForNullValue(settings, NUMBERS_OF_REPLICA, defaultValue);
    if (numbersOfReplica.trim().equals(NO_REPLICA)) {
      numbersOfReplica = defaultValue;
    }
    return numbersOfReplica;
  }

  public int doWithEachSearchResult(
      final SearchRequest.Builder searchRequest, final Consumer searchHitConsumer) {
    return executeWithRetries(
        () -> {
          int doneOnSearchHits = 0;
          searchRequest.scroll(Time.of(t -> t.time(SCROLL_KEEP_ALIVE_MS)));
          SearchResponse response = openSearchClient.search(searchRequest.build(), Object.class);

          String scrollId = null;
          while (response.hits().hits().size() > 0) {
            response.hits().hits().stream().forEach(searchHitConsumer);
            doneOnSearchHits += response.hits().hits().size();

            scrollId = response.scrollId();
            final ScrollRequest scrollRequest =
                new ScrollRequest.Builder()
                    .scrollId(scrollId)
                    .scroll(Time.of(t -> t.time(SCROLL_KEEP_ALIVE_MS)))
                    .build();

            response = openSearchClient.scroll(scrollRequest, Object.class);
          }
          OpenSearchUtil.clearScroll(scrollId, openSearchClient);
          return doneOnSearchHits;
        });
  }

  public Response getLifecyclePolicy(final String policyName) {
    final Request request = new Request("GET", "/_plugins/_ism/policies/" + policyName);
    try {
      return opensearchRestClient.performRequest(request);
    } catch (final ResponseException e) {
      if (e.getResponse().getStatusLine().getStatusCode() == HttpStatus.NOT_FOUND.value()) {
        return null;
      } else {
        throw new TasklistRuntimeException("Communication error with OpenSearch", e);
      }
    } catch (final IOException e) {
      // Handle other I/O errors
      throw new TasklistRuntimeException("Communication error with OpenSearch", e);
    }
  }

  public Response putLifeCyclePolicy(final String indexName, final String policyName) {
    final Request request = new Request("PUT", indexName + "/_settings");

    final JSONObject settings = new JSONObject();
    final JSONObject indexSettings = new JSONObject();
    indexSettings.put(
        "plugins.index_state_management.policy_id",
        Objects.requireNonNullElse(policyName, JSONObject.NULL));
    settings.put("index", indexSettings);

    request.setJsonEntity(settings.toString());

    try {
      return opensearchRestClient.performRequest(request);
    } catch (final IOException e) {
      throw new TasklistRuntimeException(e);
    }
  }

  public JsonArray getIndexTemplateSettings(final String templatePattern) {
    final Request request =
        new Request("GET", "/_index_template/" + templatePattern); // Change PUT to GET
    try {
      final Response response = opensearchRestClient.performRequest(request);

      // Parse the response entity into a JsonObject and extract the "index_templates" JsonArray
      final InputStream responseStream = response.getEntity().getContent();
      final JsonReader jsonReader = Json.createReader(responseStream);
      final JsonObject responseObject = jsonReader.readObject();
      jsonReader.close();

      return responseObject.getJsonArray(
          "index_templates"); // Ensure this is the correct key based on your API response
    } catch (final ResponseException e) {
      if (e.getResponse().getStatusLine().getStatusCode() == HttpStatus.NOT_FOUND.value()) {
        return null;
      } else {
        throw new TasklistRuntimeException("Communication error with OpenSearch", e);
      }
    } catch (final IOException e) {
      // Handle other I/O errors
      throw new TasklistRuntimeException("Communication error with OpenSearch", e);
    }
  }

  public void putIndexTemplateSettings(final String templateName, final String updateJson)
      throws IOException {
    final Request request = new Request("PUT", "/_index_template/" + templateName);
    request.setJsonEntity(updateJson);
    opensearchRestClient.performRequest(request);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy