Skip to content

Commit 99d6465

Browse files
authored
Implement settable timeouts in Functions. (#224)
Changes the default timeout for FirebaseFunctions to 70s and adds an API to override that timeout on a per-function basis.
1 parent 115ff55 commit 99d6465

8 files changed

Lines changed: 128 additions & 18 deletions

File tree

firebase-functions/firebase-functions.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ dependencies {
5454
}
5555
implementation 'com.google.firebase:firebase-iid-interop:16.0.1'
5656

57-
implementation 'com.squareup.okhttp:okhttp:2.7.5'
57+
implementation 'com.squareup.okhttp3:okhttp:3.12.1'
5858

5959
annotationProcessor 'com.google.auto.value:auto-value:1.6'
6060

firebase-functions/src/androidTest/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
<!--Although the *SdkVersion is captured in gradle build files, this is required for non gradle builds-->
44
<!--<uses-sdk android:minSdkVersion="15" android:targetSdkVersion="23" />-->
55
<uses-permission android:name="android.permission.INTERNET"/>
6-
<application>
6+
<application
7+
android:usesCleartextTraffic="true">
78
<uses-library android:name="android.test.runner" />
89
</application>
910

firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.HashMap;
3232
import java.util.Map;
3333
import java.util.concurrent.ExecutionException;
34+
import java.util.concurrent.TimeUnit;
3435
import org.junit.BeforeClass;
3536
import org.junit.Test;
3637
import org.junit.runner.RunWith;
@@ -238,4 +239,21 @@ public void testHttpError() throws InterruptedException, ExecutionException {
238239
assertEquals("INVALID_ARGUMENT", ffe.getMessage());
239240
assertNull(ffe.getDetails());
240241
}
242+
243+
@Test
244+
public void testTimeout() throws InterruptedException, ExecutionException {
245+
FirebaseFunctions functions = FirebaseFunctions.getInstance(app);
246+
useTestURL(functions);
247+
248+
HttpsCallableReference function =
249+
functions.getHttpsCallable("timeoutTest").withTimeout(10, TimeUnit.MILLISECONDS);
250+
Task<HttpsCallableResult> result = function.call();
251+
ExecutionException exe = assertThrows(ExecutionException.class, () -> Tasks.await(result));
252+
Throwable cause = exe.getCause();
253+
assertTrue(cause.toString(), cause instanceof FirebaseFunctionsException);
254+
FirebaseFunctionsException ffe = (FirebaseFunctionsException) cause;
255+
assertEquals(Code.DEADLINE_EXCEEDED, ffe.getCode());
256+
assertEquals("DEADLINE_EXCEEDED", ffe.getMessage());
257+
assertNull(ffe.getDetails());
258+
}
241259
}

firebase-functions/src/androidTest/test/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,8 @@ exports.httpErrorTest = functions.https.onRequest((request, response) => {
105105
// Send an http error with no body.
106106
response.status(400).send();
107107
});
108+
109+
exports.timeoutTest = functions.https.onRequest((request, response) => {
110+
// Wait for longer than 500ms.
111+
setTimeout(() => response.send({data: true}), 500);
112+
});

firebase-functions/src/androidTest/test/start.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,6 @@ ${FUNCTIONS} deploy unknownErrorTest --trigger-http
6363
${FUNCTIONS} deploy unhandledErrorTest --trigger-http
6464
${FUNCTIONS} deploy explicitErrorTest --trigger-http
6565
${FUNCTIONS} deploy httpErrorTest --trigger-http
66+
${FUNCTIONS} deploy timeoutTest --trigger-http
6667

6768
echo "Finished setting up Cloud Functions emulator."

firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.java

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,19 @@
2727
import com.google.android.gms.tasks.Tasks;
2828
import com.google.firebase.FirebaseApp;
2929
import com.google.firebase.functions.FirebaseFunctionsException.Code;
30-
import com.squareup.okhttp.Call;
31-
import com.squareup.okhttp.Callback;
32-
import com.squareup.okhttp.MediaType;
33-
import com.squareup.okhttp.OkHttpClient;
34-
import com.squareup.okhttp.Request;
35-
import com.squareup.okhttp.RequestBody;
36-
import com.squareup.okhttp.Response;
3730
import java.io.IOException;
31+
import java.io.InterruptedIOException;
3832
import java.net.MalformedURLException;
3933
import java.net.URL;
4034
import java.util.HashMap;
4135
import java.util.Map;
36+
import okhttp3.Call;
37+
import okhttp3.Callback;
38+
import okhttp3.MediaType;
39+
import okhttp3.OkHttpClient;
40+
import okhttp3.Request;
41+
import okhttp3.RequestBody;
42+
import okhttp3.Response;
4243
import org.json.JSONException;
4344
import org.json.JSONObject;
4445

@@ -197,7 +198,7 @@ public void useFunctionsEmulator(String origin) {
197198
* @param data Parameters to pass to the function. Can be anything encodable as JSON.
198199
* @return A Task that will be completed when the request is complete.
199200
*/
200-
Task<HttpsCallableResult> call(String name, @Nullable Object data) {
201+
Task<HttpsCallableResult> call(String name, @Nullable Object data, HttpsCallOptions options) {
201202
return providerInstalled
202203
.getTask()
203204
.continueWithTask(task -> contextProvider.getContext())
@@ -207,7 +208,7 @@ Task<HttpsCallableResult> call(String name, @Nullable Object data) {
207208
return Tasks.forException(task.getException());
208209
}
209210
HttpsCallableContext context = task.getResult();
210-
return call(name, data, context);
211+
return call(name, data, context, options);
211212
});
212213
}
213214

