https://github.com/elastic/elasticsearch/blob/099fb84cfcf87be6689c3516674f45438bafa767/server/src/main/java/org/elasticsearch/action/ActionListener.java#L420-L432

Rather than keeping hold of the `delegate` and using an `AtomicBoolean` to track the completion, we could use an `AtomicReference` and drop the whole listener on completion, which might free up heap in some fail-fast cases.
server/src/main/java/org/elasticsearch/action/StepListener.java
private final ListenableFuture<Response> delegate;
    public StepListener() {
        this.delegate = new ListenableFuture<>();
    }
    @Override
    protected void innerOnResponse(Response response) {
        delegate.onResponse(response);
    }
    @Override
    protected void innerOnFailure(Exception e) {
        delegate.onFailure(e);
    }
    /**
     * Registers the given actions which are called when this step is completed. If this step is completed successfully,
     * the {@code onResponse} is called with the result; otherwise the {@code onFailure} is called with the failure.
     *
     * @param onResponse is called when this step is completed successfully
     * @param onFailure  is called when this step is completed with a failure
     */
    public void whenComplete(CheckedConsumer<Response, Exception> onResponse, Consumer<Exception> onFailure) {
        addListener(ActionListener.wrap(onResponse, onFailure));
    }
    /**
     * Combines this listener with another one, waiting for both to successfully complete and combining their results.
     *
     * @param other the other step listener to combine with
     * @param fn    the function that combines the results
     * @return the combined listener
     */
    public <OtherResponse, OuterResponse> StepListener<OuterResponse> thenCombine(