Skip to content

Commit d824db8

Browse files
authored
Add http redirect support to the cct transport backend. (#750)
Redirects are followed up to 4 times(after initial request).
1 parent 8169558 commit d824db8

File tree

6 files changed

+258
-21
lines changed

6 files changed

+258
-21
lines changed

transport/transport-backend-cct/src/main/java/com/google/android/datatransport/cct/CctTransportBackend.java

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414

1515
package com.google.android.datatransport.cct;
1616

17+
import static com.google.android.datatransport.runtime.retries.Retries.retry;
18+
1719
import android.content.Context;
1820
import android.net.ConnectivityManager;
1921
import android.net.NetworkInfo;
2022
import android.os.Build;
23+
import androidx.annotation.Nullable;
2124
import androidx.annotation.VisibleForTesting;
2225
import com.google.android.datatransport.backend.cct.BuildConfig;
2326
import com.google.android.datatransport.cct.proto.AndroidClientInfo;
@@ -36,7 +39,6 @@
3639
import com.google.android.datatransport.runtime.backends.TransportBackend;
3740
import com.google.android.datatransport.runtime.time.Clock;
3841
import com.google.protobuf.ByteString;
39-
import com.google.protobuf.InvalidProtocolBufferException;
4042
import java.io.ByteArrayOutputStream;
4143
import java.io.IOException;
4244
import java.io.InputStream;
@@ -212,8 +214,9 @@ private BatchedLogRequest getRequestBody(BackendRequest backendRequest) {
212214
return batchedRequestBuilder.build();
213215
}
214216

215-
private BackendResponse doSend(BatchedLogRequest requestBody, String apiKey) throws IOException {
216-
HttpURLConnection connection = (HttpURLConnection) endPoint.openConnection();
217+
private HttpResponse doSend(HttpRequest request) throws IOException {
218+
219+
HttpURLConnection connection = (HttpURLConnection) request.url.openConnection();
217220
connection.setConnectTimeout(CONNECTION_TIME_OUT);
218221
connection.setReadTimeout(readTimeout);
219222
connection.setDoOutput(true);
@@ -224,8 +227,8 @@ private BackendResponse doSend(BatchedLogRequest requestBody, String apiKey) thr
224227
connection.setRequestProperty(CONTENT_ENCODING_HEADER_KEY, GZIP_CONTENT_ENCODING);
225228
connection.setRequestProperty(CONTENT_TYPE_HEADER_KEY, PROTOBUF_CONTENT_TYPE);
226229

227-
if (apiKey != null) {
228-
connection.setRequestProperty(API_KEY_HEADER_KEY, apiKey);
230+
if (request.apiKey != null) {
231+
connection.setRequestProperty(API_KEY_HEADER_KEY, request.apiKey);
229232
}
230233

231234
WritableByteChannel channel = Channels.newChannel(connection.getOutputStream());
@@ -234,32 +237,29 @@ private BackendResponse doSend(BatchedLogRequest requestBody, String apiKey) thr
234237
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(output);
235238

236239
try {
237-
requestBody.writeTo(gzipOutputStream);
240+
request.requestBody.writeTo(gzipOutputStream);
238241
} finally {
239242
gzipOutputStream.close();
240243
}
241244
channel.write(ByteBuffer.wrap(output.toByteArray()));
242245
int responseCode = connection.getResponseCode();
243246
LOGGER.info("Status Code: " + responseCode);
244247

245-
long nextRequestMillis;
248+
if (responseCode == 302 || responseCode == 301) {
249+
String redirect = connection.getHeaderField("Location");
250+
return new HttpResponse(responseCode, new URL(redirect), 0);
251+
}
252+
if (responseCode != 200) {
253+
return new HttpResponse(responseCode, null, 0);
254+
}
255+
246256
InputStream inputStream = connection.getInputStream();
247257
try {
248-
try {
249-
nextRequestMillis = LogResponse.parseFrom(inputStream).getNextRequestWaitMillis();
250-
} catch (InvalidProtocolBufferException e) {
251-
return BackendResponse.fatalError();
252-
}
258+
long nextRequestMillis = LogResponse.parseFrom(inputStream).getNextRequestWaitMillis();
259+
return new HttpResponse(responseCode, null, nextRequestMillis);
253260
} finally {
254261
inputStream.close();
255262
}
256-
if (responseCode == 200) {
257-
return BackendResponse.ok(nextRequestMillis);
258-
} else if (responseCode >= 500 || responseCode == 404) {
259-
return BackendResponse.transientError();
260-
} else {
261-
return BackendResponse.fatalError();
262-
}
263263
} finally {
264264
channel.close();
265265
}
@@ -275,7 +275,27 @@ public BackendResponse send(BackendRequest request) {
275275
request.getExtras() == null ? null : LegacyFlgDestination.decodeExtras(request.getExtras());
276276

277277
try {
278-
return doSend(requestBody, apiKey);
278+
HttpResponse response =
279+
retry(
280+
5,
281+
new HttpRequest(endPoint, requestBody, apiKey),
282+
this::doSend,
283+
(req, resp) -> {
284+
if (resp.redirectUrl != null) {
285+
// retry with different url
286+
return req.withUrl(resp.redirectUrl);
287+
}
288+
// don't retry
289+
return null;
290+
});
291+
292+
if (response.code == 200) {
293+
return BackendResponse.ok(response.nextRequestMillis);
294+
} else if (response.code >= 500 || response.code == 404) {
295+
return BackendResponse.transientError();
296+
} else {
297+
return BackendResponse.fatalError();
298+
}
279299
} catch (IOException e) {
280300
LOGGER.log(Level.SEVERE, "Could not make request to the backend", e);
281301
return BackendResponse.transientError();
@@ -288,4 +308,32 @@ static long getTzOffset() {
288308
TimeZone tz = TimeZone.getDefault();
289309
return tz.getOffset(Calendar.getInstance().getTimeInMillis()) / 1000;
290310
}
311+
312+
static final class HttpResponse {
313+
final int code;
314+
@Nullable final URL redirectUrl;
315+
final long nextRequestMillis;
316+
317+
HttpResponse(int code, @Nullable URL redirectUrl, long nextRequestMillis) {
318+
this.code = code;
319+
this.redirectUrl = redirectUrl;
320+
this.nextRequestMillis = nextRequestMillis;
321+
}
322+
}
323+
324+
static final class HttpRequest {
325+
final URL url;
326+
final BatchedLogRequest requestBody;
327+
@Nullable final String apiKey;
328+
329+
HttpRequest(URL url, BatchedLogRequest requestBody, @Nullable String apiKey) {
330+
this.url = url;
331+
this.requestBody = requestBody;
332+
this.apiKey = apiKey;
333+
}
334+
335+
HttpRequest withUrl(URL newUrl) {
336+
return new HttpRequest(newUrl, requestBody, apiKey);
337+
}
338+
}
291339
}

transport/transport-backend-cct/src/test/java/com/google/android/datatransport/cct/CctTransportBackendTest.java

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ public void testGarbageFromServer() {
237237
verify(
238238
postRequestedFor(urlEqualTo("/api"))
239239
.withHeader("Content-Type", equalTo("application/x-protobuf")));
240-
assertEquals(BackendResponse.fatalError(), response);
240+
assertEquals(BackendResponse.transientError(), response);
241241
}
242242

243243
@Test
@@ -305,6 +305,69 @@ public void decorate_whenOffline_shouldProperlyPopulateNetworkInfo() {
305305
String.valueOf(NetworkConnectionInfo.MobileSubtype.UNKNOWN_MOBILE_SUBTYPE_VALUE));
306306
}
307307

308+
@Test
309+
public void send_whenBackendRedirects_shouldCorrectlyFollowTheRedirectViaPost() {
310+
stubFor(
311+
post(urlEqualTo("/api"))
312+
.willReturn(
313+
aResponse().withStatus(302).withHeader("Location", TEST_ENDPOINT + "/hello")));
314+
stubFor(
315+
post(urlEqualTo("/api/hello"))
316+
.willReturn(
317+
aResponse()
318+
.withStatus(200)
319+
.withHeader("Content-Type", "application/x-protobuf;charset=UTF8;hello=world")
320+
.withBody(
321+
LogResponse.newBuilder()
322+
.setNextRequestWaitMillis(3)
323+
.build()
324+
.toByteArray())));
325+
BackendRequest backendRequest = getCCTBackendRequest();
326+
wallClock.tick();
327+
uptimeClock.tick();
328+
329+
BackendResponse response = BACKEND.send(backendRequest);
330+
331+
verify(
332+
postRequestedFor(urlEqualTo("/api"))
333+
.withHeader("Content-Type", equalTo("application/x-protobuf")));
334+
335+
verify(
336+
postRequestedFor(urlEqualTo("/api/hello"))
337+
.withHeader("Content-Type", equalTo("application/x-protobuf")));
338+
339+
assertEquals(BackendResponse.ok(3), response);
340+
}
341+
342+
@Test
343+
public void send_whenBackendRedirectsMoreThan5Times_shouldOnlyRedirect4Times() {
344+
stubFor(
345+
post(urlEqualTo("/api"))
346+
.willReturn(
347+
aResponse().withStatus(302).withHeader("Location", TEST_ENDPOINT + "/hello")));
348+
stubFor(
349+
post(urlEqualTo("/api/hello"))
350+
.willReturn(
351+
aResponse().withStatus(302).withHeader("Location", TEST_ENDPOINT + "/hello")));
352+
353+
BackendRequest backendRequest = getCCTBackendRequest();
354+
wallClock.tick();
355+
uptimeClock.tick();
356+
357+
BackendResponse response = BACKEND.send(backendRequest);
358+
359+
verify(
360+
postRequestedFor(urlEqualTo("/api"))
361+
.withHeader("Content-Type", equalTo("application/x-protobuf")));
362+
363+
verify(
364+
4,
365+
postRequestedFor(urlEqualTo("/api/hello"))
366+
.withHeader("Content-Type", equalTo("application/x-protobuf")));
367+
368+
assertEquals(BackendResponse.fatalError(), response);
369+
}
370+
308371
// When there is no active network, the ConnectivityManager returns null when
309372
// getActiveNetworkInfo() is called.
310373
@Implements(ConnectivityManager.class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.android.datatransport.runtime.retries;
16+
17+
/** Generic functional interface which supports checked exceptions. */
18+
public interface Function<TInput, TResult, TException extends Throwable> {
19+
TResult apply(TInput input) throws TException;
20+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.android.datatransport.runtime.retries;
16+
17+
/** Provides utilities to retry function calls. */
18+
public final class Retries {
19+
private Retries() {}
20+
21+
/**
22+
* Retries given {@link Function} upto {@code maxAttempts} times.
23+
*
24+
* <p>It takes an {@code input} parameter that is passed to the first {@link Function} call. The
25+
* rest of the retries are called with the value produced by the {@code retryStrategy}. If the
26+
* {@code retryStrategy} returns null, the retries are stopped and the result of the last retry is
27+
* returned.
28+
*
29+
* <p>Example
30+
*
31+
* <pre>{@code
32+
* int initialParameter = 10;
33+
*
34+
* // finalResult is 12.
35+
* int finalResult = retry(5, initialParameter, Integer::valueOf, (input, result) -> {
36+
* if ( result.equals(12)) {
37+
* return null;
38+
* }
39+
* return input + 1;
40+
* });
41+
* }</pre>
42+
*/
43+
public static <TInput, TResult, TException extends Throwable> TResult retry(
44+
int maxAttempts,
45+
TInput input,
46+
Function<TInput, TResult, TException> function,
47+
RetryStrategy<TInput, TResult> retryStrategy)
48+
throws TException {
49+
if (maxAttempts < 1) {
50+
return function.apply(input);
51+
}
52+
53+
while (true) {
54+
TResult result = function.apply(input);
55+
input = retryStrategy.shouldRetry(input, result);
56+
57+
if (input == null || --maxAttempts < 1) {
58+
return result;
59+
}
60+
}
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.android.datatransport.runtime.retries;
16+
17+
import androidx.annotation.Nullable;
18+
19+
public interface RetryStrategy<TInput, TResult> {
20+
/**
21+
* Whether a function call should be retried with a new input.
22+
*
23+
* <p>Given an input and result of a function call, return a new input to cause call to be
24+
* retried, returning null will prevent further retries.
25+
*/
26+
@Nullable
27+
TInput shouldRetry(TInput input, TResult result);
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/** @hide */
16+
package com.google.android.datatransport.runtime.retries;

0 commit comments

Comments
 (0)