Skip to content

Comments

Add clone step to DLM#141638

Open
seanzatzdev wants to merge 47 commits intoelastic:mainfrom
seanzatzdev:dlm-clone-step
Open

Add clone step to DLM#141638
seanzatzdev wants to merge 47 commits intoelastic:mainfrom
seanzatzdev:dlm-clone-step

Conversation

@seanzatzdev
Copy link
Contributor

@seanzatzdev seanzatzdev commented Feb 2, 2026

elasticsearchmachine and others added 9 commits February 2, 2026 06:12
# Conflicts:
#	modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/transitions/steps/CloneStep.java
#	modules/data-streams/src/test/java/org/elasticsearch/datastreams/lifecycle/transitions/steps/CloneStepTests.java
@seanzatzdev seanzatzdev marked this pull request as ready for review February 2, 2026 23:23
@elasticsearchmachine elasticsearchmachine added the needs:triage Requires assignment of a team area label label Feb 2, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a clone step to the Data Lifecycle Management (DLM) system. The clone step creates a copy of an index with zero replicas to prepare it for force merging, optimizing resource usage during the force merge operation.

Changes:

  • Implements CloneStep that clones indices with 0 replicas or marks indices that already have 0 replicas for force merging
  • Adds MarkIndexToBeForceMergedAction to update cluster state metadata marking which index should be force merged
  • Adds comprehensive test coverage for the clone step functionality

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
CloneStep.java Implements the core clone step logic including index cloning, metadata marking, and cleanup operations
MarkIndexToBeForceMergedAction.java Defines the action and request/response structures for marking indices to be force merged
TransportMarkIndexToBeForceMergedAction.java Implements the transport layer for the mark index action
CloneStepTests.java Provides comprehensive unit tests for the clone step functionality
DataStreamsPlugin.java Registers the new mark index action handler

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-storage-engine (Team:StorageEngine)

@elasticsearchmachine elasticsearchmachine removed the needs:triage Requires assignment of a team area label label Feb 3, 2026
@lukewhiting
Copy link
Contributor

@elasticsearchmachine test this

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this cause an infinite loop?

If this step runs again while the clone is in progress (Which may happen as we keep re-running execute(..) until stepComplete(...) is true, relying on the action duplicators to prevent us actually making changes twice) won't it delete the currently cloning index then try and clone it again next cycle?

Copy link
Contributor Author

@seanzatzdev seanzatzdev Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, the deduplicator alone wouldn't fix this though if i understand correctly? Struggling to think of a solution here that allows us to clean up without a potential conflict

I also just realized i forgot to add the deduplicators on the other requests... let me fix that now!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a note here that we discussed this offline and will be following up on this problem to discuss workarounds

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the timeout we discussed, figured the best thing to do if the timeout hasn't been reached is just return and let the next DLM run check again rather than have the step's thread waiting around and checking periodically or something

public void onResponse(CreateIndexResponse createIndexResponse) {
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe all actions need to be executed via the deduplicator. I'm also not sure what effect calling an action inside a listener vs callback would have on thread usage... Perhaps @dakrone can weigh in on this pattern?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My mistake, i totally forgot to wrap the other actions in the deduplicator... will fix that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not sure what effect calling an action inside a listener vs callback would have on thread usage

True, I can move this to a callback pattern

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do have a SubscribableListener which can be used for this, where you can chain actions with the .andThen(…) method. I've seen it used multiple places, though I don't claim to be an expert in its use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up adapting the way some other places in the codebase do this, e.g. TransportReindexAction, if I understand correctly the execute's should be async? The SubscribableListener didn't seem as common to me so I was a little apprehensive to use it. Maybe I need some more pointers on this though if there's problems with my approach, I'm finding elasticsearch's listeners to be a little confusing to work with to say the least...

Copy link
Member

@dakrone dakrone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked at the test cases yet, but I left some comments about the implementation so far, thanks for working on this Sean!

if (indexRoutingTable == null) {
return false;
}
return indexRoutingTable.allPrimaryShardsActive();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have index metadata that links the two indices? Should we add a check for that here if we do?

Copy link
Contributor Author

@seanzatzdev seanzatzdev Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first conditional in the chain (getIndexToBeForceMerged) should check for that, it checks whether there has been an index marked for force merge for the source index.

Copy link
Contributor

@lukewhiting lukewhiting left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid progress :-) Just a few bugs left to iron out.

