88import org .junit .jupiter .api .Test ;
99import software .amazon .lambda .durable .config .StepConfig ;
1010import software .amazon .lambda .durable .config .StepSemantics ;
11+ import software .amazon .lambda .durable .config .StepSemanticsPerRetry ;
1112import software .amazon .lambda .durable .model .ExecutionStatus ;
1213import software .amazon .lambda .durable .retry .RetryStrategies ;
1314import software .amazon .lambda .durable .testing .LocalDurableTestRunner ;
@@ -37,6 +38,29 @@ void testAtLeastOnceCompletesSuccessfully() {
3738 assertEquals (1 , executionCount .get ());
3839 }
3940
41+ @ Test
42+ void testSemanticsPerRetry_atLeastOnceCompletesSuccessfully () {
43+ var executionCount = new AtomicInteger (0 );
44+
45+ var runner = LocalDurableTestRunner .create (
46+ String .class ,
47+ (input , ctx ) -> ctx .step (
48+ "my-step" ,
49+ String .class ,
50+ stepCtx -> {
51+ executionCount .incrementAndGet ();
52+ return "result" ;
53+ },
54+ StepConfig .builder ()
55+ .semanticsPerRetry (StepSemanticsPerRetry .AT_LEAST_ONCE_PER_RETRY )
56+ .build ()));
57+
58+ var result = runner .run ("test-input" );
59+
60+ assertEquals (ExecutionStatus .SUCCEEDED , result .getStatus ());
61+ assertEquals (1 , executionCount .get ());
62+ }
63+
4064 @ Test
4165 void testAtMostOnceCompletesSuccessfully () {
4266 var executionCount = new AtomicInteger (0 );
@@ -60,6 +84,29 @@ void testAtMostOnceCompletesSuccessfully() {
6084 assertEquals (1 , executionCount .get ());
6185 }
6286
87+ @ Test
88+ void testSemanticsPerRetry_atMostOnceCompletesSuccessfully () {
89+ var executionCount = new AtomicInteger (0 );
90+
91+ var runner = LocalDurableTestRunner .create (
92+ String .class ,
93+ (input , ctx ) -> ctx .step (
94+ "my-step" ,
95+ String .class ,
96+ stepCtx -> {
97+ executionCount .incrementAndGet ();
98+ return "result" ;
99+ },
100+ StepConfig .builder ()
101+ .semanticsPerRetry (StepSemanticsPerRetry .AT_MOST_ONCE_PER_RETRY )
102+ .build ()));
103+
104+ var result = runner .run ("test-input" );
105+
106+ assertEquals (ExecutionStatus .SUCCEEDED , result .getStatus ());
107+ assertEquals (1 , executionCount .get ());
108+ }
109+
63110 @ Test
64111 void testAtMostOnceNoRetryFailsImmediately () {
65112 var executionCount = new AtomicInteger (0 );
@@ -84,6 +131,30 @@ void testAtMostOnceNoRetryFailsImmediately() {
84131 assertEquals (1 , executionCount .get ());
85132 }
86133
134+ @ Test
135+ void testSemanticsPerRetry_atMostOnceNoRetryFailsImmediately () {
136+ var executionCount = new AtomicInteger (0 );
137+
138+ var runner = LocalDurableTestRunner .create (
139+ String .class ,
140+ (input , ctx ) -> ctx .step (
141+ "my-step" ,
142+ String .class ,
143+ stepCtx -> {
144+ executionCount .incrementAndGet ();
145+ throw new RuntimeException ("Always fails" );
146+ },
147+ StepConfig .builder ()
148+ .semanticsPerRetry (StepSemanticsPerRetry .AT_MOST_ONCE_PER_RETRY )
149+ .retryStrategy (RetryStrategies .Presets .NO_RETRY )
150+ .build ()));
151+
152+ var result = runner .run ("test-input" );
153+
154+ assertEquals (ExecutionStatus .FAILED , result .getStatus ());
155+ assertEquals (1 , executionCount .get ());
156+ }
157+
87158 @ Test
88159 void testDefaultSemanticsIsAtLeastOnce () {
89160 var executionCount = new AtomicInteger (0 );
@@ -129,6 +200,34 @@ void testAtLeastOnceReExecutesAfterCheckpointLoss() {
129200 assertEquals (2 , executionCount .get ());
130201 }
131202
203+ @ Test
204+ void testSemanticsPerRetry_atLeastOnceReExecutesAfterCheckpointLoss () {
205+ var executionCount = new AtomicInteger (0 );
206+
207+ var runner = LocalDurableTestRunner .create (String .class , (input , context ) -> {
208+ return context .step (
209+ "step" ,
210+ String .class ,
211+ stepCtx -> {
212+ var count = executionCount .incrementAndGet ();
213+ return "Executed " + count + " times" ;
214+ },
215+ StepConfig .builder ()
216+ .semanticsPerRetry (StepSemanticsPerRetry .AT_LEAST_ONCE_PER_RETRY )
217+ .build ());
218+ });
219+
220+ runner .run ("test" );
221+ assertEquals (1 , executionCount .get ());
222+
223+ runner .simulateFireAndForgetCheckpointLoss ("step" );
224+
225+ var result = runner .run ("test" );
226+
227+ assertEquals (ExecutionStatus .SUCCEEDED , result .getStatus ());
228+ assertEquals (2 , executionCount .get ());
229+ }
230+
132231 @ Test
133232 void testAtLeastOnceReExecutesAfterCheckpointFailure () {
134233 var executionCount = new AtomicInteger (0 );
@@ -157,7 +256,37 @@ void testAtLeastOnceReExecutesAfterCheckpointFailure() {
157256 }
158257
159258 @ Test
160- void testAtMostOnceThrowsExceptionAfterCheckpointFailure () {
259+ void testSemanticsPerRetry_atLeastOnceReExecutesAfterCheckpointFailure () {
260+ var executionCount = new AtomicInteger (0 );
261+
262+ var runner = LocalDurableTestRunner .create (String .class , (input , context ) -> {
263+ return context .step (
264+ "step" ,
265+ String .class ,
266+ stepCtx -> {
267+ var count = executionCount .incrementAndGet ();
268+ return "Executed " + count + " times" ;
269+ },
270+ StepConfig .builder ()
271+ .semanticsPerRetry (StepSemanticsPerRetry .AT_LEAST_ONCE_PER_RETRY )
272+ .build ());
273+ });
274+
275+ runner .run ("test" );
276+ assertEquals (1 , executionCount .get ());
277+
278+ runner .resetCheckpointToStarted ("step" );
279+ var result = runner .runUntilComplete ("test" );
280+
281+ assertEquals (ExecutionStatus .SUCCEEDED , result .getStatus ());
282+ assertEquals (2 , executionCount .get ());
283+ }
284+
285+ // This behavior is incorrect (the step should retry after interruption), but is kept for backward
286+ // compatibility. The deprecated StepSemantics.AT_MOST_ONCE_PER_RETRY preserves this behavior.
287+ // Use StepSemanticsPerRetry.AT_MOST_ONCE_PER_RETRY for the corrected behavior (see below test).
288+ @ Test
289+ void testAtMostOnceThrowsExceptionAfterCheckpointFailure_deprecatedBackwardCompatibility () {
161290 var executionCount = new AtomicInteger (0 );
162291
163292 var runner = LocalDurableTestRunner .create (String .class , (input , context ) -> {
@@ -183,4 +312,32 @@ void testAtMostOnceThrowsExceptionAfterCheckpointFailure() {
183312 assertEquals (ExecutionStatus .FAILED , result .getStatus ());
184313 assertEquals (1 , executionCount .get ());
185314 }
315+
316+ @ Test
317+ void testSemanticsPerRetry_atMostOnceRetriesAfterInterruption () {
318+ var executionCount = new AtomicInteger (0 );
319+
320+ var runner = LocalDurableTestRunner .create (String .class , (input , context ) -> {
321+ return context .step (
322+ "step" ,
323+ String .class ,
324+ stepCtx -> {
325+ executionCount .incrementAndGet ();
326+ return "result" ;
327+ },
328+ StepConfig .builder ()
329+ .semanticsPerRetry (StepSemanticsPerRetry .AT_MOST_ONCE_PER_RETRY )
330+ .build ());
331+ });
332+
333+ runner .run ("test" );
334+ assertEquals (1 , executionCount .get ());
335+
336+ runner .resetCheckpointToStarted ("step" );
337+
338+ var result = runner .runUntilComplete ("test" );
339+
340+ assertEquals (ExecutionStatus .SUCCEEDED , result .getStatus ());
341+ assertEquals (2 , executionCount .get ());
342+ }
186343}
0 commit comments