Как обработать длительную задачу, которая может превысить тайм-аут
Что проверяют
Понимаете ли вы, что таймаут на ожидание и остановка самой задачи это разные вещи. Следом обычно спрашивают, что станет с задачей после таймаута: продолжит работать, будет прервана или запустится повторно.
Как отвечать
Сначала уточните, чей это таймаут: HTTP-запроса на балансировщике, транзакции или вызова внешнего сервиса. Если задача не укладывается в HTTP-запрос, её выносят из запроса:
- асинхронный API: сервер отвечает
202 Acceptedс идентификатором задачи, клиент опрашивает статус или получает вебхук. Сама работа уходит в очередь (Kafka, RabbitMQ) или в пул через@Async; - пачки с прогрессом: обработка частями с сохранением позиции, чтобы после падения продолжить с места остановки. Для этого есть Spring Batch;
- идемпотентность: повторный запуск не должен дублировать результат.
Если нужно ограничить ожидание в коде:
Future.get(timeout, unit): бросаетTimeoutException, но задачу не останавливает, нужен явныйfuture.cancel(true);orTimeoutиcompleteOnTimeoutуCompletableFuture(с Java 9): завершают future исключением или значением по умолчанию. Задача в пуле при этом работает дальше,cancelуCompletableFutureпоток не прерывает;- прерывание кооперативное:
cancel(true)лишь выставляет флаг. Задача должна проверятьThread.currentThread().isInterrupted()или стоять в прерываемом вызове вродеsleepилиBlockingQueue.take; - свой пул:
runAsyncбез executor уходит вForkJoinPool.commonPool(), и блокирующий ввод-вывод забивает общий пул. Нужен отдельныйExecutorService, с Java 21 удобенExecutors.newVirtualThreadPerTaskExecutor().
Где спотыкаются
Предлагают просто увеличить таймаут. Это откладывает проблему до следующего роста данных и держит поток занятым всё это время.
Считают, что orTimeout или get с таймаутом останавливают задачу. Она дорабатывает в фоне, а после повтора запроса таких задач уже две.
Глотают InterruptedException пустым catch. Флаг прерывания сброшен, и задача так и не узнает, что её просили остановиться. Минимум: вернуть флаг через Thread.currentThread().interrupt().