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

org.elasticsearch.xpack.core.ilm.SearchableSnapshotAction Maven / Gradle / Ivy

/*
 * 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; you may not use this file except in compliance with the Elastic License
 * 2.0.
 */
package org.elasticsearch.xpack.core.ilm;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.Version;
import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.elasticsearch.cluster.metadata.IndexAbstraction;
import org.elasticsearch.cluster.metadata.IndexMetadata;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.license.LicenseUtils;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.ilm.Step.StepKey;
import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotRequest;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * A {@link LifecycleAction} that will convert the index into a searchable snapshot, by taking a snapshot of the index, creating a
 * searchable snapshot and the corresponding "searchable snapshot index", deleting the original index and swapping its aliases to the
 * newly created searchable snapshot backed index.
 */
public class SearchableSnapshotAction implements LifecycleAction {
    private static final Logger logger = LogManager.getLogger(SearchableSnapshotAction.class);

    public static final String NAME = "searchable_snapshot";

    public static final ParseField SNAPSHOT_REPOSITORY = new ParseField("snapshot_repository");
    public static final ParseField FORCE_MERGE_INDEX = new ParseField("force_merge_index");
    public static final String CONDITIONAL_DATASTREAM_CHECK_KEY = BranchingStep.NAME + "-on-datastream-check";
    public static final String CONDITIONAL_SKIP_ACTION_STEP = BranchingStep.NAME + "-check-prerequisites";
    public static final String CONDITIONAL_SKIP_GENERATE_AND_CLEAN = BranchingStep.NAME + "-check-existing-snapshot";

    public static final String FULL_RESTORED_INDEX_PREFIX = "restored-";
    public static final String PARTIAL_RESTORED_INDEX_PREFIX = "partial-";

