|
4 | 4 |
|
5 | 5 | import com.amazonaws.services.lambda.runtime.Context; |
6 | 6 | import com.amazonaws.services.lambda.runtime.RequestHandler; |
7 | | -import java.nio.charset.StandardCharsets; |
8 | | -import java.util.concurrent.CompletableFuture; |
9 | 7 | import java.util.function.BiFunction; |
10 | | -import org.slf4j.Logger; |
11 | | -import org.slf4j.LoggerFactory; |
12 | | -import software.amazon.awssdk.services.lambda.model.ErrorObject; |
13 | | -import software.amazon.awssdk.services.lambda.model.Operation; |
14 | | -import software.amazon.awssdk.services.lambda.model.OperationAction; |
15 | | -import software.amazon.awssdk.services.lambda.model.OperationType; |
16 | | -import software.amazon.awssdk.services.lambda.model.OperationUpdate; |
17 | | -import software.amazon.lambda.durable.exception.DurableOperationException; |
18 | | -import software.amazon.lambda.durable.exception.IllegalDurableOperationException; |
19 | | -import software.amazon.lambda.durable.exception.UnrecoverableDurableExecutionException; |
20 | 8 | import software.amazon.lambda.durable.execution.ExecutionManager; |
21 | | -import software.amazon.lambda.durable.execution.SuspendExecutionException; |
22 | | -import software.amazon.lambda.durable.execution.ThreadContext; |
23 | | -import software.amazon.lambda.durable.execution.ThreadType; |
24 | 9 | import software.amazon.lambda.durable.model.DurableExecutionInput; |
25 | 10 | import software.amazon.lambda.durable.model.DurableExecutionOutput; |
26 | | -import software.amazon.lambda.durable.serde.SerDes; |
27 | | -import software.amazon.lambda.durable.util.ExceptionHelper; |
28 | 11 |
|
29 | 12 | public class DurableExecutor { |
30 | | - private static final Logger logger = LoggerFactory.getLogger(DurableExecutor.class); |
31 | | - |
32 | | - // Lambda response size limit is 6MB minus small epsilon for envelope |
33 | | - private static final int LAMBDA_RESPONSE_SIZE_LIMIT = 6 * 1024 * 1024 - 50; |
34 | 13 |
|
35 | 14 | public static <I, O> DurableExecutionOutput execute( |
36 | 15 | DurableExecutionInput input, |
37 | 16 | Context lambdaContext, |
38 | 17 | Class<I> inputType, |
39 | 18 | BiFunction<I, DurableContext, O> handler, |
40 | 19 | DurableConfig config) { |
41 | | - var executionManager = new ExecutionManager( |
42 | | - input.durableExecutionArn(), input.checkpointToken(), input.initialExecutionState(), config); |
43 | | - |
44 | | - var handlerFuture = CompletableFuture.supplyAsync( |
45 | | - () -> { |
46 | | - var userInput = |
47 | | - extractUserInput(executionManager.getExecutionOperation(), config.getSerDes(), inputType); |
48 | | - // Create context in the executor thread so it detects the correct thread name |
49 | | - var context = DurableContext.createRootContext(executionManager, config, lambdaContext); |
50 | | - executionManager.registerActiveThread(null); |
51 | | - executionManager.setCurrentThreadContext(new ThreadContext(null, ThreadType.CONTEXT)); |
52 | | - return handler.apply(userInput, context); |
53 | | - }, |
54 | | - config.getExecutorService()); // Get executor from config for running user code |
55 | | - |
56 | | - // Execute the handlerFuture in ExecutionManager. If it completes successfully, the output of user function |
57 | | - // will be returned. Otherwise, it will complete exceptionally with a SuspendExecutionException or a failure. |
58 | | - return executionManager |
59 | | - .runUntilCompleteOrSuspend(handlerFuture) |
60 | | - .handle((result, ex) -> { |
61 | | - if (ex != null) { |
62 | | - // an exception thrown from handlerFuture or suspension/termination occurred |
63 | | - Throwable cause = ExceptionHelper.unwrapCompletableFuture(ex); |
64 | | - if (cause instanceof SuspendExecutionException) { |
65 | | - return DurableExecutionOutput.pending(); |
66 | | - } |
67 | | - |
68 | | - logger.debug("Execution failed: {}", cause.getMessage()); |
69 | | - return DurableExecutionOutput.failure(buildErrorObject(cause, config.getSerDes())); |
70 | | - } |
71 | | - // user handler complete successfully |
72 | | - var outputPayload = config.getSerDes().serialize(result); |
73 | | - |
74 | | - logger.debug("Execution completed"); |
75 | | - return DurableExecutionOutput.success(handleLargePayload(executionManager, outputPayload)); |
76 | | - }) |
77 | | - .whenComplete((v, ex) -> { |
78 | | - // We shutdown the execution to make sure remaining checkpoint calls in the queue are drained |
79 | | - // We DO NOT shutdown the executor since it should stay warm for re-invokes against a warm Lambda |
80 | | - // runtime. |
81 | | - // For example, a re-invoke after a wait should re-use the same executor instance from |
82 | | - // DurableConfig. |
83 | | - // userExecutor.shutdown(); |
84 | | - executionManager.shutdown(); |
85 | | - }) |
86 | | - .join(); |
87 | | - } |
88 | | - |
89 | | - private static String handleLargePayload(ExecutionManager executionManager, String outputPayload) { |
90 | | - // Check if the serialized payload exceeds Lambda response size limit |
91 | | - var payloadSize = outputPayload != null ? outputPayload.getBytes(StandardCharsets.UTF_8).length : 0; |
92 | | - |
93 | | - if (payloadSize > LAMBDA_RESPONSE_SIZE_LIMIT) { |
94 | | - logger.debug( |
95 | | - "Response size ({} bytes) exceeds Lambda limit ({} bytes). Checkpointing result.", |
96 | | - payloadSize, |
97 | | - LAMBDA_RESPONSE_SIZE_LIMIT); |
98 | | - |
99 | | - // Checkpoint the large result and wait for it to complete |
100 | | - executionManager |
101 | | - .sendOperationUpdate(OperationUpdate.builder() |
102 | | - .type(OperationType.EXECUTION) |
103 | | - .id(executionManager.getExecutionOperation().id()) |
104 | | - .action(OperationAction.SUCCEED) |
105 | | - .payload(outputPayload) |
106 | | - .build()) |
107 | | - .join(); |
108 | 20 |
|
109 | | - // Return empty result, we checkpointed the data manually |
110 | | - logger.debug("Execution completed (large response checkpointed)"); |
111 | | - return ""; |
| 21 | + // We shut down the executionManager to make sure remaining checkpoint calls in the queue are drained |
| 22 | + // We DO NOT shut down the executor since it should stay warm for re-invokes against a warm Lambda runtime. |
| 23 | + // For example, a re-invoke after a wait should re-use the same executor instance from DurableConfig. |
| 24 | + try (var executionManager = new ExecutionManager(input, config)) { |
| 25 | + var context = DurableContext.createRootContext(executionManager, config, lambdaContext); |
| 26 | + return context.execute(inputType, handler); |
112 | 27 | } |
113 | | - |
114 | | - // If response size is acceptable, return the result directly |
115 | | - return outputPayload; |
116 | | - } |
117 | | - |
118 | | - private static ErrorObject buildErrorObject(Throwable e, SerDes serDes) { |
119 | | - // exceptions thrown from operations, e.g. Step |
120 | | - if (e instanceof DurableOperationException) { |
121 | | - return ((DurableOperationException) e).getErrorObject(); |
122 | | - } |
123 | | - if (e instanceof UnrecoverableDurableExecutionException) { |
124 | | - return ((UnrecoverableDurableExecutionException) e).getErrorObject(); |
125 | | - } |
126 | | - // exceptions thrown from non-operation code |
127 | | - return ExceptionHelper.buildErrorObject(e, serDes); |
128 | | - } |
129 | | - |
130 | | - private static <I> I extractUserInput(Operation executionOp, SerDes serDes, Class<I> inputType) { |
131 | | - if (executionOp.executionDetails() == null) { |
132 | | - throw new IllegalDurableOperationException("EXECUTION operation missing executionDetails"); |
133 | | - } |
134 | | - |
135 | | - var inputPayload = executionOp.executionDetails().inputPayload(); |
136 | | - return serDes.deserialize(inputPayload, TypeToken.get(inputType)); |
137 | 28 | } |
138 | 29 |
|
139 | 30 | public static <I, O> RequestHandler<DurableExecutionInput, DurableExecutionOutput> wrap( |
|
0 commit comments