-
Notifications
You must be signed in to change notification settings - Fork 25.8k
Add clone step to DLM #141638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add clone step to DLM #141638
Changes from 10 commits
8ee2c86
0183020
88ceed6
8975131
c81bcfb
666f179
99e6e63
41f6830
342c1b7
f54f575
c3e7e7f
d4dee8b
fb81e8c
76cf7dd
bdcf9d7
93ea13b
5536e78
a1e8560
650fbda
d46e003
7219fc1
142b622
f5d909a
654b5d9
3e6b735
3ad95d6
642d1ed
f9fe9ba
be7e0e1
b213d9d
466ff73
716ee73
20d6702
21720e1
0642435
ffd9f9d
f5d23e1
a85656b
6c3bd98
d7e92d0
241b5d7
b23456d
c004bbb
754cfd9
4d72f77
29e212f
176cead
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,281 @@ | ||
| /* | ||
| * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public | ||
| * License v3.0 only", or the "Server Side Public License, v 1". | ||
| */ | ||
|
|
||
| package org.elasticsearch.datastreams.lifecycle.transitions.steps; | ||
|
|
||
| import org.apache.logging.log4j.Logger; | ||
| import org.elasticsearch.ElasticsearchException; | ||
| import org.elasticsearch.action.ActionListener; | ||
| import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; | ||
| import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; | ||
| import org.elasticsearch.action.admin.indices.shrink.ResizeRequest; | ||
| import org.elasticsearch.action.admin.indices.shrink.ResizeType; | ||
| import org.elasticsearch.action.admin.indices.shrink.TransportResizeAction; | ||
| import org.elasticsearch.action.support.IndicesOptions; | ||
| import org.elasticsearch.action.support.master.AcknowledgedRequest; | ||
| import org.elasticsearch.action.support.master.AcknowledgedResponse; | ||
| import org.elasticsearch.action.support.master.MasterNodeRequest; | ||
| import org.elasticsearch.cluster.ProjectState; | ||
| import org.elasticsearch.cluster.metadata.IndexMetadata; | ||
| import org.elasticsearch.cluster.metadata.ProjectId; | ||
| import org.elasticsearch.cluster.metadata.ProjectMetadata; | ||
| import org.elasticsearch.cluster.routing.IndexRoutingTable; | ||
| import org.elasticsearch.common.Strings; | ||
| import org.elasticsearch.common.hash.MessageDigests; | ||
| import org.elasticsearch.common.settings.Settings; | ||
| import org.elasticsearch.core.TimeValue; | ||
| import org.elasticsearch.datastreams.lifecycle.transitions.DlmStep; | ||
| import org.elasticsearch.datastreams.lifecycle.transitions.DlmStepContext; | ||
| import org.elasticsearch.index.Index; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Locale; | ||
| import java.util.Map; | ||
|
|
||
| import static org.apache.logging.log4j.LogManager.getLogger; | ||
| import static org.elasticsearch.datastreams.DataStreamsPlugin.LIFECYCLE_CUSTOM_INDEX_METADATA_KEY; | ||
| import static org.elasticsearch.datastreams.lifecycle.DataStreamLifecycleService.FORCE_MERGE_COMPLETED_TIMESTAMP_METADATA_KEY; | ||
|
|
||
| /** | ||
| * This step clones the index into a new index with 0 replicas. | ||
| */ | ||
| public class CloneStep implements DlmStep { | ||
|
|
||
| private static final String DLM_INDEX_TO_BE_MERGED_KEY = "dlm_index_to_be_force_merged"; | ||
| private static final IndicesOptions IGNORE_MISSING_OPTIONS = IndicesOptions.fromOptions(true, true, false, false); | ||
| private static final Logger logger = getLogger(CloneStep.class); | ||
|
|
||
| @Override | ||
| public boolean stepCompleted(Index index, ProjectState projectState) { | ||
| // the index can either be "cloned" or the original index if it had 0 replicas | ||
| String indexToBeForceMerged = getIndexToBeForceMerged(index.getName(), projectState); | ||
| if (indexToBeForceMerged == null) { | ||
| return false; | ||
| } | ||
| boolean cloneExists = projectState.metadata().indices().containsKey(indexToBeForceMerged); | ||
| if (cloneExists == false) { | ||
| return false; | ||
| } | ||
| IndexMetadata indexToBeForceMergedMetadata = projectState.metadata().index(indexToBeForceMerged); | ||
| if (indexToBeForceMergedMetadata == null) { | ||
| return false; | ||
| } | ||
| IndexRoutingTable indexRoutingTable = projectState.routingTable().index(indexToBeForceMerged); | ||
| if (indexRoutingTable == null) { | ||
| return false; | ||
| } | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return indexRoutingTable.allPrimaryShardsActive(); | ||
|
||
| } | ||
|
|
||
| @Override | ||
| public void execute(DlmStepContext stepContext) { | ||
| Index index = stepContext.index(); | ||
| String indexName = index.getName(); | ||
| ProjectState projectState = stepContext.projectState(); | ||
| ProjectId projectId = stepContext.projectId(); | ||
| ProjectMetadata projectMetadata = projectState.metadata(); | ||
| IndexMetadata indexMetadata = projectMetadata.index(index); | ||
|
|
||
| if (indexMetadata == null) { | ||
| logger.warn("Index [{}] not found in project metadata, skipping clone step", indexName); | ||
| return; | ||
| } | ||
|
|
||
| if (isForceMergeComplete(indexMetadata)) { | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| logger.info("Skipping clone step for index [{}] as force merge is already complete", indexName); | ||
| return; | ||
| } | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (indexMetadata.getNumberOfReplicas() == 0) { | ||
| logger.info( | ||
| "Skipping clone step for index [{}] as it already has 0 replicas and can be used for force merge directly", | ||
| indexName | ||
| ); | ||
| // mark the index to be force merged directly | ||
| markIndexToBeForceMerged(indexName, indexName, stepContext, ActionListener.noop()); | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return; | ||
| } | ||
| String cloneIndexName = generateCloneIndexName(indexName); | ||
| if (projectMetadata.indices().containsKey(cloneIndexName)) { | ||
| logger.info("DLM cleaning up clone index [{}] for index [{}] as it already exists.", cloneIndexName, indexName); | ||
| deleteCloneIndexIfExists(stepContext); | ||
|
||
| } | ||
|
|
||
| ResizeRequest cloneIndexRequest = new ResizeRequest( | ||
| MasterNodeRequest.INFINITE_MASTER_NODE_TIMEOUT, | ||
| AcknowledgedRequest.DEFAULT_ACK_TIMEOUT, | ||
| ResizeType.CLONE, | ||
| indexName, | ||
| cloneIndexName | ||
| ); | ||
| cloneIndexRequest.setTargetIndexSettings(Settings.builder().put("index.number_of_replicas", 0)); | ||
| stepContext.executeDeduplicatedRequest( | ||
| cloneIndexRequest, | ||
| Strings.format("DLM service encountered an error when trying to clone index [%s]", indexName), | ||
| (req, reqListener) -> cloneIndex(projectId, cloneIndexRequest, reqListener, stepContext) | ||
| ); | ||
| } | ||
|
|
||
| @Override | ||
| public String stepName() { | ||
| return "Clone Index"; | ||
| } | ||
|
|
||
| private static class CloneIndexResizeActionListener implements ActionListener<CreateIndexResponse> { | ||
| private final String sourceIndexName; | ||
| private final String targetIndexName; | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| private final ActionListener<Void> listener; | ||
| private final DlmStepContext stepContext; | ||
|
|
||
| private CloneIndexResizeActionListener( | ||
| String sourceIndexName, | ||
| String targetIndexName, | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ActionListener<Void> listener, | ||
| DlmStepContext stepContext | ||
| ) { | ||
| this.sourceIndexName = sourceIndexName; | ||
| this.targetIndexName = targetIndexName; | ||
| this.listener = listener; | ||
| this.stepContext = stepContext; | ||
| } | ||
|
|
||
| @Override | ||
| public void onResponse(CreateIndexResponse createIndexResponse) { | ||
seanzatzdev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| logger.debug("DLM successfully cloned index [{}] to index [{}]", sourceIndexName, targetIndexName); | ||
| // on success, write the cloned index name to the custom metadata of the index metadata of original index | ||
| markIndexToBeForceMerged(sourceIndexName, targetIndexName, stepContext, listener); | ||
|
||
| } | ||
|
|
||
| @Override | ||
| public void onFailure(Exception e) { | ||
| logger.error(() -> Strings.format("DLM failed to clone index [%s] to index [%s]", sourceIndexName, targetIndexName), e); | ||
| deleteCloneIndexIfExists(stepContext); | ||
| listener.onFailure(e); | ||
| } | ||
| } | ||
|
|
||
| private void cloneIndex(ProjectId projectId, ResizeRequest cloneRequest, ActionListener<Void> listener, DlmStepContext stepContext) { | ||
| assert cloneRequest.indices() != null && cloneRequest.indices().length == 1 : "DLM should clone one index at a time"; | ||
| String sourceIndex = cloneRequest.getSourceIndex(); | ||
| String targetIndex = cloneRequest.getTargetIndexRequest().index(); | ||
| logger.trace("DLM issuing request to clone index [{}] to index [{}]", sourceIndex, targetIndex); | ||
| CloneIndexResizeActionListener responseListener = new CloneIndexResizeActionListener( | ||
| sourceIndex, | ||
| targetIndex, | ||
| listener, | ||
| stepContext | ||
| ); | ||
| stepContext.client().projectClient(projectId).execute(TransportResizeAction.TYPE, cloneRequest, responseListener); | ||
| } | ||
|
|
||
| /* | ||
| * Generates a unique name deterministically for the clone index based on the original index name. | ||
| */ | ||
| private static String generateCloneIndexName(String originalName) { | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| String hash = MessageDigests.toHexString(MessageDigests.sha256().digest(originalName.getBytes(StandardCharsets.UTF_8))) | ||
| .substring(0, 8); | ||
| return originalName + "-dlm-clone-" + hash; | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /* | ||
| * Returns true if a value has been set for the custom index metadata field "force_merge_completed_timestamp" within the field | ||
| * "data_stream_lifecycle". | ||
| */ | ||
| private static boolean isForceMergeComplete(IndexMetadata backingIndex) { | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Map<String, String> customMetadata = backingIndex.getCustomData(LIFECYCLE_CUSTOM_INDEX_METADATA_KEY); | ||
| return customMetadata != null && customMetadata.containsKey(FORCE_MERGE_COMPLETED_TIMESTAMP_METADATA_KEY); | ||
| } | ||
|
|
||
| /* | ||
| * Updates the custom metadata of the index metadata of the source index to mark the target index as that to be force merged by DLM. | ||
| * This method performs the update asynchronously using a transport action. | ||
| */ | ||
| private static void markIndexToBeForceMerged( | ||
| String sourceIndex, | ||
| String indexToBeForceMerged, | ||
| DlmStepContext stepContext, | ||
| ActionListener<Void> listener | ||
| ) { | ||
| MarkIndexToBeForceMergedAction.Request request = new MarkIndexToBeForceMergedAction.Request( | ||
| stepContext.projectId(), | ||
| sourceIndex, | ||
| indexToBeForceMerged, | ||
| MasterNodeRequest.INFINITE_MASTER_NODE_TIMEOUT | ||
| ); | ||
|
|
||
| stepContext.client() | ||
| .projectClient(stepContext.projectId()) | ||
| .execute(MarkIndexToBeForceMergedAction.INSTANCE, request, listener.delegateFailure((delegate, response) -> { | ||
| logger.debug("DLM successfully marked index [{}] to be force merged", indexToBeForceMerged); | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| delegate.onResponse(null); | ||
| })); | ||
| } | ||
|
|
||
| /* | ||
| * Returns the name of index to be force merged from the custom metadata of the index metadata of the source index. | ||
| * If no such index has been marked in the custom metadata, returns null. | ||
| */ | ||
| private static String getIndexToBeForceMerged(String sourceIndex, ProjectState projectState) { | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| IndexMetadata sourceIndexMetadata = projectState.metadata().index(sourceIndex); | ||
| if (sourceIndexMetadata == null) { | ||
| return null; | ||
| } | ||
| Map<String, String> customMetadata = sourceIndexMetadata.getCustomData(LIFECYCLE_CUSTOM_INDEX_METADATA_KEY); | ||
| if (customMetadata == null) { | ||
| return null; | ||
| } else { | ||
| return customMetadata.get(DLM_INDEX_TO_BE_MERGED_KEY); | ||
| } | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| private static void deleteCloneIndexIfExists(DlmStepContext stepContext) { | ||
| String cloneIndex = generateCloneIndexName(stepContext.indexName()); | ||
| logger.debug("Attempting to delete index [{}]", cloneIndex); | ||
|
|
||
| DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(cloneIndex).indicesOptions(IGNORE_MISSING_OPTIONS) | ||
| .masterNodeTimeout(TimeValue.MAX_VALUE); | ||
| String errorMessage = String.format(Locale.ROOT, "Failed to acknowledge delete of index [%s]", cloneIndex); | ||
| DeleteCloneIndexActionListener listener = new DeleteCloneIndexActionListener(cloneIndex); | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| stepContext.client() | ||
| .projectClient(stepContext.projectId()) | ||
| .admin() | ||
| .indices() | ||
| .delete(deleteIndexRequest, failIfNotAcknowledged(listener, errorMessage)); | ||
| } | ||
|
|
||
| private static class DeleteCloneIndexActionListener implements ActionListener<AcknowledgedResponse> { | ||
| private final String targetIndex; | ||
|
|
||
| private DeleteCloneIndexActionListener(String targetIndex) { | ||
| this.targetIndex = targetIndex; | ||
| } | ||
|
|
||
| @Override | ||
| public void onResponse(AcknowledgedResponse response) { | ||
| logger.debug("DLM successfully deleted clone index [{}]", targetIndex); | ||
| } | ||
|
|
||
| @Override | ||
| public void onFailure(Exception e) { | ||
| logger.error(() -> Strings.format("DLM failed to delete clone index [%s]", targetIndex), e); | ||
| } | ||
| } | ||
|
|
||
| private static <U extends AcknowledgedResponse> ActionListener<U> failIfNotAcknowledged( | ||
seanzatzdev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ActionListener<U> listener, | ||
| String errorMessage | ||
| ) { | ||
| return listener.delegateFailure((delegate, response) -> { | ||
| if (response.isAcknowledged()) { | ||
| delegate.onResponse(null); | ||
| } else { | ||
| delegate.onFailure(new ElasticsearchException(errorMessage)); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.