diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java index 4bb1b0a241..9381152f18 100644 --- a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java @@ -15,6 +15,7 @@ import io.dapr.spring.workflows.config.EnableDaprWorkflows; import io.dapr.springboot.examples.wfp.chain.ChainWorkflow; +import io.dapr.springboot.examples.wfp.compensation.BookTripWorkflow; import io.dapr.springboot.examples.wfp.child.ParentWorkflow; import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog; import io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow; @@ -191,6 +192,19 @@ public Decision suspendResumeContinue(@RequestParam("orderId") String orderId, @ return workflowInstanceStatus.readOutputAs(Decision.class); } + /** + * Run Compensation Demo Workflow (Book Trip with Saga pattern). + * @return the output of the BookTripWorkflow execution + */ + @PostMapping("wfp/compensation") + public String compensation() throws TimeoutException { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(BookTripWorkflow.class); + logger.info("Workflow instance " + instanceId + " started"); + return daprWorkflowClient + .waitForWorkflowCompletion(instanceId, Duration.ofSeconds(30), true) + .readOutputAs(String.class); + } + @PostMapping("wfp/durationtimer") public String durationTimerWorkflow() { return daprWorkflowClient.scheduleNewWorkflow(DurationTimerWorkflow.class); diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookCarActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookCarActivity.java new file mode 100644 index 0000000000..0026e674df --- /dev/null +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookCarActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +import org.springframework.stereotype.Component; + +@Component +public class BookCarActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(BookCarActivity.class); + + @Override + public Object run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + logger.info("Forcing Failure to trigger compensation for activity: " + ctx.getName()); + throw new RuntimeException("Failed to book car"); + } +} diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookFlightActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookFlightActivity.java new file mode 100644 index 0000000000..450942f6e0 --- /dev/null +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookFlightActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class BookFlightActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(BookFlightActivity.class); + + @Override + public Object run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + String result = "Flight booked successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +} diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookHotelActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookHotelActivity.java new file mode 100644 index 0000000000..b4434ad17f --- /dev/null +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookHotelActivity.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class BookHotelActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(BookHotelActivity.class); + + @Override + public Object run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Activity '{}' was interrupted.", ctx.getName(), e); + throw new RuntimeException("Activity was interrupted", e); + } + + String result = "Hotel booked successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +} diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookTripWorkflow.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookTripWorkflow.java new file mode 100644 index 0000000000..9f2253053f --- /dev/null +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/BookTripWorkflow.java @@ -0,0 +1,97 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.compensation; + +import io.dapr.durabletask.TaskFailedException; +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryPolicy; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Component +public class BookTripWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + List compensations = new ArrayList<>(); + + WorkflowTaskRetryPolicy compensationRetryPolicy = WorkflowTaskRetryPolicy.newBuilder() + .setFirstRetryInterval(Duration.ofSeconds(1)) + .setMaxNumberOfAttempts(3) + .build(); + + WorkflowTaskOptions compensationOptions = new WorkflowTaskOptions(compensationRetryPolicy); + + try { + String flightResult = ctx.callActivity( + BookFlightActivity.class.getName(), null, String.class).await(); + ctx.getLogger().info("Flight booking completed: {}", flightResult); + compensations.add("CancelFlight"); + + String hotelResult = ctx.callActivity( + BookHotelActivity.class.getName(), null, String.class).await(); + ctx.getLogger().info("Hotel booking completed: {}", hotelResult); + compensations.add("CancelHotel"); + + String carResult = ctx.callActivity( + BookCarActivity.class.getName(), null, String.class).await(); + ctx.getLogger().info("Car booking completed: {}", carResult); + compensations.add("CancelCar"); + + String result = String.format("%s, %s, %s", flightResult, hotelResult, carResult); + ctx.getLogger().info("Trip booked successfully: {}", result); + ctx.complete(result); + + } catch (TaskFailedException e) { + ctx.getLogger().info("******** executing compensation logic ********"); + ctx.getLogger().error("Activity failed", e); + + Collections.reverse(compensations); + for (String compensation : compensations) { + try { + switch (compensation) { + case "CancelCar": + String carCancelResult = ctx.callActivity( + CancelCarActivity.class.getName(), null, compensationOptions, String.class).await(); + ctx.getLogger().info("Car cancellation completed: {}", carCancelResult); + break; + case "CancelHotel": + String hotelCancelResult = ctx.callActivity( + CancelHotelActivity.class.getName(), null, compensationOptions, String.class).await(); + ctx.getLogger().info("Hotel cancellation completed: {}", hotelCancelResult); + break; + case "CancelFlight": + String flightCancelResult = ctx.callActivity( + CancelFlightActivity.class.getName(), null, compensationOptions, String.class).await(); + ctx.getLogger().info("Flight cancellation completed: {}", flightCancelResult); + break; + default: + break; + } + } catch (TaskFailedException ex) { + ctx.getLogger().error("Activity failed during compensation", ex); + } + } + ctx.complete("Workflow failed, compensation applied"); + } + }; + } +} diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/CancelCarActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/CancelCarActivity.java new file mode 100644 index 0000000000..9c6143e2f2 --- /dev/null +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/CancelCarActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class CancelCarActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(CancelCarActivity.class); + + @Override + public Object run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + String result = "Car canceled successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +} diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/CancelFlightActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/CancelFlightActivity.java new file mode 100644 index 0000000000..f24970613b --- /dev/null +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/CancelFlightActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class CancelFlightActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(CancelFlightActivity.class); + + @Override + public Object run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + String result = "Flight canceled successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +} diff --git a/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/CancelHotelActivity.java b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/CancelHotelActivity.java new file mode 100644 index 0000000000..1d4b741dcb --- /dev/null +++ b/spring-boot-examples/workflows/patterns/src/main/java/io/dapr/springboot/examples/wfp/compensation/CancelHotelActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.springboot.examples.wfp.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class CancelHotelActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(CancelHotelActivity.class); + + @Override + public Object run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + String result = "Hotel canceled successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +}