Что происходит с зависимой стадией CompletableFuture, если отменить исходную незавершённую future?
Отмена исходной CompletableFuture завершает её с исключением CancellationException, а незавершённые зависимые стадии обычно также завершаются исключительно, не выполняя свои функции. При этом зависимая стадия не обязана иметь признак isCancelled() == true: она может считаться обычным исключительным завершением, вызванным отменой предшественника.
CompletableFuture появилась в Java 8 как средство асинхронной композиции операций без ручного управления потоками и постоянной блокировки через Future.get(). Обычный Future позволял получить результат и отменить задачу, но плохо описывал цепочки зависимых вычислений и обработку их завершения.
При проектировании цепочек важно различать два механизма: завершение стадии из-за отмены и непосредственную отмену конкретной стадии. Это различие определяет, какие функции будут запущены и как вызывающий код увидит ошибку.
Представим цепочку: одна future получает данные, следующая преобразует их, а третья сохраняет результат. Если исходная операция больше не нужна, её могут отменить до завершения.
Неверное предположение состоит в том, что отмена автоматически прерывает уже выполняющийся пользовательский код или что все стадии цепочки будут отмечены как отменённые. Из-за этого можно ошибочно ожидать CancellationException непосредственно от каждой стадии, потерять корректную обработку ошибки или принять зависимую future за успешно завершённую.
Вызов cancel(false) у незавершённой CompletableFuture переводит её в состояние исключительного завершения с CancellationException. Если у неё есть зависимые стадии, например созданные через thenApply, thenCompose или thenAccept, они не запускают свои функции, пока исходная стадия не дала нормальный результат.
Зависимая стадия завершается исключительно. При наблюдении через join() обычно возникает CompletionException, причиной которой является CancellationException; при использовании get() действует checked-исключение ExecutionException, также с причиной отмены. Поэтому способ ожидания влияет на оболочку исключения, но не меняет сам факт отмены цепочки.
Пример:
Здесь функция String::length не выполняется. source отменена непосредственно, поэтому её isCancelled() возвращает true; length завершилась вследствие исключительного завершения предшественника, но сама не была отменена вызовом cancel().
Отмена не гарантирует остановку уже исполняющегося действия. Параметр mayInterruptIfRunning у CompletableFuture.cancel не используется для прерывания выполняющегося вычисления: CompletableFuture не владеет механизмом принудительной остановки произвольного пользовательского кода. Если вычисление запущено через ExecutorService, отдельно созданный Future этого executor может иметь собственную семантику прерывания, но отмена результирующей CompletableFuture сама по себе не обязана прервать поток.
Если зависимая стадия уже завершилась, отмена исходной future на неё не повлияет. Если нужно преобразовать отмену в резервное значение, следует явно добавить обработчик вроде exceptionally или handle, учитывая, что обработчик получит исключение, связанное с отменой.
Сервис загружает профиль пользователя и затем строит рекомендации. При отмене HTTP-запроса команда хочет прекратить построение рекомендаций. Вариант с отменой исходной CompletableFuture корректно предотвращает запуск ещё не начатых зависимых стадий, но не останавливает код, который уже выполняется; его плюс — простая композиция, минус — отсутствие гарантированного прерывания.
Вариант с передачей отдельного Future из ExecutorService позволяет вызвать cancel(true) и запросить interrupt рабочего потока. Это полезнее для прерываемых блокирующих операций, но требует, чтобы код действительно проверял interrupt и корректно освобождал ресурсы; кроме того, появляется необходимость синхронизировать две модели завершения.
Практически выбирают комбинацию: отмену цепочки используют для распространения результата «работа больше не нужна», а фактическое прерывание обеспечивают на уровне executor и самого кода задачи. В результате новые стадии не запускаются, уже работающие операции завершаются кооперативно, а отмена не маскируется под успешный результат.
Вопрос: Будет ли вызвана функция exceptionally после отмены исходной future?
Ответ: Да, если exceptionally подключена к отменённой future или к зависимой стадии, завершившейся из-за этой отмены. Обработчик получает исключение, связанное с отменой, и может вернуть запасное значение. Но если обработчик сам выбросит исключение, следующая стадия получит уже новое исключительное завершение.
Вопрос: Отменяет ли отмена зависимой стадии её исходную future?
Ответ: Нет, направление зависимости не становится двусторонним. Если вызвать cancel() на дочерней стадии, она завершится отменой, но исходная future и другие зависимые стадии автоматически не отменяются. Это важно учитывать при построении нескольких ветвей: отмена одной ветви не является отменой всей графовой цепочки.
Вопрос: Что произойдёт с зависимой стадией, если исходная future отменена после того, как функция этой стадии уже начала выполняться?
Ответ: Отмена исходной future не откатывает уже начатую функцию и не прерывает её автоматически. Зависимая стадия может продолжить вычисление и завершиться нормально, если исходный результат к моменту запуска уже был успешно передан функции; если же зависимость ещё не была успешно завершена, функция не запустится. Для остановки длительной работы нужны кооперативная проверка признака отмены или interrupt и соответствующая поддержка со стороны выполняемого кода.