У меня есть приложение Spring Boot с запланированной задачей, которая загружает файлы в корзину S3 с помощью AWS SDK для Java 2.13.10. При завершении работы приложения я хотел бы, чтобы запланированная задача завершила sh загрузку текущего файла и завершилась вместо перехода к следующему файлу. Однако после запуска завершения работы приложения через SIGINT
SDK AWS немедленно выдает AbortedException
(в некоторых случаях вызывается SdkInterruptedException
), что приводит к сбою текущей загрузки и некорректное завершение запланированной задачи.
Вот скелет запланированной задачи, которая демонстрирует такое же поведение:
@Component
public class TestScheduledTaskService implements ApplicationListener<ContextClosedEvent> {
public static final String AWS_ACCESS_KEY_ID = "";
public static final String AWS_ACCESS_KEY_SECRET = "";
public static final String AWS_BUCKET_NAME = "";
private static final Logger LOGGER = LoggerFactory.getLogger(TestScheduledTaskService.class);
private boolean shutdownStarted = false;
private S3Client s3Client;
public TestScheduledTaskService() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(AWS_ACCESS_KEY_ID, AWS_ACCESS_KEY_SECRET);
s3Client = S3Client.builder()
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.EU_CENTRAL_1)
.build();
}
@Scheduled(fixedDelay = 10_0000)
public void scheduledTask() {
LOGGER.info("Starting scheduled task");
for (int i = 0; i < 10; i++) {
if (shutdownStarted) {
LOGGER.info("Application shutting down, stopping scheduled task");
return;
}
String key = String.valueOf(Instant.now().getEpochSecond());
LOGGER.info("Creating and uploading 5MiB test file with key {}", key);
byte[] randomBytes = new byte[1024 * 1024 * 5];
new Random().nextBytes(randomBytes);
PutObjectRequest putRequest = PutObjectRequest.builder()
.bucket(AWS_BUCKET_NAME)
.key(key)
.build();
s3Client.putObject(putRequest, RequestBody.fromBytes(randomBytes));
}
LOGGER.info("Scheduled task complete.");
}
@Override
public void onApplicationEvent(final ContextClosedEvent event) {
shutdownStarted = true;
LOGGER.info("Application is shutting down, stopping after next upload");
}
}
Планировщик Spring был настроен путем определения bean-компонента в классе конфигурации:
@Bean
public TaskSchedulerCustomizer taskSchedulerCustomizer() {
return taskScheduler -> {
taskScheduler.setAwaitTerminationSeconds(60);
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
};
}
Вывод журнала и полная трассировка стека при завершении:
2020-05-07 00:44:56.384 INFO 144232 --- [extShutdownHook] c.e.d.service.TestScheduledTaskService : Application is shutting down, stopping after next upload
2020-05-07 00:44:56.386 INFO 144232 --- [extShutdownHook] o.s.s.c.ThreadPoolTaskScheduler : Shutting down ExecutorService 'taskScheduler'
2020-05-07 00:44:56.701 ERROR 144232 --- [ scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler : Unexpected error occurred in scheduled task
software.amazon.awssdk.core.exception.AbortedException: null
at software.amazon.awssdk.core.exception.AbortedException$BuilderImpl.build(AbortedException.java:84) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.io.SdkFilterInputStream.abortIfNeeded(SdkFilterInputStream.java:45) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.io.SdkFilterInputStream.read(SdkFilterInputStream.java:65) ~[sdk-core-2.13.10.jar:na]
at org.apache.http.entity.InputStreamEntity.writeTo(InputStreamEntity.java:140) ~[httpcore-4.4.13.jar:4.4.13]
at software.amazon.awssdk.http.apache.internal.RepeatableInputStreamRequestEntity.writeTo(RepeatableInputStreamRequestEntity.java:149) ~[apache-client-2.13.10.jar:na]
at org.apache.http.impl.DefaultBHttpClientConnection.sendRequestEntity(DefaultBHttpClientConnection.java:156) ~[httpcore-4.4.13.jar:4.4.13]
at org.apache.http.impl.conn.CPoolProxy.sendRequestEntity(CPoolProxy.java:152) ~[httpclient-4.5.12.jar:4.5.12]
at org.apache.http.protocol.HttpRequestExecutor.doSendRequest(HttpRequestExecutor.java:238) ~[httpcore-4.4.13.jar:4.4.13]
at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:123) ~[httpcore-4.4.13.jar:4.4.13]
at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:272) ~[httpclient-4.5.12.jar:4.5.12]
at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) ~[httpclient-4.5.12.jar:4.5.12]
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) ~[httpclient-4.5.12.jar:4.5.12]
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) ~[httpclient-4.5.12.jar:4.5.12]
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56) ~[httpclient-4.5.12.jar:4.5.12]
at software.amazon.awssdk.http.apache.internal.impl.ApacheSdkHttpClient.execute(ApacheSdkHttpClient.java:72) ~[apache-client-2.13.10.jar:na]
at software.amazon.awssdk.http.apache.ApacheHttpClient.execute(ApacheHttpClient.java:232) ~[apache-client-2.13.10.jar:na]
at software.amazon.awssdk.http.apache.ApacheHttpClient.access$500(ApacheHttpClient.java:98) ~[apache-client-2.13.10.jar:na]
at software.amazon.awssdk.http.apache.ApacheHttpClient$1.call(ApacheHttpClient.java:213) ~[apache-client-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage.executeHttpRequest(MakeHttpRequestStage.java:66) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage.execute(MakeHttpRequestStage.java:51) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage.execute(MakeHttpRequestStage.java:35) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:73) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallAttemptTimeoutTrackingStage.execute(ApiCallAttemptTimeoutTrackingStage.java:42) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:77) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.TimeoutExceptionHandlingStage.execute(TimeoutExceptionHandlingStage.java:39) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:64) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.RetryableStage.execute(RetryableStage.java:34) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:56) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.StreamManagingStage.execute(StreamManagingStage.java:36) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.executeWithTimer(ApiCallTimeoutTrackingStage.java:80) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:60) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.ApiCallTimeoutTrackingStage.execute(ApiCallTimeoutTrackingStage.java:42) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.RequestPipelineBuilder$ComposingRequestPipelineStage.execute(RequestPipelineBuilder.java:206) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:37) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.pipeline.stages.ExecutionFailureExceptionReportingStage.execute(ExecutionFailureExceptionReportingStage.java:26) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.http.AmazonSyncHttpClient$RequestExecutionBuilderImpl.execute(AmazonSyncHttpClient.java:189) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.invoke(BaseSyncClientHandler.java:121) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.doExecute(BaseSyncClientHandler.java:147) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.internal.handler.BaseSyncClientHandler.execute(BaseSyncClientHandler.java:101) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.core.client.handler.SdkSyncClientHandler.execute(SdkSyncClientHandler.java:45) ~[sdk-core-2.13.10.jar:na]
at software.amazon.awssdk.awscore.client.handler.AwsSyncClientHandler.execute(AwsSyncClientHandler.java:55) ~[aws-core-2.13.10.jar:na]
at software.amazon.awssdk.services.s3.DefaultS3Client.putObject(DefaultS3Client.java:7376) ~[s3-2.13.10.jar:na]
at com.example.demo.service.TestScheduledTaskService.scheduledTask(TestScheduledTaskService.java:59) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:567) ~[na:na]
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]
at java.base/java.util.concurrent.FutureTask.runAndReset$$$capture(FutureTask.java:305) ~[na:na]
at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java) ~[na:na]
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:830) ~[na:na]
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
Как я могу предотвратить такое поведение и разрешить завершение текущей загрузки файла?
Есть много вопросов о переполнении стека, связанных с плавным завершением работы приложений Spring Boot, но мне не удалось найти ни одного, похожего на проблему здесь. Я считаю, что проблема связана с обработкой AWS клиентом InterruptedException
. Замена загрузки файла S3 в этом примере какой-либо другой операцией блокировки приводит к плавному завершению при запуске следующей l oop итерации после того, как завершение работы было инициировано, без влияния на операцию блокировки.
Я рассмотрел один обходной путь. перехват исключения, созданного вызовом putObject
, и попытка удалить ключ с тем же именем для очистки, но вызов deleteObject
внутри блока catch вызывает повторное создание исключения. Однако в идеале я хотел бы по возможности избегать такой очистки при завершении работы приложения.