GH-630, GH-530 Additional improvements in AWS Custom Runtime

Ensured we have Custom Runtime examples for functional and '@Bean' style
Improve AWSLambdaUtils to ensure it works without APIGatewayProxyRequestEvent on classpath
This commit is contained in:
Oleg Zhurakousky
2021-01-22 12:31:31 +01:00
parent a1d10f0771
commit 75112076f7
9 changed files with 71 additions and 104 deletions

View File

@@ -29,7 +29,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
@@ -37,6 +36,7 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
*
@@ -52,21 +52,20 @@ final class AWSLambdaUtils {
}
public static Message<byte[]> generateMessage(byte[] payload, MessageHeaders headers,
FunctionInvocationWrapper function, JsonMapper mapper) {
return generateMessage(payload, headers, function, mapper, null);
Type inputType, JsonMapper mapper) {
return generateMessage(payload, headers, inputType, mapper, null);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public static Message<byte[]> generateMessage(byte[] payload, MessageHeaders headers,
FunctionInvocationWrapper function, JsonMapper mapper, @Nullable Context awsContext) {
Type inputType, JsonMapper mapper, @Nullable Context awsContext) {
if (logger.isInfoEnabled()) {
logger.info("Incoming JSON for ApiGateway Event: " + new String(payload));
logger.info("Incoming JSON Event: " + new String(payload));
}
MessageBuilder messageBuilder = null;
Object request = mapper.fromJson(payload, Object.class);
Type inputType = function.getInputType();
if (FunctionTypeUtils.isMessage(inputType)) {
inputType = FunctionTypeUtils.getImmediateGenericType(inputType, 0);
}
@@ -81,7 +80,7 @@ final class AWSLambdaUtils {
}
else if (requestMap.containsKey("httpMethod")) { // API Gateway
logger.info("Incoming request is API Gateway");
if (inputType.getTypeName().endsWith(APIGatewayProxyRequestEvent.class.getSimpleName())) {
if (isTypeAnApiGatewayRequest(inputType)) {
APIGatewayProxyRequestEvent gatewayEvent = mapper.fromJson(requestMap, APIGatewayProxyRequestEvent.class);
messageBuilder = MessageBuilder.withPayload(gatewayEvent);
}
@@ -108,7 +107,8 @@ final class AWSLambdaUtils {
public static byte[] generateOutput(Message requestMessage, Message<byte[]> responseMessage,
JsonMapper mapper) {
byte[] responseBytes = responseMessage.getPayload();
if (requestMessage.getHeaders().containsKey("httpMethod") || requestMessage.getPayload() instanceof APIGatewayProxyRequestEvent) { // API Gateway
if (requestMessage.getHeaders().containsKey("httpMethod")
|| isPayloadAnApiGatewayRequest(responseMessage.getPayload())) { // API Gateway
Map<String, Object> response = new HashMap<String, Object>();
response.put("isBase64Encoded", false);
@@ -136,6 +136,23 @@ final class AWSLambdaUtils {
return responseBytes;
}
private static boolean isPayloadAnApiGatewayRequest(Object payload) {
return isAPIGatewayProxyRequestEventPresent()
? payload instanceof APIGatewayProxyRequestEvent
: false;
}
private static boolean isTypeAnApiGatewayRequest(Type type) {
return isAPIGatewayProxyRequestEventPresent()
? type.getTypeName().endsWith(APIGatewayProxyRequestEvent.class.getSimpleName())
: false;
}
private static boolean isAPIGatewayProxyRequestEventPresent() {
return ClassUtils.isPresent("com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent",
ClassUtils.getDefaultClassLoader());
}
private static void logEvent(List<Map<String, ?>> records) {
if (isKinesisEvent(records.get(0))) {
logger.info("Incoming request is Kinesis Event");

View File

@@ -95,7 +95,7 @@ public class CustomRuntimeEventLoop {
FunctionInvocationWrapper function = locateFunction(functionCatalog, response.getHeaders().getContentType());
Message<byte[]> eventMessage = AWSLambdaUtils.generateMessage(response.getBody().getBytes(StandardCharsets.UTF_8),
fromHttp(response.getHeaders()), function, mapper);
fromHttp(response.getHeaders()), function.getInputType(), mapper);
if (logger.isDebugEnabled()) {
logger.debug("Event message: " + eventMessage);
}

View File

@@ -16,6 +16,9 @@
package org.springframework.cloud.function.adapter.aws;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogInitializer;
import org.springframework.cloud.function.web.source.DestinationResolver;
@@ -31,18 +34,33 @@ import org.springframework.util.StringUtils;
@Order(0)
public class CustomRuntimeInitializer implements ApplicationContextInitializer<GenericApplicationContext> {
private static Log logger = LogFactory.getLog(CustomRuntimeInitializer.class);
@Override
public void initialize(GenericApplicationContext context) {
Boolean enabled = context.getEnvironment().getProperty("spring.cloud.function.web.export.enabled",
Boolean.class);
if (enabled == null || !enabled) {
if (StringUtils.hasText(System.getenv("AWS_LAMBDA_RUNTIME_API"))) {
if (context.getBeanFactory().getBeanNamesForType(CustomRuntimeEventLoop.class, false, false).length == 0) {
context.registerBean(StringUtils.uncapitalize(CustomRuntimeEventLoop.class.getSimpleName()),
CommandLineRunner.class, () -> args -> CustomRuntimeEventLoop.eventLoop(context));
}
if (logger.isDebugEnabled()) {
logger.debug("AWS Environment: " + System.getenv());
}
// the presence of AWS_LAMBDA_RUNTIME_API signifies Custom Runtime
if (!this.isWebExportEnabled(context) && StringUtils.hasText(System.getenv("AWS_LAMBDA_RUNTIME_API"))) {
if (context.getBeanFactory().getBeanNamesForType(CustomRuntimeEventLoop.class, false, false).length == 0) {
context.registerBean(StringUtils.uncapitalize(CustomRuntimeEventLoop.class.getSimpleName()),
CommandLineRunner.class, () -> args -> CustomRuntimeEventLoop.eventLoop(context));
}
}
// Boolean enabled = context.getEnvironment()
// .getProperty("spring.cloud.function.web.export.enabled", Boolean.class);
// if (enabled == null || !enabled) {
// if (StringUtils.hasText(System.getenv("AWS_LAMBDA_RUNTIME_API"))) {
// if (context.getBeanFactory().getBeanNamesForType(CustomRuntimeEventLoop.class, false, false).length == 0) {
// context.registerBean(StringUtils.uncapitalize(CustomRuntimeEventLoop.class.getSimpleName()),
// CommandLineRunner.class, () -> args -> CustomRuntimeEventLoop.eventLoop(context));
// }
// }
// }
else if (ContextFunctionCatalogInitializer.enabled
&& context.getEnvironment().getProperty("spring.functional.enabled", Boolean.class, false)) {
if (context.getBeanFactory().getBeanNamesForType(DestinationResolver.class, false, false).length == 0) {
@@ -53,4 +71,10 @@ public class CustomRuntimeInitializer implements ApplicationContextInitializer<G
}
}
private boolean isWebExportEnabled(GenericApplicationContext context) {
Boolean enabled = context.getEnvironment()
.getProperty("spring.cloud.function.web.export.enabled", Boolean.class);
return enabled != null && enabled;
}
}

View File

@@ -1,47 +1,11 @@
This sample uses the custom runtime type on AWS lambda. You can run
the app locally (in your IDE or using the `bootstrap` script), but it
won't see any messages unless you have an HTTP server running on
localhost (port 80) serving endpoints in the same place as AWS
does. It's a useful check that the app is working.
This sample uses the custom runtime type on AWS lambda using function bean registration style configuration.
However, changing configuration to @Bean registration is supported as well and shown in `function-sample-aws-custom-bean` example.
To run the app in AWS choose the "custom" runtime type, and upload the
To run the app in AWS choose the "Custom Runtime" runtime type, and upload the
.zip file that gets built on the command line with `mvn package` (look
in `target`). The function is a simple uppercaser, so you can test it
with any String as input, but the Lambda UI only allows valid JSON as
in `target`).
There is a single function defined in the `com.example.LambdaApplication` - `uppercase` which you would typically
identified as "Handler", but since it's the only one any value would do, so keeping default "hello.handler" is fine.
You can test it with any String as input, but the Lambda UI only allows valid JSON as
test data, so you will have to escape the input with double quotes.
Example output from a cold start:
```
Execution result: succeeded(logs)
Details
The area below shows the result returned by your custom runtime function execution. Learn more about returning results from your function.
"HELLO"
Summary
Code SHA-256
sIkZo8zXjswqUjc06sCkf9O9UymMF+X6v5is3IOVw0k=
Request ID
468c9e2d-3921-4620-b750-00ee119fedb3
Init duration
1578.85 ms
Duration
178.39 ms
Billed duration
1800 ms
Resources configured
1024 MB
Max memory used
145 MB
Log output
The section below shows the logging calls in your code. These correspond to a single row within the CloudWatch log group corresponding to this Lambda function. Click here to view the CloudWatch log group.
START RequestId: 468c9e2d-3921-4620-b750-00ee119fedb3 Version: $LATEST
[2019-07-08 14:40:59.111] - 11 INFO [reactor-http-nio-4] --- reactor.Flux.MonoRepeatPredicate.1: onNext(GenericMessage [payload="hello", headers={date=Mon, 08 Jul 2019 14:40:58 GMT, lambda-runtime-trace-id=Root=1-5d2355f9-a865a5293c8070e84f764595;Parent=287c30562def3f40;Sampled=0, lambda-runtime-aws-request-id=468c9e2d-3921-4620-b750-00ee119fedb3, id=9cb1ae53-b512-f119-06da-5d27ca130487, lambda-runtime-invoked-function-arn=arn:aws:lambda:eu-west-1:816194980775:function:func, lambda-runtime-deadline-ms=1562596918977, timestamp=1562596859110}])
[2019-07-08 14:40:59.112] - 11 INFO [reactor-http-nio-4] --- com.example.LambdaApplication: Processing: "hello"
[2019-07-08 14:40:59.112] - 11 INFO [reactor-http-nio-4] --- org.springframework.cloud.function.web.source.SupplierExporter: Posting to: 468c9e2d-3921-4620-b750-00ee119fedb3
[2019-07-08 14:40:59.115] - 11 INFO [reactor-http-nio-4] --- reactor.Mono.Defer.2: onSubscribe(FluxSwitchIfEmpty.SwitchIfEmptySubscriber)
[2019-07-08 14:40:59.116] - 11 INFO [reactor-http-nio-4] --- reactor.Mono.Defer.2: request(32)
END RequestId: 468c9e2d-3921-4620-b750-00ee119fedb3
REPORT RequestId: 468c9e2d-3921-4620-b750-00ee119fedb3 Init Duration: 1578.85 ms Duration: 178.39 ms Billed Duration: 1800 ms Memory Size: 1024 MB Max Memory Used: 145 MB
```

View File

@@ -21,32 +21,6 @@
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</exclusion>
<exclusion>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
<exclusion>
<groupId>org.synchronoss.cloud</groupId>
<artifactId>nio-multipart-parser</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId>

View File

@@ -1,4 +1,4 @@
spring.cloud.function.web.export.enabled=true
spring.cloud.function.web.export.debug=true
#spring.cloud.function.web.export.enabled=true
#spring.cloud.function.web.export.debug=true
spring.main.web-application-type=none
logging.level.org.springframework.cloud=DEBUG

View File

@@ -18,6 +18,7 @@ package com.example;
import java.util.concurrent.TimeUnit;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.ToStringConsumer;
@@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class ContainerTests {
@Test
@Disabled
void test() throws Exception {
ToStringConsumer consumer = new ToStringConsumer();
try (@SuppressWarnings("resource")

View File

@@ -1,15 +0,0 @@
package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.function.context.test.FunctionalSpringBootTest;
@FunctionalSpringBootTest
public class LambdaApplicationTests {
@Test
public void contextLoads() {
}
}

View File

@@ -19,6 +19,7 @@
<module>function-sample-pojo</module>
<module>function-sample-aws</module>
<module>function-sample-aws-custom</module>
<module>function-sample-aws-custom-bean</module>
<module>function-sample-supplier-exporter</module>
<module>function-sample-azure</module>
<module>function-sample-spring-integration</module>