    private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME,
        a -> new SearchableSnapshotAction((String) a[0], a[1] == null || (boolean) a[1]));

    static {
        PARSER.declareString(ConstructingObjectParser.constructorArg(), SNAPSHOT_REPOSITORY);
        PARSER.declareBoolean(ConstructingObjectParser.optionalConstructorArg(), FORCE_MERGE_INDEX);
    }


    public static SearchableSnapshotAction parse(XContentParser parser) {
        return PARSER.apply(parser, null);
    }

    private final String snapshotRepository;
    private final boolean forceMergeIndex;

    public SearchableSnapshotAction(String snapshotRepository, boolean forceMergeIndex) {
        if (Strings.hasText(snapshotRepository) == false) {
            throw new IllegalArgumentException("the snapshot repository must be specified");
        }
        this.snapshotRepository = snapshotRepository;
        this.forceMergeIndex = forceMergeIndex;
    }

    public SearchableSnapshotAction(String snapshotRepository) {
        this(snapshotRepository, true);
    }

    public SearchableSnapshotAction(StreamInput in) throws IOException {
        this.snapshotRepository = in.readString();
        if (in.getVersion().onOrAfter(Version.V_7_10_0)) {
            this.forceMergeIndex = in.readBoolean();
        } else {
            this.forceMergeIndex = true;
        }
    }

    boolean isForceMergeIndex() {
        return forceMergeIndex;
    }

    public String getSnapshotRepository() {
        return snapshotRepository;
    }

    @Override
    public List toSteps(Client client, String phase, StepKey nextStepKey) {
        StepKey preActionBranchingKey = new StepKey(phase, NAME, CONDITIONAL_SKIP_ACTION_STEP);
        StepKey checkNoWriteIndex = new StepKey(phase, NAME, CheckNotDataStreamWriteIndexStep.NAME);
        StepKey waitForNoFollowerStepKey = new StepKey(phase, NAME, WaitForNoFollowersStep.NAME);
        StepKey forceMergeStepKey = new StepKey(phase, NAME, ForceMergeStep.NAME);
        StepKey waitForSegmentCountKey = new StepKey(phase, NAME, SegmentCountStep.NAME);
        StepKey skipGeneratingSnapshotKey = new StepKey(phase, NAME, CONDITIONAL_SKIP_GENERATE_AND_CLEAN);
        StepKey generateSnapshotNameKey = new StepKey(phase, NAME, GenerateSnapshotNameStep.NAME);
        StepKey cleanSnapshotKey = new StepKey(phase, NAME, CleanupSnapshotStep.NAME);
        StepKey createSnapshotKey = new StepKey(phase, NAME, CreateSnapshotStep.NAME);
        StepKey mountSnapshotKey = new StepKey(phase, NAME, MountSnapshotStep.NAME);
        StepKey waitForGreenRestoredIndexKey = new StepKey(phase, NAME, WaitForIndexColorStep.NAME);
        StepKey copyMetadataKey = new StepKey(phase, NAME, CopyExecutionStateStep.NAME);
        StepKey dataStreamCheckBranchingKey = new StepKey(phase, NAME, CONDITIONAL_DATASTREAM_CHECK_KEY);
        StepKey copyLifecyclePolicySettingKey = new StepKey(phase, NAME, CopySettingsStep.NAME);
        StepKey swapAliasesKey = new StepKey(phase, NAME, SwapAliasesAndDeleteSourceIndexStep.NAME);
        StepKey replaceDataStreamIndexKey = new StepKey(phase, NAME, ReplaceDataStreamBackingIndexStep.NAME);
        StepKey deleteIndexKey = new StepKey(phase, NAME, DeleteStep.NAME);

        // Before going through all these steps, first check if we need to do them at all. For example, the index could already be
        // a searchable snapshot of the same type and repository, in which case we don't need to do anything. If that is detected,
        // this branching step jumps right to the end, skipping the searchable snapshot action entirely. We also check the license
        // here before generating snapshots that can't be used if the user doesn't have the right license level.
        BranchingStep conditionalSkipActionStep = new BranchingStep(preActionBranchingKey, checkNoWriteIndex, nextStepKey,
            (index, clusterState) -> {
                XPackLicenseState licenseState = XPackPlugin.getSharedLicenseState();
                if (licenseState.isAllowed(XPackLicenseState.Feature.SEARCHABLE_SNAPSHOTS) == false) {
                    logger.error("[{}] action is not available in the current license", SearchableSnapshotAction.NAME);
                    throw LicenseUtils.newComplianceException("searchable-snapshots");
                }

                IndexMetadata indexMetadata = clusterState.getMetadata().index(index);
                assert indexMetadata != null : "index " + index.getName() + " must exist in the cluster state";
                String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings());
                if (indexMetadata.getSettings().get(LifecycleSettings.SNAPSHOT_INDEX_NAME) != null) {
                    // The index is already a searchable snapshot, let's see if the repository matches
                    // TODO: move the searchable snapshot settings into x-pack
                    //  core in the future, so the Settings can be used instead
                    //  of strings here
                    String repo = indexMetadata.getSettings().get("index.store.snapshot.repository_name");
                    if (this.snapshotRepository.equals(repo) == false) {
                        // Okay, different repo, we need to go ahead with the searchable snapshot
                        logger.debug("[{}] action is configured for index [{}] in policy [{}] which is already mounted as a searchable " +
                                "snapshot, but with a different repository (existing: [{}] vs new: [{}]), a new snapshot and " +
                                "index will be created",
                            SearchableSnapshotAction.NAME, index.getName(), policyName, repo, this.snapshotRepository);
                        return false;
                    }

                    // Check to the storage type to see if we need to convert between full <-> partial
                    boolean partial = indexMetadata.getSettings().getAsBoolean("index.store.snapshot.partial", false);
                    MountSearchableSnapshotRequest.Storage existingType =
                        partial ? MountSearchableSnapshotRequest.Storage.SHARED_CACHE : MountSearchableSnapshotRequest.Storage.FULL_COPY;
                    MountSearchableSnapshotRequest.Storage type = getConcreteStorageType(preActionBranchingKey);
                    if (existingType == type) {
                        logger.debug("[{}] action is configured for index [{}] in policy [{}] which is already mounted " +
                            "as a searchable snapshot with the same repository [{}] and storage type [{}], skipping this action",
                            SearchableSnapshotAction.NAME, index.getName(), policyName, repo, type);
                        return true;
                    }

                    logger.debug("[{}] action is configured for index [{}] in policy [{}] which is already mounted " +
                        "as a searchable snapshot in repository [{}], however, the storage type ([{}] vs [{}]) " +
                        "differs, so a new index will be created",
                        SearchableSnapshotAction.NAME, index.getName(), policyName, this.snapshotRepository, existingType, type);
                    // Perform the searchable snapshot
                    return false;
                }
                // Perform the searchable snapshot, as the index is not currently a searchable snapshot
                return false;
            });
        CheckNotDataStreamWriteIndexStep checkNoWriteIndexStep =
            new CheckNotDataStreamWriteIndexStep(checkNoWriteIndex, waitForNoFollowerStepKey);
        WaitForNoFollowersStep waitForNoFollowersStep =
            new WaitForNoFollowersStep(waitForNoFollowerStepKey, skipGeneratingSnapshotKey, client);

        // When generating a snapshot, we either jump to the force merge step, or we skip the
        // forcemerge and go straight to steps for creating the snapshot
        StepKey keyForSnapshotGeneration = forceMergeIndex ? forceMergeStepKey : generateSnapshotNameKey;
        // Branch, deciding whether there is an existing searchable snapshot snapshot that can be used for mounting the index
        // (in which case, skip generating a new name and the snapshot cleanup), or if we need to generate a new snapshot
        BranchingStep skipGeneratingSnapshotStep =
            new BranchingStep(skipGeneratingSnapshotKey, keyForSnapshotGeneration, mountSnapshotKey, (index, clusterState) -> {
                IndexMetadata indexMetadata = clusterState.getMetadata().index(index);
                String policyName = LifecycleSettings.LIFECYCLE_NAME_SETTING.get(indexMetadata.getSettings());
                LifecycleExecutionState lifecycleExecutionState = LifecycleExecutionState.fromIndexMetadata(indexMetadata);
                if (lifecycleExecutionState.getSnapshotName() == null) {
                    // No name exists, so it must be generated
                    logger.trace("no snapshot name for index [{}] in policy [{}] exists, so one will be generated",
                        index.getName(), policyName);
                    return false;
                }

                if (this.snapshotRepository.equals(lifecycleExecutionState.getSnapshotRepository()) == false) {
                    // A different repository is being used
                    // TODO: allow this behavior instead of throwing an exception
                    throw new IllegalArgumentException("searchable snapshot indices may be converted only within the same repository");
                }

                // We can skip the generate, initial cleanup, and snapshot taking for this index, as we already have a generated snapshot.
                // This will jump ahead directly to the "mount snapshot" step
                logger.debug("an existing snapshot [{}] in repository [{}] (index name: [{}]) " +
                        "will be used for mounting [{}] as a searchable snapshot",
                    lifecycleExecutionState.getSnapshotName(), lifecycleExecutionState.getSnapshotRepository(),
                    lifecycleExecutionState.getSnapshotIndexName(), index.getName());
                return true;
            });

        // If a new snapshot is needed, these steps are executed
        ForceMergeStep forceMergeStep = new ForceMergeStep(forceMergeStepKey, waitForSegmentCountKey, client, 1);
        SegmentCountStep segmentCountStep = new SegmentCountStep(waitForSegmentCountKey, generateSnapshotNameKey, client, 1);
        GenerateSnapshotNameStep generateSnapshotNameStep = new GenerateSnapshotNameStep(generateSnapshotNameKey, cleanSnapshotKey,
            snapshotRepository);
        CleanupSnapshotStep cleanupSnapshotStep = new CleanupSnapshotStep(cleanSnapshotKey, createSnapshotKey, client);
        AsyncActionBranchingStep createSnapshotBranchingStep = new AsyncActionBranchingStep(
            new CreateSnapshotStep(createSnapshotKey, mountSnapshotKey, client), cleanSnapshotKey, client);

        // Now mount the snapshot to create the new index, if the skipGeneratingSnapshotStep determined a snapshot already existed that
        // can be used, it jumps directly here, skipping the snapshot generation steps above.
        MountSnapshotStep mountSnapshotStep = new MountSnapshotStep(mountSnapshotKey, waitForGreenRestoredIndexKey,
            client, getRestoredIndexPrefix(mountSnapshotKey), getConcreteStorageType(mountSnapshotKey));
        WaitForIndexColorStep waitForGreenIndexHealthStep = new WaitForIndexColorStep(waitForGreenRestoredIndexKey,
            copyMetadataKey, ClusterHealthStatus.GREEN, getRestoredIndexPrefix(waitForGreenRestoredIndexKey));
        CopyExecutionStateStep copyMetadataStep = new CopyExecutionStateStep(copyMetadataKey, copyLifecyclePolicySettingKey,
            getRestoredIndexPrefix(copyMetadataKey), nextStepKey);
        CopySettingsStep copySettingsStep = new CopySettingsStep(copyLifecyclePolicySettingKey, dataStreamCheckBranchingKey,
            getRestoredIndexPrefix(copyLifecyclePolicySettingKey), LifecycleSettings.LIFECYCLE_NAME);
        BranchingStep isDataStreamBranchingStep = new BranchingStep(dataStreamCheckBranchingKey, swapAliasesKey, replaceDataStreamIndexKey,
            (index, clusterState) -> {
                IndexAbstraction indexAbstraction = clusterState.metadata().getIndicesLookup().get(index.getName());
                assert indexAbstraction != null : "invalid cluster metadata. index [" + index.getName() + "] was not found";
                return indexAbstraction.getParentDataStream() != null;
            });
        ReplaceDataStreamBackingIndexStep replaceDataStreamBackingIndex = new ReplaceDataStreamBackingIndexStep(replaceDataStreamIndexKey,
            deleteIndexKey, getRestoredIndexPrefix(replaceDataStreamIndexKey));
        DeleteStep deleteSourceIndexStep = new DeleteStep(deleteIndexKey, null, client);
        // sending this step to null as the restored index (which will after this step essentially be the source index) was sent to the next
        // key after we restored the lifecycle execution state
        SwapAliasesAndDeleteSourceIndexStep swapAliasesAndDeleteSourceIndexStep = new SwapAliasesAndDeleteSourceIndexStep(swapAliasesKey,
            null, client, getRestoredIndexPrefix(swapAliasesKey));

        List steps = new ArrayList<>();
        steps.add(conditionalSkipActionStep);
        steps.add(checkNoWriteIndexStep);
        steps.add(waitForNoFollowersStep);
        steps.add(skipGeneratingSnapshotStep);
        if (forceMergeIndex) {
            steps.add(forceMergeStep);
            steps.add(segmentCountStep);
        }
        steps.add(generateSnapshotNameStep);
        steps.add(cleanupSnapshotStep);
        steps.add(createSnapshotBranchingStep);
        steps.add(mountSnapshotStep);
        steps.add(waitForGreenIndexHealthStep);
        steps.add(copyMetadataStep);
        steps.add(copySettingsStep);
        steps.add(isDataStreamBranchingStep);
        steps.add(replaceDataStreamBackingIndex);
        steps.add(deleteSourceIndexStep);
        steps.add(swapAliasesAndDeleteSourceIndexStep);
        return steps;
    }

    /**
     * Resolves the prefix to be used for the mounted index depending on the provided key
     */
    String getRestoredIndexPrefix(StepKey currentKey) {
        if (currentKey.getPhase().equals(TimeseriesLifecycleType.FROZEN_PHASE)) {
            return PARTIAL_RESTORED_INDEX_PREFIX;
        } else {
            return FULL_RESTORED_INDEX_PREFIX;
        }
    }

    // Resolves the storage type depending on which phase the index is in
    MountSearchableSnapshotRequest.Storage getConcreteStorageType(StepKey currentKey) {
        if (currentKey.getPhase().equals(TimeseriesLifecycleType.FROZEN_PHASE)) {
            return MountSearchableSnapshotRequest.Storage.SHARED_CACHE;
        } else {
            return MountSearchableSnapshotRequest.Storage.FULL_COPY;
        }
    }

    @Override
    public boolean isSafeAction() {
        return true;
    }

    @Override
    public String getWriteableName() {
        return NAME;
    }

    @Override
    public void writeTo(StreamOutput out) throws IOException {
        out.writeString(snapshotRepository);
        if (out.getVersion().onOrAfter(Version.V_7_10_0)) {
            out.writeBoolean(forceMergeIndex);
        }
    }

    @Override
    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
        builder.startObject();
        builder.field(SNAPSHOT_REPOSITORY.getPreferredName(), snapshotRepository);
        builder.field(FORCE_MERGE_INDEX.getPreferredName(), forceMergeIndex);
        builder.endObject();
        return builder;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        SearchableSnapshotAction that = (SearchableSnapshotAction) o;
        return Objects.equals(snapshotRepository, that.snapshotRepository);
    }

    @Override
    public int hashCode() {
        return Objects.hash(snapshotRepository);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy