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
IndexMetadata newIndexMeta = newState.metadata().index(index);
                            Step indexMetaCurrentStep = getCurrentStep(stepRegistry, policy, newIndexMeta);
                            StepKey stepKey = indexMetaCurrentStep.getKey();
                            if (stepKey != null && stepKey != TerminalPolicyStep.KEY && newIndexMeta != null) {
                                logger.trace(
                                    "policy [{}] for index [{}] was moved back on the failed step for as part of an automatic "
                                        + "retry. Attempting to execute the failed step [{}] if it's an async action",
                                    policy,
                                    index,
                                    stepKey
                                );
                                maybeRunAsyncAction(newState, newIndexMeta, policy, stepKey);
                            }
                        }
                    }
                },
                newExecutor()
            );
        } else {
            logger.debug("policy [{}] for index [{}] on an error step after a terminal error, skipping execution", policy, index);
        }
    }
    /**
     * If the current step (matching the expected step key) is an asynchronous action step, run it
     */
    void maybeRunAsyncAction(ClusterState currentState, IndexMetadata indexMetadata, String policy, StepKey expectedStepKey) {
        String index = indexMetadata.getIndex().getName();
        LifecycleExecutionState lifecycleState = indexMetadata.getLifecycleExecutionState();
        final Step currentStep;
        try {