Skip to content

Commit f2236e0

Browse files
authored
HTTP Retry Support (#255)
* Basic framework for HTTP retries * Implementing support for retry-after header * Cleaned up the retry-after processing logic * Moved the status code checking logic * Updated tests * Updated class names and tests * Refactored retry impl and tests * Simplified the retry handler * More tests and docs * Further cleaned up the impl and tests * Decoupled retry initializer from credentials * More code cleanup * Cleaning up tests * Not calling any retry code when RetryConfig = null * Added an option to enable/disable retries on IO errors. Added some comments * New test case * Updated some comments; Cleaned up tests * Fixed a typo in a comment * Removing the hard dependency on Apache HTTP Client (#259) * Copied DateUtils source from Apache HC * Updated reference link * Used locks instead of thread locals; Added tests * Added a NOTICE file for third-party code
1 parent 4a3f6f6 commit f2236e0

13 files changed

+1456
-35
lines changed

NOTICE.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Firebase Admin Java SDK
2+
Copyright 2019 Google Inc.
3+
4+
This product includes software developed at
5+
The Apache Software Foundation (http://www.apache.org/).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.google.firebase.internal;
16+
17+
import static com.google.common.base.Preconditions.checkNotNull;
18+
19+
import java.text.ParsePosition;
20+
import java.text.SimpleDateFormat;
21+
import java.util.Calendar;
22+
import java.util.Date;
23+
import java.util.TimeZone;
24+
25+
/**
26+
* A utility class for parsing and formatting HTTP dates as used in cookies and
27+
* other headers. This class handles dates as defined by RFC 2616 section
28+
* 3.3.1 as well as some other common non-standard formats.
29+
*
30+
* <p>Most of this class was borrowed from the
31+
* <a href="http://svn.apache.org/repos/asf/httpcomponents/httpclient/tags/4.3/httpclient/src/main/java/org/apache/http/client/utils/DateUtils.java">
32+
* Apache HTTP client</a> in order to avoid a direct dependency on it. We currently
33+
* have a transitive dependency on this library (via Google API client), but the API
34+
* client team is working towards removing it, so we won't have it in the classpath for long.
35+
*
36+
* <p>The original implementation of this class uses
37+
* thread locals to cache the {@code SimpleDateFormat} instances. Instead, this implementation
38+
* uses static constants and explicit locking to ensure thread safety. This is probably slower,
39+
* but also simpler and avoids memory leaks that may result from unreleased thread locals.
40+
*/
41+
final class DateUtils {
42+
43+
/**
44+
* Date format pattern used to parse HTTP date headers in RFC 1123 format.
45+
*/
46+
static final String PATTERN_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
47+
48+
/**
49+
* Date format pattern used to parse HTTP date headers in RFC 1036 format.
50+
*/
51+
static final String PATTERN_RFC1036 = "EEE, dd-MMM-yy HH:mm:ss zzz";
52+
53+
/**
54+
* Date format pattern used to parse HTTP date headers in ANSI C
55+
* {@code asctime()} format.
56+
*/
57+
static final String PATTERN_ASCTIME = "EEE MMM d HH:mm:ss yyyy";
58+
59+
private static final SimpleDateFormat[] DEFAULT_PATTERNS = new SimpleDateFormat[] {
60+
new SimpleDateFormat(PATTERN_RFC1123),
61+
new SimpleDateFormat(PATTERN_RFC1036),
62+
new SimpleDateFormat(PATTERN_ASCTIME)
63+
};
64+
65+
static final TimeZone GMT = TimeZone.getTimeZone("GMT");
66+
67+
static {
68+
final Calendar calendar = Calendar.getInstance();
69+
calendar.setTimeZone(GMT);
70+
calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
71+
calendar.set(Calendar.MILLISECOND, 0);
72+
final Date defaultTwoDigitYearStart = calendar.getTime();
73+
74+
for (final SimpleDateFormat datePattern : DEFAULT_PATTERNS) {
75+
datePattern.set2DigitYearStart(defaultTwoDigitYearStart);
76+
}
77+
}
78+
79+
/**
80+
* Parses the date value using the given date formats.
81+
*
82+
* @param dateValue the date value to parse
83+
* @return the parsed date or null if input could not be parsed
84+
*/
85+
public static Date parseDate(final String dateValue) {
86+
String v = checkNotNull(dateValue);
87+
// trim single quotes around date if present
88+
// see issue #5279
89+
if (v.length() > 1 && v.startsWith("'") && v.endsWith("'")) {
90+
v = v.substring(1, v.length() - 1);
91+
}
92+
93+
for (final SimpleDateFormat datePattern : DEFAULT_PATTERNS) {
94+
final ParsePosition pos = new ParsePosition(0);
95+
synchronized (datePattern) {
96+
final Date result = datePattern.parse(v, pos);
97+
if (pos.getIndex() != 0) {
98+
return result;
99+
}
100+
}
101+
}
102+
return null;
103+
}
104+
105+
/** This class should not be instantiated. */
106+
private DateUtils() {
107+
}
108+
}

src/main/java/com/google/firebase/internal/FirebaseRequestInitializer.java

+40-15
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,57 @@
1919
import com.google.api.client.http.HttpRequest;
2020
import com.google.api.client.http.HttpRequestInitializer;
2121
import com.google.auth.http.HttpCredentialsAdapter;
22-
import com.google.auth.oauth2.GoogleCredentials;
22+
import com.google.common.collect.ImmutableList;
2323
import com.google.firebase.FirebaseApp;
24+
import com.google.firebase.FirebaseOptions;
2425
import com.google.firebase.ImplFirebaseTrampolines;
2526
import java.io.IOException;
27+
import java.util.List;
2628

2729
/**
28-
* {@code HttpRequestInitializer} for configuring outgoing REST calls. Handles OAuth2 authorization
29-
* and setting timeout values.
30+
* {@code HttpRequestInitializer} for configuring outgoing REST calls. Initializes requests with
31+
* OAuth2 credentials, timeout and retry settings.
3032
*/
31-
public class FirebaseRequestInitializer implements HttpRequestInitializer {
33+
public final class FirebaseRequestInitializer implements HttpRequestInitializer {
3234

33-
private final HttpCredentialsAdapter credentialsAdapter;
34-
private final int connectTimeout;
35-
private final int readTimeout;
35+
private final List<HttpRequestInitializer> initializers;
3636

3737
public FirebaseRequestInitializer(FirebaseApp app) {
38-
GoogleCredentials credentials = ImplFirebaseTrampolines.getCredentials(app);
39-
this.credentialsAdapter = new HttpCredentialsAdapter(credentials);
40-
this.connectTimeout = app.getOptions().getConnectTimeout();
41-
this.readTimeout = app.getOptions().getReadTimeout();
38+
this(app, null);
39+
}
40+
41+
public FirebaseRequestInitializer(FirebaseApp app, @Nullable RetryConfig retryConfig) {
42+
ImmutableList.Builder<HttpRequestInitializer> initializers =
43+
ImmutableList.<HttpRequestInitializer>builder()
44+
.add(new HttpCredentialsAdapter(ImplFirebaseTrampolines.getCredentials(app)))
45+
.add(new TimeoutInitializer(app.getOptions()));
46+
if (retryConfig != null) {
47+
initializers.add(new RetryInitializer(retryConfig));
48+
}
49+
this.initializers = initializers.build();
4250
}
4351

4452
@Override
45-
public void initialize(HttpRequest httpRequest) throws IOException {
46-
credentialsAdapter.initialize(httpRequest);
47-
httpRequest.setConnectTimeout(connectTimeout);
48-
httpRequest.setReadTimeout(readTimeout);
53+
public void initialize(HttpRequest request) throws IOException {
54+
for (HttpRequestInitializer initializer : initializers) {
55+
initializer.initialize(request);
56+
}
57+
}
58+
59+
private static class TimeoutInitializer implements HttpRequestInitializer {
60+
61+
private final int connectTimeoutMillis;
62+
private final int readTimeoutMillis;
63+
64+
TimeoutInitializer(FirebaseOptions options) {
65+
this.connectTimeoutMillis = options.getConnectTimeout();
66+
this.readTimeoutMillis = options.getReadTimeout();
67+
}
68+
69+
@Override
70+
public void initialize(HttpRequest request) {
71+
request.setConnectTimeout(connectTimeoutMillis);
72+
request.setReadTimeout(readTimeoutMillis);
73+
}
4974
}
5075
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2019 Google Inc.
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+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.internal;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
22+
import com.google.api.client.util.BackOff;
23+
import com.google.api.client.util.ExponentialBackOff;
24+
import com.google.api.client.util.Sleeper;
25+
import com.google.common.annotations.VisibleForTesting;
26+
import com.google.common.collect.ImmutableList;
27+
import java.util.List;
28+
import java.util.concurrent.TimeUnit;
29+
30+
/**
31+
* Configures when and how HTTP requests should be retried.
32+
*/
33+
public final class RetryConfig {
34+
35+
private static final int INITIAL_INTERVAL_MILLIS = 500;
36+
37+
private final List<Integer> retryStatusCodes;
38+
private final boolean retryOnIOExceptions;
39+
private final int maxRetries;
40+
private final Sleeper sleeper;
41+
private final ExponentialBackOff.Builder backOffBuilder;
42+
43+
private RetryConfig(Builder builder) {
44+
if (builder.retryStatusCodes != null) {
45+
this.retryStatusCodes = ImmutableList.copyOf(builder.retryStatusCodes);
46+
} else {
47+
this.retryStatusCodes = ImmutableList.of();
48+
}
49+
50+
this.retryOnIOExceptions = builder.retryOnIOExceptions;
51+
checkArgument(builder.maxRetries >= 0, "maxRetries must not be negative");
52+
this.maxRetries = builder.maxRetries;
53+
this.sleeper = checkNotNull(builder.sleeper);
54+
this.backOffBuilder = new ExponentialBackOff.Builder()
55+
.setInitialIntervalMillis(INITIAL_INTERVAL_MILLIS)
56+
.setMaxIntervalMillis(builder.maxIntervalMillis)
57+
.setMultiplier(builder.backOffMultiplier)
58+
.setRandomizationFactor(0);
59+
60+
// Force validation of arguments by building the BackOff object
61+
this.backOffBuilder.build();
62+
}
63+
64+
List<Integer> getRetryStatusCodes() {
65+
return retryStatusCodes;
66+
}
67+
68+
boolean isRetryOnIOExceptions() {
69+
return retryOnIOExceptions;
70+
}
71+
72+
int getMaxRetries() {
73+
return maxRetries;
74+
}
75+
76+
int getMaxIntervalMillis() {
77+
return backOffBuilder.getMaxIntervalMillis();
78+
}
79+
80+
double getBackOffMultiplier() {
81+
return backOffBuilder.getMultiplier();
82+
}
83+
84+
Sleeper getSleeper() {
85+
return sleeper;
86+
}
87+
88+
BackOff newBackOff() {
89+
return backOffBuilder.build();
90+
}
91+
92+
public static Builder builder() {
93+
return new Builder();
94+
}
95+
96+
public static final class Builder {
97+
98+
private List<Integer> retryStatusCodes;
99+
private boolean retryOnIOExceptions;
100+
private int maxRetries;
101+
private int maxIntervalMillis = (int) TimeUnit.MINUTES.toMillis(2);
102+
private double backOffMultiplier = 2.0;
103+
private Sleeper sleeper = Sleeper.DEFAULT;
104+
105+
private Builder() { }
106+
107+
/**
108+
* Sets a list of HTTP status codes that should be retried. If null or empty, HTTP requests
109+
* will not be retried as long as they result in some HTTP response message.
110+
*
111+
* @param retryStatusCodes A list of status codes.
112+
* @return This builder.
113+
*/
114+
public Builder setRetryStatusCodes(List<Integer> retryStatusCodes) {
115+
this.retryStatusCodes = retryStatusCodes;
116+
return this;
117+
}
118+
119+
/**
120+
* Sets whether requests should be retried on IOExceptions.
121+
*
122+
* @param retryOnIOExceptions A boolean indicating whether to retry on IOExceptions.
123+
* @return This builder.
124+
*/
125+
public Builder setRetryOnIOExceptions(boolean retryOnIOExceptions) {
126+
this.retryOnIOExceptions = retryOnIOExceptions;
127+
return this;
128+
}
129+
130+
/**
131+
* Maximum number of retry attempts for a request. This is the cumulative total for all retries
132+
* regardless of their cause (I/O errors and HTTP error responses).
133+
*
134+
* @param maxRetries A non-negative integer.
135+
* @return This builder.
136+
*/
137+
public Builder setMaxRetries(int maxRetries) {
138+
this.maxRetries = maxRetries;
139+
return this;
140+
}
141+
142+
/**
143+
* Maximum interval to wait before a request should be retried. Must be at least 500
144+
* milliseconds. Defaults to 2 minutes.
145+
*
146+
* @param maxIntervalMillis Interval in milliseconds.
147+
* @return This builder.
148+
*/
149+
public Builder setMaxIntervalMillis(int maxIntervalMillis) {
150+
this.maxIntervalMillis = maxIntervalMillis;
151+
return this;
152+
}
153+
154+
/**
155+
* Factor by which the retry interval is multiplied when employing exponential back
156+
* off to delay consecutive retries of the same request. Must be at least 1. Defaults
157+
* to 2.
158+
*
159+
* @param backOffMultiplier Multiplication factor for exponential back off.
160+
* @return This builder.
161+
*/
162+
public Builder setBackOffMultiplier(double backOffMultiplier) {
163+
this.backOffMultiplier = backOffMultiplier;
164+
return this;
165+
}
166+
167+
@VisibleForTesting
168+
Builder setSleeper(Sleeper sleeper) {
169+
this.sleeper = sleeper;
170+
return this;
171+
}
172+
173+
public RetryConfig build() {
174+
return new RetryConfig(this);
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)