@@ -220,7 +221,7 @@ Task<HttpsCallableResult> call(String name, @Nullable Object data) {
220221
* @return A Task that will be completed when the request is complete.
221222
*/
222223
private Task<HttpsCallableResult> call(
223-
String name, @Nullable Object data, HttpsCallableContext context) {
224+
String name, @Nullable Object data, HttpsCallableContext context, HttpsCallOptions options) {
224225
if (name == null) {
225226
throw new IllegalArgumentException("name cannot be null");
226227
}
@@ -243,17 +244,28 @@ private Task<HttpsCallableResult> call(
243244
request = request.header("Firebase-Instance-ID-Token", context.getInstanceIdToken());
244245
}
245246

247+
OkHttpClient callClient = options.apply(client);
248+
Call call = callClient.newCall(request.build());
249+
246250
TaskCompletionSource<HttpsCallableResult> tcs = new TaskCompletionSource<>();
247-
Call call = client.newCall(request.build());
248251
call.enqueue(
249252
new Callback() {
250253
@Override
251-
public void onFailure(Request request, IOException e) {
252-
tcs.setException(e);
254+
public void onFailure(Call ignored, IOException e) {
255+
if (e instanceof InterruptedIOException) {
256+
FirebaseFunctionsException exception =
257+
new FirebaseFunctionsException(
258+
Code.DEADLINE_EXCEEDED.name(), Code.DEADLINE_EXCEEDED, null, e);
259+
tcs.setException(exception);
260+
} else {
261+
FirebaseFunctionsException exception =
262+
new FirebaseFunctionsException(Code.INTERNAL.name(), Code.INTERNAL, null, e);
263+
tcs.setException(exception);
264+
}
253265
}
254266

255267
@Override
256-
public void onResponse(Response response) throws IOException {
268+
public void onResponse(Call ignored, Response response) throws IOException {
257269
Code code = Code.fromHttpStatus(response.code());
258270
String body = response.body().string();
259271

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2018 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.firebase.functions;
16+
17+
import java.util.concurrent.TimeUnit;
18+
import okhttp3.OkHttpClient;
19+
20+
/** An internal class for keeping track of options applied to an HttpsCallableReference. */
21+
class HttpsCallOptions {
22+
23+
// The default timeout to use for all calls.
24+
private static final long DEFAULT_TIMEOUT = 70;
25+
private static final TimeUnit DEFAULT_TIMEOUT_UNITS = TimeUnit.SECONDS;
26+
27+
// The timeout to use for calls from references created by this Functions.
28+
private long timeout = DEFAULT_TIMEOUT;
29+
private TimeUnit timeoutUnits = DEFAULT_TIMEOUT_UNITS;
30+
31+
/**
32+
* Changes the timeout for calls from this instance of Functions. The default is 60 seconds.
33+
*
34+
* @param timeout The length of the timeout, in the given units.
35+
* @param units The units for the specified timeout.
36+
*/
37+
void setTimeout(long timeout, TimeUnit units) {
38+
this.timeout = timeout;
39+
this.timeoutUnits = units;
40+
}
41+
42+
/** Creates a new OkHttpClient with these options applied to it. */
43+
OkHttpClient apply(OkHttpClient client) {
44+
return client.newBuilder().callTimeout(timeout, timeoutUnits).build();
45+
}
46+
}

firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616

1717
import android.support.annotation.Nullable;
1818
import com.google.android.gms.tasks.Task;
19+
import java.util.concurrent.TimeUnit;
1920

2021
/** A reference to a particular Callable HTTPS trigger in Cloud Functions. */
2122
public class HttpsCallableReference {
23+
2224
// The functions client to use for making calls.
2325
private final FirebaseFunctions functionsClient;
2426

2527
// The name of the HTTPS endpoint this reference refers to.
2628
private final String name;
2729

30+
// Options for how to do the HTTPS call.
31+
HttpsCallOptions options = new HttpsCallOptions();
32+
2833
/** Creates a new reference with the given options. */
2934
HttpsCallableReference(FirebaseFunctions functionsClient, String name) {
3035
this.functionsClient = functionsClient;
@@ -71,7 +76,7 @@ public class HttpsCallableReference {
7176
* @see FirebaseFunctionsException
7277
*/
7378
public Task<HttpsCallableResult> call(@Nullable Object data) {
74-
return functionsClient.call(name, data);
79+
return functionsClient.call(name, data, options);
7580
}
7681

7782
/**
@@ -89,6 +94,28 @@ public Task<HttpsCallableResult> call(@Nullable Object data) {
8994
* @return A Task that will be completed when the HTTPS request has completed.
9095
*/
9196
public Task<HttpsCallableResult> call() {
92-
return functionsClient.call(name, null);
97+
return functionsClient.call(name, null, options);
98+
}
99+
100+
/**
101+
* Changes the timeout for calls from this instance of Functions. The default is 60 seconds.
102+
*
103+
* @param timeout The length of the timeout, in the given units.
104+
* @param units The units for the specified timeout.
105+
*/
106+
public void setTimeout(long timeout, TimeUnit units) {
107+
options.setTimeout(timeout, units);
108+
}
109+
110+
/**
111+
* Creates a new reference with the given timeout for calls. The default is 60 seconds.
112+
*
113+
* @param timeout The length of the timeout, in the given units.
114+
* @param units The units for the specified timeout.
115+
*/
116+
public HttpsCallableReference withTimeout(long timeout, TimeUnit units) {
117+
HttpsCallableReference other = new HttpsCallableReference(functionsClient, name);
118+
other.setTimeout(timeout, units);
119+
return other;
93120
}
94121
}

0 commit comments

Comments
 (0)