Skip to content

Commit 84a2f6d

Browse files
committed
Import of Cloud Functions JVM from Git-on-Borg.
- a6111561e09808f60fd1e79b7d3f2035fcb047bc Change invoker to depend on API version 1.0.0-alpha-2-rc3. by Éamonn McManus <[email protected]> - 7007628de5e20984babc8be8764325c2c2cad607 Revise background functions API. by Éamonn McManus <[email protected]> PiperOrigin-RevId: 279283731
1 parent 52ce928 commit 84a2f6d

File tree

9 files changed

+312
-74
lines changed

9 files changed

+312
-74
lines changed

functions-framework-api/pom.xml

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
<groupId>com.google.cloud.functions</groupId>
2626
<artifactId>functions-framework-api</artifactId>
27-
<version>1.0.0-alpha-2-rc2-SNAPSHOT</version>
27+
<version>1.0.0-alpha-2-rc3-SNAPSHOT</version>
2828

2929
<properties>
3030
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -179,12 +179,4 @@
179179
</build>
180180
</profile>
181181
</profiles>
182-
<dependencies>
183-
<dependency>
184-
<groupId>com.google.code.gson</groupId>
185-
<artifactId>gson</artifactId>
186-
<version>2.8.6</version>
187-
<type>jar</type>
188-
</dependency>
189-
</dependencies>
190182
</project>

functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,22 @@
1414

1515
package com.google.cloud.functions;
1616

