55import io .temporal .common .WorkerDeploymentVersion ;
66import java .io .IOException ;
77import java .time .Duration ;
8+ import java .util .List ;
89import java .util .Objects ;
910import java .util .function .Consumer ;
1011import org .slf4j .Logger ;
@@ -19,6 +20,19 @@ public final class LambdaWorker {
1920
2021 private LambdaWorker () {}
2122
23+ /**
24+ * Configures options for one Lambda invocation before Temporal service stubs, client, and worker
25+ * are created.
26+ *
27+ * <p>The supplied builder is a fresh copy of the base options for the current invocation.
28+ * Shutdown hooks added by this callback run for only that invocation, including when this
29+ * callback throws.
30+ */
31+ @ FunctionalInterface
32+ public interface InvocationConfigurator {
33+ void configure (LambdaWorkerOptions .Builder builder , Context context );
34+ }
35+
2236 /**
2337 * Returns an AWS Lambda Java handler that creates, starts, and shuts down one Temporal worker per
2438 * invocation.
@@ -40,13 +54,57 @@ public static RequestHandler<Object, Void> run(
4054 }
4155 }
4256
57+ /**
58+ * Returns an AWS Lambda Java handler with both cold-start and per-invocation configuration.
59+ *
60+ * @param version worker deployment version to advertise for this worker.
61+ * @param configure callback invoked once while the Lambda handler is constructed.
62+ * @param invocationConfigure callback invoked for each Lambda invocation before Temporal service
63+ * stubs, client, and worker are created. Required fields may be supplied by this callback.
64+ */
65+ public static RequestHandler <Object , Void > run (
66+ WorkerDeploymentVersion version ,
67+ Consumer <LambdaWorkerOptions .Builder > configure ,
68+ InvocationConfigurator invocationConfigure ) {
69+ LambdaWorkerOptions .validateVersion (version );
70+ Objects .requireNonNull (configure , "configure" );
71+ Objects .requireNonNull (invocationConfigure , "invocationConfigure" );
72+ try {
73+ LambdaWorkerOptions .Builder builder =
74+ LambdaWorkerOptions .newBuilderFromEnvironment (System .getenv ());
75+ configure .accept (builder );
76+ return newHandler (version , builder .build (), invocationConfigure );
77+ } catch (IOException e ) {
78+ throw new RuntimeException ("Unable to load Temporal client configuration" , e );
79+ }
80+ }
81+
4382 /** Returns an AWS Lambda Java handler using already-configured Lambda worker options. */
4483 public static RequestHandler <Object , Void > newHandler (
4584 WorkerDeploymentVersion version , LambdaWorkerOptions options ) {
4685 return newHandler (
4786 version , options , new DefaultLambdaWorkerRuntime (), sleep (), systemNanoClock ());
4887 }
4988
89+ /**
90+ * Returns an AWS Lambda Java handler using base options and per-invocation configuration.
91+ *
92+ * <p>The supplied options are copied for each invocation before {@code invocationConfigure} runs.
93+ * Required fields may be supplied by {@code invocationConfigure}.
94+ */
95+ public static RequestHandler <Object , Void > newHandler (
96+ WorkerDeploymentVersion version ,
97+ LambdaWorkerOptions options ,
98+ InvocationConfigurator invocationConfigure ) {
99+ return newHandler (
100+ version ,
101+ options ,
102+ invocationConfigure ,
103+ new DefaultLambdaWorkerRuntime (),
104+ sleep (),
105+ systemNanoClock ());
106+ }
107+
50108 static RequestHandler <Object , Void > newHandler (
51109 WorkerDeploymentVersion version ,
52110 LambdaWorkerOptions options ,
@@ -56,6 +114,16 @@ static RequestHandler<Object, Void> newHandler(
56114 return newHandler (version , options , runtime , sleeper , systemNanoClock ());
57115 }
58116
117+ static RequestHandler <Object , Void > newHandler (
118+ WorkerDeploymentVersion version ,
119+ LambdaWorkerOptions options ,
120+ InvocationConfigurator invocationConfigure ,
121+ LambdaWorkerRuntime runtime ,
122+ Sleeper sleeper ) {
123+ // This overload exists to let tests inject a fake runtime and sleeper.
124+ return newHandler (version , options , invocationConfigure , runtime , sleeper , systemNanoClock ());
125+ }
126+
59127 static RequestHandler <Object , Void > newHandler (
60128 WorkerDeploymentVersion version ,
61129 LambdaWorkerOptions options ,
@@ -67,7 +135,34 @@ static RequestHandler<Object, Void> newHandler(
67135 Objects .requireNonNull (options , "options" ).prepare (version ),
68136 Objects .requireNonNull (runtime , "runtime" ),
69137 Objects .requireNonNull (sleeper , "sleeper" ),
70- Objects .requireNonNull (clock , "clock" ));
138+ Objects .requireNonNull (clock , "clock" ),
139+ false );
140+ }
141+
142+ static RequestHandler <Object , Void > newHandler (
143+ WorkerDeploymentVersion version ,
144+ LambdaWorkerOptions options ,
145+ InvocationConfigurator invocationConfigure ,
146+ LambdaWorkerRuntime runtime ,
147+ Sleeper sleeper ,
148+ NanoClock clock ) {
149+ LambdaWorkerOptions .validateVersion (version );
150+ Objects .requireNonNull (options , "options" );
151+ Objects .requireNonNull (invocationConfigure , "invocationConfigure" );
152+ return new Handler (
153+ context -> {
154+ LambdaWorkerOptions .Builder builder = options .toBuilder ();
155+ try {
156+ invocationConfigure .configure (builder , context );
157+ return builder .build ().prepare (version ).materialize (identityFor (context ));
158+ } catch (RuntimeException e ) {
159+ throw new InvocationConfigurationException (e , builder );
160+ }
161+ },
162+ Objects .requireNonNull (runtime , "runtime" ),
163+ Objects .requireNonNull (sleeper , "sleeper" ),
164+ Objects .requireNonNull (clock , "clock" ),
165+ true );
71166 }
72167
73168 private static Sleeper sleep () {
@@ -86,32 +181,73 @@ private static NanoClock systemNanoClock() {
86181 return System ::nanoTime ;
87182 }
88183
184+ private static String identityFor (Context context ) {
185+ return emptyToUnknown (context .getAwsRequestId ())
186+ + "@"
187+ + emptyToUnknown (context .getInvokedFunctionArn ());
188+ }
189+
190+ private static String emptyToUnknown (String value ) {
191+ return value == null || value .isEmpty () ? "unknown" : value ;
192+ }
193+
194+ private interface OptionsMaterializer {
195+ LambdaWorkerOptions .Materialized materialize (Context context );
196+ }
197+
89198 private static final class Handler implements RequestHandler <Object , Void > {
90- private final LambdaWorkerOptions . Prepared preparedOptions ;
199+ private final OptionsMaterializer optionsMaterializer ;
91200 private final LambdaWorkerRuntime runtime ;
92201 private final Sleeper sleeper ;
93202 private final NanoClock clock ;
203+ private final boolean runShutdownHooksBeforeRuntimeCreation ;
94204
95205 private Handler (
96206 LambdaWorkerOptions .Prepared preparedOptions ,
97207 LambdaWorkerRuntime runtime ,
98208 Sleeper sleeper ,
209+ NanoClock clock ,
210+ boolean runShutdownHooksBeforeRuntimeCreation ) {
211+ this (
212+ context -> preparedOptions .materialize (identityFor (context )),
213+ runtime ,
214+ sleeper ,
215+ clock ,
216+ runShutdownHooksBeforeRuntimeCreation );
217+ Objects .requireNonNull (preparedOptions , "preparedOptions" );
218+ }
219+
220+ private Handler (
221+ OptionsMaterializer optionsMaterializer ,
222+ LambdaWorkerRuntime runtime ,
223+ Sleeper sleeper ,
99224 NanoClock clock ) {
100- this .preparedOptions = Objects .requireNonNull (preparedOptions , "preparedOptions" );
225+ this (optionsMaterializer , runtime , sleeper , clock , false );
226+ }
227+
228+ private Handler (
229+ OptionsMaterializer optionsMaterializer ,
230+ LambdaWorkerRuntime runtime ,
231+ Sleeper sleeper ,
232+ NanoClock clock ,
233+ boolean runShutdownHooksBeforeRuntimeCreation ) {
234+ this .optionsMaterializer = Objects .requireNonNull (optionsMaterializer , "optionsMaterializer" );
101235 this .runtime = runtime ;
102236 this .sleeper = sleeper ;
103237 this .clock = clock ;
238+ this .runShutdownHooksBeforeRuntimeCreation = runShutdownHooksBeforeRuntimeCreation ;
104239 }
105240
106241 @ Override
107242 public Void handleRequest (Object input , Context context ) {
108243 Objects .requireNonNull (context , "context" );
109244
110- LambdaWorkerOptions .Materialized options = preparedOptions .materialize (identityFor (context ));
111- validateRemainingTime (context , options .shutdownDeadlineBuffer );
112-
245+ LambdaWorkerOptions .Materialized options = null ;
113246 LambdaWorkerRuntime .Invocation invocation = null ;
114247 try {
248+ options = optionsMaterializer .materialize (context );
249+ validateRemainingTime (context , options .shutdownDeadlineBuffer );
250+
115251 invocation =
116252 runtime .create (
117253 options .serviceStubsOptions ,
@@ -134,8 +270,26 @@ public Void handleRequest(Object input, Context context) {
134270
135271 sleepUntilShutdownWindow (context , options );
136272 return null ;
273+ } catch (InvocationConfigurationException e ) {
274+ runShutdownHooks (
275+ context ,
276+ e .taskQueue ,
277+ e .shutdownHooks ,
278+ cleanupDeadlineNanos (context , e .gracefulShutdownTimeout , e .shutdownDeadlineBuffer ));
279+ throw e .failure ;
280+ } catch (RuntimeException e ) {
281+ if (invocation == null && options != null && runShutdownHooksBeforeRuntimeCreation ) {
282+ runShutdownHooks (
283+ context ,
284+ options .taskQueue ,
285+ options .shutdownHooks ,
286+ cleanupDeadlineNanos (context , options ));
287+ }
288+ throw e ;
137289 } finally {
138- shutdownInvocation (context , invocation , options );
290+ if (invocation != null ) {
291+ shutdownInvocation (context , invocation , options );
292+ }
139293 }
140294 }
141295
@@ -191,17 +345,15 @@ private void shutdownInvocation(
191345 }
192346 runShutdownHooks (context , options , cleanupDeadlineNanos .longValue ());
193347
194- if (invocation != null ) {
195- try {
196- invocation .closeStubs (remainingCleanupTime (cleanupDeadlineNanos .longValue ()));
197- } catch (RuntimeException e ) {
198- log .error (
199- "Temporal Lambda worker service stubs close failed awsRequestId={} invokedFunctionArn={} taskQueue={}" ,
200- context .getAwsRequestId (),
201- context .getInvokedFunctionArn (),
202- options .taskQueue ,
203- e );
204- }
348+ try {
349+ invocation .closeStubs (remainingCleanupTime (cleanupDeadlineNanos .longValue ()));
350+ } catch (RuntimeException e ) {
351+ log .error (
352+ "Temporal Lambda worker service stubs close failed awsRequestId={} invokedFunctionArn={} taskQueue={}" ,
353+ context .getAwsRequestId (),
354+ context .getInvokedFunctionArn (),
355+ options .taskQueue ,
356+ e );
205357 }
206358 }
207359
@@ -267,7 +419,15 @@ private boolean isTerminated(
267419
268420 private void runShutdownHooks (
269421 Context context , LambdaWorkerOptions .Materialized options , long cleanupDeadlineNanos ) {
270- for (Runnable hook : options .shutdownHooks ) {
422+ runShutdownHooks (context , options .taskQueue , options .shutdownHooks , cleanupDeadlineNanos );
423+ }
424+
425+ private void runShutdownHooks (
426+ Context context ,
427+ String taskQueue ,
428+ List <Runnable > shutdownHooks ,
429+ long cleanupDeadlineNanos ) {
430+ for (Runnable hook : shutdownHooks ) {
271431 try {
272432 if (hook instanceof TimedShutdownHook ) {
273433 ((TimedShutdownHook ) hook ).run (remainingCleanupTime (cleanupDeadlineNanos ));
@@ -279,7 +439,7 @@ private void runShutdownHooks(
279439 "Temporal Lambda worker shutdown hook failed awsRequestId={} invokedFunctionArn={} taskQueue={}" ,
280440 context .getAwsRequestId (),
281441 context .getInvokedFunctionArn (),
282- options . taskQueue ,
442+ taskQueue ,
283443 e );
284444 }
285445 }
@@ -311,12 +471,25 @@ private void validateRemainingTime(Context context, Duration shutdownDeadlineBuf
311471 }
312472
313473 private long cleanupDeadlineNanos (Context context , LambdaWorkerOptions .Materialized options ) {
314- return clock .nanoTime () + cleanupWindow (context , options ).toNanos ();
474+ return cleanupDeadlineNanos (
475+ context , options .gracefulShutdownTimeout , options .shutdownDeadlineBuffer );
476+ }
477+
478+ private long cleanupDeadlineNanos (
479+ Context context , Duration gracefulShutdownTimeout , Duration shutdownDeadlineBuffer ) {
480+ return clock .nanoTime ()
481+ + cleanupWindow (context , gracefulShutdownTimeout , shutdownDeadlineBuffer ).toNanos ();
315482 }
316483
317484 private Duration cleanupWindow (Context context , LambdaWorkerOptions .Materialized options ) {
485+ return cleanupWindow (
486+ context , options .gracefulShutdownTimeout , options .shutdownDeadlineBuffer );
487+ }
488+
489+ private Duration cleanupWindow (
490+ Context context , Duration gracefulShutdownTimeout , Duration shutdownDeadlineBuffer ) {
318491 Duration configuredWindow =
319- nonNegative (options . shutdownDeadlineBuffer .minus (options . gracefulShutdownTimeout ));
492+ nonNegative (shutdownDeadlineBuffer .minus (gracefulShutdownTimeout ));
320493 Duration remaining = remainingInvocationTime (context );
321494 return configuredWindow .compareTo (remaining ) <= 0 ? configuredWindow : remaining ;
322495 }
@@ -332,15 +505,23 @@ private Duration remainingInvocationTime(Context context) {
332505 private static Duration nonNegative (Duration duration ) {
333506 return duration .isNegative () ? Duration .ZERO : duration ;
334507 }
508+ }
335509
336- private static String identityFor (Context context ) {
337- return emptyToUnknown (context .getAwsRequestId ())
338- + "@"
339- + emptyToUnknown (context .getInvokedFunctionArn ());
340- }
341-
342- private static String emptyToUnknown (String value ) {
343- return value == null || value .isEmpty () ? "unknown" : value ;
510+ private static final class InvocationConfigurationException extends RuntimeException {
511+ private final RuntimeException failure ;
512+ private final String taskQueue ;
513+ private final Duration gracefulShutdownTimeout ;
514+ private final Duration shutdownDeadlineBuffer ;
515+ private final List <Runnable > shutdownHooks ;
516+
517+ private InvocationConfigurationException (
518+ RuntimeException failure , LambdaWorkerOptions .Builder builder ) {
519+ super (failure );
520+ this .failure = failure ;
521+ this .taskQueue = builder .getTaskQueue ();
522+ this .gracefulShutdownTimeout = builder .getGracefulShutdownTimeout ();
523+ this .shutdownDeadlineBuffer = builder .getShutdownDeadlineBuffer ();
524+ this .shutdownHooks = builder .getShutdownHooks ();
344525 }
345526 }
346527}
0 commit comments