1 /*
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */

15
16 package software.amazon.awssdk.core.internal.http.pipeline.stages;
17
18 import static software.amazon.awssdk.core.internal.http.timers.TimerUtils.resolveTimeoutInMillis;
19 import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely;
20
21 import java.io.IOException;
22 import software.amazon.awssdk.annotations.SdkInternalApi;
23 import software.amazon.awssdk.core.Response;
24 import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
25 import software.amazon.awssdk.core.client.config.SdkClientOption;
26 import software.amazon.awssdk.core.exception.AbortedException;
27 import software.amazon.awssdk.core.exception.ApiCallAttemptTimeoutException;
28 import software.amazon.awssdk.core.exception.SdkInterruptedException;
29 import software.amazon.awssdk.core.internal.http.HttpClientDependencies;
30 import software.amazon.awssdk.core.internal.http.RequestExecutionContext;
31 import software.amazon.awssdk.core.internal.http.pipeline.RequestPipeline;
32 import software.amazon.awssdk.core.internal.http.pipeline.RequestToResponsePipeline;
33 import software.amazon.awssdk.http.SdkHttpFullRequest;
34
35 /**
36  * Check if an {@link Exception} is caused by either ApiCallTimeout or ApiAttemptTimeout and translate that
37  * exception to a more appropriate timeout related exception so that it can be handled in other stages.
38  */

39 @SdkInternalApi
40 public final class TimeoutExceptionHandlingStage<OutputT> implements RequestToResponsePipeline<OutputT> {
41
42     private final HttpClientDependencies dependencies;
43     private final RequestPipeline<SdkHttpFullRequest, Response<OutputT>> requestPipeline;
44
45     public TimeoutExceptionHandlingStage(HttpClientDependencies dependencies, RequestPipeline<SdkHttpFullRequest,
46         Response<OutputT>> requestPipeline) {
47         this.dependencies = dependencies;
48         this.requestPipeline = requestPipeline;
49     }
50
51     /**
52      * Translate an {@link Exception} caused by timeout based on the following criteria:
53      *
54      * <ul>
55      * <li>If the {@link Exception} is caused by {@link ClientOverrideConfiguration#apiCallTimeout}, translates it to
56      * {@link InterruptedException} so it can be handled
57      * appropriately in {@link ApiCallTimeoutTrackingStage}. </li>
58      * <li>
59      * If it is caused by {@link ClientOverrideConfiguration#apiCallAttemptTimeout()}, clear the interrupt status,
60      * translates it to {@link ApiCallAttemptTimeoutException} so that it might be retried
61      * in {@link RetryableStage}
62      * </li>
63      * </ul>
64      *
65      * <p>
66      * ApiCallTimeout takes precedence because it is not retryable.
67      *
68      * @param request the request
69      * @param context Context containing both request dependencies, and a container for any mutable state that must be shared
70      * between stages.
71      * @return the response
72      * @throws Exception the translated exception or the original exception
73      */

74     @Override
75     public Response<OutputT> execute(SdkHttpFullRequest request, RequestExecutionContext context) throws Exception {
76         try {
77             return requestPipeline.execute(request, context);
78         } catch (Exception e) {
79             throw translatePipelineException(context, e);
80         }
81     }
82
83     /**
84      * Take the given exception thrown from the wrapped pipeline and return a more appropriate
85      * timeout related exception based on its type and the the execution status.
86      *
87      * @param context The execution context.
88      * @param e The exception thrown from the inner pipeline.
89      * @return The translated exception.
90      */

91     private Exception translatePipelineException(RequestExecutionContext context, Exception e) {
92         if (e instanceof InterruptedException || e instanceof IOException ||
93             e instanceof AbortedException || Thread.currentThread().isInterrupted()) {
94             return handleTimeoutCausedException(context, e);
95         }
96         return e;
97     }
98
99     private Exception handleTimeoutCausedException(RequestExecutionContext context, Exception e) {
100         if (e instanceof SdkInterruptedException) {
101             ((SdkInterruptedException) e).getResponseStream().ifPresent(r -> invokeSafely(r::close));
102         }
103
104         if (isCausedByApiCallTimeout(context)) {
105             return new InterruptedException();
106         }
107
108         if (isCausedByApiCallAttemptTimeout(context)) {
109             // Clear the interrupt status
110             Thread.interrupted();
111             return generateApiCallAttemptTimeoutException(context);
112         }
113
114         if (e instanceof InterruptedException) {
115             Thread.currentThread().interrupt();
116             return AbortedException.create("Thread was interrupted", e);
117         }
118
119         return e;
120     }
121
122     /**
123      * Detects if the exception thrown was triggered by the api call attempt timeout.
124      *
125      * @param context {@link RequestExecutionContext} object.
126      * @return True if the exception was caused by the attempt timeout, false if not.
127      */

128     private boolean isCausedByApiCallAttemptTimeout(RequestExecutionContext context) {
129         return context.apiCallAttemptTimeoutTracker().hasExecuted();
130     }
131
132     /**
133      * Detects if the exception thrown was triggered by the api call timeout.
134      *
135      * @param context {@link RequestExecutionContext} object.
136      * @return True if the exception was caused by the call timeout, false if not.
137      */

138     private boolean isCausedByApiCallTimeout(RequestExecutionContext context) {
139         return context.apiCallTimeoutTracker().hasExecuted();
140     }
141
142     private ApiCallAttemptTimeoutException generateApiCallAttemptTimeoutException(RequestExecutionContext context) {
143         return ApiCallAttemptTimeoutException.create(
144             resolveTimeoutInMillis(context.requestConfig()::apiCallAttemptTimeout,
145                                    dependencies.clientConfiguration().option(SdkClientOption.API_CALL_ATTEMPT_TIMEOUT)));
146     }
147 }
148