-
Notifications
You must be signed in to change notification settings - Fork 11
feat: withRetry helper #343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
2fe64a7
feat: first attempt at retryableOperation
hsilan 7fd1628
feat: add example tests and integration tests for new RetryableOperat…
hsilan 30e2968
chore: rename to withRetry from RetryableOperation
hsilan e12daee
chore: rename to WithRetry from RetryOperation
hsilan 9ff33b8
chore: add some edge case tests for WithRetryHelperTest
hsilan b102747
chore: rename retryOperation to withRetry
hsilan d901827
chore: added WithRetryAsync functions
hsilan 23ed3df
feat: first attempt at retryableOperation
hsilan ac7697d
feat: add example tests and integration tests for new RetryableOperat…
hsilan ca928ab
Merge branch 'main' into retryable-operation
hsilan 344f828
chore: set default retry strategy when missing config
hsilan eae41bd
feat: refactor WithRetry to be implemented by consumers of DurableCon…
hsilan 4badc89
feat: first attempt at retryableOperation
hsilan 254dab1
feat: add example tests and integration tests for new RetryableOperat…
hsilan 8357d38
Merge branch 'main' into retryable-operation
hsilan fadce47
chore: use virtual context when wrapInChildContext is false
hsilan 9af4a42
chore: refactor withRetry to use withRetryAsync
hsilan 7c1e17d
Merge branch 'main' into retryable-operation
hsilan f54d8a4
chore: remove anonymous form function override
hsilan b9438b7
chore: add cloud based integration tests for withRetry, add missing r…
hsilan fbcd329
chore: make config optional by adding override with config parameter …
hsilan 3b98eec
chore: use BiFunction<Integer,DurableContext, T> instead of WithRetry…
hsilan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
50 changes: 50 additions & 0 deletions
50
...in/java/software/amazon/lambda/durable/examples/callback/RetryWaitForCallbackExample.java
This file contains hidden or 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,50 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| package software.amazon.lambda.durable.examples.callback; | ||
|
|
||
| import java.time.Duration; | ||
| import software.amazon.lambda.durable.DurableContext; | ||
| import software.amazon.lambda.durable.DurableHandler; | ||
| import software.amazon.lambda.durable.config.WithRetryConfig; | ||
| import software.amazon.lambda.durable.examples.types.ApprovalRequest; | ||
| import software.amazon.lambda.durable.retry.RetryDecision; | ||
|
|
||
| /** | ||
| * Example demonstrating {@code context.withRetry} with {@code context.waitForCallback}. | ||
| * | ||
| * <p>Submits an approval request to an external system via a callback. If the callback fails (e.g., the external system | ||
| * rejects the request), the helper retries the entire waitForCallback cycle — creating a fresh callback with a new ID | ||
| * each time. | ||
| * | ||
| * <p>Each attempt uses a unique callback name ({@code "approval-1"}, {@code "approval-2"}, etc.) so the execution | ||
| * history stays clean and replay-safe. A {@code null} name is used, so attempts are grouped under a default-named child | ||
| * context. | ||
| */ | ||
| public class RetryWaitForCallbackExample extends DurableHandler<ApprovalRequest, String> { | ||
|
|
||
| private static final int MAX_ATTEMPTS = 3; | ||
|
|
||
| @Override | ||
| public String handleRequest(ApprovalRequest input, DurableContext context) { | ||
| // Step 1: Prepare the approval request | ||
| var prepared = context.step( | ||
| "prepare", | ||
| String.class, | ||
| stepCtx -> "Approval for: " + input.description() + " ($" + input.amount() + ")"); | ||
|
|
||
| // Step 2: waitForCallback with retry — if the external system fails, try again with a fresh callback | ||
| var approvalResult = context.withRetry( | ||
| null, | ||
| (attempt, ctx) -> ctx.waitForCallback( | ||
| "approval-" + attempt, String.class, (callbackId, stepCtx) -> stepCtx.getLogger() | ||
| .info("Attempt {}: sending callback {} to approval system", attempt, callbackId)), | ||
| WithRetryConfig.builder() | ||
| .retryStrategy((error, attempt) -> attempt < MAX_ATTEMPTS | ||
| ? RetryDecision.retry(Duration.ofSeconds(2)) | ||
| : RetryDecision.fail()) | ||
| .build()); | ||
|
|
||
| // Step 3: Process the result | ||
| return context.step("process-result", String.class, stepCtx -> prepared + " - Result: " + approvalResult); | ||
| } | ||
| } | ||
40 changes: 40 additions & 0 deletions
40
...ples/src/main/java/software/amazon/lambda/durable/examples/invoke/RetryInvokeExample.java
This file contains hidden or 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,40 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| package software.amazon.lambda.durable.examples.invoke; | ||
|
|
||
| import java.time.Duration; | ||
| import software.amazon.lambda.durable.DurableContext; | ||
| import software.amazon.lambda.durable.DurableHandler; | ||
| import software.amazon.lambda.durable.config.WithRetryConfig; | ||
| import software.amazon.lambda.durable.examples.types.GreetingRequest; | ||
| import software.amazon.lambda.durable.retry.RetryDecision; | ||
|
|
||
| /** | ||
| * Example demonstrating {@code context.withRetry} with {@code context.invoke}. | ||
| * | ||
| * <p>Retries a chained Lambda invocation up to 3 times with a fixed 2-second backoff between attempts. Each attempt | ||
| * uses a unique operation name ({@code "call-greeting-1"}, {@code "call-greeting-2"}, etc.) so the execution history | ||
| * stays clean and replay-safe. | ||
| * | ||
| * <p>A {@code null} name is used, so attempts are grouped under a default-named child context. | ||
| */ | ||
| public class RetryInvokeExample extends DurableHandler<GreetingRequest, String> { | ||
|
|
||
| private static final int MAX_ATTEMPTS = 3; | ||
|
|
||
| @Override | ||
| public String handleRequest(GreetingRequest input, DurableContext context) { | ||
| return context.withRetry( | ||
| null, | ||
| (attempt, ctx) -> ctx.invoke( | ||
| "call-greeting-" + attempt, | ||
| "simple-step-example" + input.getName() + ":$LATEST", | ||
| input, | ||
| String.class), | ||
| WithRetryConfig.builder() | ||
| .retryStrategy((error, attempt) -> attempt < MAX_ATTEMPTS | ||
| ? RetryDecision.retry(Duration.ofSeconds(2)) | ||
| : RetryDecision.fail()) | ||
| .build()); | ||
| } | ||
| } |
This file contains hidden or 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
...ava/software/amazon/lambda/durable/examples/callback/RetryWaitForCallbackExampleTest.java
This file contains hidden or 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 @@ | ||
| // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| package software.amazon.lambda.durable.examples.callback; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| import org.junit.jupiter.api.Test; | ||
| import software.amazon.awssdk.services.lambda.model.ErrorObject; | ||
| import software.amazon.lambda.durable.examples.types.ApprovalRequest; | ||
| import software.amazon.lambda.durable.model.ExecutionStatus; | ||
| import software.amazon.lambda.durable.testing.LocalDurableTestRunner; | ||
|
|
||
| class RetryWaitForCallbackExampleTest { | ||
|
|
||
| @Test | ||
| void succeedsOnFirstAttempt() { | ||
| var handler = new RetryWaitForCallbackExample(); | ||
| var runner = LocalDurableTestRunner.create(ApprovalRequest.class, handler); | ||
| var input = new ApprovalRequest("New laptop", 1500.00); | ||
|
|
||
| // First run — prepares request, starts waitForCallback, suspends | ||
| var result = runner.run(input); | ||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
|
|
||
| // Complete the callback (waitForCallback names it "approval-1-callback" internally) | ||
| var callbackId = runner.getCallbackId("approval-1-callback"); | ||
| assertNotNull(callbackId, "Callback 'approval-1-callback' should have been created"); | ||
| runner.completeCallback(callbackId, "\"Approved by manager\""); | ||
|
|
||
| // Run to completion | ||
| result = runner.runUntilComplete(input); | ||
|
|
||
| assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus()); | ||
| assertEquals( | ||
| "Approval for: New laptop ($1500.0) - Result: Approved by manager", result.getResult(String.class)); | ||
| } | ||
|
|
||
| @Test | ||
| void retriesAfterFirstCallbackFails() { | ||
| var handler = new RetryWaitForCallbackExample(); | ||
| var runner = LocalDurableTestRunner.create(ApprovalRequest.class, handler); | ||
| var input = new ApprovalRequest("Server upgrade", 5000.00); | ||
|
|
||
| // First run — prepares, starts waitForCallback attempt 1, suspends | ||
| var result = runner.run(input); | ||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
|
|
||
| // Fail the first callback | ||
| var callbackId1 = runner.getCallbackId("approval-1-callback"); | ||
| assertNotNull(callbackId1); | ||
| runner.failCallback( | ||
| callbackId1, | ||
| ErrorObject.builder() | ||
| .errorType("RejectedError") | ||
| .errorMessage("Rejected by first reviewer") | ||
| .build()); | ||
|
|
||
| // Run — processes failure, hits backoff wait, suspends | ||
| result = runner.run(input); | ||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
|
|
||
| // Advance past the backoff wait | ||
| runner.advanceTime(); | ||
|
|
||
| // Run — starts waitForCallback attempt 2, suspends | ||
| result = runner.run(input); | ||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
|
|
||
| // Complete the second callback | ||
| var callbackId2 = runner.getCallbackId("approval-2-callback"); | ||
| assertNotNull(callbackId2, "Callback 'approval-2-callback' should have been created after retry"); | ||
| runner.completeCallback(callbackId2, "\"Approved on second try\""); | ||
|
|
||
| // Run to completion | ||
| result = runner.runUntilComplete(input); | ||
|
|
||
| assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus()); | ||
| assertEquals( | ||
| "Approval for: Server upgrade ($5000.0) - Result: Approved on second try", | ||
| result.getResult(String.class)); | ||
| } | ||
|
|
||
| @Test | ||
| void failsAfterAllRetriesExhausted() { | ||
| var handler = new RetryWaitForCallbackExample(); | ||
| var runner = LocalDurableTestRunner.create(ApprovalRequest.class, handler); | ||
| var input = new ApprovalRequest("Expensive item", 10000.00); | ||
|
|
||
| // First run — starts waitForCallback attempt 1 | ||
| var result = runner.run(input); | ||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
|
|
||
| // Fail callback attempt 1 | ||
| var callbackId1 = runner.getCallbackId("approval-1-callback"); | ||
| runner.failCallback( | ||
| callbackId1, | ||
| ErrorObject.builder() | ||
| .errorType("Rejected") | ||
| .errorMessage("fail 1") | ||
| .build()); | ||
| result = runner.run(input); | ||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
|
|
||
| // Advance past backoff 1, run to start attempt 2 | ||
| runner.advanceTime(); | ||
| result = runner.run(input); | ||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
|
|
||
| // Fail callback attempt 2 | ||
| var callbackId2 = runner.getCallbackId("approval-2-callback"); | ||
| runner.failCallback( | ||
| callbackId2, | ||
| ErrorObject.builder() | ||
| .errorType("Rejected") | ||
| .errorMessage("fail 2") | ||
| .build()); | ||
| result = runner.run(input); | ||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
|
|
||
| // Advance past backoff 2, run to start attempt 3 | ||
| runner.advanceTime(); | ||
| result = runner.run(input); | ||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
|
|
||
| // Fail callback attempt 3 — last attempt, retryStrategy returns fail() | ||
| var callbackId3 = runner.getCallbackId("approval-3-callback"); | ||
| runner.failCallback( | ||
| callbackId3, | ||
| ErrorObject.builder() | ||
| .errorType("Rejected") | ||
| .errorMessage("fail 3") | ||
| .build()); | ||
| result = runner.run(input); | ||
|
|
||
| assertEquals(ExecutionStatus.FAILED, result.getStatus()); | ||
| } | ||
|
|
||
| @Test | ||
| void suspendsOnFirstRun() { | ||
| var handler = new RetryWaitForCallbackExample(); | ||
| var runner = LocalDurableTestRunner.create(ApprovalRequest.class, handler); | ||
| var input = new ApprovalRequest("Test item", 100.00); | ||
|
|
||
| var result = runner.run(input); | ||
|
|
||
| assertEquals(ExecutionStatus.PENDING, result.getStatus()); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.