-
Notifications
You must be signed in to change notification settings - Fork 561
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
23 changed files
with
730 additions
and
144 deletions.
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
...ntainer-core/src/main/java/com/amazonaws/serverless/proxy/AsyncInitializationWrapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package com.amazonaws.serverless.proxy; | ||
|
||
import com.amazonaws.serverless.exceptions.ContainerInitializationException; | ||
import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; | ||
import com.amazonaws.services.lambda.runtime.Context; | ||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.time.Instant; | ||
import java.util.concurrent.CountDownLatch; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
/** | ||
* An async implementation of the <code>InitializationWrapper</code> interface. This initializer calls the | ||
* {@link LambdaContainerHandler#initialize()} in a separate thread. Then uses a latch to wait for the maximum Lambda | ||
* initialization time of 10 seconds, if the <code>initialize</code> method takes longer than 10 seconds to return, the | ||
* {@link #start(LambdaContainerHandler)} returns control to the caller and lets the initialization thread continue in | ||
* the background. The {@link LambdaContainerHandler#proxy(Object, Context)} automatically waits for the latch of the | ||
* initializer to be released. | ||
* | ||
* The constructor of this class expects an epoch long. This is meant to be as close as possible to the time the Lambda | ||
* function actually started. In most cases, the first action in the constructor of the handler class should be to populate | ||
* this long value ({@code Instant.now().toEpochMs();}). This class uses the value to estimate how much of the init 10 | ||
* seconds has already been used up. | ||
*/ | ||
public class AsyncInitializationWrapper extends InitializationWrapper { | ||
private static final int INIT_GRACE_TIME_MS = 250; | ||
private static final int LAMBDA_MAX_INIT_TIME_MS = 10_000; | ||
|
||
private CountDownLatch initializationLatch; | ||
private long actualStartTime; | ||
private Logger log = LoggerFactory.getLogger(AsyncInitializationWrapper.class); | ||
|
||
/** | ||
* Creates a new instance of the async initializer. | ||
* @param startTime The epoch ms start time of the Lambda function, this should be measured as close as possible to | ||
* the initialization of the function. | ||
*/ | ||
public AsyncInitializationWrapper(long startTime) { | ||
actualStartTime = startTime; | ||
} | ||
|
||
@Override | ||
public void start(LambdaContainerHandler handler) throws ContainerInitializationException { | ||
initializationLatch = new CountDownLatch(1); | ||
AsyncInitializer initializer = new AsyncInitializer(initializationLatch, handler); | ||
Thread initThread = new Thread(initializer); | ||
initThread.start(); | ||
try { | ||
long curTime = Instant.now().toEpochMilli(); | ||
// account for the time it took to call the various constructors with the actual start time + a grace of 500ms | ||
long awaitTime = LAMBDA_MAX_INIT_TIME_MS - (curTime - actualStartTime) - INIT_GRACE_TIME_MS; | ||
log.info("Async initialization will wait for " + awaitTime + "ms"); | ||
if (!initializationLatch.await(awaitTime, TimeUnit.MILLISECONDS)) { | ||
log.info("Initialization took longer than " + LAMBDA_MAX_INIT_TIME_MS + ", setting new CountDownLatch and " + | ||
"continuing in event handler"); | ||
initializationLatch = new CountDownLatch(1); | ||
initializer.replaceLatch(initializationLatch); | ||
} | ||
} catch (InterruptedException e) { | ||
// at the moment we assume that this happened because of a timeout since the init thread calls System.exit | ||
// when an exception is thrown. | ||
throw new ContainerInitializationException("Container initialization interrupted", e); | ||
} | ||
} | ||
|
||
@Override | ||
public CountDownLatch getInitializationLatch() { | ||
return initializationLatch; | ||
} | ||
|
||
private static class AsyncInitializer implements Runnable { | ||
private LambdaContainerHandler handler; | ||
private CountDownLatch initLatch; | ||
private Logger log = LoggerFactory.getLogger(AsyncInitializationWrapper.class); | ||
|
||
AsyncInitializer(CountDownLatch latch, LambdaContainerHandler h) { | ||
initLatch = latch; | ||
handler = h; | ||
} | ||
|
||
synchronized void replaceLatch(CountDownLatch newLatch) { | ||
initLatch = newLatch; | ||
} | ||
|
||
@Override | ||
@SuppressFBWarnings("DM_EXIT") | ||
public void run() { | ||
log.info("Starting async initializer"); | ||
try { | ||
handler.initialize(); | ||
} catch (ContainerInitializationException e) { | ||
log.error("Failed to initialize container handler", e); | ||
// we cannot return the exception so we crash the whole kaboodle here | ||
System.exit(1); | ||
} | ||
synchronized(this) { | ||
initLatch.countDown(); | ||
} | ||
} | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
...va-container-core/src/main/java/com/amazonaws/serverless/proxy/InitializationWrapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package com.amazonaws.serverless.proxy; | ||
|
||
import com.amazonaws.serverless.exceptions.ContainerInitializationException; | ||
import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; | ||
|
||
import java.util.concurrent.CountDownLatch; | ||
|
||
/** | ||
* This class is in charge of initializing a {@link LambdaContainerHandler}. | ||
* In most cases, this means calling the {@link LambdaContainerHandler#initialize()} method. Some implementations may | ||
* require additional initialization steps, in this case implementations should provide their own | ||
* <code>InitializationWrapper</code>. This library includes an async implementation of this class | ||
* {@link AsyncInitializationWrapper} for frameworks that are likely to take longer than 10 seconds to start. | ||
*/ | ||
public class InitializationWrapper { | ||
/** | ||
* This is the main entry point. Container handler builder and the static <code>getAwsProxyHandler()</code> methods | ||
* of the various implementations will call this to initialize the underlying framework | ||
* @param handler The container handler to be initializer | ||
* @throws ContainerInitializationException If anything goes wrong during container initialization. | ||
*/ | ||
public void start(LambdaContainerHandler handler) throws ContainerInitializationException { | ||
handler.initialize(); | ||
} | ||
|
||
/** | ||
* Asynchronous implementations of the framework should return a latch that the container handler can use to decide | ||
* whether it can start handling events. Synchronous implementations of this interface should return <code>null</code>. | ||
* @return An initialized latch if the underlying container is starting in a separate thread, null otherwise. | ||
*/ | ||
public CountDownLatch getInitializationLatch() { | ||
return null; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
...com/amazonaws/serverless/proxy/internal/servlet/ServletLambdaContainerHandlerBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
package com.amazonaws.serverless.proxy.internal.servlet; | ||
|
||
import com.amazonaws.serverless.exceptions.ContainerInitializationException; | ||
import com.amazonaws.serverless.proxy.*; | ||
import com.amazonaws.serverless.proxy.model.AwsProxyRequest; | ||
import com.amazonaws.serverless.proxy.model.AwsProxyResponse; | ||
|
||
import javax.servlet.http.HttpServletRequest; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
/** | ||
* Base builder class for {@link AwsLambdaServletContainerHandler}. Implmentations can extend this class to have setters | ||
* for the basic parameters. | ||
* @param <RequestType> The event object class | ||
* @param <ResponseType> The output object class | ||
* @param <ContainerRequestType> The container request type. For proxy implementations, this is {@link AwsProxyHttpServletRequest}. | ||
* The response type is hardcoded to {@link AwsHttpServletResponse} since it is a generic | ||
* servlet response implementation. | ||
* @param <HandlerType> The type of the handler we are building | ||
* @param <Builder> The builder object itself. This is used to allow implementations to re-use the setter method from this | ||
* abstract class through <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=133275"> | ||
* "curiously recurring generic patterns"</a> | ||
*/ | ||
public abstract class ServletLambdaContainerHandlerBuilder< | ||
RequestType, | ||
ResponseType, | ||
ContainerRequestType extends HttpServletRequest, | ||
HandlerType extends AwsLambdaServletContainerHandler<RequestType, ResponseType, ContainerRequestType, AwsHttpServletResponse>, | ||
Builder extends ServletLambdaContainerHandlerBuilder<RequestType, ResponseType, ContainerRequestType, HandlerType, Builder>> | ||
{ | ||
private static final String MISSING_FIELD_ERROR = "Missing %s in lambda container handler builder"; | ||
|
||
protected InitializationWrapper initializationWrapper; | ||
protected RequestReader<RequestType, ContainerRequestType> requestReader; | ||
protected ResponseWriter<AwsHttpServletResponse, ResponseType> responseWriter; | ||
protected SecurityContextWriter<RequestType> securityContextWriter; | ||
protected ExceptionHandler<ResponseType> exceptionHandler; | ||
protected Class<RequestType> requestTypeClass; | ||
protected Class<ResponseType> responseTypeClass; | ||
|
||
/** | ||
* Validates that all of the required fields are populated. | ||
* @throws ContainerInitializationException If values have not been set on the builder. The message in the exception | ||
* contains a standard error message {@link ServletLambdaContainerHandlerBuilder#MISSING_FIELD_ERROR} populated with | ||
* the list of missing fields. | ||
*/ | ||
protected void validate() throws ContainerInitializationException { | ||
List<String> errFields = new ArrayList<>(); | ||
if (requestTypeClass == null) { | ||
errFields.add("request type class"); | ||
} | ||
if (responseTypeClass == null) { | ||
errFields.add("response type class"); | ||
} | ||
if (requestReader == null) { | ||
errFields.add("request reader"); | ||
} | ||
if (responseWriter == null) { | ||
errFields.add("response writer"); | ||
} | ||
if (securityContextWriter == null) { | ||
errFields.add("security context writer"); | ||
} | ||
if (exceptionHandler == null) { | ||
errFields.add("exception handler"); | ||
} | ||
if (initializationWrapper == null) { | ||
errFields.add("initialization wrapper"); | ||
} | ||
if (!errFields.isEmpty()) { | ||
throw new ContainerInitializationException(String.format(MISSING_FIELD_ERROR, String.join(", ", errFields)), null); | ||
} | ||
} | ||
|
||
/** | ||
* Sets all of the required fields in the builder to the default settings for a Servlet-compatible framework that wants | ||
* to support AWS proxy event and output types. | ||
* @return A populated builder | ||
*/ | ||
public Builder defaultProxy() { | ||
initializationWrapper(new InitializationWrapper()) | ||
.requestReader((RequestReader<RequestType, ContainerRequestType>) new AwsProxyHttpServletRequestReader()) | ||
.responseWriter((ResponseWriter<AwsHttpServletResponse, ResponseType>) new AwsProxyHttpServletResponseWriter()) | ||
.securityContextWriter((SecurityContextWriter<RequestType>) new AwsProxySecurityContextWriter()) | ||
.exceptionHandler((ExceptionHandler<ResponseType>) new AwsProxyExceptionHandler()) | ||
.requestTypeClass((Class<RequestType>) AwsProxyRequest.class) | ||
.responseTypeClass((Class<ResponseType>) AwsProxyResponse.class); | ||
return self(); | ||
} | ||
|
||
/** | ||
* Sets the initialization wrapper to be used by the {@link ServletLambdaContainerHandlerBuilder#buildAndInitialize()} | ||
* method to start the framework implementations | ||
* @param initializationWrapper An implementation of <code>InitializationWrapper</code>. In most cases, this will be | ||
* set to {@link InitializationWrapper}. The {@link ServletLambdaContainerHandlerBuilder#asyncInit(long)} | ||
* method sets this to {@link AsyncInitializationWrapper}. | ||
* @return This builder object | ||
*/ | ||
public Builder initializationWrapper(InitializationWrapper initializationWrapper) { | ||
this.initializationWrapper = initializationWrapper; | ||
return self(); | ||
} | ||
|
||
public Builder requestReader(RequestReader<RequestType, ContainerRequestType> requestReader) { | ||
this.requestReader = requestReader; | ||
return self(); | ||
} | ||
|
||
public Builder responseWriter(ResponseWriter<AwsHttpServletResponse, ResponseType> responseWriter) { | ||
this.responseWriter = responseWriter; | ||
return self(); | ||
} | ||
|
||
public Builder securityContextWriter(SecurityContextWriter<RequestType> securityContextWriter) { | ||
this.securityContextWriter = securityContextWriter; | ||
return self(); | ||
} | ||
|
||
public Builder exceptionHandler(ExceptionHandler<ResponseType> exceptionHandler) { | ||
this.exceptionHandler = exceptionHandler; | ||
return self(); | ||
} | ||
|
||
public Builder requestTypeClass(Class<RequestType> requestType) { | ||
this.requestTypeClass = requestType; | ||
return self(); | ||
} | ||
|
||
public Builder responseTypeClass(Class<ResponseType> responseType) { | ||
this.responseTypeClass = responseType; | ||
return self(); | ||
} | ||
|
||
public Builder asyncInit(long actualStartTime) { | ||
this.initializationWrapper = new AsyncInitializationWrapper(actualStartTime); | ||
return self(); | ||
} | ||
|
||
/** | ||
* Implementations should implement this method to return their type. All of the builder methods in this abstract | ||
* class use this method to return the correct builder type. | ||
* @return The current builder. | ||
*/ | ||
protected abstract Builder self(); | ||
public abstract HandlerType build() throws ContainerInitializationException; | ||
public abstract HandlerType buildAndInitialize() throws ContainerInitializationException; | ||
} |
Oops, something went wrong.