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

org.elasticsearch.action.admin.cluster.snapshots.get.TransportGetSnapshotsAction Maven / Gradle / Ivy

There is a newer version: 8.14.0
Show newest version
/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */

package org.elasticsearch.action.admin.cluster.snapshots.get;

import org.apache.lucene.util.CollectionUtil;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.StepListener;
import org.elasticsearch.action.admin.cluster.repositories.get.TransportGetRepositoriesAction;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.GroupedActionListener;
import org.elasticsearch.action.support.master.TransportMasterNodeAction;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.SnapshotsInProgress;
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.metadata.RepositoryMetadata;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.repositories.GetSnapshotInfoContext;
import org.elasticsearch.repositories.IndexId;
import org.elasticsearch.repositories.RepositoriesService;
import org.elasticsearch.repositories.Repository;
import org.elasticsearch.repositories.RepositoryData;
import org.elasticsearch.repositories.RepositoryMissingException;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.snapshots.Snapshot;
import org.elasticsearch.snapshots.SnapshotId;
import org.elasticsearch.snapshots.SnapshotInfo;
import org.elasticsearch.snapshots.SnapshotMissingException;
import org.elasticsearch.snapshots.SnapshotsService;
import org.elasticsearch.tasks.CancellableTask;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.function.ToLongFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Transport Action for get snapshots operation
 */
public class TransportGetSnapshotsAction extends TransportMasterNodeAction {

    private final RepositoriesService repositoriesService;

    @Inject
    public TransportGetSnapshotsAction(
        TransportService transportService,
        ClusterService clusterService,
        ThreadPool threadPool,
        RepositoriesService repositoriesService,
        ActionFilters actionFilters,
        IndexNameExpressionResolver indexNameExpressionResolver
    ) {
        super(
            GetSnapshotsAction.NAME,
            transportService,
            clusterService,
            threadPool,
            actionFilters,
            GetSnapshotsRequest::new,
            indexNameExpressionResolver,
            GetSnapshotsResponse::new,
            ThreadPool.Names.MANAGEMENT // Execute this on the management pool because creating the response can become fairly expensive
                                        // for large repositories in the verbose=false case when there are a lot of indices per snapshot.
                                        // This is intentionally not using the snapshot_meta pool because that pool is sized rather large
                                        // to accommodate concurrent IO and could consume excessive CPU resources through concurrent
                                        // verbose=false requests that are CPU bound only.
        );
        this.repositoriesService = repositoriesService;
    }

    @Override
    protected ClusterBlockException checkBlock(GetSnapshotsRequest request, ClusterState state) {
        return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
    }

    @Override
    protected void masterOperation(GetSnapshotsRequest request, ClusterState state, ActionListener listener)
        throws Exception {
        throw new UnsupportedOperationException("The task parameter is required");
    }

    @Override
    protected void masterOperation(
        final Task task,
        final GetSnapshotsRequest request,
        final ClusterState state,
        final ActionListener listener
    ) {
        assert task instanceof CancellableTask : task + " not cancellable";

        getMultipleReposSnapshotInfo(
            request.isSingleRepositoryRequest() == false,
            state.custom(SnapshotsInProgress.TYPE, SnapshotsInProgress.EMPTY),
            maybeFilterRepositories(
                TransportGetRepositoriesAction.getRepositories(state, request.repositories()),
                request.sort(),
                request.order(),
                request.fromSortValue()
            ),
            request.snapshots(),
            request.ignoreUnavailable(),
            request.verbose(),
            (CancellableTask) task,
            request.sort(),
            request.after(),
            request.offset(),
            request.size(),
            request.order(),
            SnapshotPredicates.fromRequest(request),
            listener
        );
    }

    /**
     * Filters the list of repositories that a request will fetch snapshots from in the special case of sorting by repository
     * name and having a non-null value for {@link GetSnapshotsRequest#fromSortValue()} on the request to exclude repositories outside
     * the sort value range if possible.
     */
    private static List maybeFilterRepositories(
        List repositories,
        GetSnapshotsRequest.SortBy sortBy,
        SortOrder order,
        @Nullable String fromSortValue
    ) {
        if (sortBy != GetSnapshotsRequest.SortBy.REPOSITORY || fromSortValue == null) {
            return repositories;
        }
        final Predicate predicate = order == SortOrder.ASC
            ? repositoryMetadata -> fromSortValue.compareTo(repositoryMetadata.name()) <= 0
            : repositoryMetadata -> fromSortValue.compareTo(repositoryMetadata.name()) >= 0;
        return Collections.unmodifiableList(repositories.stream().filter(predicate).collect(Collectors.toList()));
    }

