Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@
package org.springframework.cloud.function.adapter.aws;

import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.SocketException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

import com.amazonaws.services.lambda.runtime.ClientContext;
import com.amazonaws.services.lambda.runtime.CognitoIdentity;
Expand Down Expand Up @@ -72,6 +73,8 @@ public final class CustomRuntimeEventLoop implements SmartLifecycle {
private static final String LAMBDA_ERROR_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/error";
private static final String LAMBDA_RUNTIME_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/next";
private static final String LAMBDA_INVOCATION_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/response";
private static final String LAMBDA_RUNTIME_FUNCTION_ERROR_TYPE = "Lambda-Runtime-Function-Error-Type";

private static final String USER_AGENT_VALUE = String.format(
"spring-cloud-function/%s-%s",
System.getProperty("java.runtime.version"),
Expand Down Expand Up @@ -282,12 +285,11 @@ public String toString() {

private void propagateAwsError(String requestId, Exception e, JsonMapper mapper, String runtimeApi, RestTemplate rest) {
String errorMessage = e.getMessage();
String errorType = e.getClass().getSimpleName();
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
String stackTrace = sw.toString();
Map<String, String> em = new HashMap<>();
String errorType = e.getClass().getName();
List<String> stackTrace = Arrays.stream(e.getStackTrace())
.map(StackTraceElement::toString)
.collect(Collectors.toList());
Map<String, Object> em = new HashMap<>();
em.put("errorMessage", errorMessage);
em.put("errorType", errorType);
em.put("stackTrace", stackTrace);
Expand All @@ -296,6 +298,7 @@ private void propagateAwsError(String requestId, Exception e, JsonMapper mapper,
String errorUrl = MessageFormat.format(LAMBDA_ERROR_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId);
ResponseEntity<Object> result = rest.exchange(RequestEntity.post(URI.create(errorUrl))
.header(USER_AGENT, USER_AGENT_VALUE)
.header(LAMBDA_RUNTIME_FUNCTION_ERROR_TYPE, errorType)
.body(outputBody), Object.class);
if (logger.isInfoEnabled()) {
logger.info("Result ERROR status: " + result.getStatusCode());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public class AWSCustomRuntime {

BlockingQueue<Message<String>> outputQueue = new ArrayBlockingQueue<>(3);

/** Queue for capturing error responses from the Custom Runtime event loop. */
public BlockingQueue<Message<String>> errorQueue = new ArrayBlockingQueue<>(3);

public AWSCustomRuntime(ConfigurableApplicationContext context) {
context.getEnvironment().getPropertySources().addFirst(
new MapPropertySource("AWSCustomRuntime",
Expand All @@ -55,6 +58,11 @@ Consumer<Message<String>> consume() {
return v -> outputQueue.offer(v);
}

@Bean("2018-06-01/runtime/invocation/consume/error")
Consumer<Message<String>> consumeError() {
return v -> errorQueue.offer(v);
}

@SuppressWarnings("unchecked")
@Bean("2018-06-01/runtime/invocation/next")
Supplier<Message<String>> supply() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package org.springframework.cloud.function.adapter.aws;

import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;

import org.junit.jupiter.api.Test;
Expand All @@ -26,6 +28,7 @@
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.adapter.test.aws.AWSCustomRuntime;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down Expand Up @@ -217,6 +220,76 @@ public void test_definitionLookupAndComposition() throws Exception {

}

@Test
public void testFunctionThatThrowsException() throws Exception {
try (ConfigurableApplicationContext userContext =
new SpringApplicationBuilder(ErrorFunctionConfiguration.class, AWSCustomRuntime.class)
.web(WebApplicationType.SERVLET)
.properties("_HANDLER=hello", "server.port=0")
.run()) {

AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class);
JsonMapper mapper = userContext.getBean(JsonMapper.class);

aws.exchange("\"test\"");
Message<String> errorMessage = aws.errorQueue.poll(5000, java.util.concurrent.TimeUnit.MILLISECONDS);

assertThat(errorMessage).isNotNull();
Map<String, Object> errorMap = mapper.fromJson(errorMessage.getPayload(), Map.class);

assertThat(errorMap)
.containsEntry("errorMessage", "Something went wrong")
.containsEntry("errorType", "java.lang.RuntimeException")
.containsKey("stackTrace");
assertThat(errorMap.get("stackTrace")).isInstanceOf(List.class);
assertThat((List<?>) errorMap.get("stackTrace"))
.isNotEmpty()
.first().asString().contains("CustomRuntimeEventLoopTest");
}
}

@Test
public void testFunctionThatThrowsNestedException() throws Exception {
try (ConfigurableApplicationContext userContext =
new SpringApplicationBuilder(ErrorFunctionConfiguration.class, AWSCustomRuntime.class)
.web(WebApplicationType.SERVLET)
.properties("_HANDLER=helloNested", "server.port=0")
.run()) {

AWSCustomRuntime aws = userContext.getBean(AWSCustomRuntime.class);
JsonMapper mapper = userContext.getBean(JsonMapper.class);

aws.exchange("\"test\"");
Message<String> errorMessage = aws.errorQueue.poll(5000, java.util.concurrent.TimeUnit.MILLISECONDS);

assertThat(errorMessage).isNotNull();
Map<String, Object> errorMap = mapper.fromJson(errorMessage.getPayload(), Map.class);

assertThat(errorMap)
.containsEntry("errorMessage", "Wrapper exception")
.containsEntry("errorType", "java.lang.RuntimeException");
assertThat(errorMap.get("stackTrace")).isInstanceOf(List.class);
}
}

@EnableAutoConfiguration
protected static class ErrorFunctionConfiguration {
@Bean
public Function<String, String> hello() {
return event -> {
throw new RuntimeException("Something went wrong");
};
}

@Bean
public Function<String, String> helloNested() {
return event -> {
Exception cause = new IllegalStateException("Root cause error");
throw new RuntimeException("Wrapper exception", cause);
};
}
}

@EnableAutoConfiguration
protected static class SingleFunctionConfiguration {
@Bean
Expand Down
Loading