String cloneIndexName = getCloneIndexName(indexName);
IndexMetadata cloneIndexMetadata = stepContext.projectState().metadata().index(cloneIndexName);
long cloneCreationTime = cloneIndexMetadata.getCreationDate();
long currentTime = System.currentTimeMillis();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I would avoid using System.currentTimeMillis and use a Clock instead as it will make testing these timeouts much easier.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add a Clock to the step context then, so it's available to every step.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably add a Clock to the step context then, so it's available to every step.

Clock.systemUTC().millis() is a static method, would we still want to add a clock to the context?

Copy link
Member

@dakrone dakrone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm only partway through, but I wanted to leave some overall feedback to unblock you from continuing to work on this while I continue the rest of the review.

* Request to mark an index to be force merged.
*/
public static class Request extends MasterNodeRequest<Request> {
private final ProjectId projectId;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I see ProjectId here, it fires a mental alarm that we're passing through too much state. I mentioned elsewhere that the service should be the one that handles cluster state updates, but here we're passing through too much info. I think moving the logic into the service would be better.

Copy link
Contributor Author

@seanzatzdev seanzatzdev Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an easier way to do this without moving transport logic to the service class as you mentioned?Since the transport action will need that projectId to do the block check.

If we move the logic from the transport action to the service it'd be easy to avoid this kind of awkwardness, but if we don't rely on the transport action, my impression was that it would be difficult to do the throttling/deduplication. If that's the case, do you think it's warranted to just do this in a hacky way?

Copy link
Member

@dakrone dakrone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left more comments, and I'm happy to discuss if it helps!

Comment on lines 242 to 251
stepContext.executeDeduplicatedRequest(
MarkIndexForDLMForceMergeAction.TYPE.name(),
markIndexForForceMergeRequest,
Strings.format(
"DLM service encountered an error when trying to mark index [%s] to be force merged for source index [%s]",
indexToBeForceMerged,
sourceIndex
),
(req, unused) -> markIndexToBeForceMergedCallback(markIndexForForceMergeRequest, stepContext, listener)
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know, now that I think about this more, I'm not sure we actually need to make the cluster state update a transport action. We're really only making it a separate transport action so that we can use the deduplicator (and thus avoid issuing multiple cluster state updates). Really what we want is something that does something like:

stepContext.executeOnce(
    <some-object-that-we-use-as-a-key>,
    "error message because failure",
    (unused, unused2) -> taskQueue.submitTask("mark-" + indexName + "-ready-for-merge", new MarkIndexAsForceMergeReady(indexName)));

I wonder if we could/should add something to the stepContext that could allow us to do these without having to add the transport bits. Is it going to happen enough as a pattern (where we really want to execute something that is not a transport action, like a CS update) that we'd want to do it?

What do you think @seanzatzdev @lukewhiting ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, we'd also have to thread some kind of task queue through the context so that we could submit things to it from steps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will have to do a fair bit of cluster state updates in the latter steps so adding transport actions for everything could get tedious/messy. Curious to hear Luke's thoughts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I've just been looking at this too long, but do we actually need this marker in the cluster state? Since the index for force merge will either be a deterministically named clone or the original (if it has 0 replicas already) is that something we can just write a function to check for in later steps and avoid this extra write to cluster state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could/should add something to the stepContext that could allow us to do these without having to add the transport bits. Is it going to happen enough as a pattern (where we really want to execute something that is not a transport action, like a CS update) that we'd want to do it?

Without the transport action serving as a wrapper (and making use of the transport action deduplicator), is there an easy way to do the throttling? My impression from our previous meetings was that it'd be difficult to implement the throttling without the transport action deduplicator.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants