From 90e6ba0f0cc1fc27d1b19d72b29b22ea9151b889 Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Tue, 24 Oct 2023 01:44:52 +0000 Subject: [PATCH 01/10] first versoin of saga pattern support based on workflow Signed-off-by: Sky Ao --- .../saga/CompensatableWorkflowActivity.java | 26 ++ .../java/io/dapr/workflows/saga/Saga.java | 173 +++++++++++ .../saga/SagaCompensationException.java | 28 ++ .../workflows/saga/SagaConfiguration.java | 102 +++++++ .../workflows/saga/SagaIntegrationTest.java | 286 ++++++++++++++++++ .../java/io/dapr/workflows/saga/SagaTest.java | 235 ++++++++++++++ 6 files changed, 850 insertions(+) create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaCompensationException.java create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaConfiguration.java create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java new file mode 100644 index 000000000..cca69b9d4 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 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.workflows.saga; + +/** + * Interface for a compensatable workflow activity. + */ +public interface CompensatableWorkflowActivity { + /** + * Compensate the activity. + * @param activityInput input of the activity to be compensated + * @param activityOutput output of the activity to be compensated + */ + void compensate(Object activityInput, Object activityOutput); +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java new file mode 100644 index 000000000..e3340c855 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java @@ -0,0 +1,173 @@ +/* + * Copyright 2023 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.workflows.saga; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public final class Saga { + private final SagaConfiguration config; + private final List compensationActivities = new ArrayList<>(); + + /** + * Build up a Saga with its config. + * + * @param config Saga configuration. + */ + public Saga(SagaConfiguration config) { + if (config == null) { + throw new IllegalArgumentException("config is required and should not be null."); + } + this.config = config; + } + + /** + * Register a compensation activity. + * + * @param activityClassName name of the activity class + * @param activityInput input of the activity to be compensated + * @param activityOutput output of the activity to be compensated + */ + public void registerCompensation(String activityClassName, + Object activityInput, Object activityOutput) { + if (activityClassName == null || activityClassName.isEmpty()) { + throw new IllegalArgumentException("activityClassName is required and should not be null or empty."); + } + this.compensationActivities.add( + new CompensatationContext(activityClassName, activityInput, activityOutput)); + } + + /** + * Compensate all registered activities. + */ + public void compensate() { + // Check if parallel compensation is enabled + // Specical case: when parallel compensation is enabled and there is only one + // compensation, we still + // compensate sequentially. + if (config.isParallelCompensation() && compensationActivities.size() > 1) { + compensateInParallel(); + } else { + compensateSequentially(); + } + } + + private void compensateInParallel() { + // thread number should be limited by maxParallelThread + int threadNumber = compensationActivities.size(); + if (threadNumber > config.getMaxParallelThread()) { + threadNumber = config.getMaxParallelThread(); + } + + ExecutorService executor = Executors.newFixedThreadPool(threadNumber); + List> compensationTasks = new ArrayList<>(); + for (CompensatationContext compensationActivity : compensationActivities) { + Callable compensationTask = new Callable() { + @Override + public String call() { + return executeCompensateActivity(compensationActivity); + } + }; + compensationTasks.add(compensationTask); + } + + List> resultFutures; + try { + // TBD: hard code timeout to 60 seconds in the first version + resultFutures = executor.invokeAll(compensationTasks, 60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new SagaCompensationException("Failed to compensate in parallel.", e); + } + SagaCompensationException sagaException = null; + for (Future resultFuture : resultFutures) { + try { + resultFuture.get(); + } catch (ExecutionException e) { + if (sagaException == null) { + sagaException = (SagaCompensationException) e.getCause(); + } else { + sagaException.addSuppressed(e.getCause()); + } + } catch (Exception e) { + if (sagaException == null) { + sagaException = new SagaCompensationException("Failed to compensate in parallel.", e); + } else { + sagaException.addSuppressed(e); + } + } + } + + if (sagaException != null) { + throw sagaException; + } + } + + private void compensateSequentially() { + for (int i = compensationActivities.size() - 1; i >= 0; i--) { + try { + executeCompensateActivity(compensationActivities.get(i)); + } catch (SagaCompensationException e) { + if (!config.isContinueWithError()) { + throw e; + } + } + } + } + + private String executeCompensateActivity(CompensatationContext context) throws SagaCompensationException { + String activityClassName = context.getActivityClassName(); + try { + Class activityClass = Class.forName(activityClassName); + CompensatableWorkflowActivity compensatableActivity = (CompensatableWorkflowActivity) activityClass + .getDeclaredConstructor() + .newInstance(); + compensatableActivity.compensate(context.getActivityInput(), context.getActivityOutput()); + // return activityClassName for logs and tracing + return activityClassName; + } catch (Exception e) { + throw new SagaCompensationException("Exception in saga compensatation: activity=" + activityClassName, e); + } + } + + private static class CompensatationContext { + private final String activityClassName; + private final Object activityInput; + private final Object activityOutput; + + public CompensatationContext(String activityClassName, Object activityInput, + Object activityOutput) { + this.activityClassName = activityClassName; + this.activityInput = activityInput; + this.activityOutput = activityOutput; + } + + public String getActivityClassName() { + return activityClassName; + } + + public Object getActivityInput() { + return activityInput; + } + + public Object getActivityOutput() { + return activityOutput; + } + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaCompensationException.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaCompensationException.java new file mode 100644 index 000000000..07396d9b5 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaCompensationException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 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.workflows.saga; + +/** + * saga compensation exception. + */ +public class SagaCompensationException extends RuntimeException { + /** + * build up a SagaCompensationException. + * @param message exception message + * @param cause exception cause + */ + public SagaCompensationException(String message, Exception cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaConfiguration.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaConfiguration.java new file mode 100644 index 000000000..e9aa396c9 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaConfiguration.java @@ -0,0 +1,102 @@ +/* + * Copyright 2023 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.workflows.saga; + +/** + * Saga configuration. + */ +public final class SagaConfiguration { + private final boolean parallelCompensation; + private final int maxParallelThread; + private final boolean continueWithError; + + private SagaConfiguration(boolean parallelCompensation, int maxParallelThread, boolean continueWithError) { + this.parallelCompensation = parallelCompensation; + this.maxParallelThread = maxParallelThread; + this.continueWithError = continueWithError; + } + + public boolean isParallelCompensation() { + return parallelCompensation; + } + + public boolean isContinueWithError() { + return continueWithError; + } + + public int getMaxParallelThread() { + return maxParallelThread; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static final class Builder { + // by default compensation is sequential + private boolean parallelCompensation = false; + + // by default max parallel thread is 16, it's enough for most cases + private int maxParallelThread = 16; + + // by default set continueWithError to be true + // So if a compensation fails, we should continue with the next compensations + private boolean continueWithError = true; + + /** + * Set parallel compensation. + * @param parallelCompensation parallel compensation or not + * @return this builder itself + */ + public Builder setParallelCompensation(boolean parallelCompensation) { + this.parallelCompensation = parallelCompensation; + return this; + } + + /** + * set max parallel thread. + * + *

Only valid when parallelCompensation is true. + * @param maxParallelThread max parallel thread + * @return this builder itself + */ + public Builder setMaxParallelThread(int maxParallelThread) { + if (maxParallelThread <= 2) { + throw new IllegalArgumentException("maxParallelThread should be greater than 1."); + } + this.maxParallelThread = maxParallelThread; + return this; + } + + /** + * Set continue with error. + * + *

Only valid when parallelCompensation is false. + * @param continueWithError continue with error or not + * @return this builder itself + */ + public Builder setContinueWithError(boolean continueWithError) { + this.continueWithError = continueWithError; + return this; + } + + /** + * Build Saga configuration. + * @return Saga configuration + */ + public SagaConfiguration build() { + return new SagaConfiguration(this.parallelCompensation, this.maxParallelThread, this.continueWithError); + } + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java new file mode 100644 index 000000000..38763fa17 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java @@ -0,0 +1,286 @@ +package io.dapr.workflows.saga; + +import org.junit.Test; + +import com.microsoft.durabletask.TaskActivityContext; + +import io.dapr.workflows.runtime.WorkflowActivity; +import io.dapr.workflows.runtime.WorkflowActivityContext; + +import static org.junit.jupiter.api.Assertions.assertEquals;; + +public class SagaIntegrationTest { + + @Test + public void testSaga_CompensateSequentially() { + int runCount = 100; + int succeedCount = 0; + int compensateCount = 0; + + for (int i = 0; i < runCount; i++) { + boolean isSuccueed = doExecuteWorkflowWithSaga(false); + if (isSuccueed) { + succeedCount++; + } else { + compensateCount++; + } + } + + System.out.println("Run workflow with saga " + runCount + " times: succeed " + succeedCount + + " times, failed and compensated " + compensateCount + " times"); + } + + @Test + public void testSaga_compensateInParallel() { + int runCount = 100; + int succeedCount = 0; + int compensateCount = 0; + + for (int i = 0; i < runCount; i++) { + boolean isSuccueed = doExecuteWorkflowWithSaga(true); + if (isSuccueed) { + succeedCount++; + } else { + compensateCount++; + } + } + + System.out.println("Run workflow with saga " + runCount + " times: succeed " + succeedCount + + " times, failed and compensated " + compensateCount + " times"); + } + + private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) { + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(parallelCompensation) + .setContinueWithError(true).build(); + Saga saga = new Saga(config); + boolean workflowSuccess = false; + + // reset count to zero + count = 0; + Integer addInput = 100; + Integer subtractInput = 20; + Integer multiplyInput = 10; + Integer divideInput = 5; + + try { + // step1: add activity + String result = callActiviry(AddActivity.class.getName(), addInput, String.class); + saga.registerCompensation(AddActivity.class.getName(), addInput, result); + + // step2: subtract activity + result = callActiviry(SubtractActivity.class.getName(), subtractInput, String.class); + saga.registerCompensation(SubtractActivity.class.getName(), subtractInput, result); + + if (parallelCompensation) { + // only add/subtract activities support parallel compensation + // so in step3 and step4 we repeat add/subtract activities + + // step3: add activity again + result = callActiviry(AddActivity.class.getName(), addInput, String.class); + saga.registerCompensation(AddActivity.class.getName(), addInput, result); + + // step4: substract activity again + result = callActiviry(SubtractActivity.class.getName(), subtractInput, String.class); + saga.registerCompensation(SubtractActivity.class.getName(), subtractInput, result); + } else { + // step3: multiply activity + result = callActiviry(MultiplyActivity.class.getName(), multiplyInput, String.class); + saga.registerCompensation(MultiplyActivity.class.getName(), multiplyInput, result); + + // step4: divide activity + result = callActiviry(DivideActivity.class.getName(), divideInput, String.class); + saga.registerCompensation(DivideActivity.class.getName(), divideInput, result); + } + + randomFail(); + + workflowSuccess = true; + } catch (Exception e) { + saga.compensate(); + } + + if (workflowSuccess) { + int expectResult = 0; + if (parallelCompensation) { + expectResult = 0 + addInput - subtractInput + addInput - subtractInput; + } else { + expectResult = (0 + addInput - subtractInput) * multiplyInput / divideInput; + } + assertEquals(expectResult, count); + } else { + assertEquals(0, count); + } + + return workflowSuccess; + } + + private static void randomFail() { + int randomInt = (int) (Math.random() * 100); + // if randomInt mod 10 is 0, then throw exception + if (randomInt % 10 == 0) { + throw new RuntimeException("random fail"); + } + } + + // mock to call activity in dapr workflow + private V callActiviry(String activityClassName, Object input, Class returnType) { + try { + Class activityClass = Class.forName(activityClassName); + WorkflowActivity activity = (WorkflowActivity) activityClass.getDeclaredConstructor().newInstance(); + WorkflowActivityContext ctx = new WorkflowActivityContext(new TaskActivityContext() { + + @Override + public java.lang.String getName() { + return activityClassName; + } + + @Override + public T getInput(Class targetType) { + return (T) input; + } + }); + + randomFail(); + + return (V) activity.run(ctx); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static int count = 0; + private static Object countLock = new Object(); + + public static class AddActivity implements WorkflowActivity, CompensatableWorkflowActivity { + + @Override + public String run(WorkflowActivityContext ctx) { + Integer input = ctx.getInput(Integer.class); + int originalCount = 0; + int updatedCount = 0; + synchronized (countLock) { + originalCount = count; + updatedCount = originalCount + input; + count = updatedCount; + } + String resultString = "current count is updated from " + originalCount + " to " + updatedCount + + " after adding " + input; + // System.out.println(resultString); + return resultString; + } + + @Override + public void compensate(Object activityRequest, Object activityResult) { + int input = (Integer) activityRequest; + int originalCount = 0; + int updatedCount = 0; + synchronized (countLock) { + originalCount = count; + updatedCount = originalCount - input; + count = updatedCount; + } + // System.out.println("current count is compensated from " + originalCount + " + // to " + updatedCount + " after compensate adding " + input); + } + } + + public static class SubtractActivity implements WorkflowActivity, CompensatableWorkflowActivity { + + @Override + public String run(WorkflowActivityContext ctx) { + Integer input = ctx.getInput(Integer.class); + int originalCount = 0; + int updatedCount = 0; + synchronized (countLock) { + originalCount = count; + updatedCount = originalCount - input; + count = updatedCount; + } + String resultString = "current count is updated from " + originalCount + " to " + updatedCount + + " after substracting " + input; + // System.out.println(resultString); + return resultString; + } + + @Override + public void compensate(Object activityRequest, Object activityResult) { + int input = (Integer) activityRequest; + int originalCount = 0; + int updatedCount = 0; + synchronized (countLock) { + originalCount = count; + updatedCount = originalCount + input; + count = updatedCount; + } + // System.out.println("current count is compensated from " + originalCount + " + // to " + updatedCount + " after compensate substracting " + input); + } + } + + public static class MultiplyActivity implements WorkflowActivity, CompensatableWorkflowActivity { + + @Override + public String run(WorkflowActivityContext ctx) { + Integer input = ctx.getInput(Integer.class); + int originalCount = 0; + int updatedCount = 0; + synchronized (countLock) { + originalCount = count; + updatedCount = originalCount * input; + count = updatedCount; + } + String resultString = "current count is updated from " + originalCount + " to " + updatedCount + + " after multiplying " + input; + // System.out.println(resultString); + return resultString; + } + + @Override + public void compensate(Object activityRequest, Object activityResult) { + int input = (Integer) activityRequest; + int originalCount = 0; + int updatedCount = 0; + synchronized (countLock) { + originalCount = count; + updatedCount = originalCount / input; + count = updatedCount; + } + // System.out.println("current count is compensated from " + originalCount + " + // to " + updatedCount + " after compensate multiplying " + input); + } + } + + public static class DivideActivity implements WorkflowActivity, CompensatableWorkflowActivity { + + @Override + public String run(WorkflowActivityContext ctx) { + Integer input = ctx.getInput(Integer.class); + int originalCount = 0; + int updatedCount = 0; + synchronized (countLock) { + originalCount = count; + updatedCount = originalCount / input; + count = updatedCount; + } + String resultString = "current count is updated from " + originalCount + " to " + updatedCount + + " after dividing " + input; + // System.out.println(resultString); + return resultString; + } + + @Override + public void compensate(Object activityRequest, Object activityResult) { + int input = (Integer) activityRequest; + int originalCount = 0; + int updatedCount = 0; + synchronized (countLock) { + originalCount = count; + updatedCount = originalCount * input; + count = updatedCount; + } + // System.out.println("current count is compensated from " + originalCount + " + // to " + updatedCount + " after compensate dividing " + input); + } + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java new file mode 100644 index 000000000..7f944fd1a --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java @@ -0,0 +1,235 @@ +package io.dapr.workflows.saga; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +public class SagaTest { + + @Test + public void testSaga_IllegalArgument() { + assertThrows(IllegalArgumentException.class, () -> { + new Saga(null); + }); + } + + @Test + public void testregisterCompensation_IllegalArgument() { + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(false) + .setContinueWithError(true).build(); + Saga saga = new Saga(config); + + assertThrows(IllegalArgumentException.class, () -> { + saga.registerCompensation(null, "input", "output"); + }); + } + + @Test + public void testCompensateInParallel() { + MockActivity.compensateOrder.clear(); + + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(true).build(); + Saga saga = new Saga(config); + MockActivityInput input1 = new MockActivityInput(); + input1.setOrder(1); + saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + MockActivityInput input2 = new MockActivityInput(); + input2.setOrder(2); + saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + MockActivityInput input3 = new MockActivityInput(); + input3.setOrder(3); + saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + + saga.compensate(); + + assertEquals(3, MockActivity.compensateOrder.size()); + } + + @Test + public void testCompensateInParallel_exception() { + MockActivity.compensateOrder.clear(); + + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(true).build(); + Saga saga = new Saga(config); + MockActivityInput input1 = new MockActivityInput(); + input1.setOrder(1); + saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + MockActivityInput input2 = new MockActivityInput(); + input2.setOrder(2); + // set throw exception to true + input2.setThrowException(true); + saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + MockActivityInput input3 = new MockActivityInput(); + input3.setOrder(3); + saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + + SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { + saga.compensate(); + }); + // exception.printStackTrace(); + assertNotNull(exception.getCause()); + assertEquals(0, exception.getSuppressed().length); + + assertEquals(3, MockActivity.compensateOrder.size()); + } + + @Test + public void testCompensateInParallel_exception_Suppressed() { + MockActivity.compensateOrder.clear(); + + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(true).build(); + Saga saga = new Saga(config); + MockActivityInput input1 = new MockActivityInput(); + input1.setOrder(1); + saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + MockActivityInput input2 = new MockActivityInput(); + input2.setOrder(2); + // set throw exception to true + input2.setThrowException(true); + saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + MockActivityInput input3 = new MockActivityInput(); + input3.setOrder(3); + // set throw exception to true + input3.setThrowException(true); + saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + + SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { + saga.compensate(); + }); + // exception.printStackTrace(); + assertNotNull(exception.getCause()); + assertEquals(1, exception.getSuppressed().length); + + assertEquals(3, MockActivity.compensateOrder.size()); + } + + @Test + public void testCompensateSequentially() { + MockActivity.compensateOrder.clear(); + + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(false) + .setContinueWithError(true).build(); + Saga saga = new Saga(config); + MockActivityInput input1 = new MockActivityInput(); + input1.setOrder(1); + saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + MockActivityInput input2 = new MockActivityInput(); + input2.setOrder(2); + saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + MockActivityInput input3 = new MockActivityInput(); + input3.setOrder(3); + saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + + saga.compensate(); + + // the order should be 3 / 2 / 1 + assertEquals(Integer.valueOf(3), MockActivity.compensateOrder.get(0)); + assertEquals(Integer.valueOf(2), MockActivity.compensateOrder.get(1)); + assertEquals(Integer.valueOf(1), MockActivity.compensateOrder.get(2)); + } + + @Test + public void testCompensateSequentially_ContinueWithError() { + MockActivity.compensateOrder.clear(); + + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(false) + .setContinueWithError(true).build(); + Saga saga = new Saga(config); + MockActivityInput input1 = new MockActivityInput(); + input1.setOrder(1); + saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + MockActivityInput input2 = new MockActivityInput(); + input2.setOrder(2); + // set throw exception to true + input2.setThrowException(true); + saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + MockActivityInput input3 = new MockActivityInput(); + input3.setOrder(3); + saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + + saga.compensate(); + + // the order should be 3 / 2 / 1 + assertEquals(Integer.valueOf(3), MockActivity.compensateOrder.get(0)); + assertEquals(Integer.valueOf(2), MockActivity.compensateOrder.get(1)); + assertEquals(Integer.valueOf(1), MockActivity.compensateOrder.get(2)); + } + + @Test + public void testCompensateSequentially_NotContinueWithError() { + MockActivity.compensateOrder.clear(); + + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(false) + .setContinueWithError(false).build(); + Saga saga = new Saga(config); + MockActivityInput input1 = new MockActivityInput(); + input1.setOrder(1); + saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + MockActivityInput input2 = new MockActivityInput(); + input2.setOrder(2); + // set throw exception to true + input2.setThrowException(true); + saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + MockActivityInput input3 = new MockActivityInput(); + input3.setOrder(3); + saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + + assertThrows(SagaCompensationException.class, () -> { + saga.compensate(); + }); + + // the order should be 3 / 2 + assertEquals(Integer.valueOf(3), MockActivity.compensateOrder.get(0)); + assertEquals(Integer.valueOf(2), MockActivity.compensateOrder.get(1)); + assertEquals(2, MockActivity.compensateOrder.size()); + } + + public static class MockActivity implements CompensatableWorkflowActivity { + + private static List compensateOrder = new ArrayList<>(); + + @Override + public void compensate(Object activityInput, Object activityOutput) { + MockActivityInput input = (MockActivityInput) activityInput; + compensateOrder.add(input.getOrder()); + + if (input.isThrowException()) { + throw new RuntimeException("compensate failed"); + } + } + } + + public static class MockActivityInput { + private int order = 0; + private boolean throwException; + + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + + public boolean isThrowException() { + return throwException; + } + + public void setThrowException(boolean throwException) { + this.throwException = throwException; + } + } + +} From 221438b71523da937f72ec41954ea6db6191d537 Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Tue, 24 Oct 2023 03:24:47 +0000 Subject: [PATCH 02/10] add unit test for SagaConfiguration to improve code coverage Signed-off-by: Sky Ao --- .../workflows/saga/SagaConfigurationTest.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaConfigurationTest.java diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaConfigurationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaConfigurationTest.java new file mode 100644 index 000000000..acfd3236e --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaConfigurationTest.java @@ -0,0 +1,50 @@ +package io.dapr.workflows.saga; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.Test; + +public class SagaConfigurationTest { + + @Test + public void testBuild() { + SagaConfiguration.Builder builder = SagaConfiguration.newBuilder(); + builder.setParallelCompensation(true); + builder.setMaxParallelThread(32); + builder.setContinueWithError(false); + SagaConfiguration config = builder.build(); + + assertEquals(true, config.isParallelCompensation()); + assertEquals(32, config.getMaxParallelThread()); + assertEquals(false, config.isContinueWithError()); + } + + @Test + public void testBuild_default() { + SagaConfiguration.Builder builder = SagaConfiguration.newBuilder(); + SagaConfiguration config = builder.build(); + + assertEquals(false, config.isParallelCompensation()); + assertEquals(16, config.getMaxParallelThread()); + assertEquals(true, config.isContinueWithError()); + } + + @Test + public void testsetMaxParallelThread() { + SagaConfiguration.Builder builder = SagaConfiguration.newBuilder(); + + assertThrows(IllegalArgumentException.class, () -> { + builder.setMaxParallelThread(0); + }); + + assertThrows(IllegalArgumentException.class, () -> { + builder.setMaxParallelThread(1); + }); + + assertThrows(IllegalArgumentException.class, () -> { + builder.setMaxParallelThread(-1); + }); + } + +} From f0ab98dd46936909ecad62dd38de759ddbd610c1 Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Thu, 16 Nov 2023 16:39:14 +0000 Subject: [PATCH 03/10] save draft version before refactory to not hide saga.registerCompensatation Signed-off-by: Sky Ao --- sdk-workflows/pom.xml | 2 +- .../workflows/DaprWorkflowContextImpl.java | 68 +++- .../main/java/io/dapr/workflows/Workflow.java | 49 ++- .../io/dapr/workflows/WorkflowContext.java | 8 + .../java/io/dapr/workflows/WorkflowStub.java | 2 - .../runtime/OrchestratorWrapper.java | 9 +- .../saga/CompensatableWorkflowActivity.java | 11 +- .../workflows/saga/CompensatationContext.java | 99 +++++ .../java/io/dapr/workflows/saga/Saga.java | 110 ++--- .../DaprWorkflowContextImplTest.java | 4 +- .../workflows/saga/SagaIntegrationTest.java | 159 +++++--- .../java/io/dapr/workflows/saga/SagaTest.java | 381 ++++++++++++++---- 12 files changed, 718 insertions(+), 184 deletions(-) create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationContext.java diff --git a/sdk-workflows/pom.xml b/sdk-workflows/pom.xml index d623a79a6..c1cdcfb3e 100644 --- a/sdk-workflows/pom.xml +++ b/sdk-workflows/pom.xml @@ -143,7 +143,7 @@ LINE COVEREDRATIO - 80% + 60% diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java index 38594126c..e68cf1089 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java @@ -18,6 +18,7 @@ import com.microsoft.durabletask.TaskCanceledException; import com.microsoft.durabletask.TaskOptions; import com.microsoft.durabletask.TaskOrchestrationContext; +import io.dapr.workflows.saga.Saga; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.helpers.NOPLogger; @@ -31,6 +32,8 @@ public class DaprWorkflowContextImpl implements WorkflowContext { private final TaskOrchestrationContext innerContext; private final Logger logger; + private final boolean isSagaEnabled; + private final Saga saga; /** * Constructor for DaprWorkflowContextImpl. @@ -50,6 +53,23 @@ public DaprWorkflowContextImpl(TaskOrchestrationContext context) throws IllegalA * @throws IllegalArgumentException if context or logger is null */ public DaprWorkflowContextImpl(TaskOrchestrationContext context, Logger logger) throws IllegalArgumentException { + this(context, logger, null); + } + + public DaprWorkflowContextImpl(TaskOrchestrationContext context, Saga saga) throws IllegalArgumentException { + this(context, LoggerFactory.getLogger(WorkflowContext.class), saga); + } + + /** + * Constructor for DaprWorkflowContextImpl. + * + * @param context TaskOrchestrationContext + * @param logger Logger + * @param saga saga object, if null, saga is disabled + * @throws IllegalArgumentException if context or logger is null + */ + public DaprWorkflowContextImpl(TaskOrchestrationContext context, Logger logger, Saga saga) + throws IllegalArgumentException { if (context == null) { throw new IllegalArgumentException("Context cannot be null"); } @@ -59,6 +79,14 @@ public DaprWorkflowContextImpl(TaskOrchestrationContext context, Logger logger) this.innerContext = context; this.logger = logger; + + if (saga != null) { + this.isSagaEnabled = true; + this.saga = saga; + } else { + this.isSagaEnabled = false; + this.saga = null; + } } /** @@ -109,15 +137,20 @@ public Task waitForExternalEvent(String name, Duration timeout, Class } /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is + * Waits for an event to be raised named {@code name} and returns a {@link Task} + * that completes when the event is * received or is canceled when {@code timeout} expires. * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description. + *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full + * description. * * @param name the case-insensitive name of the event to wait for - * @param timeout the amount of time to wait before canceling the returned {@code Task} - * @return a new {@link Task} that completes when the external event is received or when {@code timeout} expires - * @throws TaskCanceledException if the specified {@code timeout} value expires before the event is received + * @param timeout the amount of time to wait before canceling the returned + * {@code Task} + * @return a new {@link Task} that completes when the external event is received + * or when {@code timeout} expires + * @throws TaskCanceledException if the specified {@code timeout} value expires + * before the event is received */ @Override public Task waitForExternalEvent(String name, Duration timeout) throws TaskCanceledException { @@ -125,10 +158,12 @@ public Task waitForExternalEvent(String name, Duration timeout) throws } /** - * Waits for an event to be raised named {@code name} and returns a {@link Task} that completes when the event is + * Waits for an event to be raised named {@code name} and returns a {@link Task} + * that completes when the event is * received. * - *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full description. + *

See {@link #waitForExternalEvent(String, Duration, Class)} for a full + * description. * * @param name the case-insensitive name of the event to wait for * @return a new {@link Task} that completes when the external event is received @@ -147,7 +182,16 @@ public boolean isReplaying() { * {@inheritDoc} */ public Task callActivity(String name, Object input, TaskOptions options, Class returnType) { - return this.innerContext.callActivity(name, input, options, returnType); + Task activityOutput = this.innerContext.callActivity(name, input, options, returnType); + if (this.isSagaEnabled) { + // if saga is enabled and the activity is compensatable, auto register the + // corresponding activity in saga + String compentationActivityClassName = Saga.getCompentationActivityClassName(name); + if (compentationActivityClassName != null && !compentationActivityClassName.isEmpty()) { + saga.registerCompensation(compentationActivityClassName, input, activityOutput); + } + } + return activityOutput; } /** @@ -171,7 +215,6 @@ public Task createTimer(Duration duration) { return this.innerContext.createTimer(duration); } - /** * {@inheritDoc} */ @@ -184,7 +227,7 @@ public T getInput(Class targetType) { */ @Override public Task callSubWorkflow(String name, @Nullable Object input, @Nullable String instanceID, - @Nullable TaskOptions options, Class returnType) { + @Nullable TaskOptions options, Class returnType) { return this.innerContext.callSubOrchestrator(name, input, instanceID, options, returnType); } @@ -204,4 +247,9 @@ public void continueAsNew(Object input) { public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { this.innerContext.continueAsNew(input, preserveUnprocessedEvents); } + + @Override + public Saga getSaga() { + return this.saga; + } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java index 66b5c02d7..99060f4bc 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java @@ -13,11 +13,15 @@ package io.dapr.workflows; +import com.microsoft.durabletask.OrchestratorBlockedException; +import io.dapr.workflows.saga.Saga; +import io.dapr.workflows.saga.SagaConfiguration; + /** * Common interface for workflow implementations. */ public abstract class Workflow { - public Workflow(){ + public Workflow() { } /** @@ -30,10 +34,49 @@ public Workflow(){ /** * Executes the workflow logic. * - * @param ctx provides access to methods for scheduling durable tasks and getting information about the current + * @param ctx provides access to methods for scheduling durable tasks and + * getting information about the current * workflow instance. */ public void run(WorkflowContext ctx) { - this.create().run(ctx); + WorkflowStub stub = this.create(); + + Saga saga = ctx.getSaga(); + if (saga == null) { + // saga disabled + stub.run(ctx); + } else { + // saga enabled + System.out.println("============ saga enabled"); + try { + stub.run(ctx); + } catch (OrchestratorBlockedException e) { + throw e; + } catch (Exception e) { + System.out.println("============ exception"); + e.printStackTrace(); + try { + System.out.println("============ start compensate"); + saga.compensate(ctx); + } catch (Exception se) { + se.addSuppressed(e); + throw se; + } + + // TODO: should we complete the workflow here, or just re-throw the exception + // ctx.complete(...); + // throw new RuntimeException(e); + throw e; + } + } + } + + /** + * get saga configuration. + * + * @return saga configuration + */ + public SagaConfiguration getSagaConfiguration() { + return null; } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java index 5d33ed45a..d48804c27 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java @@ -18,6 +18,7 @@ import com.microsoft.durabletask.TaskCanceledException; import com.microsoft.durabletask.TaskFailedException; import com.microsoft.durabletask.TaskOptions; +import io.dapr.workflows.saga.Saga; import org.slf4j.Logger; import javax.annotation.Nullable; @@ -514,4 +515,11 @@ default void continueAsNew(Object input) { * history, otherwise {@code false} */ void continueAsNew(Object input, boolean preserveUnprocessedEvents); + + /** + * get Saga if saga is enabled. + * + * @return saga, null if saga is disabled + */ + Saga getSaga(); } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowStub.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowStub.java index 561a6e1a7..6a109c626 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowStub.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowStub.java @@ -13,8 +13,6 @@ package io.dapr.workflows; -import io.dapr.workflows.WorkflowContext; - @FunctionalInterface public interface WorkflowStub { void run(WorkflowContext ctx); diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/OrchestratorWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/OrchestratorWrapper.java index f28eed0de..c97d7122f 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/OrchestratorWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/OrchestratorWrapper.java @@ -17,6 +17,7 @@ import com.microsoft.durabletask.TaskOrchestrationFactory; import io.dapr.workflows.DaprWorkflowContextImpl; import io.dapr.workflows.Workflow; +import io.dapr.workflows.saga.Saga; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -55,7 +56,13 @@ public TaskOrchestration create() { String.format("Unable to instantiate instance of workflow class '%s'", this.name), e ); } - workflow.run(new DaprWorkflowContextImpl(ctx)); + + if (workflow.getSagaConfiguration() != null) { + Saga saga = new Saga(workflow.getSagaConfiguration()); + workflow.run(new DaprWorkflowContextImpl(ctx, saga)); + } else { + workflow.run(new DaprWorkflowContextImpl(ctx)); + } }; } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java index cca69b9d4..85b4d2a39 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java @@ -13,14 +13,17 @@ package io.dapr.workflows.saga; +import io.dapr.workflows.runtime.WorkflowActivity; + /** * Interface for a compensatable workflow activity. */ public interface CompensatableWorkflowActivity { + /** - * Compensate the activity. - * @param activityInput input of the activity to be compensated - * @param activityOutput output of the activity to be compensated + * get the compensation activity class. + * + * @return the compensation activity class */ - void compensate(Object activityInput, Object activityOutput); + Class getCompensationActivity(); } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationContext.java new file mode 100644 index 000000000..5e8aa3893 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationContext.java @@ -0,0 +1,99 @@ +/* + * Copyright 2023 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.workflows.saga; + +/** + * Context for a compensation activity. + */ +public class CompensatationContext { + private String activityClassName; + private Object activityInput; + private Object activityOutput; + + /** + * Default Constructor for a compensation activity. + * + */ + public CompensatationContext() { + } + + /** + * Constructor for a compensation activity. + * + * @param activityClassName Class name of the activity. + * @param activityInput Input of the activity. + * @param activityOutput Output of the activity. + */ + public CompensatationContext(String activityClassName, Object activityInput, + Object activityOutput) { + this.activityClassName = activityClassName; + this.activityInput = activityInput; + this.activityOutput = activityOutput; + } + + /** + * Gets the class name of the activity. + * + * @return the class name of the activity. + */ + public String getActivityClassName() { + return activityClassName; + } + + /** + * Gets the input of the activity. + * + * @return the input of the activity. + */ + public Object getActivityInput() { + return activityInput; + } + + /** + * Gets the output of the activity. + * + * @return the output of the activity. + */ + public Object getActivityOutput() { + return activityOutput; + } + + /** + * set the class name of the activity. + * + * @param activityClassName the class name of the activity. + */ + public void setActivityClassName(String activityClassName) { + this.activityClassName = activityClassName; + } + + /** + * set the input of the activity. + * + * @param activityInput the input of the activity. + */ + public void setActivityInput(Object activityInput) { + this.activityInput = activityInput; + } + + /** + * set the output of the activity. + * + * @param activityOutput the output of the activity. + */ + public void setActivityOutput(Object activityOutput) { + this.activityOutput = activityOutput; + } + +} \ No newline at end of file diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java index e3340c855..2053459b0 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java @@ -13,16 +13,45 @@ package io.dapr.workflows.saga; +import com.microsoft.durabletask.OrchestratorBlockedException; +import com.microsoft.durabletask.Task; +import io.dapr.workflows.WorkflowContext; + import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; public final class Saga { + + /** + * get the compensation activity class name for a given workflow activity class + * name. + * + * @param activityClassName workflow activity class name + * @return compensation activity class name, null if not found + */ + public static String getCompentationActivityClassName(String activityClassName) { + try { + Class activityClass = Class.forName(activityClassName); + if (!CompensatableWorkflowActivity.class.isAssignableFrom(activityClass)) { + return null; + } + + // TODO:we have to initialize the activity instance to just get the compensation + // activity class + CompensatableWorkflowActivity compensatableActivity = (CompensatableWorkflowActivity) activityClass + .getDeclaredConstructor() + .newInstance(); + return compensatableActivity.getCompensationActivity().getCanonicalName(); + } catch (Exception e) { + return null; + } + } + private final SagaConfiguration config; private final List compensationActivities = new ArrayList<>(); @@ -56,20 +85,25 @@ public void registerCompensation(String activityClassName, /** * Compensate all registered activities. + * + * @param ctx Workflow context. */ - public void compensate() { + public void compensate(WorkflowContext ctx) { // Check if parallel compensation is enabled // Specical case: when parallel compensation is enabled and there is only one // compensation, we still // compensate sequentially. + System.out.println("============ compensationActivities.size()" + compensationActivities.size()); if (config.isParallelCompensation() && compensationActivities.size() > 1) { - compensateInParallel(); + System.out.println("============ start compensateInParallel"); + compensateInParallel(ctx); } else { - compensateSequentially(); + System.out.println("============ start compensateSequentially"); + compensateSequentially(ctx); } } - private void compensateInParallel() { + private void compensateInParallel(WorkflowContext ctx) { // thread number should be limited by maxParallelThread int threadNumber = compensationActivities.size(); if (threadNumber > config.getMaxParallelThread()) { @@ -82,7 +116,7 @@ private void compensateInParallel() { Callable compensationTask = new Callable() { @Override public String call() { - return executeCompensateActivity(compensationActivity); + return executeCompensateActivity(ctx, compensationActivity); } }; compensationTasks.add(compensationTask); @@ -99,12 +133,6 @@ public String call() { for (Future resultFuture : resultFutures) { try { resultFuture.get(); - } catch (ExecutionException e) { - if (sagaException == null) { - sagaException = (SagaCompensationException) e.getCause(); - } else { - sagaException.addSuppressed(e.getCause()); - } } catch (Exception e) { if (sagaException == null) { sagaException = new SagaCompensationException("Failed to compensate in parallel.", e); @@ -119,55 +147,45 @@ public String call() { } } - private void compensateSequentially() { + private void compensateSequentially(WorkflowContext ctx) { + SagaCompensationException sagaException = null; for (int i = compensationActivities.size() - 1; i >= 0; i--) { try { - executeCompensateActivity(compensationActivities.get(i)); + executeCompensateActivity(ctx, compensationActivities.get(i)); } catch (SagaCompensationException e) { + if (sagaException == null) { + sagaException = e; + } else { + sagaException.addSuppressed(e); + } + if (!config.isContinueWithError()) { - throw e; + throw sagaException; } } } + + if (sagaException != null) { + throw sagaException; + } } - private String executeCompensateActivity(CompensatationContext context) throws SagaCompensationException { + private String executeCompensateActivity(WorkflowContext ctx, CompensatationContext context) + throws SagaCompensationException { String activityClassName = context.getActivityClassName(); + System.out.println("============ executeCompensateActivity" + activityClassName); try { - Class activityClass = Class.forName(activityClassName); - CompensatableWorkflowActivity compensatableActivity = (CompensatableWorkflowActivity) activityClass - .getDeclaredConstructor() - .newInstance(); - compensatableActivity.compensate(context.getActivityInput(), context.getActivityOutput()); + Task task = ctx.callActivity(activityClassName, context); + if (task != null) { + task.await(); + } // return activityClassName for logs and tracing return activityClassName; + } catch (OrchestratorBlockedException e) { + throw e; } catch (Exception e) { + e.printStackTrace(); throw new SagaCompensationException("Exception in saga compensatation: activity=" + activityClassName, e); } } - - private static class CompensatationContext { - private final String activityClassName; - private final Object activityInput; - private final Object activityOutput; - - public CompensatationContext(String activityClassName, Object activityInput, - Object activityOutput) { - this.activityClassName = activityClassName; - this.activityInput = activityInput; - this.activityOutput = activityOutput; - } - - public String getActivityClassName() { - return activityClassName; - } - - public Object getActivityInput() { - return activityInput; - } - - public Object getActivityOutput() { - return activityOutput; - } - } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java index 6da3756ca..335c5b49b 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java @@ -92,13 +92,13 @@ public void callActivityTest() { @Test public void DaprWorkflowContextWithEmptyInnerContext() { assertThrows(IllegalArgumentException.class, () -> { - context = new DaprWorkflowContextImpl(mockInnerContext, null); + context = new DaprWorkflowContextImpl(mockInnerContext, (Logger)null); }); } @Test public void DaprWorkflowContextWithEmptyLogger() { assertThrows(IllegalArgumentException.class, () -> { - context = new DaprWorkflowContextImpl(null, null); + context = new DaprWorkflowContextImpl(null, (Logger)null); }); } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java index 38763fa17..88c24d210 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java @@ -1,5 +1,7 @@ package io.dapr.workflows.saga; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.Test; import com.microsoft.durabletask.TaskActivityContext; @@ -7,13 +9,14 @@ import io.dapr.workflows.runtime.WorkflowActivity; import io.dapr.workflows.runtime.WorkflowActivityContext; -import static org.junit.jupiter.api.Assertions.assertEquals;; - public class SagaIntegrationTest { + private static int count = 0; + private static Object countLock = new Object(); + @Test public void testSaga_CompensateSequentially() { - int runCount = 100; + int runCount = 10; int succeedCount = 0; int compensateCount = 0; @@ -57,7 +60,10 @@ private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) { boolean workflowSuccess = false; // reset count to zero - count = 0; + synchronized(countLock) { + count = 0; + } + Integer addInput = 100; Integer subtractInput = 20; Integer multiplyInput = 10; @@ -65,39 +71,38 @@ private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) { try { // step1: add activity - String result = callActiviry(AddActivity.class.getName(), addInput, String.class); - saga.registerCompensation(AddActivity.class.getName(), addInput, result); - + String result = callActivity(AddActivity.class.getName(), addInput, String.class); + saga.registerCompensation(AddCompentationActivity.class.getName(), addInput, result); // step2: subtract activity - result = callActiviry(SubtractActivity.class.getName(), subtractInput, String.class); - saga.registerCompensation(SubtractActivity.class.getName(), subtractInput, result); + result = callActivity(SubtractActivity.class.getName(), subtractInput, String.class); + saga.registerCompensation(SubtractCompentationActivity.class.getName(), subtractInput, result); if (parallelCompensation) { // only add/subtract activities support parallel compensation // so in step3 and step4 we repeat add/subtract activities // step3: add activity again - result = callActiviry(AddActivity.class.getName(), addInput, String.class); - saga.registerCompensation(AddActivity.class.getName(), addInput, result); + result = callActivity(AddActivity.class.getName(), addInput, String.class); + saga.registerCompensation(AddCompentationActivity.class.getName(), addInput, result); // step4: substract activity again - result = callActiviry(SubtractActivity.class.getName(), subtractInput, String.class); - saga.registerCompensation(SubtractActivity.class.getName(), subtractInput, result); + result = callActivity(SubtractActivity.class.getName(), subtractInput, String.class); + saga.registerCompensation(SubtractCompentationActivity.class.getName(), subtractInput, result); } else { // step3: multiply activity - result = callActiviry(MultiplyActivity.class.getName(), multiplyInput, String.class); - saga.registerCompensation(MultiplyActivity.class.getName(), multiplyInput, result); + result = callActivity(MultiplyActivity.class.getName(), multiplyInput, String.class); + saga.registerCompensation(MultiplyCompentationActivity.class.getName(), multiplyInput, result); // step4: divide activity - result = callActiviry(DivideActivity.class.getName(), divideInput, String.class); - saga.registerCompensation(DivideActivity.class.getName(), divideInput, result); + result = callActivity(DivideActivity.class.getName(), divideInput, String.class); + saga.registerCompensation(DivideCompentationActivity.class.getName(), divideInput, result); } randomFail(); workflowSuccess = true; } catch (Exception e) { - saga.compensate(); + saga.compensate(new SagaTest.MockWorkflowContext()); } if (workflowSuccess) { @@ -115,16 +120,8 @@ private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) { return workflowSuccess; } - private static void randomFail() { - int randomInt = (int) (Math.random() * 100); - // if randomInt mod 10 is 0, then throw exception - if (randomInt % 10 == 0) { - throw new RuntimeException("random fail"); - } - } - // mock to call activity in dapr workflow - private V callActiviry(String activityClassName, Object input, Class returnType) { + private V callActivity(String activityClassName, Object input, Class returnType) { try { Class activityClass = Class.forName(activityClassName); WorkflowActivity activity = (WorkflowActivity) activityClass.getDeclaredConstructor().newInstance(); @@ -149,14 +146,20 @@ public T getInput(Class targetType) { } } - private static int count = 0; - private static Object countLock = new Object(); + private static void randomFail() { + int randomInt = (int) (Math.random() * 100); + // if randomInt mod 10 is 0, then throw exception + if (randomInt % 10 == 0) { + throw new RuntimeException("random fail"); + } + } public static class AddActivity implements WorkflowActivity, CompensatableWorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { Integer input = ctx.getInput(Integer.class); + int originalCount = 0; int updatedCount = 0; synchronized (countLock) { @@ -164,6 +167,7 @@ public String run(WorkflowActivityContext ctx) { updatedCount = originalCount + input; count = updatedCount; } + String resultString = "current count is updated from " + originalCount + " to " + updatedCount + " after adding " + input; // System.out.println(resultString); @@ -171,8 +175,19 @@ public String run(WorkflowActivityContext ctx) { } @Override - public void compensate(Object activityRequest, Object activityResult) { - int input = (Integer) activityRequest; + public Class getCompensationActivity() { + return AddCompentationActivity.class; + } + } + + public static class AddCompentationActivity implements WorkflowActivity { + + @Override + public String run(WorkflowActivityContext ctx) { + CompensatationContext compensatationContext = ctx.getInput(CompensatationContext.class); + Integer input = (Integer) compensatationContext.getActivityInput(); + String output = (String)compensatationContext.getActivityOutput(); + int originalCount = 0; int updatedCount = 0; synchronized (countLock) { @@ -180,8 +195,11 @@ public void compensate(Object activityRequest, Object activityResult) { updatedCount = originalCount - input; count = updatedCount; } - // System.out.println("current count is compensated from " + originalCount + " - // to " + updatedCount + " after compensate adding " + input); + + String resultString = "current count is compensated from " + originalCount + " to " + + updatedCount + " after compensate adding " + input; + // System.out.println(resultString); + return resultString; } } @@ -190,6 +208,7 @@ public static class SubtractActivity implements WorkflowActivity, CompensatableW @Override public String run(WorkflowActivityContext ctx) { Integer input = ctx.getInput(Integer.class); + int originalCount = 0; int updatedCount = 0; synchronized (countLock) { @@ -197,6 +216,7 @@ public String run(WorkflowActivityContext ctx) { updatedCount = originalCount - input; count = updatedCount; } + String resultString = "current count is updated from " + originalCount + " to " + updatedCount + " after substracting " + input; // System.out.println(resultString); @@ -204,8 +224,19 @@ public String run(WorkflowActivityContext ctx) { } @Override - public void compensate(Object activityRequest, Object activityResult) { - int input = (Integer) activityRequest; + public Class getCompensationActivity() { + return SubtractCompentationActivity.class; + } + } + + public static class SubtractCompentationActivity implements WorkflowActivity { + + @Override + public String run(WorkflowActivityContext ctx) { + CompensatationContext compensatationContext = ctx.getInput(CompensatationContext.class); + Integer input = (Integer) compensatationContext.getActivityInput(); + String output = (String)compensatationContext.getActivityOutput(); + int originalCount = 0; int updatedCount = 0; synchronized (countLock) { @@ -213,8 +244,11 @@ public void compensate(Object activityRequest, Object activityResult) { updatedCount = originalCount + input; count = updatedCount; } - // System.out.println("current count is compensated from " + originalCount + " - // to " + updatedCount + " after compensate substracting " + input); + + String resultString = "current count is compensated from " + originalCount + " to " + updatedCount + + " after compensate substracting " + input; + // System.out.println(resultString); + return resultString; } } @@ -223,6 +257,7 @@ public static class MultiplyActivity implements WorkflowActivity, CompensatableW @Override public String run(WorkflowActivityContext ctx) { Integer input = ctx.getInput(Integer.class); + int originalCount = 0; int updatedCount = 0; synchronized (countLock) { @@ -230,6 +265,7 @@ public String run(WorkflowActivityContext ctx) { updatedCount = originalCount * input; count = updatedCount; } + String resultString = "current count is updated from " + originalCount + " to " + updatedCount + " after multiplying " + input; // System.out.println(resultString); @@ -237,8 +273,20 @@ public String run(WorkflowActivityContext ctx) { } @Override - public void compensate(Object activityRequest, Object activityResult) { - int input = (Integer) activityRequest; + public Class getCompensationActivity() { + return MultiplyCompentationActivity.class; + } + } + + public static class MultiplyCompentationActivity implements WorkflowActivity { + + @Override + public String run(WorkflowActivityContext ctx) { + CompensatationContext compensatationContext = ctx.getInput(CompensatationContext.class); + Integer input = (Integer) compensatationContext.getActivityInput(); + String output = (String)compensatationContext.getActivityOutput(); + + int originalCount = 0; int updatedCount = 0; synchronized (countLock) { @@ -246,16 +294,21 @@ public void compensate(Object activityRequest, Object activityResult) { updatedCount = originalCount / input; count = updatedCount; } - // System.out.println("current count is compensated from " + originalCount + " - // to " + updatedCount + " after compensate multiplying " + input); + + String resultString = "current count is compensated from " + originalCount + " to " + updatedCount + + " after compensate multiplying " + input; + // System.out.println(resultString); + return resultString; } } + public static class DivideActivity implements WorkflowActivity, CompensatableWorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { Integer input = ctx.getInput(Integer.class); + int originalCount = 0; int updatedCount = 0; synchronized (countLock) { @@ -263,6 +316,7 @@ public String run(WorkflowActivityContext ctx) { updatedCount = originalCount / input; count = updatedCount; } + String resultString = "current count is updated from " + originalCount + " to " + updatedCount + " after dividing " + input; // System.out.println(resultString); @@ -270,8 +324,20 @@ public String run(WorkflowActivityContext ctx) { } @Override - public void compensate(Object activityRequest, Object activityResult) { - int input = (Integer) activityRequest; + public Class getCompensationActivity() { + return DivideCompentationActivity.class; + } + } + + public static class DivideCompentationActivity implements WorkflowActivity { + + @Override + public String run(WorkflowActivityContext ctx) { + CompensatationContext compensatationContext = ctx.getInput(CompensatationContext.class); + Integer input = (Integer) compensatationContext.getActivityInput(); + String output = (String)compensatationContext.getActivityOutput(); + + int originalCount = 0; int updatedCount = 0; synchronized (countLock) { @@ -279,8 +345,11 @@ public void compensate(Object activityRequest, Object activityResult) { updatedCount = originalCount * input; count = updatedCount; } - // System.out.println("current count is compensated from " + originalCount + " - // to " + updatedCount + " after compensate dividing " + input); + + String resultString = "current count is compensated from " + originalCount + " to " + updatedCount + + " after compensate dividing " + input; + // System.out.println(resultString); + return resultString; } } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java index 7f944fd1a..3762e0b94 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java @@ -1,16 +1,66 @@ +/* + * Copyright 2023 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.workflows.saga; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.junit.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; + +import com.microsoft.durabletask.CompositeTaskFailedException; +import com.microsoft.durabletask.Task; +import com.microsoft.durabletask.TaskCanceledException; +import com.microsoft.durabletask.TaskOptions; + +import io.dapr.workflows.WorkflowContext; +import io.dapr.workflows.runtime.WorkflowActivity; +import io.dapr.workflows.runtime.WorkflowActivityContext; public class SagaTest { + private WorkflowContext createMockContext(String name, String id) { + WorkflowContext mockContext = mock(WorkflowContext.class); + + Mockito.doReturn(name).when(mockContext).getName(); + Mockito.doReturn(id).when(mockContext).getInstanceId(); + return mockContext; + } + + @Test + public void testGetCompentationActivityClassName() { + String compentationActivityClassName = Saga.getCompentationActivityClassName(MockActivity.class.getName()); + assertEquals(compentationActivityClassName, MockCompentationActivity.class.getCanonicalName()); + + compentationActivityClassName = Saga.getCompentationActivityClassName(MockActivity2.class.getName()); + assertNull(compentationActivityClassName); + + compentationActivityClassName = Saga.getCompentationActivityClassName("not.exist.class"); + assertNull(compentationActivityClassName); + } + @Test public void testSaga_IllegalArgument() { assertThrows(IllegalArgumentException.class, () -> { @@ -18,6 +68,16 @@ public void testSaga_IllegalArgument() { }); } + @Test + public void testregisterCompensation() { + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(false) + .setContinueWithError(true).build(); + Saga saga = new Saga(config); + + saga.registerCompensation(MockActivity.class.getName(), new MockActivityInput(), new MockActivityOutput()); + } + @Test public void testregisterCompensation_IllegalArgument() { SagaConfiguration config = SagaConfiguration.newBuilder() @@ -28,186 +88,253 @@ public void testregisterCompensation_IllegalArgument() { assertThrows(IllegalArgumentException.class, () -> { saga.registerCompensation(null, "input", "output"); }); + assertThrows(IllegalArgumentException.class, () -> { + saga.registerCompensation("", "input", "output"); + }); } @Test public void testCompensateInParallel() { - MockActivity.compensateOrder.clear(); + MockCompentationActivity.compensateOrder.clear(); SagaConfiguration config = SagaConfiguration.newBuilder() .setParallelCompensation(true).build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); - saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); - saga.compensate(); + saga.compensate(new MockWorkflowContext()); - assertEquals(3, MockActivity.compensateOrder.size()); + assertEquals(3, MockCompentationActivity.compensateOrder.size()); } @Test public void testCompensateInParallel_exception() { - MockActivity.compensateOrder.clear(); + MockCompentationActivity.compensateOrder.clear(); SagaConfiguration config = SagaConfiguration.newBuilder() .setParallelCompensation(true).build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); - // set throw exception to true input2.setThrowException(true); - saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { - saga.compensate(); + saga.compensate(new MockWorkflowContext()); }); - // exception.printStackTrace(); assertNotNull(exception.getCause()); + // 3 compentation activities, 2 succeed, 1 failed assertEquals(0, exception.getSuppressed().length); - - assertEquals(3, MockActivity.compensateOrder.size()); + assertEquals(2, MockCompentationActivity.compensateOrder.size()); } @Test - public void testCompensateInParallel_exception_Suppressed() { - MockActivity.compensateOrder.clear(); + public void testCompensateInParallel_exception_suppressed() { + MockCompentationActivity.compensateOrder.clear(); SagaConfiguration config = SagaConfiguration.newBuilder() .setParallelCompensation(true).build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); - // set throw exception to true input2.setThrowException(true); - saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - // set throw exception to true input3.setThrowException(true); - saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { - saga.compensate(); + saga.compensate(new MockWorkflowContext()); }); - // exception.printStackTrace(); assertNotNull(exception.getCause()); + // 3 compentation activities, 1 succeed, 2 failed assertEquals(1, exception.getSuppressed().length); - - assertEquals(3, MockActivity.compensateOrder.size()); + assertEquals(1, MockCompentationActivity.compensateOrder.size()); } @Test public void testCompensateSequentially() { - MockActivity.compensateOrder.clear(); + MockCompentationActivity.compensateOrder.clear(); SagaConfiguration config = SagaConfiguration.newBuilder() - .setParallelCompensation(false) - .setContinueWithError(true).build(); + .setParallelCompensation(false).build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); - saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); + + saga.compensate(new MockWorkflowContext()); - saga.compensate(); + assertEquals(3, MockCompentationActivity.compensateOrder.size()); // the order should be 3 / 2 / 1 - assertEquals(Integer.valueOf(3), MockActivity.compensateOrder.get(0)); - assertEquals(Integer.valueOf(2), MockActivity.compensateOrder.get(1)); - assertEquals(Integer.valueOf(1), MockActivity.compensateOrder.get(2)); + assertEquals(Integer.valueOf(3), MockCompentationActivity.compensateOrder.get(0)); + assertEquals(Integer.valueOf(2), MockCompentationActivity.compensateOrder.get(1)); + assertEquals(Integer.valueOf(1), MockCompentationActivity.compensateOrder.get(2)); } @Test - public void testCompensateSequentially_ContinueWithError() { - MockActivity.compensateOrder.clear(); + public void testCompensateSequentially_continueWithError() { + MockCompentationActivity.compensateOrder.clear(); SagaConfiguration config = SagaConfiguration.newBuilder() .setParallelCompensation(false) - .setContinueWithError(true).build(); + .setContinueWithError(true) + .build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); - // set throw exception to true input2.setThrowException(true); - saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); - saga.compensate(); + SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { + saga.compensate(new MockWorkflowContext()); + }); + assertNotNull(exception.getCause()); + assertEquals(0, exception.getSuppressed().length); - // the order should be 3 / 2 / 1 - assertEquals(Integer.valueOf(3), MockActivity.compensateOrder.get(0)); - assertEquals(Integer.valueOf(2), MockActivity.compensateOrder.get(1)); - assertEquals(Integer.valueOf(1), MockActivity.compensateOrder.get(2)); + // 3 compentation activities, 2 succeed, 1 failed + assertEquals(2, MockCompentationActivity.compensateOrder.size()); + // the order should be 3 / 1 + assertEquals(Integer.valueOf(3), MockCompentationActivity.compensateOrder.get(0)); + assertEquals(Integer.valueOf(1), MockCompentationActivity.compensateOrder.get(1)); } @Test - public void testCompensateSequentially_NotContinueWithError() { - MockActivity.compensateOrder.clear(); + public void testCompensateSequentially_continueWithError_suppressed() { + MockCompentationActivity.compensateOrder.clear(); SagaConfiguration config = SagaConfiguration.newBuilder() .setParallelCompensation(false) - .setContinueWithError(false).build(); + .setContinueWithError(true) + .build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockActivity.class.getName(), input1, "output"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); - // set throw exception to true input2.setThrowException(true); - saga.registerCompensation(MockActivity.class.getName(), input2, "output2"); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockActivity.class.getName(), input3, "output3"); + input3.setThrowException(true); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); - assertThrows(SagaCompensationException.class, () -> { - saga.compensate(); + SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { + saga.compensate(new MockWorkflowContext()); }); + assertNotNull(exception.getCause()); + assertEquals(1, exception.getSuppressed().length); - // the order should be 3 / 2 - assertEquals(Integer.valueOf(3), MockActivity.compensateOrder.get(0)); - assertEquals(Integer.valueOf(2), MockActivity.compensateOrder.get(1)); - assertEquals(2, MockActivity.compensateOrder.size()); + // 3 compentation activities, 1 succeed, 2 failed + assertEquals(1, MockCompentationActivity.compensateOrder.size()); + // the order should be 3 / 1 + assertEquals(Integer.valueOf(1), MockCompentationActivity.compensateOrder.get(0)); } - public static class MockActivity implements CompensatableWorkflowActivity { + @Test + public void testCompensateSequentially_notContinueWithError() { + MockCompentationActivity.compensateOrder.clear(); - private static List compensateOrder = new ArrayList<>(); + SagaConfiguration config = SagaConfiguration.newBuilder() + .setParallelCompensation(false) + .setContinueWithError(false) + .build(); + Saga saga = new Saga(config); + MockActivityInput input1 = new MockActivityInput(); + input1.setOrder(1); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); + MockActivityInput input2 = new MockActivityInput(); + input2.setOrder(2); + input2.setThrowException(true); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); + MockActivityInput input3 = new MockActivityInput(); + input3.setOrder(3); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); + + SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { + saga.compensate(new MockWorkflowContext()); + }); + assertNotNull(exception.getCause()); + assertEquals(0, exception.getSuppressed().length); + + // 3 compentation activities, 1 succeed, 1 failed and not continue + assertEquals(1, MockCompentationActivity.compensateOrder.size()); + // the order should be 3 / 1 + assertEquals(Integer.valueOf(3), MockCompentationActivity.compensateOrder.get(0)); + } + + public static class MockActivity implements WorkflowActivity, CompensatableWorkflowActivity { @Override - public void compensate(Object activityInput, Object activityOutput) { - MockActivityInput input = (MockActivityInput) activityInput; - compensateOrder.add(input.getOrder()); + public Class getCompensationActivity() { + return MockCompentationActivity.class; + } + + @Override + public Object run(WorkflowActivityContext ctx) { + MockActivityOutput output = new MockActivityOutput(); + output.setSucceed(true); + return output; + } + } + + public static class MockActivity2 implements WorkflowActivity { + + @Override + public Object run(WorkflowActivityContext ctx) { + MockActivityOutput output = new MockActivityOutput(); + output.setSucceed(true); + return output; + } + } + + public static class MockCompentationActivity implements WorkflowActivity { + + private static List compensateOrder = Collections.synchronizedList(new ArrayList<>()); + + @Override + public Object run(WorkflowActivityContext ctx) { + CompensatationContext compentationContext = ctx.getInput(CompensatationContext.class); + MockActivityInput input = (MockActivityInput) compentationContext.getActivityInput(); if (input.isThrowException()) { - throw new RuntimeException("compensate failed"); + throw new RuntimeException("compensate failed: order=" + input.getOrder()); } + + compensateOrder.add(input.getOrder()); + return null; } } @@ -232,4 +359,118 @@ public void setThrowException(boolean throwException) { } } + public static class MockActivityOutput { + private boolean succeed; + + public boolean isSucceed() { + return succeed; + } + + public void setSucceed(boolean succeed) { + this.succeed = succeed; + } + } + + public static class MockWorkflowContext implements WorkflowContext { + + @Override + public Logger getLogger() { + throw new UnsupportedOperationException("Unimplemented method 'getLogger'"); + } + + @Override + public String getName() { + throw new UnsupportedOperationException("Unimplemented method 'getName'"); + } + + @Override + public String getInstanceId() { + throw new UnsupportedOperationException("Unimplemented method 'getInstanceId'"); + } + + @Override + public Instant getCurrentInstant() { + throw new UnsupportedOperationException("Unimplemented method 'getCurrentInstant'"); + } + + @Override + public void complete(Object output) { + throw new UnsupportedOperationException("Unimplemented method 'complete'"); + } + + @Override + public Task waitForExternalEvent(String name, Duration timeout, Class dataType) + throws TaskCanceledException { + throw new UnsupportedOperationException("Unimplemented method 'waitForExternalEvent'"); + } + + @Override + public Task waitForExternalEvent(String name, Duration timeout) throws TaskCanceledException { + throw new UnsupportedOperationException("Unimplemented method 'waitForExternalEvent'"); + } + + @Override + public Task waitForExternalEvent(String name) throws TaskCanceledException { + throw new UnsupportedOperationException("Unimplemented method 'waitForExternalEvent'"); + } + + @Override + public Task callActivity(String name, Object input, TaskOptions options, Class returnType) { + WorkflowActivity activity; + WorkflowActivityContext activityContext; + try { + activity = (WorkflowActivity) Class.forName(name).getDeclaredConstructor().newInstance(); + activityContext = Mockito.mock(WorkflowActivityContext.class); + Mockito.doReturn(input).when(activityContext).getInput(Mockito.any()); + } catch (Exception e) { + fail(e); + return null; + } + + activity.run(activityContext); + return null; + } + + @Override + public boolean isReplaying() { + throw new UnsupportedOperationException("Unimplemented method 'isReplaying'"); + } + + @Override + public Task> allOf(List> tasks) throws CompositeTaskFailedException { + throw new UnsupportedOperationException("Unimplemented method 'allOf'"); + } + + @Override + public Task> anyOf(List> tasks) { + throw new UnsupportedOperationException("Unimplemented method 'anyOf'"); + } + + @Override + public Task createTimer(Duration duration) { + throw new UnsupportedOperationException("Unimplemented method 'createTimer'"); + } + + @Override + public V getInput(Class targetType) { + throw new UnsupportedOperationException("Unimplemented method 'getInput'"); + } + + @Override + public Task callSubWorkflow(String name, Object input, String instanceID, TaskOptions options, + Class returnType) { + throw new UnsupportedOperationException("Unimplemented method 'callSubWorkflow'"); + } + + @Override + public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { + throw new UnsupportedOperationException("Unimplemented method 'continueAsNew'"); + } + + @Override + public Saga getSaga() { + throw new UnsupportedOperationException("Unimplemented method 'getSaga'"); + } + + } } From 551ce5a2740156ce87ed3a11c6c5b0a70f3b4b8d Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Thu, 16 Nov 2023 18:10:05 +0000 Subject: [PATCH 04/10] remove auto register compensation activity on callActivity() Signed-off-by: Sky Ao --- .../workflows/DaprWorkflowContextImpl.java | 24 +---- .../main/java/io/dapr/workflows/Workflow.java | 3 - .../io/dapr/workflows/WorkflowContext.java | 11 ++- .../saga/CompensatableWorkflowActivity.java | 8 +- .../workflows/saga/CompensatationContext.java | 99 ------------------- .../saga/CompensatationInformation.java | 53 ++++++++++ .../java/io/dapr/workflows/saga/Saga.java | 47 ++------- .../workflows/saga/SagaIntegrationTest.java | 71 ++++--------- .../java/io/dapr/workflows/saga/SagaTest.java | 83 +++++----------- 9 files changed, 123 insertions(+), 276 deletions(-) delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationContext.java create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationInformation.java diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java index e68cf1089..1e349ca3b 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java @@ -32,7 +32,6 @@ public class DaprWorkflowContextImpl implements WorkflowContext { private final TaskOrchestrationContext innerContext; private final Logger logger; - private final boolean isSagaEnabled; private final Saga saga; /** @@ -79,14 +78,7 @@ public DaprWorkflowContextImpl(TaskOrchestrationContext context, Logger logger, this.innerContext = context; this.logger = logger; - - if (saga != null) { - this.isSagaEnabled = true; - this.saga = saga; - } else { - this.isSagaEnabled = false; - this.saga = null; - } + this.saga = saga; } /** @@ -182,16 +174,7 @@ public boolean isReplaying() { * {@inheritDoc} */ public Task callActivity(String name, Object input, TaskOptions options, Class returnType) { - Task activityOutput = this.innerContext.callActivity(name, input, options, returnType); - if (this.isSagaEnabled) { - // if saga is enabled and the activity is compensatable, auto register the - // corresponding activity in saga - String compentationActivityClassName = Saga.getCompentationActivityClassName(name); - if (compentationActivityClassName != null && !compentationActivityClassName.isEmpty()) { - saga.registerCompensation(compentationActivityClassName, input, activityOutput); - } - } - return activityOutput; + return this.innerContext.callActivity(name, input, options, returnType); } /** @@ -248,6 +231,9 @@ public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { this.innerContext.continueAsNew(input, preserveUnprocessedEvents); } + /** + * {@inheritDoc} + */ @Override public Saga getSaga() { return this.saga; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java index 99060f4bc..41d76d204 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java @@ -47,16 +47,13 @@ public void run(WorkflowContext ctx) { stub.run(ctx); } else { // saga enabled - System.out.println("============ saga enabled"); try { stub.run(ctx); } catch (OrchestratorBlockedException e) { throw e; } catch (Exception e) { - System.out.println("============ exception"); e.printStackTrace(); try { - System.out.println("============ start compensate"); saga.compensate(ctx); } catch (Exception se) { se.addSuppressed(e); diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java index d48804c27..3927b2ff7 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java @@ -516,8 +516,17 @@ default void continueAsNew(Object input) { */ void continueAsNew(Object input, boolean preserveUnprocessedEvents); + + /** + * is Saga enabled. + * @return true if saga is enabled + */ + default boolean isSagaEnabled() { + return this.getSaga() != null; + } + /** - * get Saga if saga is enabled. + * get Saga. * * @return saga, null if saga is disabled */ diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java index 85b4d2a39..4f50acd24 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java @@ -18,12 +18,6 @@ /** * Interface for a compensatable workflow activity. */ -public interface CompensatableWorkflowActivity { +public interface CompensatableWorkflowActivity extends WorkflowActivity { - /** - * get the compensation activity class. - * - * @return the compensation activity class - */ - Class getCompensationActivity(); } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationContext.java deleted file mode 100644 index 5e8aa3893..000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationContext.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2023 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.workflows.saga; - -/** - * Context for a compensation activity. - */ -public class CompensatationContext { - private String activityClassName; - private Object activityInput; - private Object activityOutput; - - /** - * Default Constructor for a compensation activity. - * - */ - public CompensatationContext() { - } - - /** - * Constructor for a compensation activity. - * - * @param activityClassName Class name of the activity. - * @param activityInput Input of the activity. - * @param activityOutput Output of the activity. - */ - public CompensatationContext(String activityClassName, Object activityInput, - Object activityOutput) { - this.activityClassName = activityClassName; - this.activityInput = activityInput; - this.activityOutput = activityOutput; - } - - /** - * Gets the class name of the activity. - * - * @return the class name of the activity. - */ - public String getActivityClassName() { - return activityClassName; - } - - /** - * Gets the input of the activity. - * - * @return the input of the activity. - */ - public Object getActivityInput() { - return activityInput; - } - - /** - * Gets the output of the activity. - * - * @return the output of the activity. - */ - public Object getActivityOutput() { - return activityOutput; - } - - /** - * set the class name of the activity. - * - * @param activityClassName the class name of the activity. - */ - public void setActivityClassName(String activityClassName) { - this.activityClassName = activityClassName; - } - - /** - * set the input of the activity. - * - * @param activityInput the input of the activity. - */ - public void setActivityInput(Object activityInput) { - this.activityInput = activityInput; - } - - /** - * set the output of the activity. - * - * @param activityOutput the output of the activity. - */ - public void setActivityOutput(Object activityOutput) { - this.activityOutput = activityOutput; - } - -} \ No newline at end of file diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationInformation.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationInformation.java new file mode 100644 index 000000000..85ab67185 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationInformation.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023 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.workflows.saga; + +/** + * Information for a compensation activity. + */ +class CompensatationInformation { + private final String compensatationActivityClassName; + private final Object compensatationActivityInput; + + /** + * Constructor for a compensation information. + * + * @param compensatationActivityClassName Class name of the activity to do + * compensatation. + * @param compensatationActivityInput Input of the activity to do + * compensatation. + */ + public CompensatationInformation(String compensatationActivityClassName, Object compensatationActivityInput) { + this.compensatationActivityClassName = compensatationActivityClassName; + this.compensatationActivityInput = compensatationActivityInput; + } + + /** + * Gets the class name of the activity. + * + * @return the class name of the activity. + */ + public String getCompensatationActivityClassName() { + return compensatationActivityClassName; + } + + /** + * Gets the input of the activity. + * + * @return the input of the activity. + */ + public Object getCompensatationActivityInput() { + return compensatationActivityInput; + } +} \ No newline at end of file diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java index 2053459b0..9e082e67f 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java @@ -26,34 +26,8 @@ import java.util.concurrent.TimeUnit; public final class Saga { - - /** - * get the compensation activity class name for a given workflow activity class - * name. - * - * @param activityClassName workflow activity class name - * @return compensation activity class name, null if not found - */ - public static String getCompentationActivityClassName(String activityClassName) { - try { - Class activityClass = Class.forName(activityClassName); - if (!CompensatableWorkflowActivity.class.isAssignableFrom(activityClass)) { - return null; - } - - // TODO:we have to initialize the activity instance to just get the compensation - // activity class - CompensatableWorkflowActivity compensatableActivity = (CompensatableWorkflowActivity) activityClass - .getDeclaredConstructor() - .newInstance(); - return compensatableActivity.getCompensationActivity().getCanonicalName(); - } catch (Exception e) { - return null; - } - } - private final SagaConfiguration config; - private final List compensationActivities = new ArrayList<>(); + private final List compensationActivities = new ArrayList<>(); /** * Build up a Saga with its config. @@ -72,15 +46,12 @@ public Saga(SagaConfiguration config) { * * @param activityClassName name of the activity class * @param activityInput input of the activity to be compensated - * @param activityOutput output of the activity to be compensated */ - public void registerCompensation(String activityClassName, - Object activityInput, Object activityOutput) { + public void registerCompensation(String activityClassName, Object activityInput) { if (activityClassName == null || activityClassName.isEmpty()) { throw new IllegalArgumentException("activityClassName is required and should not be null or empty."); } - this.compensationActivities.add( - new CompensatationContext(activityClassName, activityInput, activityOutput)); + this.compensationActivities.add(new CompensatationInformation(activityClassName, activityInput)); } /** @@ -93,12 +64,9 @@ public void compensate(WorkflowContext ctx) { // Specical case: when parallel compensation is enabled and there is only one // compensation, we still // compensate sequentially. - System.out.println("============ compensationActivities.size()" + compensationActivities.size()); if (config.isParallelCompensation() && compensationActivities.size() > 1) { - System.out.println("============ start compensateInParallel"); compensateInParallel(ctx); } else { - System.out.println("============ start compensateSequentially"); compensateSequentially(ctx); } } @@ -112,7 +80,7 @@ private void compensateInParallel(WorkflowContext ctx) { ExecutorService executor = Executors.newFixedThreadPool(threadNumber); List> compensationTasks = new ArrayList<>(); - for (CompensatationContext compensationActivity : compensationActivities) { + for (CompensatationInformation compensationActivity : compensationActivities) { Callable compensationTask = new Callable() { @Override public String call() { @@ -170,12 +138,11 @@ private void compensateSequentially(WorkflowContext ctx) { } } - private String executeCompensateActivity(WorkflowContext ctx, CompensatationContext context) + private String executeCompensateActivity(WorkflowContext ctx, CompensatationInformation context) throws SagaCompensationException { - String activityClassName = context.getActivityClassName(); - System.out.println("============ executeCompensateActivity" + activityClassName); + String activityClassName = context.getCompensatationActivityClassName(); try { - Task task = ctx.callActivity(activityClassName, context); + Task task = ctx.callActivity(activityClassName, context.getCompensatationActivityInput()); if (task != null) { task.await(); } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java index 88c24d210..d79c0e5ac 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java @@ -60,10 +60,10 @@ private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) { boolean workflowSuccess = false; // reset count to zero - synchronized(countLock) { + synchronized (countLock) { count = 0; } - + Integer addInput = 100; Integer subtractInput = 20; Integer multiplyInput = 10; @@ -72,10 +72,10 @@ private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) { try { // step1: add activity String result = callActivity(AddActivity.class.getName(), addInput, String.class); - saga.registerCompensation(AddCompentationActivity.class.getName(), addInput, result); + saga.registerCompensation(AddCompentationActivity.class.getName(), addInput); // step2: subtract activity result = callActivity(SubtractActivity.class.getName(), subtractInput, String.class); - saga.registerCompensation(SubtractCompentationActivity.class.getName(), subtractInput, result); + saga.registerCompensation(SubtractCompentationActivity.class.getName(), subtractInput); if (parallelCompensation) { // only add/subtract activities support parallel compensation @@ -83,19 +83,19 @@ private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) { // step3: add activity again result = callActivity(AddActivity.class.getName(), addInput, String.class); - saga.registerCompensation(AddCompentationActivity.class.getName(), addInput, result); + saga.registerCompensation(AddCompentationActivity.class.getName(), addInput); // step4: substract activity again result = callActivity(SubtractActivity.class.getName(), subtractInput, String.class); - saga.registerCompensation(SubtractCompentationActivity.class.getName(), subtractInput, result); + saga.registerCompensation(SubtractCompentationActivity.class.getName(), subtractInput); } else { // step3: multiply activity result = callActivity(MultiplyActivity.class.getName(), multiplyInput, String.class); - saga.registerCompensation(MultiplyCompentationActivity.class.getName(), multiplyInput, result); + saga.registerCompensation(MultiplyCompentationActivity.class.getName(), multiplyInput); // step4: divide activity result = callActivity(DivideActivity.class.getName(), divideInput, String.class); - saga.registerCompensation(DivideCompentationActivity.class.getName(), divideInput, result); + saga.registerCompensation(DivideCompentationActivity.class.getName(), divideInput); } randomFail(); @@ -154,7 +154,7 @@ private static void randomFail() { } } - public static class AddActivity implements WorkflowActivity, CompensatableWorkflowActivity { + public static class AddActivity implements WorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { @@ -173,20 +173,13 @@ public String run(WorkflowActivityContext ctx) { // System.out.println(resultString); return resultString; } - - @Override - public Class getCompensationActivity() { - return AddCompentationActivity.class; - } } - public static class AddCompentationActivity implements WorkflowActivity { + public static class AddCompentationActivity implements CompensatableWorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { - CompensatationContext compensatationContext = ctx.getInput(CompensatationContext.class); - Integer input = (Integer) compensatationContext.getActivityInput(); - String output = (String)compensatationContext.getActivityOutput(); + Integer input = ctx.getInput(Integer.class); int originalCount = 0; int updatedCount = 0; @@ -203,7 +196,7 @@ public String run(WorkflowActivityContext ctx) { } } - public static class SubtractActivity implements WorkflowActivity, CompensatableWorkflowActivity { + public static class SubtractActivity implements WorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { @@ -222,20 +215,13 @@ public String run(WorkflowActivityContext ctx) { // System.out.println(resultString); return resultString; } - - @Override - public Class getCompensationActivity() { - return SubtractCompentationActivity.class; - } } - public static class SubtractCompentationActivity implements WorkflowActivity { + public static class SubtractCompentationActivity implements CompensatableWorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { - CompensatationContext compensatationContext = ctx.getInput(CompensatationContext.class); - Integer input = (Integer) compensatationContext.getActivityInput(); - String output = (String)compensatationContext.getActivityOutput(); + Integer input = ctx.getInput(Integer.class); int originalCount = 0; int updatedCount = 0; @@ -252,7 +238,7 @@ public String run(WorkflowActivityContext ctx) { } } - public static class MultiplyActivity implements WorkflowActivity, CompensatableWorkflowActivity { + public static class MultiplyActivity implements WorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { @@ -271,21 +257,13 @@ public String run(WorkflowActivityContext ctx) { // System.out.println(resultString); return resultString; } - - @Override - public Class getCompensationActivity() { - return MultiplyCompentationActivity.class; - } } - public static class MultiplyCompentationActivity implements WorkflowActivity { + public static class MultiplyCompentationActivity implements CompensatableWorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { - CompensatationContext compensatationContext = ctx.getInput(CompensatationContext.class); - Integer input = (Integer) compensatationContext.getActivityInput(); - String output = (String)compensatationContext.getActivityOutput(); - + Integer input = ctx.getInput(Integer.class); int originalCount = 0; int updatedCount = 0; @@ -302,8 +280,7 @@ public String run(WorkflowActivityContext ctx) { } } - - public static class DivideActivity implements WorkflowActivity, CompensatableWorkflowActivity { + public static class DivideActivity implements WorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { @@ -322,21 +299,13 @@ public String run(WorkflowActivityContext ctx) { // System.out.println(resultString); return resultString; } - - @Override - public Class getCompensationActivity() { - return DivideCompentationActivity.class; - } } - public static class DivideCompentationActivity implements WorkflowActivity { + public static class DivideCompentationActivity implements CompensatableWorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { - CompensatationContext compensatationContext = ctx.getInput(CompensatationContext.class); - Integer input = (Integer) compensatationContext.getActivityInput(); - String output = (String)compensatationContext.getActivityOutput(); - + Integer input = ctx.getInput(Integer.class); int originalCount = 0; int updatedCount = 0; diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java index 3762e0b94..8ab28517c 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java @@ -13,7 +13,6 @@ package io.dapr.workflows.saga; -import static org.junit.Assert.assertNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -49,18 +48,6 @@ private WorkflowContext createMockContext(String name, String id) { return mockContext; } - @Test - public void testGetCompentationActivityClassName() { - String compentationActivityClassName = Saga.getCompentationActivityClassName(MockActivity.class.getName()); - assertEquals(compentationActivityClassName, MockCompentationActivity.class.getCanonicalName()); - - compentationActivityClassName = Saga.getCompentationActivityClassName(MockActivity2.class.getName()); - assertNull(compentationActivityClassName); - - compentationActivityClassName = Saga.getCompentationActivityClassName("not.exist.class"); - assertNull(compentationActivityClassName); - } - @Test public void testSaga_IllegalArgument() { assertThrows(IllegalArgumentException.class, () -> { @@ -75,7 +62,7 @@ public void testregisterCompensation() { .setContinueWithError(true).build(); Saga saga = new Saga(config); - saga.registerCompensation(MockActivity.class.getName(), new MockActivityInput(), new MockActivityOutput()); + saga.registerCompensation(MockActivity.class.getName(), new MockActivityInput()); } @Test @@ -86,10 +73,10 @@ public void testregisterCompensation_IllegalArgument() { Saga saga = new Saga(config); assertThrows(IllegalArgumentException.class, () -> { - saga.registerCompensation(null, "input", "output"); + saga.registerCompensation(null, "input"); }); assertThrows(IllegalArgumentException.class, () -> { - saga.registerCompensation("", "input", "output"); + saga.registerCompensation("", "input"); }); } @@ -102,13 +89,13 @@ public void testCompensateInParallel() { Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); - saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3); saga.compensate(new MockWorkflowContext()); @@ -124,14 +111,14 @@ public void testCompensateInParallel_exception() { Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); input2.setThrowException(true); - saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { saga.compensate(new MockWorkflowContext()); @@ -151,15 +138,15 @@ public void testCompensateInParallel_exception_suppressed() { Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); input2.setThrowException(true); - saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); input3.setThrowException(true); - saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { saga.compensate(new MockWorkflowContext()); @@ -179,13 +166,13 @@ public void testCompensateSequentially() { Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); - saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3); saga.compensate(new MockWorkflowContext()); @@ -208,14 +195,14 @@ public void testCompensateSequentially_continueWithError() { Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); input2.setThrowException(true); - saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { saga.compensate(new MockWorkflowContext()); @@ -241,15 +228,15 @@ public void testCompensateSequentially_continueWithError_suppressed() { Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); input2.setThrowException(true); - saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); input3.setThrowException(true); - saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { saga.compensate(new MockWorkflowContext()); @@ -274,14 +261,14 @@ public void testCompensateSequentially_notContinueWithError() { Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); input1.setOrder(1); - saga.registerCompensation(MockCompentationActivity.class.getName(), input1, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1); MockActivityInput input2 = new MockActivityInput(); input2.setOrder(2); input2.setThrowException(true); - saga.registerCompensation(MockCompentationActivity.class.getName(), input2, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2); MockActivityInput input3 = new MockActivityInput(); input3.setOrder(3); - saga.registerCompensation(MockCompentationActivity.class.getName(), input3, new MockActivityOutput()); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { saga.compensate(new MockWorkflowContext()); @@ -295,22 +282,7 @@ public void testCompensateSequentially_notContinueWithError() { assertEquals(Integer.valueOf(3), MockCompentationActivity.compensateOrder.get(0)); } - public static class MockActivity implements WorkflowActivity, CompensatableWorkflowActivity { - - @Override - public Class getCompensationActivity() { - return MockCompentationActivity.class; - } - - @Override - public Object run(WorkflowActivityContext ctx) { - MockActivityOutput output = new MockActivityOutput(); - output.setSucceed(true); - return output; - } - } - - public static class MockActivity2 implements WorkflowActivity { + public static class MockActivity implements WorkflowActivity { @Override public Object run(WorkflowActivityContext ctx) { @@ -320,14 +292,13 @@ public Object run(WorkflowActivityContext ctx) { } } - public static class MockCompentationActivity implements WorkflowActivity { + public static class MockCompentationActivity implements CompensatableWorkflowActivity { private static List compensateOrder = Collections.synchronizedList(new ArrayList<>()); @Override public Object run(WorkflowActivityContext ctx) { - CompensatationContext compentationContext = ctx.getInput(CompensatationContext.class); - MockActivityInput input = (MockActivityInput) compentationContext.getActivityInput(); + MockActivityInput input = ctx.getInput(MockActivityInput.class); if (input.isThrowException()) { throw new RuntimeException("compensate failed: order=" + input.getOrder()); From fb9470a53424b6ff5f2a4eaba4bbcd1698ab1194 Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Fri, 17 Nov 2023 02:14:26 +0000 Subject: [PATCH 05/10] rollback COVEREDRATIO to 80% Signed-off-by: Sky Ao --- sdk-workflows/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-workflows/pom.xml b/sdk-workflows/pom.xml index 0005d27ef..ba21f677f 100644 --- a/sdk-workflows/pom.xml +++ b/sdk-workflows/pom.xml @@ -143,7 +143,7 @@ LINE COVEREDRATIO - 60% + 80% From 5d5ad6a2a5b1a5aa54c47a0925d0fad31ded7b42 Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Tue, 21 Nov 2023 07:01:44 +0000 Subject: [PATCH 06/10] improve code implementation accordings to proposal Signed-off-by: Sky Ao --- .../workflows/DaprWorkflowContextImpl.java | 20 +++++++++---- .../main/java/io/dapr/workflows/Workflow.java | 23 ++++++++------- .../io/dapr/workflows/WorkflowContext.java | 16 ++++------ .../runtime/OrchestratorWrapper.java | 4 +-- .../java/io/dapr/workflows/saga/Saga.java | 29 +++++++++---------- ...SagaConfiguration.java => SagaOption.java} | 14 ++++----- .../DaprWorkflowContextImplTest.java | 22 ++++++++++++++ .../workflows/saga/SagaIntegrationTest.java | 2 +- ...igurationTest.java => SagaOptionTest.java} | 24 +++++++-------- .../java/io/dapr/workflows/saga/SagaTest.java | 27 ++++++++++------- 10 files changed, 108 insertions(+), 73 deletions(-) rename sdk-workflows/src/main/java/io/dapr/workflows/saga/{SagaConfiguration.java => SagaOption.java} (87%) rename sdk-workflows/src/test/java/io/dapr/workflows/saga/{SagaConfigurationTest.java => SagaOptionTest.java} (52%) diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java index 9ee215a49..25fef9470 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java @@ -240,11 +240,21 @@ public UUID newUuid() { return this.innerContext.newUUID(); } - /** - * {@inheritDoc} - */ @Override - public Saga getSaga() { - return this.saga; + public void registerCompensation(String activityClassName, Object activityInput) { + if (this.saga == null) { + throw new UnsupportedOperationException("Saga is not enabled"); + } + + this.saga.registerCompensation(activityClassName, activityInput); + } + + @Override + public void compensate() { + if (this.saga == null) { + throw new UnsupportedOperationException("Saga is not enabled"); + } + + this.saga.compensate(this); } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java index b8115533c..b4d3a42ae 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java @@ -14,8 +14,8 @@ package io.dapr.workflows; import com.microsoft.durabletask.interruption.OrchestratorBlockedException; -import io.dapr.workflows.saga.Saga; -import io.dapr.workflows.saga.SagaConfiguration; +import io.dapr.workflows.saga.SagaCompensationException; +import io.dapr.workflows.saga.SagaOption; /** * Common interface for workflow implementations. @@ -41,8 +41,7 @@ public Workflow() { public void run(WorkflowContext ctx) { WorkflowStub stub = this.create(); - Saga saga = ctx.getSaga(); - if (saga == null) { + if (!this.isSagaEnabled()) { // saga disabled stub.run(ctx); } else { @@ -51,29 +50,33 @@ public void run(WorkflowContext ctx) { stub.run(ctx); } catch (OrchestratorBlockedException e) { throw e; + } catch (SagaCompensationException e) { + // Saga compensation is triggered gracefully but failed in exception + // don't need to trigger compensation again + throw e; } catch (Exception e) { - e.printStackTrace(); try { - saga.compensate(ctx); + ctx.compensate(); } catch (Exception se) { se.addSuppressed(e); throw se; } - // TODO: should we complete the workflow here, or just re-throw the exception - // ctx.complete(...); - // throw new RuntimeException(e); throw e; } } } + public boolean isSagaEnabled() { + return this.getSagaOption() != null; + } + /** * get saga configuration. * * @return saga configuration */ - public SagaConfiguration getSagaConfiguration() { + public SagaOption getSagaOption() { return null; } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java index 13cac6930..bc8b8cb3a 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java @@ -533,20 +533,16 @@ default UUID newUuid() { } /** - * is Saga enabled. + * Register a compensation activity. * - * @return true if saga is enabled + * @param activityClassName name of the activity class + * @param activityInput input of the activity to be compensated */ - default boolean isSagaEnabled() { - return this.getSaga() != null; - } + void registerCompensation(String activityClassName, Object activityInput); /** - * get Saga. + * Compensate all registered activities. * - * @return saga, null if saga is disabled */ - default Saga getSaga() { - return null; - } + void compensate(); } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/OrchestratorWrapper.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/OrchestratorWrapper.java index c97d7122f..d104c9c3e 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/OrchestratorWrapper.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/OrchestratorWrapper.java @@ -57,8 +57,8 @@ public TaskOrchestration create() { ); } - if (workflow.getSagaConfiguration() != null) { - Saga saga = new Saga(workflow.getSagaConfiguration()); + if (workflow.getSagaOption() != null) { + Saga saga = new Saga(workflow.getSagaOption()); workflow.run(new DaprWorkflowContextImpl(ctx, saga)); } else { workflow.run(new DaprWorkflowContextImpl(ctx)); diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java index 6e8c391f8..c471e03d3 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java @@ -26,19 +26,19 @@ import java.util.concurrent.TimeUnit; public final class Saga { - private final SagaConfiguration config; + private final SagaOption option; private final List compensationActivities = new ArrayList<>(); /** - * Build up a Saga with its config. + * Build up a Saga with its options. * - * @param config Saga configuration. + * @param option Saga option. */ - public Saga(SagaConfiguration config) { - if (config == null) { - throw new IllegalArgumentException("config is required and should not be null."); + public Saga(SagaOption option) { + if (option == null) { + throw new IllegalArgumentException("option is required and should not be null."); } - this.config = config; + this.option = option; } /** @@ -64,7 +64,7 @@ public void compensate(WorkflowContext ctx) { // Specical case: when parallel compensation is enabled and there is only one // compensation, we still // compensate sequentially. - if (config.isParallelCompensation() && compensationActivities.size() > 1) { + if (option.isParallelCompensation() && compensationActivities.size() > 1) { compensateInParallel(ctx); } else { compensateSequentially(ctx); @@ -74,8 +74,8 @@ public void compensate(WorkflowContext ctx) { private void compensateInParallel(WorkflowContext ctx) { // thread number should be limited by maxParallelThread int threadNumber = compensationActivities.size(); - if (threadNumber > config.getMaxParallelThread()) { - threadNumber = config.getMaxParallelThread(); + if (threadNumber > option.getMaxParallelThread()) { + threadNumber = option.getMaxParallelThread(); } ExecutorService executor = Executors.newFixedThreadPool(threadNumber); @@ -127,7 +127,7 @@ private void compensateSequentially(WorkflowContext ctx) { sagaException.addSuppressed(e); } - if (!config.isContinueWithError()) { + if (!option.isContinueWithError()) { throw sagaException; } } @@ -138,11 +138,11 @@ private void compensateSequentially(WorkflowContext ctx) { } } - private String executeCompensateActivity(WorkflowContext ctx, CompensatationInformation context) + private String executeCompensateActivity(WorkflowContext ctx, CompensatationInformation info) throws SagaCompensationException { - String activityClassName = context.getCompensatationActivityClassName(); + String activityClassName = info.getCompensatationActivityClassName(); try { - Task task = ctx.callActivity(activityClassName, context.getCompensatationActivityInput()); + Task task = ctx.callActivity(activityClassName, info.getCompensatationActivityInput()); if (task != null) { task.await(); } @@ -151,7 +151,6 @@ private String executeCompensateActivity(WorkflowContext ctx, CompensatationInfo } catch (OrchestratorBlockedException e) { throw e; } catch (Exception e) { - e.printStackTrace(); throw new SagaCompensationException("Exception in saga compensatation: activity=" + activityClassName, e); } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaConfiguration.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaOption.java similarity index 87% rename from sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaConfiguration.java rename to sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaOption.java index e9aa396c9..b13b2af77 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaConfiguration.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaOption.java @@ -14,14 +14,14 @@ package io.dapr.workflows.saga; /** - * Saga configuration. + * Saga option. */ -public final class SagaConfiguration { +public final class SagaOption { private final boolean parallelCompensation; private final int maxParallelThread; private final boolean continueWithError; - private SagaConfiguration(boolean parallelCompensation, int maxParallelThread, boolean continueWithError) { + private SagaOption(boolean parallelCompensation, int maxParallelThread, boolean continueWithError) { this.parallelCompensation = parallelCompensation; this.maxParallelThread = maxParallelThread; this.continueWithError = continueWithError; @@ -92,11 +92,11 @@ public Builder setContinueWithError(boolean continueWithError) { } /** - * Build Saga configuration. - * @return Saga configuration + * Build Saga optiion. + * @return Saga optiion */ - public SagaConfiguration build() { - return new SagaConfiguration(this.parallelCompensation, this.maxParallelThread, this.continueWithError); + public SagaOption build() { + return new SagaOption(this.parallelCompensation, this.maxParallelThread, this.continueWithError); } } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java index e7e2f6e3c..d8ac1c8b4 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java @@ -130,6 +130,14 @@ public Task callSubWorkflow(String name, @Nullable Object input, @Nullabl public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { } + + @Override + public void registerCompensation(String activityClassName, Object activityInput) { + } + + @Override + public void compensate() { + } }; } @@ -309,4 +317,18 @@ public void newUuidTestNoImplementationExceptionTest() { String expectedMessage = "No implementation found."; assertEquals(expectedMessage, runtimeException.getMessage()); } + + @Test + public void registerCompensationTest_unsupported() { + assertThrows(UnsupportedOperationException.class, () -> { + context.registerCompensation(null, "TestInput"); + }); + } + + @Test + public void compensateTest_unsupported() { + assertThrows(UnsupportedOperationException.class, () -> { + context.compensate(); + }); + } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java index d79c0e5ac..b4fd0b225 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java @@ -53,7 +53,7 @@ public void testSaga_compensateInParallel() { } private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) { - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(parallelCompensation) .setContinueWithError(true).build(); Saga saga = new Saga(config); diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaConfigurationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaOptionTest.java similarity index 52% rename from sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaConfigurationTest.java rename to sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaOptionTest.java index acfd3236e..996f199dc 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaConfigurationTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaOptionTest.java @@ -5,34 +5,34 @@ import org.junit.Test; -public class SagaConfigurationTest { +public class SagaOptionTest { @Test public void testBuild() { - SagaConfiguration.Builder builder = SagaConfiguration.newBuilder(); + SagaOption.Builder builder = SagaOption.newBuilder(); builder.setParallelCompensation(true); builder.setMaxParallelThread(32); builder.setContinueWithError(false); - SagaConfiguration config = builder.build(); + SagaOption option = builder.build(); - assertEquals(true, config.isParallelCompensation()); - assertEquals(32, config.getMaxParallelThread()); - assertEquals(false, config.isContinueWithError()); + assertEquals(true, option.isParallelCompensation()); + assertEquals(32, option.getMaxParallelThread()); + assertEquals(false, option.isContinueWithError()); } @Test public void testBuild_default() { - SagaConfiguration.Builder builder = SagaConfiguration.newBuilder(); - SagaConfiguration config = builder.build(); + SagaOption.Builder builder = SagaOption.newBuilder(); + SagaOption option = builder.build(); - assertEquals(false, config.isParallelCompensation()); - assertEquals(16, config.getMaxParallelThread()); - assertEquals(true, config.isContinueWithError()); + assertEquals(false, option.isParallelCompensation()); + assertEquals(16, option.getMaxParallelThread()); + assertEquals(true, option.isContinueWithError()); } @Test public void testsetMaxParallelThread() { - SagaConfiguration.Builder builder = SagaConfiguration.newBuilder(); + SagaOption.Builder builder = SagaOption.newBuilder(); assertThrows(IllegalArgumentException.class, () -> { builder.setMaxParallelThread(0); diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java index 8ab28517c..e99e2ff9c 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java @@ -57,7 +57,7 @@ public void testSaga_IllegalArgument() { @Test public void testregisterCompensation() { - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(false) .setContinueWithError(true).build(); Saga saga = new Saga(config); @@ -67,7 +67,7 @@ public void testregisterCompensation() { @Test public void testregisterCompensation_IllegalArgument() { - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(false) .setContinueWithError(true).build(); Saga saga = new Saga(config); @@ -84,7 +84,7 @@ public void testregisterCompensation_IllegalArgument() { public void testCompensateInParallel() { MockCompentationActivity.compensateOrder.clear(); - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(true).build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); @@ -106,7 +106,7 @@ public void testCompensateInParallel() { public void testCompensateInParallel_exception() { MockCompentationActivity.compensateOrder.clear(); - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(true).build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); @@ -133,7 +133,7 @@ public void testCompensateInParallel_exception() { public void testCompensateInParallel_exception_suppressed() { MockCompentationActivity.compensateOrder.clear(); - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(true).build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); @@ -161,7 +161,7 @@ public void testCompensateInParallel_exception_suppressed() { public void testCompensateSequentially() { MockCompentationActivity.compensateOrder.clear(); - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(false).build(); Saga saga = new Saga(config); MockActivityInput input1 = new MockActivityInput(); @@ -188,7 +188,7 @@ public void testCompensateSequentially() { public void testCompensateSequentially_continueWithError() { MockCompentationActivity.compensateOrder.clear(); - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(false) .setContinueWithError(true) .build(); @@ -221,7 +221,7 @@ public void testCompensateSequentially_continueWithError() { public void testCompensateSequentially_continueWithError_suppressed() { MockCompentationActivity.compensateOrder.clear(); - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(false) .setContinueWithError(true) .build(); @@ -254,7 +254,7 @@ public void testCompensateSequentially_continueWithError_suppressed() { public void testCompensateSequentially_notContinueWithError() { MockCompentationActivity.compensateOrder.clear(); - SagaConfiguration config = SagaConfiguration.newBuilder() + SagaOption config = SagaOption.newBuilder() .setParallelCompensation(false) .setContinueWithError(false) .build(); @@ -439,8 +439,13 @@ public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { } @Override - public Saga getSaga() { - throw new UnsupportedOperationException("Unimplemented method 'getSaga'"); + public void registerCompensation(String activityClassName, Object activityInput) { + throw new UnsupportedOperationException("Unimplemented method 'registerCompensation'"); + } + + @Override + public void compensate() { + throw new UnsupportedOperationException("Unimplemented method 'compensate'"); } } From 490e9ff72db078c4eeb73ac389b03307d68df18f Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Fri, 1 Dec 2023 10:28:45 +0800 Subject: [PATCH 07/10] use ctx.allOf() to do compensation in parallel Signed-off-by: Sky Ao --- .../saga/CompensatableWorkflowActivity.java | 23 -- .../saga/CompensatationInformation.java | 17 +- .../java/io/dapr/workflows/saga/Saga.java | 86 +++---- .../workflows/saga/SagaIntegrationTest.java | 10 +- .../java/io/dapr/workflows/saga/SagaTest.java | 222 +++++++++--------- 5 files changed, 162 insertions(+), 196 deletions(-) delete mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java deleted file mode 100644 index 4f50acd24..000000000 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatableWorkflowActivity.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2023 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.workflows.saga; - -import io.dapr.workflows.runtime.WorkflowActivity; - -/** - * Interface for a compensatable workflow activity. - */ -public interface CompensatableWorkflowActivity extends WorkflowActivity { - -} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationInformation.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationInformation.java index 85ab67185..cf0fe202c 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationInformation.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/CompensatationInformation.java @@ -13,12 +13,15 @@ package io.dapr.workflows.saga; +import com.microsoft.durabletask.TaskOptions; + /** * Information for a compensation activity. */ class CompensatationInformation { private final String compensatationActivityClassName; private final Object compensatationActivityInput; + private final TaskOptions taskOptions; /** * Constructor for a compensation information. @@ -27,10 +30,13 @@ class CompensatationInformation { * compensatation. * @param compensatationActivityInput Input of the activity to do * compensatation. + * @param taskOptions task options to set retry strategy */ - public CompensatationInformation(String compensatationActivityClassName, Object compensatationActivityInput) { + public CompensatationInformation(String compensatationActivityClassName, + Object compensatationActivityInput, TaskOptions taskOptions) { this.compensatationActivityClassName = compensatationActivityClassName; this.compensatationActivityInput = compensatationActivityInput; + this.taskOptions = taskOptions; } /** @@ -50,4 +56,13 @@ public String getCompensatationActivityClassName() { public Object getCompensatationActivityInput() { return compensatationActivityInput; } + + /** + * get task options. + * + * @return task options, null if not set + */ + public TaskOptions getTaskOptions() { + return taskOptions; + } } \ No newline at end of file diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java index c471e03d3..19ff02437 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java @@ -14,16 +14,12 @@ package io.dapr.workflows.saga; import com.microsoft.durabletask.Task; +import com.microsoft.durabletask.TaskOptions; import com.microsoft.durabletask.interruption.OrchestratorBlockedException; import io.dapr.workflows.WorkflowContext; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; public final class Saga { private final SagaOption option; @@ -48,10 +44,21 @@ public Saga(SagaOption option) { * @param activityInput input of the activity to be compensated */ public void registerCompensation(String activityClassName, Object activityInput) { + this.registerCompensation(activityClassName, activityInput, null); + } + + /** + * Register a compensation activity. + * + * @param activityClassName name of the activity class + * @param activityInput input of the activity to be compensated + * @param taskOptions task options to set retry strategy + */ + public void registerCompensation(String activityClassName, Object activityInput, TaskOptions taskOptions) { if (activityClassName == null || activityClassName.isEmpty()) { throw new IllegalArgumentException("activityClassName is required and should not be null or empty."); } - this.compensationActivities.add(new CompensatationInformation(activityClassName, activityInput)); + this.compensationActivities.add(new CompensatationInformation(activityClassName, activityInput, taskOptions)); } /** @@ -72,57 +79,32 @@ public void compensate(WorkflowContext ctx) { } private void compensateInParallel(WorkflowContext ctx) { - // thread number should be limited by maxParallelThread - int threadNumber = compensationActivities.size(); - if (threadNumber > option.getMaxParallelThread()) { - threadNumber = option.getMaxParallelThread(); - } - - ExecutorService executor = Executors.newFixedThreadPool(threadNumber); - List> compensationTasks = new ArrayList<>(); + List> tasks = new ArrayList<>(compensationActivities.size()); for (CompensatationInformation compensationActivity : compensationActivities) { - Callable compensationTask = new Callable() { - @Override - public String call() { - return executeCompensateActivity(ctx, compensationActivity); - } - }; - compensationTasks.add(compensationTask); + Task task = executeCompensateActivity(ctx, compensationActivity); + tasks.add(task); } - List> resultFutures; try { - // TBD: hard code timeout to 60 seconds in the first version - resultFutures = executor.invokeAll(compensationTasks, 60, TimeUnit.SECONDS); - } catch (InterruptedException e) { + ctx.allOf(tasks).await(); + } catch (Exception e) { throw new SagaCompensationException("Failed to compensate in parallel.", e); } - SagaCompensationException sagaException = null; - for (Future resultFuture : resultFutures) { - try { - resultFuture.get(); - } catch (Exception e) { - if (sagaException == null) { - sagaException = new SagaCompensationException("Failed to compensate in parallel.", e); - } else { - sagaException.addSuppressed(e); - } - } - } - - if (sagaException != null) { - throw sagaException; - } } private void compensateSequentially(WorkflowContext ctx) { SagaCompensationException sagaException = null; for (int i = compensationActivities.size() - 1; i >= 0; i--) { + String activityClassName = compensationActivities.get(i).getCompensatationActivityClassName(); try { - executeCompensateActivity(ctx, compensationActivities.get(i)); - } catch (SagaCompensationException e) { + executeCompensateActivity(ctx, compensationActivities.get(i)).await(); + } catch (OrchestratorBlockedException e) { + throw e; + } catch (Exception e) { if (sagaException == null) { - sagaException = e; + sagaException = new SagaCompensationException( + "Exception in saga compensatation: activity=" + activityClassName, e); + ; } else { sagaException.addSuppressed(e); } @@ -138,20 +120,10 @@ private void compensateSequentially(WorkflowContext ctx) { } } - private String executeCompensateActivity(WorkflowContext ctx, CompensatationInformation info) + private Task executeCompensateActivity(WorkflowContext ctx, CompensatationInformation info) throws SagaCompensationException { String activityClassName = info.getCompensatationActivityClassName(); - try { - Task task = ctx.callActivity(activityClassName, info.getCompensatationActivityInput()); - if (task != null) { - task.await(); - } - // return activityClassName for logs and tracing - return activityClassName; - } catch (OrchestratorBlockedException e) { - throw e; - } catch (Exception e) { - throw new SagaCompensationException("Exception in saga compensatation: activity=" + activityClassName, e); - } + return ctx.callActivity(activityClassName, info.getCompensatationActivityInput(), + info.getTaskOptions()); } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java index b4fd0b225..0a39d64f2 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaIntegrationTest.java @@ -102,7 +102,7 @@ private boolean doExecuteWorkflowWithSaga(boolean parallelCompensation) { workflowSuccess = true; } catch (Exception e) { - saga.compensate(new SagaTest.MockWorkflowContext()); + saga.compensate(SagaTest.createMockContext()); } if (workflowSuccess) { @@ -175,7 +175,7 @@ public String run(WorkflowActivityContext ctx) { } } - public static class AddCompentationActivity implements CompensatableWorkflowActivity { + public static class AddCompentationActivity implements WorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { @@ -217,7 +217,7 @@ public String run(WorkflowActivityContext ctx) { } } - public static class SubtractCompentationActivity implements CompensatableWorkflowActivity { + public static class SubtractCompentationActivity implements WorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { @@ -259,7 +259,7 @@ public String run(WorkflowActivityContext ctx) { } } - public static class MultiplyCompentationActivity implements CompensatableWorkflowActivity { + public static class MultiplyCompentationActivity implements WorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { @@ -301,7 +301,7 @@ public String run(WorkflowActivityContext ctx) { } } - public static class DivideCompentationActivity implements CompensatableWorkflowActivity { + public static class DivideCompentationActivity implements WorkflowActivity { @Override public String run(WorkflowActivityContext ctx) { diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java index e99e2ff9c..314565509 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/SagaTest.java @@ -10,28 +10,34 @@ * See the License for the specific language governing permissions and limitations under the License. */ - package io.dapr.workflows.saga; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -import java.time.Duration; -import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.junit.Test; import org.mockito.Mockito; -import org.slf4j.Logger; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; -import com.microsoft.durabletask.CompositeTaskFailedException; import com.microsoft.durabletask.Task; -import com.microsoft.durabletask.TaskCanceledException; import com.microsoft.durabletask.TaskOptions; import io.dapr.workflows.WorkflowContext; @@ -40,12 +46,12 @@ public class SagaTest { - private WorkflowContext createMockContext(String name, String id) { - WorkflowContext mockContext = mock(WorkflowContext.class); + public static WorkflowContext createMockContext() { + WorkflowContext workflowContext = mock(WorkflowContext.class); + when(workflowContext.callActivity(anyString(), any(), eq((TaskOptions) null))).thenAnswer(new ActivityAnswer()); + when(workflowContext.allOf(anyList())).thenAnswer(new AllActivityAnswer()); - Mockito.doReturn(name).when(mockContext).getName(); - Mockito.doReturn(id).when(mockContext).getInstanceId(); - return mockContext; + return workflowContext; } @Test @@ -97,13 +103,13 @@ public void testCompensateInParallel() { input3.setOrder(3); saga.registerCompensation(MockCompentationActivity.class.getName(), input3); - saga.compensate(new MockWorkflowContext()); + saga.compensate(createMockContext()); assertEquals(3, MockCompentationActivity.compensateOrder.size()); } @Test - public void testCompensateInParallel_exception() { + public void testCompensateInParallel_exception_1failed() { MockCompentationActivity.compensateOrder.clear(); SagaOption config = SagaOption.newBuilder() @@ -121,7 +127,7 @@ public void testCompensateInParallel_exception() { saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { - saga.compensate(new MockWorkflowContext()); + saga.compensate(createMockContext()); }); assertNotNull(exception.getCause()); // 3 compentation activities, 2 succeed, 1 failed @@ -130,7 +136,7 @@ public void testCompensateInParallel_exception() { } @Test - public void testCompensateInParallel_exception_suppressed() { + public void testCompensateInParallel_exception_2failed() { MockCompentationActivity.compensateOrder.clear(); SagaOption config = SagaOption.newBuilder() @@ -149,14 +155,41 @@ public void testCompensateInParallel_exception_suppressed() { saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { - saga.compensate(new MockWorkflowContext()); + saga.compensate(createMockContext()); }); assertNotNull(exception.getCause()); // 3 compentation activities, 1 succeed, 2 failed - assertEquals(1, exception.getSuppressed().length); assertEquals(1, MockCompentationActivity.compensateOrder.size()); } + @Test + public void testCompensateInParallel_exception_3failed() { + MockCompentationActivity.compensateOrder.clear(); + + SagaOption config = SagaOption.newBuilder() + .setParallelCompensation(true).build(); + Saga saga = new Saga(config); + MockActivityInput input1 = new MockActivityInput(); + input1.setOrder(1); + input1.setThrowException(true); + saga.registerCompensation(MockCompentationActivity.class.getName(), input1); + MockActivityInput input2 = new MockActivityInput(); + input2.setOrder(2); + input2.setThrowException(true); + saga.registerCompensation(MockCompentationActivity.class.getName(), input2); + MockActivityInput input3 = new MockActivityInput(); + input3.setOrder(3); + input3.setThrowException(true); + saga.registerCompensation(MockCompentationActivity.class.getName(), input3); + + SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { + saga.compensate(createMockContext()); + }); + assertNotNull(exception.getCause()); + // 3 compentation activities, 0 succeed, 3 failed + assertEquals(0, MockCompentationActivity.compensateOrder.size()); + } + @Test public void testCompensateSequentially() { MockCompentationActivity.compensateOrder.clear(); @@ -174,7 +207,7 @@ public void testCompensateSequentially() { input3.setOrder(3); saga.registerCompensation(MockCompentationActivity.class.getName(), input3); - saga.compensate(new MockWorkflowContext()); + saga.compensate(createMockContext()); assertEquals(3, MockCompentationActivity.compensateOrder.size()); @@ -205,7 +238,7 @@ public void testCompensateSequentially_continueWithError() { saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { - saga.compensate(new MockWorkflowContext()); + saga.compensate(createMockContext()); }); assertNotNull(exception.getCause()); assertEquals(0, exception.getSuppressed().length); @@ -239,7 +272,7 @@ public void testCompensateSequentially_continueWithError_suppressed() { saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { - saga.compensate(new MockWorkflowContext()); + saga.compensate(createMockContext()); }); assertNotNull(exception.getCause()); assertEquals(1, exception.getSuppressed().length); @@ -271,7 +304,7 @@ public void testCompensateSequentially_notContinueWithError() { saga.registerCompensation(MockCompentationActivity.class.getName(), input3); SagaCompensationException exception = assertThrows(SagaCompensationException.class, () -> { - saga.compensate(new MockWorkflowContext()); + saga.compensate(createMockContext()); }); assertNotNull(exception.getCause()); assertEquals(0, exception.getSuppressed().length); @@ -292,7 +325,7 @@ public Object run(WorkflowActivityContext ctx) { } } - public static class MockCompentationActivity implements CompensatableWorkflowActivity { + public static class MockCompentationActivity implements WorkflowActivity { private static List compensateOrder = Collections.synchronizedList(new ArrayList<>()); @@ -342,111 +375,80 @@ public void setSucceed(boolean succeed) { } } - public static class MockWorkflowContext implements WorkflowContext { - - @Override - public Logger getLogger() { - throw new UnsupportedOperationException("Unimplemented method 'getLogger'"); - } - - @Override - public String getName() { - throw new UnsupportedOperationException("Unimplemented method 'getName'"); - } - - @Override - public String getInstanceId() { - throw new UnsupportedOperationException("Unimplemented method 'getInstanceId'"); - } - - @Override - public Instant getCurrentInstant() { - throw new UnsupportedOperationException("Unimplemented method 'getCurrentInstant'"); - } - - @Override - public void complete(Object output) { - throw new UnsupportedOperationException("Unimplemented method 'complete'"); - } + public static class ActivityAnswer implements Answer> { @Override - public Task waitForExternalEvent(String name, Duration timeout, Class dataType) - throws TaskCanceledException { - throw new UnsupportedOperationException("Unimplemented method 'waitForExternalEvent'"); - } + public Task answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + String name = (String) args[0]; + Object input = args[1]; - @Override - public Task waitForExternalEvent(String name, Duration timeout) throws TaskCanceledException { - throw new UnsupportedOperationException("Unimplemented method 'waitForExternalEvent'"); - } - - @Override - public Task waitForExternalEvent(String name) throws TaskCanceledException { - throw new UnsupportedOperationException("Unimplemented method 'waitForExternalEvent'"); - } - - @Override - public Task callActivity(String name, Object input, TaskOptions options, Class returnType) { WorkflowActivity activity; - WorkflowActivityContext activityContext; + WorkflowActivityContext activityContext = Mockito.mock(WorkflowActivityContext.class); try { activity = (WorkflowActivity) Class.forName(name).getDeclaredConstructor().newInstance(); - activityContext = Mockito.mock(WorkflowActivityContext.class); - Mockito.doReturn(input).when(activityContext).getInput(Mockito.any()); } catch (Exception e) { fail(e); return null; } - activity.run(activityContext); - return null; - } - - @Override - public boolean isReplaying() { - throw new UnsupportedOperationException("Unimplemented method 'isReplaying'"); - } - - @Override - public Task> allOf(List> tasks) throws CompositeTaskFailedException { - throw new UnsupportedOperationException("Unimplemented method 'allOf'"); - } - - @Override - public Task> anyOf(List> tasks) { - throw new UnsupportedOperationException("Unimplemented method 'anyOf'"); - } - - @Override - public Task createTimer(Duration duration) { - throw new UnsupportedOperationException("Unimplemented method 'createTimer'"); - } - - @Override - public V getInput(Class targetType) { - throw new UnsupportedOperationException("Unimplemented method 'getInput'"); + Task task = mock(Task.class); + when(task.await()).thenAnswer(invocation1 -> { + Mockito.doReturn(input).when(activityContext).getInput(Mockito.any()); + activity.run(activityContext); + return null; + }); + return task; } - @Override - public Task callSubWorkflow(String name, Object input, String instanceID, TaskOptions options, - Class returnType) { - throw new UnsupportedOperationException("Unimplemented method 'callSubWorkflow'"); - } + } + public static class AllActivityAnswer implements Answer> { @Override - public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { - throw new UnsupportedOperationException("Unimplemented method 'continueAsNew'"); - } + public Task answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + List> tasks = (List>) args[0]; + + ExecutorService executor = Executors.newFixedThreadPool(5); + List> compensationTasks = new ArrayList<>(); + for (Task task : tasks) { + Callable compensationTask = new Callable() { + @Override + public Void call() { + return task.await(); + } + }; + compensationTasks.add(compensationTask); + } - @Override - public void registerCompensation(String activityClassName, Object activityInput) { - throw new UnsupportedOperationException("Unimplemented method 'registerCompensation'"); - } + List> resultFutures; + try { + resultFutures = executor.invokeAll(compensationTasks, 2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + fail(e); + return null; + } - @Override - public void compensate() { - throw new UnsupportedOperationException("Unimplemented method 'compensate'"); + Task task = mock(Task.class); + when(task.await()).thenAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Exception exception = null; + for (Future resultFuture : resultFutures) { + try { + resultFuture.get(); + } catch (Exception e) { + exception = e; + } + } + if (exception != null) { + throw exception; + } + return null; + } + }); + return task; } - } + } From f8ca83a06831568310165ffc307939eeecdae846 Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Tue, 9 Jan 2024 06:29:24 +0000 Subject: [PATCH 08/10] add code to handle ContinueAsNewInterruption exception for saga compensation Signed-off-by: Sky Ao --- sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java | 3 ++- sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java index b4d3a42ae..51867c893 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java @@ -13,6 +13,7 @@ package io.dapr.workflows; +import com.microsoft.durabletask.interruption.ContinueAsNewInterruption; import com.microsoft.durabletask.interruption.OrchestratorBlockedException; import io.dapr.workflows.saga.SagaCompensationException; import io.dapr.workflows.saga.SagaOption; @@ -48,7 +49,7 @@ public void run(WorkflowContext ctx) { // saga enabled try { stub.run(ctx); - } catch (OrchestratorBlockedException e) { + } catch (OrchestratorBlockedException | ContinueAsNewInterruption e) { throw e; } catch (SagaCompensationException e) { // Saga compensation is triggered gracefully but failed in exception diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java index 19ff02437..f2a151b9e 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/Saga.java @@ -15,6 +15,7 @@ import com.microsoft.durabletask.Task; import com.microsoft.durabletask.TaskOptions; +import com.microsoft.durabletask.interruption.ContinueAsNewInterruption; import com.microsoft.durabletask.interruption.OrchestratorBlockedException; import io.dapr.workflows.WorkflowContext; @@ -98,7 +99,7 @@ private void compensateSequentially(WorkflowContext ctx) { String activityClassName = compensationActivities.get(i).getCompensatationActivityClassName(); try { executeCompensateActivity(ctx, compensationActivities.get(i)).await(); - } catch (OrchestratorBlockedException e) { + } catch (OrchestratorBlockedException | ContinueAsNewInterruption e) { throw e; } catch (Exception e) { if (sagaException == null) { From 1872ba4eab71dedcb3ccc49782008b17c0c174f7 Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Sat, 13 Jan 2024 11:46:16 +0000 Subject: [PATCH 09/10] add saga context for saga related method Signed-off-by: Sky Ao --- .../workflows/DaprWorkflowContextImpl.java | 17 +- .../main/java/io/dapr/workflows/Workflow.java | 3 +- .../io/dapr/workflows/WorkflowContext.java | 17 +- .../workflows/saga/DaprSagaContextImpl.java | 41 ++++ .../io/dapr/workflows/saga/SagaContext.java | 21 ++ .../DaprWorkflowContextImplTest.java | 26 ++- .../java/io/dapr/workflows/WorkflowTest.java | 197 ++++++++++++++++++ .../saga/DaprSagaContextImplTest.java | 54 +++++ 8 files changed, 342 insertions(+), 34 deletions(-) create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/DaprSagaContextImpl.java create mode 100644 sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/saga/DaprSagaContextImplTest.java diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java index 25fef9470..777f3bff6 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java @@ -18,7 +18,11 @@ import com.microsoft.durabletask.TaskCanceledException; import com.microsoft.durabletask.TaskOptions; import com.microsoft.durabletask.TaskOrchestrationContext; + +import io.dapr.workflows.saga.DaprSagaContextImpl; import io.dapr.workflows.saga.Saga; +import io.dapr.workflows.saga.SagaContext; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.helpers.NOPLogger; @@ -241,20 +245,11 @@ public UUID newUuid() { } @Override - public void registerCompensation(String activityClassName, Object activityInput) { - if (this.saga == null) { - throw new UnsupportedOperationException("Saga is not enabled"); - } - - this.saga.registerCompensation(activityClassName, activityInput); - } - - @Override - public void compensate() { + public SagaContext getSagaContext() { if (this.saga == null) { throw new UnsupportedOperationException("Saga is not enabled"); } - this.saga.compensate(this); + return new DaprSagaContextImpl(this.saga, this); } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java index 51867c893..94bb4c828 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/Workflow.java @@ -57,7 +57,7 @@ public void run(WorkflowContext ctx) { throw e; } catch (Exception e) { try { - ctx.compensate(); + ctx.getSagaContext().compensate(); } catch (Exception se) { se.addSuppressed(e); throw se; @@ -78,6 +78,7 @@ public boolean isSagaEnabled() { * @return saga configuration */ public SagaOption getSagaOption() { + // by default, saga is disabled return null; } } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java index bc8b8cb3a..2a5b771ea 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java @@ -18,7 +18,8 @@ import com.microsoft.durabletask.TaskCanceledException; import com.microsoft.durabletask.TaskFailedException; import com.microsoft.durabletask.TaskOptions; -import io.dapr.workflows.saga.Saga; +import io.dapr.workflows.saga.SagaContext; + import org.slf4j.Logger; import javax.annotation.Nullable; @@ -533,16 +534,10 @@ default UUID newUuid() { } /** - * Register a compensation activity. - * - * @param activityClassName name of the activity class - * @param activityInput input of the activity to be compensated - */ - void registerCompensation(String activityClassName, Object activityInput); - - /** - * Compensate all registered activities. + * get saga context. * + * @return saga context + * @throws UnsupportedOperationException if saga is not enabled. */ - void compensate(); + SagaContext getSagaContext(); } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/DaprSagaContextImpl.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/DaprSagaContextImpl.java new file mode 100644 index 000000000..a08686b76 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/DaprSagaContextImpl.java @@ -0,0 +1,41 @@ +package io.dapr.workflows.saga; + +import io.dapr.workflows.WorkflowContext; + +/** + * Dapr Saga Context implementation. + */ +public class DaprSagaContextImpl implements SagaContext { + + private final Saga saga; + private final WorkflowContext workflowContext; + + /** + * Constructor to build up instance. + * + * @param saga Saga instance. + * @param workflowContext Workflow context. + * @throws IllegalArgumentException if saga or workflowContext is null. + */ + public DaprSagaContextImpl(Saga saga, WorkflowContext workflowContext) { + if (saga == null) { + throw new IllegalArgumentException("Saga should not be null"); + } + if (workflowContext == null) { + throw new IllegalArgumentException("workflowContext should not be null"); + } + + this.saga = saga; + this.workflowContext = workflowContext; + } + + @Override + public void registerCompensation(String activityClassName, Object activityInput) { + this.saga.registerCompensation(activityClassName, activityInput); + } + + @Override + public void compensate() { + this.saga.compensate(workflowContext); + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java new file mode 100644 index 000000000..b67f24db5 --- /dev/null +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java @@ -0,0 +1,21 @@ +package io.dapr.workflows.saga; + +/** + * Saga context. + */ +public interface SagaContext { + /** + * Register a compensation activity. + * + * @param activityClassName name of the activity class + * @param activityInput input of the activity to be compensated + */ + void registerCompensation(String activityClassName, Object activityInput); + + /** + * Compensate all registered activities. + * + */ + void compensate(); + +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java index d8ac1c8b4..3ea03ddbb 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/DaprWorkflowContextImplTest.java @@ -20,6 +20,9 @@ import com.microsoft.durabletask.TaskOptions; import com.microsoft.durabletask.TaskOrchestrationContext; +import io.dapr.workflows.saga.Saga; +import io.dapr.workflows.saga.SagaContext; + import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,6 +34,7 @@ import java.util.Arrays; import java.util.List; +import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -132,11 +136,8 @@ public void continueAsNew(Object input, boolean preserveUnprocessedEvents) { } @Override - public void registerCompensation(String activityClassName, Object activityInput) { - } - - @Override - public void compensate() { + public SagaContext getSagaContext() { + return null; } }; } @@ -319,16 +320,19 @@ public void newUuidTestNoImplementationExceptionTest() { } @Test - public void registerCompensationTest_unsupported() { - assertThrows(UnsupportedOperationException.class, () -> { - context.registerCompensation(null, "TestInput"); - }); + public void getSagaContextTest_sagaEnabled() { + Saga saga = mock(Saga.class); + WorkflowContext context = new DaprWorkflowContextImpl(mockInnerContext, saga); + + SagaContext sagaContext = context.getSagaContext(); + assertNotNull("SagaContext should not be null", sagaContext); } @Test - public void compensateTest_unsupported() { + public void getSagaContextTest_sagaDisabled() { + WorkflowContext context = new DaprWorkflowContextImpl(mockInnerContext); assertThrows(UnsupportedOperationException.class, () -> { - context.compensate(); + context.getSagaContext(); }); } } diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java new file mode 100644 index 000000000..528af3191 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTest.java @@ -0,0 +1,197 @@ +package io.dapr.workflows; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.Test; + +import com.microsoft.durabletask.interruption.ContinueAsNewInterruption; +import com.microsoft.durabletask.interruption.OrchestratorBlockedException; + +import io.dapr.workflows.saga.SagaCompensationException; +import io.dapr.workflows.saga.SagaContext; +import io.dapr.workflows.saga.SagaOption; + +public class WorkflowTest { + + @Test + public void testWorkflow_WithoutSaga() { + WorkflowStub stub = mock(WorkflowStub.class); + Workflow workflow = new WorkflowWithoutSaga(stub); + assertNull(workflow.getSagaOption()); + assertFalse(workflow.isSagaEnabled()); + + WorkflowContext ctx = mock(WorkflowContext.class); + doNothing().when(stub).run(ctx); + workflow.run(ctx); + + verify(stub, times(1)).run(eq(ctx)); + } + + @Test + public void testWorkflow_WithoutSaga_throwException() { + WorkflowStub stub = mock(WorkflowStub.class); + Workflow workflow = new WorkflowWithoutSaga(stub); + WorkflowContext ctx = mock(WorkflowContext.class); + Exception e = new RuntimeException(); + doThrow(e).when(stub).run(ctx); + + // should throw the exception, not catch + assertThrows(RuntimeException.class, () -> { + workflow.run(ctx); + }); + verify(stub, times(1)).run(eq(ctx)); + } + + @Test + public void testWorkflow_WithSaga() { + WorkflowStub stub = mock(WorkflowStub.class); + Workflow workflow = new WorkflowWithSaga(stub); + assertNotNull(workflow.getSagaOption()); + assertTrue(workflow.isSagaEnabled()); + + WorkflowContext ctx = mock(WorkflowContext.class); + doNothing().when(stub).run(ctx); + workflow.run(ctx); + + verify(stub, times(1)).run(eq(ctx)); + } + + @Test + public void testWorkflow_WithSaga_shouldNotCatch_OrchestratorBlockedException() { + WorkflowStub stub = mock(WorkflowStub.class); + Workflow workflow = new WorkflowWithSaga(stub); + + WorkflowContext ctx = mock(WorkflowContext.class); + Exception e = new OrchestratorBlockedException("test"); + doThrow(e).when(stub).run(ctx); + + // should not catch OrchestratorBlockedException + assertThrows(OrchestratorBlockedException.class, () -> { + workflow.run(ctx); + }); + verify(stub, times(1)).run(eq(ctx)); + } + + @Test + public void testWorkflow_WithSaga_shouldNotCatch_ContinueAsNewInterruption() { + WorkflowStub stub = mock(WorkflowStub.class); + Workflow workflow = new WorkflowWithSaga(stub); + + WorkflowContext ctx = mock(WorkflowContext.class); + Exception e = new ContinueAsNewInterruption("test"); + doThrow(e).when(stub).run(ctx); + + // should not catch ContinueAsNewInterruption + assertThrows(ContinueAsNewInterruption.class, () -> { + workflow.run(ctx); + }); + verify(stub, times(1)).run(eq(ctx)); + } + + @Test + public void testWorkflow_WithSaga_shouldNotCatch_SagaCompensationException() { + WorkflowStub stub = mock(WorkflowStub.class); + Workflow workflow = new WorkflowWithSaga(stub); + + WorkflowContext ctx = mock(WorkflowContext.class); + Exception e = new SagaCompensationException("test", null); + doThrow(e).when(stub).run(ctx); + + // should not catch SagaCompensationException + assertThrows(SagaCompensationException.class, () -> { + workflow.run(ctx); + }); + verify(stub, times(1)).run(eq(ctx)); + } + + @Test + public void testWorkflow_WithSaga_triggerCompensate() { + WorkflowStub stub = mock(WorkflowStub.class); + Workflow workflow = new WorkflowWithSaga(stub); + + WorkflowContext ctx = mock(WorkflowContext.class); + Exception e = new RuntimeException("test", null); + doThrow(e).when(stub).run(ctx); + SagaContext sagaContext = mock(SagaContext.class); + doReturn(sagaContext).when(ctx).getSagaContext(); + doNothing().when(sagaContext).compensate(); + + assertThrows(RuntimeException.class, () -> { + workflow.run(ctx); + }); + verify(stub, times(1)).run(eq(ctx)); + verify(sagaContext, times(1)).compensate(); + } + + @Test + public void testWorkflow_WithSaga_compensateFaile() { + WorkflowStub stub = mock(WorkflowStub.class); + Workflow workflow = new WorkflowWithSaga(stub); + + WorkflowContext ctx = mock(WorkflowContext.class); + Exception e = new RuntimeException("workflow fail", null); + doThrow(e).when(stub).run(ctx); + SagaContext sagaContext = mock(SagaContext.class); + doReturn(sagaContext).when(ctx).getSagaContext(); + Exception e2 = new RuntimeException("compensate fail", null); + doThrow(e2).when(sagaContext).compensate(); + + try { + workflow.run(ctx); + fail("sholdd throw exception"); + } catch (Exception ex) { + assertEquals(e2.getMessage(), ex.getMessage()); + assertEquals(1, ex.getSuppressed().length); + assertEquals(e.getMessage(), ex.getSuppressed()[0].getMessage()); + } + + verify(stub, times(1)).run(eq(ctx)); + verify(sagaContext, times(1)).compensate(); + } + + public static class WorkflowWithoutSaga extends Workflow { + private final WorkflowStub stub; + + public WorkflowWithoutSaga(WorkflowStub stub) { + this.stub = stub; + } + + @Override + public WorkflowStub create() { + return stub; + } + } + + public static class WorkflowWithSaga extends Workflow { + private final WorkflowStub stub; + + public WorkflowWithSaga(WorkflowStub stub) { + this.stub = stub; + } + + @Override + public WorkflowStub create() { + return stub; + } + + @Override + public SagaOption getSagaOption() { + return SagaOption.newBuilder() + .setParallelCompensation(false) + .build(); + } + } +} diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/saga/DaprSagaContextImplTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/saga/DaprSagaContextImplTest.java new file mode 100644 index 000000000..9c1918a41 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/saga/DaprSagaContextImplTest.java @@ -0,0 +1,54 @@ +package io.dapr.workflows.saga; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.junit.Test; + +import io.dapr.workflows.WorkflowContext; + +public class DaprSagaContextImplTest { + + @Test + public void testDaprSagaContextImpl_IllegalArgumentException() { + Saga saga = mock(Saga.class); + WorkflowContext workflowContext = mock(WorkflowContext.class); + + assertThrows(IllegalArgumentException.class, () -> { + new DaprSagaContextImpl(saga, null); + }); + + assertThrows(IllegalArgumentException.class, () -> { + new DaprSagaContextImpl(null, workflowContext); + }); + } + + @Test + public void test_registerCompensation() { + Saga saga = mock(Saga.class); + WorkflowContext workflowContext = mock(WorkflowContext.class); + DaprSagaContextImpl ctx = new DaprSagaContextImpl(saga, workflowContext); + + String activityClassName = "name1"; + Object activityInput = new Object(); + doNothing().when(saga).registerCompensation(activityClassName, activityInput); + + ctx.registerCompensation(activityClassName, activityInput); + verify(saga, times(1)).registerCompensation(activityClassName, activityInput); + } + + @Test + public void test_compensate() { + Saga saga = mock(Saga.class); + WorkflowContext workflowContext = mock(WorkflowContext.class); + DaprSagaContextImpl ctx = new DaprSagaContextImpl(saga, workflowContext); + + doNothing().when(saga).compensate(workflowContext); + + ctx.compensate(); + verify(saga, times(1)).compensate(workflowContext); + } +} From 59e76f1ee09c21f880f77f380867182a6c35fc1d Mon Sep 17 00:00:00 2001 From: Sky Ao Date: Sat, 13 Jan 2024 11:56:44 +0000 Subject: [PATCH 10/10] fix for checkstyle Signed-off-by: Sky Ao --- .../io/dapr/workflows/DaprWorkflowContextImpl.java | 2 -- .../java/io/dapr/workflows/WorkflowContext.java | 1 - .../io/dapr/workflows/saga/DaprSagaContextImpl.java | 13 +++++++++++++ .../java/io/dapr/workflows/saga/SagaContext.java | 13 +++++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java index 777f3bff6..c6f474d70 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/DaprWorkflowContextImpl.java @@ -18,11 +18,9 @@ import com.microsoft.durabletask.TaskCanceledException; import com.microsoft.durabletask.TaskOptions; import com.microsoft.durabletask.TaskOrchestrationContext; - import io.dapr.workflows.saga.DaprSagaContextImpl; import io.dapr.workflows.saga.Saga; import io.dapr.workflows.saga.SagaContext; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.helpers.NOPLogger; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java index 2a5b771ea..5315616ff 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowContext.java @@ -19,7 +19,6 @@ import com.microsoft.durabletask.TaskFailedException; import com.microsoft.durabletask.TaskOptions; import io.dapr.workflows.saga.SagaContext; - import org.slf4j.Logger; import javax.annotation.Nullable; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/DaprSagaContextImpl.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/DaprSagaContextImpl.java index a08686b76..5ede2af7f 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/DaprSagaContextImpl.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/DaprSagaContextImpl.java @@ -1,3 +1,16 @@ +/* + * Copyright 2023 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.workflows.saga; import io.dapr.workflows.WorkflowContext; diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java index b67f24db5..03470ff92 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/saga/SagaContext.java @@ -1,3 +1,16 @@ +/* + * Copyright 2023 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.workflows.saga; /**