17-
import com.google.gson.JsonElement;
18-
1917
/**
20-
* Represents a Cloud Function that is activated by an event.
18+
* Represents a Cloud Function that is activated by an event and parsed into a user-supplied class.
19+
* The payload of the event is a JSON object, which is deserialized into a user-defined class as
20+
* described for
21+
* <a href="https://github.com/google/gson/blob/master/UserGuide.md#TOC-Object-Examples">Gson</a>.
2122
*
22-
* <p>Here is an example of an implementation that operates on the JSON payload of the event
23-
* directly:
23+
* <p>Here is an example of an implementation that accesses the {@code messageId} property from
24+
* a payload that matches a user-defined {@code PubSubMessage} class:
2425
*
2526
* <pre>
26-
* public class Example implements BackgroundFunction {
27+
* public class Example implements {@code BackgroundFunction<PubSubMessage>} {
2728
* private static final Logger logger = Logger.getLogger(Example.class.getName());
2829
*
2930
* {@code @Override}
30-
* public void accept(JsonElement json, Context context) {
31-
* JsonElement messageId = json.getAsJsonObject().get("messageId");
32-
* String messageIdString = messageId.getAsJsonString();
33-
* logger.info("Got messageId " + messageIdString);
34-
* }
35-
* }
36-
* </pre>
37-
*
38-
* <p>Here is an example of an implementation that deserializes the JSON payload into a Java
39-
* object for simpler access:
40-
*
41-
* <pre>
42-
* public class Example implements BackgroundFunction {
43-
* private static final Logger logger = Logger.getLogger(Example.class.getName());
44-
*
45-
* {@code @Override}
46-
* public void accept(JsonElement json, Context context) {
47-
* PubSubMessage message = Gson.fromJson(json, PubSubMessage.class);
48-
* logger.info("Got messageId " + message.messageId);
31+
* public void accept(PubSubMessage pubSubMessage, Context context) {
32+
* logger.info("Got messageId " + pubSubMessage.messageId);
4933
* }
5034
* }
5135
*
@@ -57,17 +41,19 @@
5741
* String publishTime;
5842
* }
5943
* </pre>
44+
*
45+
* @param <T> the class of payload objects that this function expects.
6046
*/
6147
@FunctionalInterface
62-
public interface BackgroundFunction {
48+
public interface BackgroundFunction<T> {
6349
/**
6450
* Called to service an incoming event. This interface is implemented by user code to
65-
* provide the action for a given background function.
51+
* provide the action for a given background function. If this method throws any exception
6652
* (including any {@link Error}) then the HTTP response will have a 500 status code.
6753
*
68-
* @param json the payload of the event, as a parsed JSON object.
54+
* @param payload the payload of the event, deserialized from the original JSON string.
6955
* @param context the context of the event. This is a set of values that every event has,
7056
* separately from the payload, such as timestamp and event type.
7157
*/
72-
public void accept(JsonElement json, Context context);
58+
void accept(T payload, Context context);
7359
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.cloud.functions;
16+
17+
/**
18+
* Represents a Cloud Function that is activated by an event. The payload of the event is a JSON
19+
* object, which can be parsed using a JSON package such as
20+
* <a href="https://github.com/google/gson/blob/master/UserGuide.md">GSON</a>.
21+
*
22+
* <p>Here is an example of an implementation that parses the JSON payload using Gson, to access its
23+
* {@code messageId} property:
24+
*
25+
* <pre>
26+
* public class Example implements RawBackgroundFunction {
27+
* private static final Logger logger = Logger.getLogger(Example.class.getName());
28+
*
29+
* {@code @Override}
30+
* public void accept(String json, Context context) {
31+
* JsonObject jsonObject = new Gson().fromJson(json, JsonObject.class);
32+
* JsonElement messageId = jsonObject.get("messageId");
33+
* String messageIdString = messageId.getAsJsonString();
34+
* logger.info("Got messageId " + messageIdString);
35+
* }
36+
* }
37+
* </pre>
38+
*
39+
* <p>Here is an example of an implementation that deserializes the JSON payload into a Java
40+
* object for simpler access, again using Gson:
41+
*
42+
* <pre>
43+
* public class Example implements RawBackgroundFunction {
44+
* private static final Logger logger = Logger.getLogger(Example.class.getName());
45+
*
46+
* {@code @Override}
47+
* public void accept(String json, Context context) {
48+
* PubSubMessage message = new Gson().fromJson(json, PubSubMessage.class);
49+
* logger.info("Got messageId " + message.messageId);
50+
* }
51+
* }
52+
*
53+
* // Where PubSubMessage is a user-defined class like this:
54+
* public class PubSubMessage {
55+
* String data;
56+
* {@code Map<String, String>} attributes;
57+
* String messageId;
58+
* String publishTime;
59+
* }
60+
* </pre>
61+
*/
62+
@FunctionalInterface
63+
public interface RawBackgroundFunction {
64+
/**
65+
* Called to service an incoming event. This interface is implemented by user code to
66+
* provide the action for a given background function. If this method throws any exception
67+
* (including any {@link Error}) then the HTTP response will have a 500 status code.
68+
*
69+
* @param json the payload of the event, as a JSON string.
70+
* @param context the context of the event. This is a set of values that every event has,
71+
* separately from the payload, such as timestamp and event type.
72+
*/
73+
void accept(String json, Context context);
74+
}

invoker-core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<dependency>
1515
<groupId>com.google.cloud.functions</groupId>
1616
<artifactId>functions-framework-api</artifactId>
17-
<version>1.0.0-alpha-2-rc2</version>
17+
<version>1.0.0-alpha-2-rc3</version>
1818
</dependency>
1919
<dependency>
2020
<groupId>javax.servlet</groupId>

invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package com.google.cloud.functions.invoker;
22

33
import com.google.cloud.functions.BackgroundFunction;
4+
import com.google.cloud.functions.Context;
5+
import com.google.cloud.functions.RawBackgroundFunction;
46
import com.google.gson.Gson;
57
import com.google.gson.GsonBuilder;
8+
import com.google.gson.JsonParseException;
69
import com.google.gson.TypeAdapter;
710
import java.io.BufferedReader;
811
import java.io.IOException;
12+
import java.lang.reflect.Type;
13+
import java.util.Arrays;
914
import java.util.Optional;
1015
import java.util.logging.Level;
1116
import java.util.logging.Logger;
@@ -17,9 +22,9 @@
1722
public class NewBackgroundFunctionExecutor extends HttpServlet {
1823
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
1924

20-
private final BackgroundFunction function;
25+
private final RawBackgroundFunction function;
2126

22-
private NewBackgroundFunctionExecutor(BackgroundFunction function) {
27+
private NewBackgroundFunctionExecutor(RawBackgroundFunction function) {
2328
this.function = function;
2429
}
2530

@@ -30,8 +35,8 @@ private NewBackgroundFunctionExecutor(BackgroundFunction function) {
3035
* {@code Optional.empty()}.
3136
*
3237
* @throws RuntimeException if we succeed in loading the class named by {@code target} but then
33-
* either the class does not implement {@link HttpFunction} or we are unable to construct an
34-
* instance using its no-arg constructor.
38+
* either the class does not implement {@link RawBackgroundFunction} or we are unable to
39+
* construct an instance using its no-arg constructor.
3540
*/
3641
public static Optional<NewBackgroundFunctionExecutor> forTarget(String target) {
3742
Class<?> c;
@@ -40,17 +45,83 @@ public static Optional<NewBackgroundFunctionExecutor> forTarget(String target) {
4045
} catch (ClassNotFoundException e) {
4146
return Optional.empty();
4247
}
43-
if (!BackgroundFunction.class.isAssignableFrom(c)) {
48+
if (!BackgroundFunction.class.isAssignableFrom(c)
49+
&& !RawBackgroundFunction.class.isAssignableFrom(c)) {
4450
throw new RuntimeException(
45-
"Class " + c.getName() + " does not implement " + BackgroundFunction.class.getName());
51+
"Class " + c.getName() + " implements neither " + BackgroundFunction.class
52+
.getName() + " nor " + RawBackgroundFunction.class.getName());
4653
}
47-
Class<? extends BackgroundFunction> functionClass = c.asSubclass(BackgroundFunction.class);
54+
Object instance;
4855
try {
49-
BackgroundFunction function = functionClass.getConstructor().newInstance();
50-
return Optional.of(new NewBackgroundFunctionExecutor(function));
56+
instance = c.getConstructor().newInstance();
5157
} catch (ReflectiveOperationException e) {
5258
throw new RuntimeException("Could not construct an instance of " + target + ": " + e, e);
5359
}
60+
RawBackgroundFunction function =
61+
(instance instanceof RawBackgroundFunction)
62+
? (RawBackgroundFunction) instance
63+
: asRaw((BackgroundFunction<?>) instance);
64+
return Optional.of(new NewBackgroundFunctionExecutor(function));
65+
}
66+
67+
private static <T> RawBackgroundFunction asRaw(BackgroundFunction<T> backgroundFunction) {
68+
Optional<Type> maybeTargetType = backgroundFunctionTypeArgument(backgroundFunction.getClass());
69+
if (!maybeTargetType.isPresent()) {
70+
// This is probably because the user implemented just BackgroundFunction rather than
71+
// BackgroundFunction<T>.
72+
throw new RuntimeException(
73+
"Could not determine the payload type for BackgroundFunction of type "
74+
+ backgroundFunction.getClass().getName()
75+
+ "; must implement BackgroundFunction<T> for some T");
76+
}
77+
return new AsRaw<T>(maybeTargetType.get(), backgroundFunction);
78+
}
79+
80+
/**
81+
* Returns the {@code T} of a concrete class that implements
82+
* {@link BackgroundFunction BackgroundFunction<T>}. Returns an empty {@link Optional} if
83+
* {@code T} can't be determined.
84+
*/
85+
static Optional<Type> backgroundFunctionTypeArgument(
86+
Class<? extends BackgroundFunction> functionClass) {
87+
// If this is BackgroundFunction<Foo> then the user must have implemented a method
88+
// accept(Foo, Context), so we look for that method and return the type of its first argument.
89+
// We must be careful because the compiler will also have added a synthetic method
90+
// accept(Object, Context).
91+
return Arrays.stream(functionClass.getMethods())
92+
.filter(m -> m.getName().equals("accept") && m.getParameterCount() == 2
93+
&& m.getParameterTypes()[1] == Context.class
94+
&& m.getParameterTypes()[0] != Object.class)
95+
.map(m -> m.getGenericParameterTypes()[0])
96+
.findFirst();
97+
}
98+
99+
/**
100+
* Wraps a typed {@link BackgroundFunction} as a {@link RawBackgroundFunction} that takes its
101+
* input JSON string and deserializes it into the payload type of the {@link BackgroundFunction}/
102+
*/
103+
private static class AsRaw<T> implements RawBackgroundFunction {
104+
private final Gson gson = new Gson();
105+
private final Type targetType;
106+
private final BackgroundFunction<T> backgroundFunction;
107+
108+
private AsRaw(Type targetType, BackgroundFunction<T> backgroundFunction) {
109+
this.targetType = targetType;
110+
this.backgroundFunction = backgroundFunction;
111+
}
112+
113+
@Override
114+
public void accept(String json, Context context) {
115+
T payload;
116+
try {
117+
payload = gson.fromJson(json, targetType);
118+
} catch (JsonParseException e) {
119+
logger.log(Level.WARNING,
120+
"Could not convert payload to target type " + targetType.getTypeName(), e);
121+
return;
122+
}
123+
backgroundFunction.accept(payload, context);
124+
}
54125
}
55126

56127
/** Executes the user's background function, can handle all HTTP type methods. */
@@ -69,7 +140,7 @@ public void service(HttpServletRequest req, HttpServletResponse res) throws IOEx
69140

70141
Event event = gson.fromJson(body, Event.class);
71142
try {
72-
function.accept(event.getData(), event.getContext());
143+
function.accept(gson.toJson(event.getData()), event.getContext());
73144
res.setStatus(HttpServletResponse.SC_OK);
74145
} catch (Throwable t) {
75146
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import com.google.common.collect.ImmutableMap;
99
import com.google.common.io.Files;
1010
import com.google.common.io.Resources;
11+
import com.google.gson.Gson;
1112
import com.google.gson.JsonObject;
12-
import com.google.gson.JsonParser;
1313
import java.io.BufferedReader;
1414
import java.io.File;
1515
import java.io.IOException;
@@ -133,35 +133,38 @@ public void newEchoUrl() throws Exception {
133133

134134
@Test
135135
public void background() throws Exception {
136-
URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json");
137-
assertThat(resourceUrl).isNotNull();
138-
File snoopFile = File.createTempFile("FunctionsIntegrationTest", ".txt");
139-
snoopFile.deleteOnExit();
140-
String originalJson = Resources.toString(resourceUrl, StandardCharsets.UTF_8);
141-
JsonObject json = new JsonParser().parse(originalJson).getAsJsonObject();
142-
JsonObject jsonData = json.getAsJsonObject("data");
143-
jsonData.addProperty("targetFile", snoopFile.toString());
144-
testBackgroundFunction("BackgroundSnoop.snoop",
145-
TestCase.builder().setRequestText(json.toString()).build());
146-
String snooped = Files.asCharSource(snoopFile, StandardCharsets.UTF_8).read();
147-
JsonObject snoopedJson = new JsonParser().parse(snooped).getAsJsonObject();
148-
assertThat(snoopedJson).isEqualTo(json);
136+
backgroundTest("BackgroundSnoop.snoop");
149137
}
150138

151139
@Test
152140
public void newBackground() throws Exception {
141+
backgroundTest("NewBackgroundSnoop");
142+
}
143+
144+
@Test
145+
public void newTypedBackground() throws Exception {
146+
backgroundTest("NewTypedBackgroundSnoop");
147+
}
148+
149+
// In these tests, we test a number of different functions that express the same functionality
150+
// in different ways. Each function is invoked with a complete HTTP body that looks like a real
151+
// event. We start with a fixed body and insert into its JSON an extra property that tells the
152+
// function where to write what it received. We have to do this since background functions, by
153+
// design, don't return a value.
154+
private void backgroundTest(String functionTarget) throws Exception {
155+
Gson gson = new Gson();
153156
URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json");
154157
assertThat(resourceUrl).isNotNull();
155158
File snoopFile = File.createTempFile("FunctionsIntegrationTest", ".txt");
156159
snoopFile.deleteOnExit();
157160
String originalJson = Resources.toString(resourceUrl, StandardCharsets.UTF_8);
158-
JsonObject json = new JsonParser().parse(originalJson).getAsJsonObject();
161+
JsonObject json = gson.fromJson(originalJson, JsonObject.class);
159162
JsonObject jsonData = json.getAsJsonObject("data");
160163
jsonData.addProperty("targetFile", snoopFile.toString());
161-
testBackgroundFunction("NewBackgroundSnoop",
164+
testBackgroundFunction(functionTarget,
162165
TestCase.builder().setRequestText(json.toString()).build());
163166
String snooped = Files.asCharSource(snoopFile, StandardCharsets.UTF_8).read();
164-
JsonObject snoopedJson = new JsonParser().parse(snooped).getAsJsonObject();
167+
JsonObject snoopedJson = gson.fromJson(snooped, JsonObject.class);
165168
assertThat(snoopedJson).isEqualTo(json);
166169
}
167170

0 commit comments

Comments
 (0)