Лучший способ очистки ресурсов с помощью CompletableFuture

#java #java-8 #completable-future #try-with-resources

Вопрос:

Я ищу универсальное решение для очистки ресурсов при работе с асинхронным кодом в виде CompletableFuture . Он должен предоставлять те же гарантии, try-finally что и try-with-resources для синхронного кода. Я задал вопрос о проверке кода, и я получил интересный ответ, но он был закрыт, так как он лучше подходит здесь, так что поехали.

Предположим , у меня есть функция, которая принимает a FileInputStream , выполняет некоторую операцию, используя данные файла в фоновом режиме, и возвращает a CompletableFuture<Void , которая будет завершена, когда фоновая операция завершится:

 CompletableFuture<Void> asyncFileOperation(FileInputStream in) {
  var cf = new CompletableFuture<Void>();
  // ...
  return cf;
}
 

FileInputStream это ресурс, который должен быть закрыт, когда он больше не нужен. С синхронным кодом мы бы использовали finally или try-with-resources . Но с CompletableFuture этим все немного сложнее. Я могу придумать четыре способа, с плюсами и минусами:

1. Просто используя whenComplete :

 FileInputStream in = openFile("data.txt");
asyncFileOperation(in)
  .whenComplete((r, x) -> in.close());
 

Это «очевидное» решение, поскольку оно является прямым переводом из finally мира в CompletableFuture мир. Он эффективно закрывает поток по asyncFileOperation завершении, независимо от того, успешно это или неудачно. За исключением случаев, когда asyncFileOperation выбрасывает синхронное исключение RuntimeException даже до возврата CompletableFuture , whenComplete не будет вызвано, поэтому и не будет close .

Мы могли бы списать это как «никогда не бывает», но неожиданные исключения-это та самая причина, по которой мы используем finally и try-with-resources в синхронном случае. Есть ли какие-либо основания полагать, что это может быть по-другому для методов, которые возвращаются CompletableFuture ?

2. Зову asyncFileOperation внутрь thenCompose :

 CompletableFuture.completedFuture(openFile("data.txt"))
  .thenCompose(this::asyncFileOperation)
  .whenComplete((r, x) -> in.close());
 

Это решает проблему решения 1 , выполняя asyncFileOperation в контексте существующего CompletableFuture , поэтому любое исключение перехватывается и whenComplete всегда выполняется. Проблема с этим заключается в том, что, когда возникает неожиданное исключение, оно будет проглочено беззвучно (если только мы не обработаем его полностью). Он не будет всплывать в стеке (если мы не вернем CompletableFuture его, и обработчик, расположенный дальше по стеку, обработает его), и он никогда не будет передан an UncaughtExceptionHandler . Это особенно плохо для неожиданных исключений, таких как NullPointerException .

3. Using both try-catch and whenComplete :

 FileInputStream in = openFile("data.txt");
try {
  asyncFileOperation(in)
    .whenComplete((r, x) -> in.close());
} catch (RuntimeException exc) {
  in.close();
  throw exc;
}
 

This solves all of the above problems, and doesn’t really introduce new ones, except that it’s ugly and repetitive. I’ve seen this approach in the JDK implementation of HttpClient.sendAsync :
it calls unreference() both in a lambda passed to whenComplete and in a catch clause.

4. Doing it inside asyncFileOperation :

 CompletableFuture<Void> asyncFileOperation(FileInputStream in) {
  var cf = new CompletableFuture<Void>();
  // ...
  return cf.whenComplete((r, x) -> in.close());
}

asyncFileOperation(openFile("data.txt"));
 

Here, asyncFileOperation closes the stream itself ones it completes. Depending on the implementation, it might be obvious that there can be no RuntimeException, so we could eliminate some cases. If not, we need to use one of the approaches above to handle this, but we can handle it once and for all rather for every call.

Still, I don’t like this because maybe we want to continue to use the stream for some other operations, and its always nice when resources are created and cleaned up in the same place.


Итак, что вы думаете об этих вариантах? У вас есть другие?

Я думаю, что вопрос на самом деле сводится к тому, достаточно ли whenComplete одного, и я просто параноик. Если да, я бы придерживался решения 1, в противном случае, возможно, это вопрос контекста и личного вкуса, подходит ли решение 2 или 3 лучше.


Редактировать

Некоторые комментарии предлагают полностью избежать этой ситуации, синхронно открывая и закрывая ресурс внутри asyncFileOperation . Я согласен, что это было бы предпочтительнее, если бы это было возможно, но что, если это не так? Может быть, звонивший должен прочитать несколько строк и asyncFileOperation прочитать остальное. Или, может asyncFileOperation быть, читает файл асинхронно, что означает, что мы можем открывать и закрывать поток внутри функции, но мы должны закрывать его асинхронно, поэтому снова возникает вопрос, как это сделать (возможно, здесь решение 3 лучше, потому что мы можем закрыть ресурс в одном месте, как HttpClient и раньше).

Кроме того, я использовал FileInputStream просто в качестве конкретного примера. В нашем реальном проекте мы часто показываем диалоговое окно, которое возвращает CompletableFuture значение, которое завершается при закрытии диалога, и затем нам нужно выполнить некоторую очистку, например, удалить прослушиватели событий пользовательского интерфейса или закрыть соединение с БД, которое используется на протяжении всего асинхронного процесса.

Всегда ли такие сценарии плохи? Должен ли я избегать их любой ценой? Всегда ли это возможно? Если да, то существуют ли какие-либо ресурсы о том, как разрабатывать программное обеспечение таким образом? Если нет, я хотел бы знать, как лучше всего справляться с такими ситуациями в целом.

Комментарии:

1. Не можете asynFileOperation принять путь к файлу вместо того, чтобы принимать открытый поток? Вы смотрели на CompletableFuture#handle метод?

2. Да, я знаю об handle этом , по сути, это то же whenComplete самое, за исключением того, что это позволяет сопоставить как успех, так и неудачу с новым успешным значением, и мне это здесь не нужно. Я просто хочу очистить ресурсы и вернуть асинхронный результат как есть. Что касается передачи пути к файлу вместо потока, см. Мое редактирование.