All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.action.support.CancellableFanOut Maven / Gradle / Ivy

/*
 * 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 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 or the Server
 * Side Public License, v 1.
 */

package org.elasticsearch.action.support;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.Strings;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.tasks.CancellableTask;
import org.elasticsearch.tasks.Task;

import java.util.Iterator;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Allows an action to fan-out to several sub-actions and accumulate their results, but which reacts to a cancellation by releasing all
 * references to itself, and hence the partially-accumulated results, allowing them to be garbage-collected. This is a useful protection for
 * cases where the results may consume a lot of heap (e.g. stats) but the final response may be delayed by a single slow node for long
 * enough that the client gives up.
 * 

* Note that it's easy to accidentally capture another reference to this class when implementing it, and this will prevent the early release * of any accumulated results. Beware of lambdas and method references. You must test your implementation carefully (using e.g. * {@code ReachabilityChecker}) to make sure it doesn't do this. */ public abstract class CancellableFanOut { private static final Logger logger = LogManager.getLogger(CancellableFanOut.class); /** * Run the fan-out action. * * @param task The task to watch for cancellations. If {@code null} or not a {@link CancellableTask} then the fan-out still * works, just without any cancellation handling. * @param itemsIterator The items over which to fan out. Iterated on the calling thread. * @param listener A listener for the final response, which is completed after all the fanned-out actions have completed. It is not * completed promptly on cancellation. Completed on the thread that handles the final per-item response (or * the calling thread if there are no items). */ public final void run(@Nullable Task task, Iterator itemsIterator, ActionListener listener) { final var cancellableTask = task instanceof CancellableTask ct ? ct : null; // Captures the final result as soon as it's known (either on completion or on cancellation) without necessarily completing the // outer listener, because we do not want to complete the outer listener until all sub-tasks are complete final var resultListener = new SubscribableListener(); // Completes resultListener (either on completion or on cancellation). Captures a reference to 'this', but within an // 'AtomicReference' which is cleared, releasing the reference promptly, when executed. final var resultListenerCompleter = new AtomicReference(() -> { if (cancellableTask != null && cancellableTask.notifyIfCancelled(resultListener)) { return; } // It's important that we complete resultListener before returning, because otherwise there's a risk that a cancellation arrives // later which might unexpectedly complete the final listener on a transport thread. ActionListener.completeWith(resultListener, this::onCompletion); }); // Collects the per-item listeners up so they can all be completed exceptionally on cancellation. Never completed successfully. final var itemCancellationListener = new SubscribableListener(); if (cancellableTask != null) { cancellableTask.addListener(() -> { assert cancellableTask.isCancelled(); // probably on a transport thread and we don't know if any of the callbacks are slow so we must avoid running them by // blocking the thread which might add a subscriber to resultListener until after we've completed it final var semaphore = new Semaphore(0); // resultListenerCompleter is currently either a no-op, or else it immediately completes resultListener with a cancellation // while it has no subscribers, so either way this semaphore is not held for long resultListenerCompleter.getAndSet(semaphore::acquireUninterruptibly).run(); semaphore.release(); // finally, release refs to all the per-item listeners (without calling onItemFailure, so this is also fast) cancellableTask.notifyIfCancelled(itemCancellationListener); }); } try (var refs = new RefCountingRunnable(new SubtasksCompletionHandler<>(resultListenerCompleter, resultListener, listener))) { while (itemsIterator.hasNext()) { final var item = itemsIterator.next(); // Captures a reference to 'this', but within a 'notifyOnce' so it is released promptly when completed. final ActionListener itemResponseListener = ActionListener.notifyOnce(new ActionListener<>() { @Override public void onResponse(ItemResponse itemResponse) { try { onItemResponse(item, itemResponse); } catch (Exception e) { logger.error( () -> Strings.format( "unexpected exception handling [%s] for item [%s] in [%s]", itemResponse, item, CancellableFanOut.this ), e ); assert false : e; } } @Override public void onFailure(Exception e) { if (cancellableTask != null && cancellableTask.isCancelled()) { // Completed on cancellation so it is released promptly, but there's no need to handle the exception. return; } onItemFailure(item, e); // must not throw, enforced by the ActionListener#notifyOnce wrapper } @Override public String toString() { return "[" + CancellableFanOut.this + "][" + listener + "][" + item + "]"; } }); if (cancellableTask != null) { if (cancellableTask.isCancelled()) { return; } // Register this item's listener for prompt cancellation notification. itemCancellationListener.addListener(itemResponseListener); } // Process the item, capturing a ref to make sure the outer listener is completed after this item is processed. ActionListener.run(ActionListener.releaseAfter(itemResponseListener, refs.acquire()), l -> sendItemRequest(item, l)); } } catch (Exception e) { // NB the listener may have been completed already (by exiting this try block) so this exception may not be sent to the caller, // but we cannot do anything else with it; an exception here is a bug anyway. logger.error("unexpected failure in [" + this + "][" + listener + "]", e); assert false : e; throw e; } } /** * Run the action (typically by sending a transport request) for an individual item. Called in sequence on the thread that invoked * {@link #run}. May not be called for every item if the task is cancelled during the iteration. *

* Note that it's easy to accidentally capture another reference to this class when implementing this method, and that will prevent the * early release of any accumulated results. Beware of lambdas, and test carefully. */ protected abstract void sendItemRequest(Item item, ActionListener listener); /** * Handle a successful response for an item. May be called concurrently for multiple items. Not called if the task is cancelled. Must * not throw any exceptions. *

* Note that it's easy to accidentally capture another reference to this class when implementing this method, and that will prevent the * early release of any accumulated results. Beware of lambdas, and test carefully. */ protected abstract void onItemResponse(Item item, ItemResponse itemResponse); /** * Handle a failure for an item. May be called concurrently for multiple items. Not called if the task is cancelled. Must not throw any * exceptions. *

* Note that it's easy to accidentally capture another reference to this class when implementing this method, and that will prevent the * early release of any accumulated results. Beware of lambdas, and test carefully. */ protected abstract void onItemFailure(Item item, Exception e); /** * Called when responses for all items have been processed, on the thread that processed the last per-item response or possibly the * thread which called {@link #run} if all items were processed before {@link #run} returns. Not called if the task is cancelled. *

* Note that it's easy to accidentally capture another reference to this class when implementing this method, and that will prevent the * early release of any accumulated results. Beware of lambdas, and test carefully. */ protected abstract FinalResponse onCompletion() throws Exception; private static class SubtasksCompletionHandler implements Runnable { private final AtomicReference resultListenerCompleter; private final SubscribableListener resultListener; private final ActionListener listener; private SubtasksCompletionHandler( AtomicReference resultListenerCompleter, SubscribableListener resultListener, ActionListener listener ) { this.resultListenerCompleter = resultListenerCompleter; this.resultListener = resultListener; this.listener = listener; } @Override public void run() { // When all sub-tasks are complete, pass the result from resultListener to the outer listener. resultListenerCompleter.getAndSet(() -> {}).run(); // May block (very briefly) if there's a concurrent cancellation, so that we are sure the resultListener is now complete and // therefore the outer listener is completed on this thread. assert resultListener.isDone(); resultListener.addListener(listener); } @Override public String toString() { return getClass().getSimpleName() + "[" + listener.toString() + "]"; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy