
com.google.cloud.functions.invoker.BackgroundFunctionExecutor Maven / Gradle / Ivy
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.cloud.functions.invoker;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.CloudEventsFunction;
import com.google.cloud.functions.Context;
import com.google.cloud.functions.RawBackgroundFunction;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import io.cloudevents.CloudEvent;
import io.cloudevents.core.message.MessageReader;
import io.cloudevents.http.HttpMessageFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** Executes the user's background function. */
public final class BackgroundFunctionExecutor extends HttpServlet {
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");
private final FunctionExecutor> functionExecutor;
private BackgroundFunctionExecutor(FunctionExecutor> functionExecutor) {
this.functionExecutor = functionExecutor;
}
private enum FunctionKind {
BACKGROUND(BackgroundFunction.class),
RAW_BACKGROUND(RawBackgroundFunction.class),
CLOUD_EVENTS(CloudEventsFunction.class);
static final List VALUES = Arrays.asList(values());
final Class> functionClass;
FunctionKind(Class> functionClass) {
this.functionClass = functionClass;
}
/** Returns the {@link FunctionKind} that the given class implements, if any. */
static Optional forClass(Class> functionClass) {
return VALUES.stream().filter(v -> v.functionClass.isAssignableFrom(functionClass)).findFirst();
}
}
/**
* Optionally makes a {@link BackgroundFunctionExecutor} for the given class, if it implements one
* of {@link BackgroundFunction}, {@link RawBackgroundFunction}, or
* {@link CloudEventsFunction}. Otherwise returns {@link Optional#empty()}.
*
* @param functionClass the class of a possible background function implementation.
* @throws RuntimeException if the given class does implement one of the required interfaces, but we are
* unable to construct an instance using its no-arg constructor.
*/
public static Optional maybeForClass(Class> functionClass) {
Optional maybeFunctionKind = FunctionKind.forClass(functionClass);
if (!maybeFunctionKind.isPresent()) {
return Optional.empty();
}
return Optional.of(forClass(functionClass, maybeFunctionKind.get()));
}
/**
* Makes a {@link BackgroundFunctionExecutor} for the given class.
*
* @throws RuntimeException if either the class does not implement one of
* {@link BackgroundFunction}, {@link RawBackgroundFunction}, or
* {@link CloudEventsFunction}; or we are unable to construct an instance using its no-arg
* constructor.
*/
public static BackgroundFunctionExecutor forClass(Class> functionClass) {
Optional maybeFunctionKind = FunctionKind.forClass(functionClass);
if (!maybeFunctionKind.isPresent()) {
List classNames =
FunctionKind.VALUES.stream().map(v -> v.functionClass.getName()).collect(toList());
throw new RuntimeException(
"Class " + functionClass.getName() + " must implement one of these interfaces: "
+ String.join(", ", classNames));
}
return forClass(functionClass, maybeFunctionKind.get());
}
private static BackgroundFunctionExecutor forClass(Class> functionClass, FunctionKind functionKind) {
Object instance;
try {
instance = functionClass.getConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new RuntimeException(
"Could not construct an instance of " + functionClass.getName() + ": " + e, e);
}
FunctionExecutor> executor;
switch (functionKind) {
case RAW_BACKGROUND:
executor = new RawFunctionExecutor((RawBackgroundFunction) instance);
break;
case BACKGROUND:
BackgroundFunction> backgroundFunction = (BackgroundFunction>) instance;
@SuppressWarnings("unchecked")
Class extends BackgroundFunction>> c =
(Class extends BackgroundFunction>>) backgroundFunction.getClass();
Optional maybeTargetType = backgroundFunctionTypeArgument(c);
if (!maybeTargetType.isPresent()) {
// This is probably because the user implemented just BackgroundFunction rather than
// BackgroundFunction.
throw new RuntimeException(
"Could not determine the payload type for BackgroundFunction of type "
+ instance.getClass().getName()
+ "; must implement BackgroundFunction for some T");
}
executor = new TypedFunctionExecutor<>(maybeTargetType.get(), backgroundFunction);
break;
case CLOUD_EVENTS:
executor = new CloudEventFunctionExecutor((CloudEventsFunction) instance);
break;
default: // can't happen, we've listed all the FunctionKind values already.
throw new AssertionError(functionKind);
}
return new BackgroundFunctionExecutor(executor);
}
/**
* Returns the {@code T} of a concrete class that implements
* {@link BackgroundFunction BackgroundFunction}. Returns an empty {@link Optional} if
* {@code T} can't be determined.
*/
static Optional backgroundFunctionTypeArgument(
Class extends BackgroundFunction>> functionClass) {
// If this is BackgroundFunction then the user must have implemented a method
// accept(Foo, Context), so we look for that method and return the type of its first argument.
// We must be careful because the compiler will also have added a synthetic method
// accept(Object, Context).
return Arrays.stream(functionClass.getMethods())
.filter(m -> m.getName().equals("accept") && m.getParameterCount() == 2
&& m.getParameterTypes()[1] == Context.class
&& m.getParameterTypes()[0] != Object.class)
.map(m -> m.getGenericParameterTypes()[0])
.findFirst();
}
private static Event parseLegacyEvent(HttpServletRequest req) throws IOException {
try (BufferedReader bodyReader = req.getReader()) {
return parseLegacyEvent(bodyReader);
}
}
static Event parseLegacyEvent(Reader reader) throws IOException {
// A Type Adapter is required to set the type of the JsonObject because CloudFunctionsContext
// is abstract and Gson default behavior instantiates the type provided.
TypeAdapter typeAdapter =
CloudFunctionsContext.typeAdapter(new Gson());
Gson gson = new GsonBuilder()
.registerTypeAdapter(CloudFunctionsContext.class, typeAdapter)
.registerTypeAdapter(Event.class, new Event.EventDeserializer())
.create();
return gson.fromJson(reader, Event.class);
}
private static Context contextFromCloudEvent(CloudEvent cloudEvent) {
OffsetDateTime timestamp = Optional.ofNullable(cloudEvent.getTime()).orElse(OffsetDateTime.now());
String timestampString = DateTimeFormatter.ISO_INSTANT.format(timestamp);
// We don't have an obvious replacement for the Context.resource field, which with legacy events
// corresponded to a value present for some proprietary Google event types.
String resource = "{}";
Map attributesMap =
cloudEvent.getAttributeNames().stream()
.collect(toMap(a -> a, a -> String.valueOf(cloudEvent.getAttribute(a))));
return CloudFunctionsContext.builder()
.setEventId(cloudEvent.getId())
.setEventType(cloudEvent.getType())
.setResource(resource)
.setTimestamp(timestampString)
.setAttributes(attributesMap)
.build();
}
/**
* A background function, either "raw" or "typed". A raw background function is one where the user
* code receives a String parameter that is the JSON payload of the triggering event. A typed
* background function is one where the payload is deserialized into a user-provided class whose
* field names correspond to the keys of the JSON object.
*
* In addition to these two flavours, events can be either "legacy events" or "CloudEvents".
* Legacy events are the only kind that GCF originally supported, and use proprietary encodings
* for the various triggers. CloudEvents are ones that follow the standards defined by
* cloudevents.io.
*
* @param the type to be used in the {@link Unmarshallers} call when
* unmarshalling this event, if it is a CloudEvent.
*/
private abstract static class FunctionExecutor {
private final Class> functionClass;
FunctionExecutor(Class> functionClass) {
this.functionClass = functionClass;
}
final String functionName() {
return functionClass.getCanonicalName();
}
final ClassLoader functionClassLoader() {
return functionClass.getClassLoader();
}
abstract void serviceLegacyEvent(Event legacyEvent) throws Exception;
abstract void serviceCloudEvent(CloudEvent cloudEvent) throws Exception;
}
private static class RawFunctionExecutor extends FunctionExecutor