Предотвратить прерывание загрузки S3 во время корректного завершения работы Spring Boot - PullRequest
0 голосов
/ 07 мая 2020

У меня есть приложение 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 вызывает повторное создание исключения. Однако в идеале я хотел бы по возможности избегать такой очистки при завершении работы приложения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...