In https://github.com/elastic/elasticsearch/pull/78547 we introduced batching for the ILM master tasks that occur on the happy path. However if a high-shard-count cluster encounters problems while doing ILM-related things—perhaps some nodes are temporarily unavailable for taking a snapshot—then we process the resulting `ilm-retry-failed-step` and `ilm-move-to-error-step` tasks one-by-one which can significantly delay the cluster's recovery from its problems.

We should batch these things together too.

It looks like we also enqueue duplicate `ilm-retry-failed-step` on each poll interval too, although we do appear to treat the duplicates as no-ops at execution time.

Relates https://github.com/elastic/elasticsearch/issues/77466
x-pack/plugin/ilm/src/main/java/org/elasticsearch/xpack/ilm/IndexLifecycleRunner.java
if (isReadyToTransitionToThisPhase(policy, indexMetadata, currentStep.getNextStepKey().getPhase())) {
                moveToStep(indexMetadata.getIndex(), policy, currentStep.getKey(), currentStep.getNextStepKey());
            }
        } else if (currentStep instanceof ClusterStateActionStep || currentStep instanceof ClusterStateWaitStep) {
            logger.debug("[{}] running policy with current-step [{}]", indexMetadata.getIndex().getName(), currentStep.getKey());
            submitUnlessAlreadyQueued(
                String.format(Locale.ROOT, "ilm-execute-cluster-state-steps [%s]", currentStep),
                new ExecuteStepsUpdateTask(policy, indexMetadata.getIndex(), currentStep, stepRegistry, this, nowSupplier)
            );
        } else {
            logger.trace("[{}] ignoring step execution from cluster state change event [{}]", index, currentStep.getKey());
        }
    }
    /**
     * Move the index to the given {@code newStepKey}, always checks to ensure that the index's
     * current step matches the {@code currentStepKey} prior to changing the state.
     */
    private void moveToStep(Index index, String policy, Step.StepKey currentStepKey, Step.StepKey newStepKey) {
        logger.debug("[{}] moving to step [{}] {} -> {}", index.getName(), policy, currentStepKey, newStepKey);
        submitUnlessAlreadyQueued(
            String.format(
                Locale.ROOT,
                "ilm-move-to-step {policy [%s], index [%s], currentStep [%s], nextStep [%s]}",
                policy,
                index.getName(),
                currentStepKey,
                newStepKey
            ),
            new MoveToNextStepUpdateTask(index, policy, currentStepKey, newStepKey, nowSupplier, stepRegistry, clusterState -> {
                IndexMetadata indexMetadata = clusterState.metadata().index(index);