    private void getMultipleReposSnapshotInfo(
        boolean isMultiRepoRequest,
        SnapshotsInProgress snapshotsInProgress,
        List repos,
        String[] snapshots,
        boolean ignoreUnavailable,
        boolean verbose,
        CancellableTask cancellableTask,
        GetSnapshotsRequest.SortBy sortBy,
        @Nullable GetSnapshotsRequest.After after,
        int offset,
        int size,
        SortOrder order,
        SnapshotPredicates predicates,
        ActionListener listener
    ) {
        // short-circuit if there are no repos, because we can not create GroupedActionListener of size 0
        if (repos.isEmpty()) {
            listener.onResponse(new GetSnapshotsResponse(Collections.emptyList(), Collections.emptyMap(), null, 0, 0));
            return;
        }
        final GroupedActionListener, SnapshotsInRepo>> groupedActionListener =
            new GroupedActionListener<>(listener.map(responses -> {
                assert repos.size() == responses.size();
                final List allSnapshots = responses.stream()
                    .map(Tuple::v2)
                    .filter(Objects::nonNull)
                    .flatMap(snapshotsInRepo -> snapshotsInRepo.snapshotInfos.stream())
                    .collect(Collectors.toList());
                final Map failures = responses.stream()
                    .map(Tuple::v1)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toMap(Tuple::v1, Tuple::v2));
                final SnapshotsInRepo snInfos = sortSnapshots(allSnapshots, sortBy, after, offset, size, order);
                final List snapshotInfos = snInfos.snapshotInfos;
                final int remaining = snInfos.remaining + responses.stream()
                    .map(Tuple::v2)
                    .filter(Objects::nonNull)
                    .mapToInt(s -> s.remaining)
                    .sum();
                return new GetSnapshotsResponse(
                    snapshotInfos,
                    failures,
                    remaining > 0
                        ? GetSnapshotsRequest.After.from(snapshotInfos.get(snapshotInfos.size() - 1), sortBy).asQueryParam()
                        : null,
                    responses.stream().map(Tuple::v2).filter(Objects::nonNull).mapToInt(s -> s.totalCount).sum(),
                    remaining
                );
            }), repos.size());

        for (final RepositoryMetadata repo : repos) {
            final String repoName = repo.name();
            getSingleRepoSnapshotInfo(
                snapshotsInProgress,
                repoName,
                snapshots,
                predicates,
                ignoreUnavailable,
                verbose,
                cancellableTask,
                sortBy,
                after,
                order,
                groupedActionListener.delegateResponse((groupedListener, e) -> {
                    if (isMultiRepoRequest && e instanceof ElasticsearchException) {
                        groupedListener.onResponse(Tuple.tuple(Tuple.tuple(repoName, (ElasticsearchException) e), null));
                    } else {
                        groupedListener.onFailure(e);
                    }
                }).map(snInfos -> Tuple.tuple(null, snInfos))
            );
        }
    }

    private void getSingleRepoSnapshotInfo(
        SnapshotsInProgress snapshotsInProgress,
        String repo,
        String[] snapshots,
        SnapshotPredicates predicates,
        boolean ignoreUnavailable,
        boolean verbose,
        CancellableTask task,
        GetSnapshotsRequest.SortBy sortBy,
        @Nullable final GetSnapshotsRequest.After after,
        SortOrder order,
        ActionListener listener
    ) {
        final Map allSnapshotIds = new HashMap<>();
        final List currentSnapshots = new ArrayList<>();
        for (SnapshotInfo snapshotInfo : currentSnapshots(snapshotsInProgress, repo)) {
            Snapshot snapshot = snapshotInfo.snapshot();
            allSnapshotIds.put(snapshot.getSnapshotId().getName(), snapshot);
            currentSnapshots.add(snapshotInfo);
        }

        final StepListener repositoryDataListener = new StepListener<>();
        if (isCurrentSnapshotsOnly(snapshots)) {
            repositoryDataListener.onResponse(null);
        } else {
            repositoriesService.getRepositoryData(repo, repositoryDataListener);
        }

        repositoryDataListener.whenComplete(
            repositoryData -> loadSnapshotInfos(
                snapshotsInProgress,
                repo,
                snapshots,
                ignoreUnavailable,
                verbose,
                allSnapshotIds,
                currentSnapshots,
                repositoryData,
                task,
                sortBy,
                after,
                order,
                predicates,
                listener
            ),
            listener::onFailure
        );
    }

    /**
     * Returns a list of currently running snapshots from repository sorted by snapshot creation date
     *
     * @param snapshotsInProgress snapshots in progress in the cluster state
     * @param repositoryName repository name
     * @return list of snapshots
     */
    private static List currentSnapshots(SnapshotsInProgress snapshotsInProgress, String repositoryName) {
        List snapshotList = new ArrayList<>();
        List entries = SnapshotsService.currentSnapshots(
            snapshotsInProgress,
            repositoryName,
            Collections.emptyList()
        );
        for (SnapshotsInProgress.Entry entry : entries) {
            snapshotList.add(SnapshotInfo.inProgress(entry));
        }
        return snapshotList;
    }

    private void loadSnapshotInfos(
        SnapshotsInProgress snapshotsInProgress,
        String repo,
        String[] snapshots,
        boolean ignoreUnavailable,
        boolean verbose,
        Map allSnapshotIds,
        List currentSnapshots,
        @Nullable RepositoryData repositoryData,
        CancellableTask task,
        GetSnapshotsRequest.SortBy sortBy,
        @Nullable final GetSnapshotsRequest.After after,
        SortOrder order,
        SnapshotPredicates predicates,
        ActionListener listener
    ) {
        if (task.notifyIfCancelled(listener)) {
            return;
        }

        if (repositoryData != null) {
            for (SnapshotId snapshotId : repositoryData.getSnapshotIds()) {
                if (predicates.test(snapshotId, repositoryData)) {
                    allSnapshotIds.put(snapshotId.getName(), new Snapshot(repo, snapshotId));
                }
            }
        }

        final Set toResolve = new HashSet<>();
        if (TransportGetRepositoriesAction.isMatchAll(snapshots)) {
            toResolve.addAll(allSnapshotIds.values());
        } else {
            final List includePatterns = new ArrayList<>();
            final List excludePatterns = new ArrayList<>();
            boolean hasCurrent = false;
            boolean seenWildcard = false;
            for (String snapshotOrPattern : snapshots) {
                if (seenWildcard && snapshotOrPattern.length() > 1 && snapshotOrPattern.startsWith("-")) {
                    excludePatterns.add(snapshotOrPattern.substring(1));
                } else {
                    if (Regex.isSimpleMatchPattern(snapshotOrPattern)) {
                        seenWildcard = true;
                        includePatterns.add(snapshotOrPattern);
                    } else if (GetSnapshotsRequest.CURRENT_SNAPSHOT.equalsIgnoreCase(snapshotOrPattern)) {
                        hasCurrent = true;
                        seenWildcard = true;
                    } else {
                        if (ignoreUnavailable == false && allSnapshotIds.containsKey(snapshotOrPattern) == false) {
                            throw new SnapshotMissingException(repo, snapshotOrPattern);
                        }
                        includePatterns.add(snapshotOrPattern);
                    }
                }
            }
            final String[] includes = includePatterns.toArray(Strings.EMPTY_ARRAY);
            final String[] excludes = excludePatterns.toArray(Strings.EMPTY_ARRAY);
            for (Map.Entry entry : allSnapshotIds.entrySet()) {
                final Snapshot snapshot = entry.getValue();
                if (toResolve.contains(snapshot) == false
                    && Regex.simpleMatch(includes, entry.getKey())
                    && Regex.simpleMatch(excludes, entry.getKey()) == false) {
                    toResolve.add(snapshot);
                }
            }
            if (hasCurrent) {
                for (SnapshotInfo snapshotInfo : currentSnapshots) {
                    final Snapshot snapshot = snapshotInfo.snapshot();
                    if (Regex.simpleMatch(excludes, snapshot.getSnapshotId().getName()) == false) {
                        toResolve.add(snapshot);
                    }
                }
            }
            if (toResolve.isEmpty() && ignoreUnavailable == false && isCurrentSnapshotsOnly(snapshots) == false) {
                throw new SnapshotMissingException(repo, snapshots[0]);
            }
        }

        if (verbose) {
            snapshots(
                snapshotsInProgress,
                repo,
                Collections.unmodifiableList(toResolve.stream().map(Snapshot::getSnapshotId).collect(Collectors.toList())),
                ignoreUnavailable,
                task,
                sortBy,
                after,
                order,
                predicates,
                listener
            );
        } else {
            assert predicates.isMatchAll() : "filtering is not supported in non-verbose mode";
            final SnapshotsInRepo snapshotInfos;
            if (repositoryData != null) {
                // want non-current snapshots as well, which are found in the repository data
                snapshotInfos = buildSimpleSnapshotInfos(toResolve, repo, repositoryData, currentSnapshots, sortBy, after, order);
            } else {
                // only want current snapshots
                snapshotInfos = sortSnapshots(
                    currentSnapshots.stream().map(SnapshotInfo::basic).collect(Collectors.toList()),
                    sortBy,
                    after,
                    0,
                    GetSnapshotsRequest.NO_LIMIT,
                    order
                );
            }
            listener.onResponse(snapshotInfos);
        }
    }

    /**
     * Returns a list of snapshots from repository sorted by snapshot creation date
     *  @param snapshotsInProgress snapshots in progress in the cluster state
     * @param repositoryName      repository name
     * @param snapshotIds         snapshots for which to fetch snapshot information
     * @param ignoreUnavailable   if true, snapshots that could not be read will only be logged with a warning,
     */
    private void snapshots(
        SnapshotsInProgress snapshotsInProgress,
        String repositoryName,
        Collection snapshotIds,
        boolean ignoreUnavailable,
        CancellableTask task,
        GetSnapshotsRequest.SortBy sortBy,
        @Nullable GetSnapshotsRequest.After after,
        SortOrder order,
        SnapshotPredicates predicate,
        ActionListener listener
    ) {
        if (task.notifyIfCancelled(listener)) {
            return;
        }
        final Set snapshotSet = new HashSet<>();
        final Set snapshotIdsToIterate = new HashSet<>(snapshotIds);
        // first, look at the snapshots in progress
        final List entries = SnapshotsService.currentSnapshots(
            snapshotsInProgress,
            repositoryName,
            snapshotIdsToIterate.stream().map(SnapshotId::getName).collect(Collectors.toList())
        );
        for (SnapshotsInProgress.Entry entry : entries) {
            if (snapshotIdsToIterate.remove(entry.snapshot().getSnapshotId())) {
                final SnapshotInfo snapshotInfo = SnapshotInfo.inProgress(entry);
                if (predicate.test(snapshotInfo)) {
                    snapshotSet.add(SnapshotInfo.inProgress(entry));
                }
            }
        }
        // then, look in the repository if there's any matching snapshots left
        final List snapshotInfos;
        if (snapshotIdsToIterate.isEmpty()) {
            snapshotInfos = Collections.emptyList();
        } else {
            snapshotInfos = Collections.synchronizedList(new ArrayList<>());
        }
        final ActionListener allDoneListener = listener.delegateFailure((l, v) -> {
            final ArrayList snapshotList = new ArrayList<>(snapshotInfos);
            snapshotList.addAll(snapshotSet);
            listener.onResponse(sortSnapshots(snapshotList, sortBy, after, 0, GetSnapshotsRequest.NO_LIMIT, order));
        });
        if (snapshotIdsToIterate.isEmpty()) {
            allDoneListener.onResponse(null);
            return;
        }
        final Repository repository;
        try {
            repository = repositoriesService.repository(repositoryName);
        } catch (RepositoryMissingException e) {
            listener.onFailure(e);
            return;
        }
        repository.getSnapshotInfo(
            new GetSnapshotInfoContext(snapshotIdsToIterate, ignoreUnavailable == false, task::isCancelled, (context, snapshotInfo) -> {
                if (predicate.test(snapshotInfo)) {
                    snapshotInfos.add(snapshotInfo);
                }
            }, allDoneListener)
        );
    }

    private boolean isCurrentSnapshotsOnly(String[] snapshots) {
        return (snapshots.length == 1 && GetSnapshotsRequest.CURRENT_SNAPSHOT.equalsIgnoreCase(snapshots[0]));
    }

    private static SnapshotsInRepo buildSimpleSnapshotInfos(
        final Set toResolve,
        final String repoName,
        final RepositoryData repositoryData,
        final List currentSnapshots,
        final GetSnapshotsRequest.SortBy sortBy,
        @Nullable final GetSnapshotsRequest.After after,
        final SortOrder order
    ) {
        List snapshotInfos = new ArrayList<>();
        for (SnapshotInfo snapshotInfo : currentSnapshots) {
            if (toResolve.remove(snapshotInfo.snapshot())) {
                snapshotInfos.add(snapshotInfo.basic());
            }
        }
        Map> snapshotsToIndices = new HashMap<>();
        for (IndexId indexId : repositoryData.getIndices().values()) {
            for (SnapshotId snapshotId : repositoryData.getSnapshots(indexId)) {
                if (toResolve.contains(new Snapshot(repoName, snapshotId))) {
                    snapshotsToIndices.computeIfAbsent(snapshotId, (k) -> new ArrayList<>()).add(indexId.getName());
                }
            }
        }
        for (Snapshot snapshot : toResolve) {
            final List indices = snapshotsToIndices.getOrDefault(snapshot.getSnapshotId(), Collections.emptyList());
            CollectionUtil.timSort(indices);
            snapshotInfos.add(
                new SnapshotInfo(
                    snapshot,
                    indices,
                    Collections.emptyList(),
                    Collections.emptyList(),
                    repositoryData.getSnapshotState(snapshot.getSnapshotId())
                )
            );
        }
        return sortSnapshots(snapshotInfos, sortBy, after, 0, GetSnapshotsRequest.NO_LIMIT, order);
    }

    private static final Comparator BY_START_TIME = Comparator.comparingLong(SnapshotInfo::startTime)
        .thenComparing(SnapshotInfo::snapshotId);

    private static final Comparator BY_DURATION = Comparator.comparingLong(
        sni -> sni.endTime() - sni.startTime()
    ).thenComparing(SnapshotInfo::snapshotId);

    private static final Comparator BY_INDICES_COUNT = Comparator.comparingInt(sni -> sni.indices().size())
        .thenComparing(SnapshotInfo::snapshotId);

    private static final Comparator BY_SHARDS_COUNT = Comparator.comparingInt(SnapshotInfo::totalShards)
        .thenComparing(SnapshotInfo::snapshotId);

    private static final Comparator BY_FAILED_SHARDS_COUNT = Comparator.comparingInt(SnapshotInfo::failedShards)
        .thenComparing(SnapshotInfo::snapshotId);

    private static final Comparator BY_NAME = Comparator.comparing(sni -> sni.snapshotId().getName());

    private static final Comparator BY_REPOSITORY = Comparator.comparing(SnapshotInfo::repository)
        .thenComparing(SnapshotInfo::snapshotId);

    private static long getDuration(SnapshotId snapshotId, RepositoryData repositoryData) {
        final RepositoryData.SnapshotDetails details = repositoryData.getSnapshotDetails(snapshotId);
        if (details == null) {
            return -1;
        }
        final long startTime = details.getStartTimeMillis();
        if (startTime == -1) {
            return -1;
        }
        final long endTime = details.getEndTimeMillis();
        if (endTime == -1) {
            return -1;
        }
        return endTime - startTime;
    }

    private static long getStartTime(SnapshotId snapshotId, RepositoryData repositoryData) {
        final RepositoryData.SnapshotDetails details = repositoryData.getSnapshotDetails(snapshotId);
        return details == null ? -1 : details.getStartTimeMillis();
    }

    private static int indexCount(SnapshotId snapshotId, RepositoryData repositoryData) {
        // TODO: this could be made more efficient by caching this number in RepositoryData
        int indexCount = 0;
        for (IndexId idx : repositoryData.getIndices().values()) {
            if (repositoryData.getSnapshots(idx).contains(snapshotId)) {
                indexCount++;
            }
        }
        return indexCount;
    }

    private static SnapshotsInRepo sortSnapshots(
        List snapshotInfos,
        GetSnapshotsRequest.SortBy sortBy,
        @Nullable GetSnapshotsRequest.After after,
        int offset,
        int size,
        SortOrder order
    ) {
        final Comparator comparator;
        switch (sortBy) {
            case START_TIME:
                comparator = BY_START_TIME;
                break;
            case NAME:
                comparator = BY_NAME;
                break;
            case DURATION:
                comparator = BY_DURATION;
                break;
            case INDICES:
                comparator = BY_INDICES_COUNT;
                break;
            case SHARDS:
                comparator = BY_SHARDS_COUNT;
                break;
            case FAILED_SHARDS:
                comparator = BY_FAILED_SHARDS_COUNT;
                break;
            case REPOSITORY:
                comparator = BY_REPOSITORY;
                break;
            default:
                throw new AssertionError("unexpected sort column [" + sortBy + "]");
        }

        Stream infos = snapshotInfos.stream();

        if (after != null) {
            assert offset == 0 : "can't combine after and offset but saw [" + after + "] and offset [" + offset + "]";
            infos = infos.filter(buildAfterPredicate(sortBy, after, order));
        }
        infos = infos.sorted(order == SortOrder.DESC ? comparator.reversed() : comparator).skip(offset);
        final List allSnapshots = infos.collect(Collectors.toList());
        final List snapshots;
        if (size != GetSnapshotsRequest.NO_LIMIT) {
            snapshots = Collections.unmodifiableList(allSnapshots.stream().limit(size + 1).collect(Collectors.toList()));
        } else {
            snapshots = allSnapshots;
        }
        final List resultSet = size != GetSnapshotsRequest.NO_LIMIT && size < snapshots.size()
            ? snapshots.subList(0, size)
            : snapshots;
        return new SnapshotsInRepo(resultSet, snapshotInfos.size(), allSnapshots.size() - resultSet.size());
    }

    private static Predicate buildAfterPredicate(
        GetSnapshotsRequest.SortBy sortBy,
        GetSnapshotsRequest.After after,
        SortOrder order
    ) {
        final String snapshotName = after.snapshotName();
        final String repoName = after.repoName();
        final String value = after.value();
        switch (sortBy) {
            case START_TIME:
                return filterByLongOffset(SnapshotInfo::startTime, Long.parseLong(value), snapshotName, repoName, order);
            case NAME:
                // TODO: cover via pre-flight predicate
                return order == SortOrder.ASC
                    ? (info -> compareName(snapshotName, repoName, info) < 0)
                    : (info -> compareName(snapshotName, repoName, info) > 0);
            case DURATION:
                return filterByLongOffset(info -> info.endTime() - info.startTime(), Long.parseLong(value), snapshotName, repoName, order);
            case INDICES:
                // TODO: cover via pre-flight predicate
                return filterByLongOffset(info -> info.indices().size(), Integer.parseInt(value), snapshotName, repoName, order);
            case SHARDS:
                return filterByLongOffset(SnapshotInfo::totalShards, Integer.parseInt(value), snapshotName, repoName, order);
            case FAILED_SHARDS:
                return filterByLongOffset(SnapshotInfo::failedShards, Integer.parseInt(value), snapshotName, repoName, order);
            case REPOSITORY:
                // TODO: cover via pre-flight predicate
                return order == SortOrder.ASC
                    ? (info -> compareRepositoryName(snapshotName, repoName, info) < 0)
                    : (info -> compareRepositoryName(snapshotName, repoName, info) > 0);
            default:
                throw new AssertionError("unexpected sort column [" + sortBy + "]");
        }
    }

    private static Predicate filterByLongOffset(
        ToLongFunction extractor,
        long after,
        String snapshotName,
        String repoName,
        SortOrder order
    ) {
        return order == SortOrder.ASC ? info -> {
            final long val = extractor.applyAsLong(info);
            return after < val || (after == val && compareName(snapshotName, repoName, info) < 0);
        } : info -> {
            final long val = extractor.applyAsLong(info);
            return after > val || (after == val && compareName(snapshotName, repoName, info) > 0);
        };
    }

    private static int compareRepositoryName(String name, String repoName, SnapshotInfo info) {
        final int res = repoName.compareTo(info.repository());
        if (res != 0) {
            return res;
        }
        return name.compareTo(info.snapshotId().getName());
    }

    private static int compareName(String name, String repoName, SnapshotInfo info) {
        final int res = name.compareTo(info.snapshotId().getName());
        if (res != 0) {
            return res;
        }
        return repoName.compareTo(info.repository());
    }

    /**
     * A pair of predicates for the get snapshots action. The {@link #test(SnapshotId, RepositoryData)} predicate is applied to combinations
     * of snapshot id and repository data to determine which snapshots to fully load from the repository and rules out all snapshots that do
     * not match the given {@link GetSnapshotsRequest} that can be ruled out through the information in {@link RepositoryData}.
     * The predicate returned by {@link #test(SnapshotInfo)} predicate is then applied the instances of {@link SnapshotInfo} that were
     * loaded from the repository to filter out those remaining that did not match the request but could not be ruled out without loading
     * their {@link SnapshotInfo}.
     */
    private static final class SnapshotPredicates {

        private static final SnapshotPredicates MATCH_ALL = new SnapshotPredicates(null, null);

        @Nullable // if all snapshot IDs match
        private final BiPredicate preflightPredicate;

        @Nullable // if all snapshots match
        private final Predicate snapshotPredicate;

        private SnapshotPredicates(
            @Nullable BiPredicate preflightPredicate,
            @Nullable Predicate snapshotPredicate
        ) {
            this.snapshotPredicate = snapshotPredicate;
            this.preflightPredicate = preflightPredicate;
        }

        boolean test(SnapshotId snapshotId, RepositoryData repositoryData) {
            return preflightPredicate == null || preflightPredicate.test(snapshotId, repositoryData);
        }

        boolean isMatchAll() {
            return snapshotPredicate == null;
        }

        boolean test(SnapshotInfo snapshotInfo) {
            return snapshotPredicate == null || snapshotPredicate.test(snapshotInfo);
        }

        private SnapshotPredicates and(SnapshotPredicates other) {
            return this == MATCH_ALL ? other
                : other == MATCH_ALL ? this
                : new SnapshotPredicates(
                    preflightPredicate == null ? other.preflightPredicate : other.preflightPredicate == null ? preflightPredicate : null,
                    snapshotPredicate == null ? other.snapshotPredicate : other.snapshotPredicate == null ? snapshotPredicate : null
                );
        }

        static SnapshotPredicates fromRequest(GetSnapshotsRequest request) {
            return getSortValuePredicate(request.fromSortValue(), request.sort(), request.order()).and(
                getSlmPredicates(request.policies())
            );
        }

        private static SnapshotPredicates getSlmPredicates(String[] slmPolicies) {
            if (slmPolicies.length == 0) {
                return MATCH_ALL;
            }

            final List includePatterns = new ArrayList<>();
            final List excludePatterns = new ArrayList<>();
            boolean seenWildcard = false;
            boolean matchNoPolicy = false;
            for (String slmPolicy : slmPolicies) {
                if (seenWildcard && slmPolicy.length() > 1 && slmPolicy.startsWith("-")) {
                    excludePatterns.add(slmPolicy.substring(1));
                } else {
                    if (Regex.isSimpleMatchPattern(slmPolicy)) {
                        seenWildcard = true;
                    } else if (GetSnapshotsRequest.NO_POLICY_PATTERN.equals(slmPolicy)) {
                        matchNoPolicy = true;
                    }
                    includePatterns.add(slmPolicy);
                }
            }
            final String[] includes = includePatterns.toArray(Strings.EMPTY_ARRAY);
            final String[] excludes = excludePatterns.toArray(Strings.EMPTY_ARRAY);
            final boolean matchWithoutPolicy = matchNoPolicy;
            return new SnapshotPredicates(((snapshotId, repositoryData) -> {
                final RepositoryData.SnapshotDetails details = repositoryData.getSnapshotDetails(snapshotId);
                final String policy;
                if (details == null || (details.getSlmPolicy() == null)) {
                    // no SLM policy recorded
                    return true;
                } else {
                    final String policyFound = details.getSlmPolicy();
                    // empty string means that snapshot was not created by an SLM policy
                    policy = policyFound.isEmpty() ? null : policyFound;
                }
                return matchPolicy(includes, excludes, matchWithoutPolicy, policy);
            }), snapshotInfo -> {
                final Map metadata = snapshotInfo.userMetadata();
                final String policy;
                if (metadata == null) {
                    policy = null;
                } else {
                    final Object policyFound = metadata.get(SnapshotsService.POLICY_ID_METADATA_FIELD);
                    policy = policyFound instanceof String ? (String) policyFound : null;
                }
                return matchPolicy(includes, excludes, matchWithoutPolicy, policy);
            });
        }

        private static boolean matchPolicy(String[] includes, String[] excludes, boolean matchWithoutPolicy, @Nullable String policy) {
            if (policy == null) {
                return matchWithoutPolicy;
            }
            if (Regex.simpleMatch(includes, policy) == false) {
                return false;
            }
            return excludes.length == 0 || Regex.simpleMatch(excludes, policy) == false;
        }

        private static SnapshotPredicates getSortValuePredicate(String fromSortValue, GetSnapshotsRequest.SortBy sortBy, SortOrder order) {
            if (fromSortValue == null) {
                return MATCH_ALL;
            }

            switch (sortBy) {
                case START_TIME:
                    final long after = Long.parseLong(fromSortValue);
                    return new SnapshotPredicates(order == SortOrder.ASC ? (snapshotId, repositoryData) -> {
                        final long startTime = getStartTime(snapshotId, repositoryData);
                        return startTime == -1 || after <= startTime;
                    } : (snapshotId, repositoryData) -> {
                        final long startTime = getStartTime(snapshotId, repositoryData);
                        return startTime == -1 || after >= startTime;
                    }, filterByLongOffset(SnapshotInfo::startTime, after, order));

                case NAME:
                    return new SnapshotPredicates(
                        order == SortOrder.ASC
                            ? (snapshotId, repositoryData) -> fromSortValue.compareTo(snapshotId.getName()) <= 0
                            : (snapshotId, repositoryData) -> fromSortValue.compareTo(snapshotId.getName()) >= 0,
                        null
                    );

                case DURATION:
                    final long afterDuration = Long.parseLong(fromSortValue);
                    return new SnapshotPredicates(order == SortOrder.ASC ? (snapshotId, repositoryData) -> {
                        final long duration = getDuration(snapshotId, repositoryData);
                        return duration == -1 || afterDuration <= duration;
                    } : (snapshotId, repositoryData) -> {
                        final long duration = getDuration(snapshotId, repositoryData);
                        return duration == -1 || afterDuration >= duration;
                    }, filterByLongOffset(info -> info.endTime() - info.startTime(), afterDuration, order));

                case INDICES:
                    final int afterIndexCount = Integer.parseInt(fromSortValue);
                    return new SnapshotPredicates(
                        order == SortOrder.ASC
                            ? (snapshotId, repositoryData) -> afterIndexCount <= indexCount(snapshotId, repositoryData)
                            : (snapshotId, repositoryData) -> afterIndexCount >= indexCount(snapshotId, repositoryData),
                        null
                    );

                case REPOSITORY:
                    // already handled in #maybeFilterRepositories
                    return MATCH_ALL;

                case SHARDS:
                    return new SnapshotPredicates(
                        null,
                        filterByLongOffset(SnapshotInfo::totalShards, Integer.parseInt(fromSortValue), order)
                    );
                case FAILED_SHARDS:
                    return new SnapshotPredicates(
                        null,
                        filterByLongOffset(SnapshotInfo::failedShards, Integer.parseInt(fromSortValue), order)
                    );
                default:
                    throw new AssertionError("unexpected sort column [" + sortBy + "]");
            }
        }

        private static Predicate filterByLongOffset(ToLongFunction extractor, long after, SortOrder order) {
            return order == SortOrder.ASC ? info -> after <= extractor.applyAsLong(info) : info -> after >= extractor.applyAsLong(info);
        }

    }

    private static final class SnapshotsInRepo {

        private final List snapshotInfos;

        private final int totalCount;

        private final int remaining;

        SnapshotsInRepo(List snapshotInfos, int totalCount, int remaining) {
            this.snapshotInfos = snapshotInfos;
            this.totalCount = totalCount;
            this.remaining = remaining;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy