1 | /** |
2 | * Licensed to jclouds, Inc. (jclouds) under one or more |
3 | * contributor license agreements. See the NOTICE file |
4 | * distributed with this work for additional information |
5 | * regarding copyright ownership. jclouds licenses this file |
6 | * to you under the Apache License, Version 2.0 (the |
7 | * "License"); you may not use this file except in compliance |
8 | * with the License. You may obtain a copy of the License at |
9 | * |
10 | * http://www.apache.org/licenses/LICENSE-2.0 |
11 | * |
12 | * Unless required by applicable law or agreed to in writing, |
13 | * software distributed under the License is distributed on an |
14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
15 | * KIND, either express or implied. See the License for the |
16 | * specific language governing permissions and limitations |
17 | * under the License. |
18 | */ |
19 | package org.jclouds.http.handlers; |
20 | |
21 | import static org.jclouds.http.HttpUtils.releasePayload; |
22 | |
23 | import java.io.IOException; |
24 | |
25 | import javax.annotation.Resource; |
26 | import javax.inject.Named; |
27 | import javax.inject.Singleton; |
28 | |
29 | import org.jclouds.Constants; |
30 | import org.jclouds.http.HttpCommand; |
31 | import org.jclouds.http.HttpResponse; |
32 | import org.jclouds.http.HttpRetryHandler; |
33 | import org.jclouds.http.IOExceptionRetryHandler; |
34 | import org.jclouds.http.TransformingHttpCommand; |
35 | import org.jclouds.logging.Logger; |
36 | |
37 | import com.google.common.base.Throwables; |
38 | import com.google.inject.Inject; |
39 | |
40 | /** |
41 | * Allow replayable request to be retried a limited number of times, and impose an exponential |
42 | * back-off delay before returning. |
43 | * <p> |
44 | * The back-off delay grows rapidly according to the formula |
45 | * <code>50 * (<i>{@link TransformingHttpCommand#getFailureCount()}</i> ^ 2)</code>. For example: |
46 | * <table> |
47 | * <tr> |
48 | * <th>Number of Attempts</th> |
49 | * <th>Delay in milliseconds</th> |
50 | * </tr> |
51 | * <tr> |
52 | * <td>1</td> |
53 | * <td>50</td> |
54 | * </tr> |
55 | * <tr> |
56 | * <td>2</td> |
57 | * <td>200</td> |
58 | * </tr> |
59 | * <tr> |
60 | * <td>3</td> |
61 | * <td>450</td> |
62 | * </tr> |
63 | * <tr> |
64 | * <td>4</td> |
65 | * <td>800</td> |
66 | * </tr> |
67 | * <tr> |
68 | * <td>5</td> |
69 | * <td>1250</td> |
70 | * </tr> |
71 | * </table> |
72 | * <p> |
73 | * This implementation has two side-effects. It increments the command's failure count with |
74 | * {@link TransformingHttpCommand#incrementFailureCount()}, because this failure count value is used |
75 | * to determine how many times the command has already been tried. It also closes the response's |
76 | * content input stream to ensure connections are cleaned up. |
77 | * |
78 | * @author James Murty |
79 | */ |
80 | @Singleton |
81 | public class BackoffLimitedRetryHandler implements HttpRetryHandler, IOExceptionRetryHandler { |
82 | |
83 | public static BackoffLimitedRetryHandler INSTANCE = new BackoffLimitedRetryHandler(); |
84 | |
85 | @Inject(optional = true) |
86 | @Named(Constants.PROPERTY_MAX_RETRIES) |
87 | private int retryCountLimit = 5; |
88 | |
89 | @Inject(optional = true) |
90 | @Named(Constants.PROPERTY_RETRY_DELAY_START) |
91 | private long delayStart = 50L; |
92 | |
93 | @Resource |
94 | protected Logger logger = Logger.NULL; |
95 | |
96 | public boolean shouldRetryRequest(HttpCommand command, IOException error) { |
97 | return ifReplayableBackoffAndReturnTrue(command); |
98 | } |
99 | |
100 | public boolean shouldRetryRequest(HttpCommand command, HttpResponse response) { |
101 | releasePayload(response); |
102 | return ifReplayableBackoffAndReturnTrue(command); |
103 | } |
104 | |
105 | private boolean ifReplayableBackoffAndReturnTrue(HttpCommand command) { |
106 | command.incrementFailureCount(); |
107 | |
108 | if (!command.isReplayable()) { |
109 | logger.warn("Cannot retry after server error, command is not replayable: %1$s", command); |
110 | return false; |
111 | } else if (command.getFailureCount() > retryCountLimit) { |
112 | logger.warn("Cannot retry after server error, command has exceeded retry limit %1$d: %2$s", retryCountLimit, |
113 | command); |
114 | return false; |
115 | } else { |
116 | imposeBackoffExponentialDelay(command.getFailureCount(), "server error: " + command.toString()); |
117 | return true; |
118 | } |
119 | } |
120 | |
121 | public void imposeBackoffExponentialDelay(int failureCount, String commandDescription) { |
122 | imposeBackoffExponentialDelay(delayStart, 2, failureCount, retryCountLimit, commandDescription); |
123 | } |
124 | |
125 | public void imposeBackoffExponentialDelay(long period, int pow, int failureCount, int max, String commandDescription) { |
126 | imposeBackoffExponentialDelay(period, period * 10l, pow, failureCount, max, commandDescription); |
127 | } |
128 | |
129 | public void imposeBackoffExponentialDelay(long period, long maxPeriod, int pow, int failureCount, int max, |
130 | String commandDescription) { |
131 | long delayMs = (long) (period * Math.pow(failureCount, pow)); |
132 | delayMs = delayMs > maxPeriod ? maxPeriod : delayMs; |
133 | logger.debug("Retry %d/%d: delaying for %d ms: %s", failureCount, max, delayMs, commandDescription); |
134 | try { |
135 | Thread.sleep(delayMs); |
136 | } catch (InterruptedException e) { |
137 | Throwables.propagate(e); |
138 | } |
139 | } |
140 | |
141 | } |