Skip to content

Commit 301f718

Browse files
committed
Added new InitializationWrapper and Async implementation to address #210 and #234. Also added a new builder object that makes it easier to construct container handlers
1 parent d1ad9c7 commit 301f718

File tree

23 files changed

+730
-144
lines changed

23 files changed

+730
-144
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.amazonaws.serverless.proxy;
2+
3+
import com.amazonaws.serverless.exceptions.ContainerInitializationException;
4+
import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
5+
import com.amazonaws.services.lambda.runtime.Context;
6+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
10+
import java.time.Instant;
11+
import java.util.concurrent.CountDownLatch;
12+
import java.util.concurrent.TimeUnit;
13+
14+
/**
15+
* An async implementation of the <code>InitializationWrapper</code> interface. This initializer calls the
16+
* {@link LambdaContainerHandler#initialize()} in a separate thread. Then uses a latch to wait for the maximum Lambda
17+
* initialization time of 10 seconds, if the <code>initialize</code> method takes longer than 10 seconds to return, the
18+
* {@link #start(LambdaContainerHandler)} returns control to the caller and lets the initialization thread continue in
19+
* the background. The {@link LambdaContainerHandler#proxy(Object, Context)} automatically waits for the latch of the
20+
* initializer to be released.
21+
*
22+
* The constructor of this class expects an epoch long. This is meant to be as close as possible to the time the Lambda
23+
* function actually started. In most cases, the first action in the constructor of the handler class should be to populate
24+
* this long value ({@code Instant.now().toEpochMs();}). This class uses the value to estimate how much of the init 10
25+
* seconds has already been used up.
26+
*/
27+
public class AsyncInitializationWrapper extends InitializationWrapper {
28+
private static final int INIT_GRACE_TIME_MS = 250;
29+
private static final int LAMBDA_MAX_INIT_TIME_MS = 10_000;
30+
31+
private CountDownLatch initializationLatch;
32+
private long actualStartTime;
33+
private Logger log = LoggerFactory.getLogger(AsyncInitializationWrapper.class);
34+
35+
/**
36+
* Creates a new instance of the async initializer.
37+
* @param startTime The epoch ms start time of the Lambda function, this should be measured as close as possible to
38+
* the initialization of the function.
39+
*/
40+
public AsyncInitializationWrapper(long startTime) {
41+
actualStartTime = startTime;
42+
}
43+
44+
@Override
45+
public void start(LambdaContainerHandler handler) throws ContainerInitializationException {
46+
initializationLatch = new CountDownLatch(1);
47+
AsyncInitializer initializer = new AsyncInitializer(initializationLatch, handler);
48+
Thread initThread = new Thread(initializer);
49+
initThread.start();
50+
try {
51+
long curTime = Instant.now().toEpochMilli();
52+
// account for the time it took to call the various constructors with the actual start time + a grace of 500ms
53+
long awaitTime = LAMBDA_MAX_INIT_TIME_MS - (curTime - actualStartTime) - INIT_GRACE_TIME_MS;
54+
log.info("Async initialization will wait for " + awaitTime + "ms");
55+
if (!initializationLatch.await(awaitTime, TimeUnit.MILLISECONDS)) {
56+
log.info("Initialization took longer than " + LAMBDA_MAX_INIT_TIME_MS + ", setting new CountDownLatch and " +
57+
"continuing in event handler");
58+
initializationLatch = new CountDownLatch(1);
59+
initializer.replaceLatch(initializationLatch);
60+
}
61+
} catch (InterruptedException e) {
62+
// at the moment we assume that this happened because of a timeout since the init thread calls System.exit
63+
// when an exception is thrown.
64+
throw new ContainerInitializationException("Container initialization interrupted", e);
65+
}
66+
}
67+
68+
@Override
69+
public CountDownLatch getInitializationLatch() {
70+
return initializationLatch;
71+
}
72+
73+
private static class AsyncInitializer implements Runnable {
74+
private LambdaContainerHandler handler;
75+
private CountDownLatch initLatch;
76+
private Logger log = LoggerFactory.getLogger(AsyncInitializationWrapper.class);
77+
78+
AsyncInitializer(CountDownLatch latch, LambdaContainerHandler h) {
79+
initLatch = latch;
80+
handler = h;
81+
}
82+
83+
synchronized void replaceLatch(CountDownLatch newLatch) {
84+
initLatch = newLatch;
85+
}
86+
87+
@Override
88+
@SuppressFBWarnings("DM_EXIT")
89+
public void run() {
90+
log.info("Starting async initializer");
91+
try {
92+
handler.initialize();
93+
} catch (ContainerInitializationException e) {
94+
log.error("Failed to initialize container handler", e);
95+
// we cannot return the exception so we crash the whole kaboodle here
96+
System.exit(1);
97+
}
98+
synchronized(this) {
99+
initLatch.countDown();
100+
}
101+
}
102+
}
103+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.amazonaws.serverless.proxy;
2+
3+
import com.amazonaws.serverless.exceptions.ContainerInitializationException;
4+
import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler;
5+
6+
import java.util.concurrent.CountDownLatch;
7+
8+
/**
9+
* This class is in charge of initializing a {@link LambdaContainerHandler}.
10+
* In most cases, this means calling the {@link LambdaContainerHandler#initialize()} method. Some implementations may
11+
* require additional initialization steps, in this case implementations should provide their own
12+
* <code>InitializationWrapper</code>. This library includes an async implementation of this class
13+
* {@link AsyncInitializationWrapper} for frameworks that are likely to take longer than 10 seconds to start.
14+
*/
15+
public class InitializationWrapper {
16+
/**
17+
* This is the main entry point. Container handler builder and the static <code>getAwsProxyHandler()</code> methods
18+
* of the various implementations will call this to initialize the underlying framework
19+
* @param handler The container handler to be initializer
20+
* @throws ContainerInitializationException If anything goes wrong during container initialization.
21+
*/
22+
public void start(LambdaContainerHandler handler) throws ContainerInitializationException {
23+
handler.initialize();
24+
}
25+
26+
/**
27+
* Asynchronous implementations of the framework should return a latch that the container handler can use to decide
28+
* whether it can start handling events. Synchronous implementations of this interface should return <code>null</code>.
29+
* @return An initialized latch if the underlying container is starting in a separate thread, null otherwise.
30+
*/
31+
public CountDownLatch getInitializationLatch() {
32+
return null;
33+
}
34+
}

aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,9 @@
1414

1515

1616
import com.amazonaws.serverless.exceptions.ContainerInitializationException;
17-
import com.amazonaws.serverless.proxy.LogFormatter;
17+
import com.amazonaws.serverless.proxy.*;
1818
import com.amazonaws.serverless.proxy.internal.servlet.ApacheCombinedServletLogFormatter;
1919
import com.amazonaws.serverless.proxy.model.ContainerConfig;
20-
import com.amazonaws.serverless.proxy.ExceptionHandler;
21-
import com.amazonaws.serverless.proxy.RequestReader;
22-
import com.amazonaws.serverless.proxy.ResponseWriter;
23-
import com.amazonaws.serverless.proxy.SecurityContextWriter;
2420
import com.amazonaws.services.lambda.runtime.Context;
2521

2622
import com.fasterxml.jackson.core.JsonParseException;
@@ -38,6 +34,7 @@
3834
import java.io.InputStream;
3935
import java.io.OutputStream;
4036
import java.util.concurrent.CountDownLatch;
37+
import java.util.concurrent.TimeUnit;
4138

4239

4340
/**
@@ -67,6 +64,7 @@ public abstract class LambdaContainerHandler<RequestType, ResponseType, Containe
6764
private ExceptionHandler<ResponseType> exceptionHandler;
6865
private Class<RequestType> requestTypeClass;
6966
private Class<ResponseType> responseTypeClass;
67+
private InitializationWrapper initializationWrapper;
7068

7169
protected Context lambdaContext;
7270
private LogFormatter<ContainerRequestType, ContainerResponseType> logFormatter;
@@ -97,16 +95,28 @@ protected LambdaContainerHandler(Class<RequestType> requestClass,
9795
RequestReader<RequestType, ContainerRequestType> requestReader,
9896
ResponseWriter<ContainerResponseType, ResponseType> responseWriter,
9997
SecurityContextWriter<RequestType> securityContextWriter,
100-
ExceptionHandler<ResponseType> exceptionHandler) {
98+
ExceptionHandler<ResponseType> exceptionHandler,
99+
InitializationWrapper init) {
101100
log.info("Starting Lambda Container Handler");
102101
requestTypeClass = requestClass;
103102
responseTypeClass = responseClass;
104103
this.requestReader = requestReader;
105104
this.responseWriter = responseWriter;
106105
this.securityContextWriter = securityContextWriter;
107106
this.exceptionHandler = exceptionHandler;
107+
initializationWrapper = init;
108108
objectReader = getObjectMapper().readerFor(requestTypeClass);
109109
objectWriter = getObjectMapper().writerFor(responseTypeClass);
110+
111+
}
112+
113+
protected LambdaContainerHandler(Class<RequestType> requestClass,
114+
Class<ResponseType> responseClass,
115+
RequestReader<RequestType, ContainerRequestType> requestReader,
116+
ResponseWriter<ContainerResponseType, ResponseType> responseWriter,
117+
SecurityContextWriter<RequestType> securityContextWriter,
118+
ExceptionHandler<ResponseType> exceptionHandler) {
119+
this(requestClass, responseClass, requestReader, responseWriter, securityContextWriter, exceptionHandler, new InitializationWrapper());
110120
}
111121

112122

@@ -131,6 +141,23 @@ public static ObjectMapper getObjectMapper() {
131141
return objectMapper;
132142
}
133143

144+
/**
145+
* Returns the initialization wrapper this container handler will monitor to handle events
146+
* @return The initialization wrapper that was passed to the constructor and this instance will use to decide
147+
* whether it can start handling events.
148+
*/
149+
public InitializationWrapper getInitializationWrapper() {
150+
return initializationWrapper;
151+
}
152+
153+
/**
154+
* Sets a new initialization wrapper.
155+
* @param wrapper The wrapper this instance will use to decide whether it can start handling events.
156+
*/
157+
public void setInitializationWrapper(InitializationWrapper wrapper) {
158+
initializationWrapper = wrapper;
159+
}
160+
134161
/**
135162
* Configures the library to strip a base path from incoming requests before passing them on to the wrapped
136163
* framework. This was added in response to issue #34 (https://github.com/awslabs/aws-serverless-java-container/issues/34).
@@ -174,6 +201,13 @@ public ResponseType proxy(RequestType request, Context context) {
174201
ContainerRequestType containerRequest = requestReader.readRequest(request, securityContext, context, config);
175202
ContainerResponseType containerResponse = getContainerResponse(containerRequest, latch);
176203

204+
if (initializationWrapper != null && initializationWrapper.getInitializationLatch() != null) {
205+
// we let the potential InterruptedException bubble up
206+
if (!initializationWrapper.getInitializationLatch().await(config.getInitializationTimeout(), TimeUnit.MILLISECONDS)) {
207+
throw new ContainerInitializationException("Could not initialize framework within the " + config.getInitializationTimeout() + "ms timeout", null);
208+
}
209+
}
210+
177211
handleRequest(containerRequest, containerResponse, context);
178212

179213
latch.await();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.amazonaws.serverless.proxy.internal.servlet;
2+
3+
import com.amazonaws.serverless.exceptions.ContainerInitializationException;
4+
import com.amazonaws.serverless.proxy.*;
5+
import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
6+
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
7+
8+
import javax.servlet.http.HttpServletRequest;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
12+
/**
13+
* Base builder class for {@link AwsLambdaServletContainerHandler}. Implmentations can extend this class to have setters
14+
* for the basic parameters.
15+
* @param <RequestType> The event object class
16+
* @param <ResponseType> The output object class
17+
* @param <ContainerRequestType> The container request type. For proxy implementations, this is {@link AwsProxyHttpServletRequest}.
18+
* The response type is hardcoded to {@link AwsHttpServletResponse} since it is a generic
19+
* servlet response implementation.
20+
* @param <HandlerType> The type of the handler we are building
21+
* @param <Builder> The builder object itself. This is used to allow implementations to re-use the setter method from this
22+
* abstract class through <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=133275">
23+
* "curiously recurring generic patterns"</a>
24+
*/
25+
public abstract class ServletLambdaContainerHandlerBuilder<
26+
RequestType,
27+
ResponseType,
28+
ContainerRequestType extends HttpServletRequest,
29+
HandlerType extends AwsLambdaServletContainerHandler<RequestType, ResponseType, ContainerRequestType, AwsHttpServletResponse>,
30+
Builder extends ServletLambdaContainerHandlerBuilder<RequestType, ResponseType, ContainerRequestType, HandlerType, Builder>>
31+
{
32+
private static final String MISSING_FIELD_ERROR = "Missing %s in lambda container handler builder";
33+
34+
protected InitializationWrapper initializationWrapper;
35+
protected RequestReader<RequestType, ContainerRequestType> requestReader;
36+
protected ResponseWriter<AwsHttpServletResponse, ResponseType> responseWriter;
37+
protected SecurityContextWriter<RequestType> securityContextWriter;
38+
protected ExceptionHandler<ResponseType> exceptionHandler;
39+
protected Class<RequestType> requestTypeClass;
40+
protected Class<ResponseType> responseTypeClass;
41+
42+
/**
43+
* Validates that all of the required fields are populated.
44+
* @throws ContainerInitializationException If values have not been set on the builder. The message in the exception
45+
* contains a standard error message {@link ServletLambdaContainerHandlerBuilder#MISSING_FIELD_ERROR} populated with
46+
* the list of missing fields.
47+
*/
48+
protected void validate() throws ContainerInitializationException {
49+
List<String> errFields = new ArrayList<>();
50+
if (requestTypeClass == null) {
51+
errFields.add("request type class");
52+
}
53+
if (responseTypeClass == null) {
54+
errFields.add("response type class");
55+
}
56+
if (requestReader == null) {
57+
errFields.add("request reader");
58+
}
59+
if (responseWriter == null) {
60+
errFields.add("response writer");
61+
}
62+
if (securityContextWriter == null) {
63+
errFields.add("security context writer");
64+
}
65+
if (exceptionHandler == null) {
66+
errFields.add("exception handler");
67+
}
68+
if (initializationWrapper == null) {
69+
errFields.add("initialization wrapper");
70+
}
71+
if (!errFields.isEmpty()) {
72+
throw new ContainerInitializationException(String.format(MISSING_FIELD_ERROR, String.join(", ", errFields)), null);
73+
}
74+
}
75+
76+
/**
77+
* Sets all of the required fields in the builder to the default settings for a Servlet-compatible framework that wants
78+
* to support AWS proxy event and output types.
79+
* @return A populated builder
80+
*/
81+
public Builder defaultProxy() {
82+
initializationWrapper(new InitializationWrapper())
83+
.requestReader((RequestReader<RequestType, ContainerRequestType>) new AwsProxyHttpServletRequestReader())
84+
.responseWriter((ResponseWriter<AwsHttpServletResponse, ResponseType>) new AwsProxyHttpServletResponseWriter())
85+
.securityContextWriter((SecurityContextWriter<RequestType>) new AwsProxySecurityContextWriter())
86+
.exceptionHandler((ExceptionHandler<ResponseType>) new AwsProxyExceptionHandler())
87+
.requestTypeClass((Class<RequestType>) AwsProxyRequest.class)
88+
.responseTypeClass((Class<ResponseType>) AwsProxyResponse.class);
89+
return self();
90+
}
91+
92+
/**
93+
* Sets the initialization wrapper to be used by the {@link ServletLambdaContainerHandlerBuilder#buildAndInitialize()}
94+
* method to start the framework implementations
95+
* @param initializationWrapper An implementation of <code>InitializationWrapper</code>. In most cases, this will be
96+
* set to {@link InitializationWrapper}. The {@link ServletLambdaContainerHandlerBuilder#asyncInit(long)}
97+
* method sets this to {@link AsyncInitializationWrapper}.
98+
* @return This builder object
99+
*/
100+
public Builder initializationWrapper(InitializationWrapper initializationWrapper) {
101+
this.initializationWrapper = initializationWrapper;
102+
return self();
103+
}
104+
105+
public Builder requestReader(RequestReader<RequestType, ContainerRequestType> requestReader) {
106+
this.requestReader = requestReader;
107+
return self();
108+
}
109+
110+
public Builder responseWriter(ResponseWriter<AwsHttpServletResponse, ResponseType> responseWriter) {
111+
this.responseWriter = responseWriter;
112+
return self();
113+
}
114+
115+
public Builder securityContextWriter(SecurityContextWriter<RequestType> securityContextWriter) {
116+
this.securityContextWriter = securityContextWriter;
117+
return self();
118+
}
119+
120+
public Builder exceptionHandler(ExceptionHandler<ResponseType> exceptionHandler) {
121+
this.exceptionHandler = exceptionHandler;
122+
return self();
123+
}
124+
125+
public Builder requestTypeClass(Class<RequestType> requestType) {
126+
this.requestTypeClass = requestType;
127+
return self();
128+
}
129+
130+
public Builder responseTypeClass(Class<ResponseType> responseType) {
131+
this.responseTypeClass = responseType;
132+
return self();
133+
}
134+
135+
public Builder asyncInit(long actualStartTime) {
136+
this.initializationWrapper = new AsyncInitializationWrapper(actualStartTime);
137+
return self();
138+
}
139+
140+
/**
141+
* Implementations should implement this method to return their type. All of the builder methods in this abstract
142+
* class use this method to return the correct builder type.
143+
* @return The current builder.
144+
*/
145+
protected abstract Builder self();
146+
public abstract HandlerType build() throws ContainerInitializationException;
147+
public abstract HandlerType buildAndInitialize() throws ContainerInitializationException;
148+
}

0 commit comments

Comments
 (0)