diff --git a/api/src/main/java/io/serverlessworkflow/api/deserializers/ErrorsDeserializer.java b/api/src/main/java/io/serverlessworkflow/api/deserializers/ErrorsDeserializer.java new file mode 100644 index 00000000..32db6447 --- /dev/null +++ b/api/src/main/java/io/serverlessworkflow/api/deserializers/ErrorsDeserializer.java @@ -0,0 +1,107 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification 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.serverlessworkflow.api.deserializers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.serverlessworkflow.api.error.ErrorDefinition; +import io.serverlessworkflow.api.interfaces.WorkflowPropertySource; +import io.serverlessworkflow.api.utils.Utils; +import io.serverlessworkflow.api.workflow.Errors; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ErrorsDeserializer extends StdDeserializer { + + private static final long serialVersionUID = 510l; + private static Logger logger = LoggerFactory.getLogger(ErrorsDeserializer.class); + + @SuppressWarnings("unused") + private WorkflowPropertySource context; + + public ErrorsDeserializer() { + this(Errors.class); + } + + public ErrorsDeserializer(Class vc) { + super(vc); + } + + public ErrorsDeserializer(WorkflowPropertySource context) { + this(Errors.class); + this.context = context; + } + + @Override + public Errors deserialize(JsonParser jp, + DeserializationContext ctxt) throws IOException { + + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonNode node = jp.getCodec().readTree(jp); + + Errors errors = new Errors(); + List errorDefinitions = new ArrayList<>(); + + if (node.isArray()) { + for (final JsonNode nodeEle : node) { + errorDefinitions.add(mapper.treeToValue(nodeEle, ErrorDefinition.class)); + } + } else { + String errorsFileDef = node.asText(); + String errorsFileSrc = Utils.getResourceFileAsString(errorsFileDef); + JsonNode errorsRefNode; + ObjectMapper jsonWriter = new ObjectMapper(); + if (errorsFileSrc != null && errorsFileSrc.trim().length() > 0) { + // if its a yaml def convert to json first + if (!errorsFileSrc.trim().startsWith("{")) { + // convert yaml to json to validate + ObjectMapper yamlReader = new ObjectMapper(new YAMLFactory()); + Object obj = yamlReader.readValue(errorsFileSrc, Object.class); + + errorsRefNode = jsonWriter.readTree(new JSONObject(jsonWriter.writeValueAsString(obj)).toString()); + } else { + errorsRefNode = jsonWriter.readTree(new JSONObject(errorsFileSrc).toString()); + } + + JsonNode refErrors = errorsRefNode.get("errors"); + if (refErrors != null) { + for (final JsonNode nodeEle : refErrors) { + errorDefinitions.add(mapper.treeToValue(nodeEle, ErrorDefinition.class)); + } + } else { + logger.error("Unable to find error definitions in reference file: {}", errorsFileSrc); + } + + } else { + logger.error("Unable to load errors defs reference file: {}", errorsFileSrc); + } + + } + errors.setErrorDefs(errorDefinitions); + return errors; + + } +} + diff --git a/api/src/main/java/io/serverlessworkflow/api/mapper/WorkflowModule.java b/api/src/main/java/io/serverlessworkflow/api/mapper/WorkflowModule.java index 890fd824..d5d0e1bd 100644 --- a/api/src/main/java/io/serverlessworkflow/api/mapper/WorkflowModule.java +++ b/api/src/main/java/io/serverlessworkflow/api/mapper/WorkflowModule.java @@ -112,6 +112,7 @@ private void addDefaultDeserializers() { addDeserializer(DataInputSchema.class, new DataInputSchemaDeserializer(workflowPropertySource)); addDeserializer(AuthDefinition.class, new AuthDefinitionDeserializer(workflowPropertySource)); addDeserializer(StateExecTimeout.class, new StateExecTimeoutDeserializer(workflowPropertySource)); + addDeserializer(Errors.class, new ErrorsDeserializer(workflowPropertySource)); } public ExtensionSerializer getExtensionSerializer() { diff --git a/api/src/main/java/io/serverlessworkflow/api/serializers/WorkflowSerializer.java b/api/src/main/java/io/serverlessworkflow/api/serializers/WorkflowSerializer.java index dc729731..5c889838 100644 --- a/api/src/main/java/io/serverlessworkflow/api/serializers/WorkflowSerializer.java +++ b/api/src/main/java/io/serverlessworkflow/api/serializers/WorkflowSerializer.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.serverlessworkflow.api.Workflow; +import io.serverlessworkflow.api.error.ErrorDefinition; import io.serverlessworkflow.api.events.EventDefinition; import io.serverlessworkflow.api.functions.FunctionDefinition; import io.serverlessworkflow.api.interfaces.Extension; @@ -102,6 +103,10 @@ public void serialize(Workflow workflow, gen.writeBooleanField("keepActive", workflow.isKeepActive()); } + if (workflow.isAutoRetries()) { + gen.writeBooleanField("autoRetries", workflow.isAutoRetries()); + } + if (workflow.getMetadata() != null && !workflow.getMetadata().isEmpty()) { gen.writeObjectField("metadata", workflow.getMetadata()); @@ -140,6 +145,17 @@ public void serialize(Workflow workflow, gen.writeEndArray(); } + if (workflow.getErrors() != null && !workflow.getErrors().getErrorDefs().isEmpty()) { + gen.writeArrayFieldStart("errors"); + for (ErrorDefinition error : workflow.getErrors().getErrorDefs()) { + gen.writeObject(error); + } + gen.writeEndArray(); + } else { + gen.writeArrayFieldStart("errors"); + gen.writeEndArray(); + } + if (workflow.getSecrets() != null && !workflow.getSecrets().getSecretDefs().isEmpty()) { gen.writeArrayFieldStart("secrets"); for (String secretDef : workflow.getSecrets().getSecretDefs()) { diff --git a/api/src/main/java/io/serverlessworkflow/api/workflow/Errors.java b/api/src/main/java/io/serverlessworkflow/api/workflow/Errors.java new file mode 100644 index 00000000..44c7c685 --- /dev/null +++ b/api/src/main/java/io/serverlessworkflow/api/workflow/Errors.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification 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.serverlessworkflow.api.workflow; + +import io.serverlessworkflow.api.error.ErrorDefinition; + +import java.util.List; + +public class Errors { + private String refValue; + private List errorDefs; + + public Errors() { + } + + public Errors(List errorDefs) { + this.errorDefs = errorDefs; + } + + public Errors(String refValue) { + this.refValue = refValue; + } + + public String getRefValue() { + return refValue; + } + + public void setRefValue(String refValue) { + this.refValue = refValue; + } + + public List getErrorDefs() { + return errorDefs; + } + + public void setErrorDefs(List errorDefs) { + this.errorDefs = errorDefs; + } +} diff --git a/api/src/main/resources/schema/actions/action.json b/api/src/main/resources/schema/actions/action.json index b2accd1d..8dc80780 100644 --- a/api/src/main/resources/schema/actions/action.json +++ b/api/src/main/resources/schema/actions/action.json @@ -22,6 +22,26 @@ "sleep": { "$ref": "../sleep/sleep.json" }, + "retryRef": { + "type": "string", + "description": "References a defined workflow retry definition. If not defined the default retry policy is assumed" + }, + "nonRetryableErrors": { + "type": "array", + "description": "List of unique references to defined workflow errors for which the action should not be retried. Used only when `autoRetries` is set to `true`", + "minItems": 1, + "items": { + "type": "string" + } + }, + "retryableErrors": { + "type": "array", + "description": "List of unique references to defined workflow errors for which the action should be retried. Used only when `autoRetries` is set to `false`", + "minItems": 1, + "items": { + "type": "string" + } + }, "actionDataFilter": { "$ref": "../filters/actiondatafilter.json" } diff --git a/api/src/main/resources/schema/error/error.json b/api/src/main/resources/schema/error/error.json index c3996430..c51860de 100644 --- a/api/src/main/resources/schema/error/error.json +++ b/api/src/main/resources/schema/error/error.json @@ -2,20 +2,18 @@ "type": "object", "javaType": "io.serverlessworkflow.api.error.Error", "properties": { - "error": { + "errorRef": { "type": "string", - "description": "Domain-specific error name, or '*' to indicate all possible errors", + "description": "Reference to a unique workflow error definition. Used of errorRefs is not used", "minLength": 1 }, - "code": { - "type": "string", - "description": "Error code. Can be used in addition to the name to help runtimes resolve to technical errors/exceptions. Should not be defined if error is set to '*'", - "minLength": 1 - }, - "retryRef": { - "type": "string", - "description": "References a unique name of a retry definition.", - "minLength": 1 + "errorRefs": { + "type": "array", + "description": "References one or more workflow error definitions. Used if errorRef is not used", + "minItems": 1, + "items": { + "type": "string" + } }, "transition": { "$ref": "../transitions/transition.json", diff --git a/api/src/main/resources/schema/error/errordef.json b/api/src/main/resources/schema/error/errordef.json new file mode 100644 index 00000000..613d3cbf --- /dev/null +++ b/api/src/main/resources/schema/error/errordef.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "javaType": "io.serverlessworkflow.api.error.ErrorDefinition", + "properties": { + "name": { + "type": "string", + "description": "Domain-specific error name", + "minLength": 1 + }, + "code": { + "type": "string", + "description": "Error code. Can be used in addition to the name to help runtimes resolve to technical errors/exceptions. Should not be defined if error is set to '*'", + "minLength": 1 + }, + "description": { + "type": "string", + "description": "Error description" + } + }, + "required": [ + "name" + ] +} \ No newline at end of file diff --git a/api/src/main/resources/schema/workflow.json b/api/src/main/resources/schema/workflow.json index 15b3fbb1..19164ca5 100644 --- a/api/src/main/resources/schema/workflow.json +++ b/api/src/main/resources/schema/workflow.json @@ -50,6 +50,11 @@ "default": false, "description": "If 'true', workflow instances is not terminated when there are no active execution paths. Instance can be terminated via 'terminate end definition' or reaching defined 'execTimeout'" }, + "autoRetries": { + "type": "boolean", + "default": false, + "description": "If set to true, actions should automatically be retried on unchecked errors. Default is false" + }, "metadata": { "$ref": "metadata/metadata.json" }, @@ -63,6 +68,11 @@ "existingJavaType": "io.serverlessworkflow.api.workflow.Functions", "description": "Workflow function definitions" }, + "errors": { + "type": "object", + "existingJavaType": "io.serverlessworkflow.api.workflow.Errors", + "description": "Workflow error definitions" + }, "retries": { "type": "object", "existingJavaType": "io.serverlessworkflow.api.workflow.Retries", diff --git a/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java b/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java index 066053ff..abb4ed7e 100644 --- a/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java +++ b/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java @@ -700,4 +700,40 @@ public void testActionsSleep(String workflowLocation) { assertEquals("${ .customer }", functionRef2.getArguments().get("applicant").asText()); } + + @ParameterizedTest + @ValueSource(strings = {"/features/errors.json", "/features/errors.yml"}) + public void testErrorsParams(String workflowLocation) { + Workflow workflow = Workflow.fromSource(WorkflowTestUtils.readWorkflowFile(workflowLocation)); + + assertNotNull(workflow); + assertNotNull(workflow.getId()); + assertNotNull(workflow.getName()); + assertNotNull(workflow.getStates()); + assertTrue(workflow.isAutoRetries()); + + assertNotNull(workflow.getStates()); + assertEquals(1, workflow.getStates().size()); + + assertNotNull(workflow.getErrors()); + assertEquals(2, workflow.getErrors().getErrorDefs().size()); + + assertTrue(workflow.getStates().get(0) instanceof OperationState); + + OperationState operationState = (OperationState) workflow.getStates().get(0); + assertNotNull(operationState.getActions()); + assertEquals(1, operationState.getActions().size()); + List actions = operationState.getActions(); + assertNotNull(actions.get(0).getFunctionRef()); + assertEquals("addPet", actions.get(0).getFunctionRef().getRefName()); + assertNotNull(actions.get(0).getRetryRef()); + assertEquals("testRetry", actions.get(0).getRetryRef()); + assertNotNull(actions.get(0).getNonRetryableErrors()); + assertEquals(2, actions.get(0).getNonRetryableErrors().size()); + + assertNotNull(operationState.getOnErrors()); + assertEquals(1, operationState.getOnErrors().size()); + assertNotNull(operationState.getOnErrors().get(0).getErrorRefs()); + assertEquals(2, operationState.getOnErrors().get(0).getErrorRefs().size()); + } } diff --git a/api/src/test/resources/examples/jobmonitoring.json b/api/src/test/resources/examples/jobmonitoring.json index 6951bce0..186eb2dc 100644 --- a/api/src/test/resources/examples/jobmonitoring.json +++ b/api/src/test/resources/examples/jobmonitoring.json @@ -43,7 +43,7 @@ ], "onErrors": [ { - "error": "*", + "errorRef": "AllErrors", "transition": "SubmitError" } ], diff --git a/api/src/test/resources/examples/jobmonitoring.yml b/api/src/test/resources/examples/jobmonitoring.yml index 4e0fcfc0..76dc4a7a 100644 --- a/api/src/test/resources/examples/jobmonitoring.yml +++ b/api/src/test/resources/examples/jobmonitoring.yml @@ -25,7 +25,7 @@ states: actionDataFilter: results: "${ .jobuid }" onErrors: - - error: "*" + - errorRef: "AllErrors" transition: SubmitError stateDataFilter: output: "${ .jobuid }" diff --git a/api/src/test/resources/examples/provisionorder.json b/api/src/test/resources/examples/provisionorder.json index b3839507..f6a8e446 100644 --- a/api/src/test/resources/examples/provisionorder.json +++ b/api/src/test/resources/examples/provisionorder.json @@ -32,15 +32,15 @@ "transition": "ApplyOrder", "onErrors": [ { - "error": "Missing order id", + "errorRef": "Missing order id", "transition": "MissingId" }, { - "error": "Missing order item", + "errorRef": "Missing order item", "transition": "MissingItem" }, { - "error": "Missing order quantity", + "errorRef": "Missing order quantity", "transition": "MissingQuantity" } ] diff --git a/api/src/test/resources/examples/provisionorder.yml b/api/src/test/resources/examples/provisionorder.yml index 431cab2c..37e5147d 100644 --- a/api/src/test/resources/examples/provisionorder.yml +++ b/api/src/test/resources/examples/provisionorder.yml @@ -20,11 +20,11 @@ states: output: "${ .exceptions }" transition: ApplyOrder onErrors: - - error: Missing order id + - errorRef: Missing order id transition: MissingId - - error: Missing order item + - errorRef: Missing order item transition: MissingItem - - error: Missing order quantity + - errorRef: Missing order quantity transition: MissingQuantity - name: MissingId type: operation diff --git a/api/src/test/resources/features/errors.json b/api/src/test/resources/features/errors.json new file mode 100644 index 00000000..27b969ba --- /dev/null +++ b/api/src/test/resources/features/errors.json @@ -0,0 +1,38 @@ +{ + "id": "functionrefparams", + "version": "1.0", + "specVersion": "0.7", + "name": "Function Ref Params Test", + "start": "AddPluto", + "autoRetries": true, + "errors": [ + { + "name": "ErrorA", + "code": "400" + }, + { + "name": "ErrorB", + "code": "500" + } + ], + "states": [ + { + "name": "AddPluto", + "type": "operation", + "actions": [ + { + "functionRef": "addPet", + "retryRef": "testRetry", + "nonRetryableErrors": ["A", "B"] + } + ], + "onErrors": [ + { + "errorRefs": ["A", "B"], + "end": true + } + ], + "end": true + } + ] +} \ No newline at end of file diff --git a/api/src/test/resources/features/errors.yml b/api/src/test/resources/features/errors.yml new file mode 100644 index 00000000..603d062f --- /dev/null +++ b/api/src/test/resources/features/errors.yml @@ -0,0 +1,26 @@ +id: functionrefparams +version: '1.0' +specVersion: '0.7' +name: Function Ref Params Test +start: AddPluto +autoRetries: true +errors: + - name: ErrorA + code: '400' + - name: ErrorB + code: '500' +states: + - name: AddPluto + type: operation + actions: + - functionRef: addPet + retryRef: testRetry + nonRetryableErrors: + - A + - B + onErrors: + - errorRefs: + - A + - B + end: true + end: true diff --git a/api/src/test/resources/features/retriesprops.json b/api/src/test/resources/features/retriesprops.json index bfcd8f19..1a6a191e 100644 --- a/api/src/test/resources/features/retriesprops.json +++ b/api/src/test/resources/features/retriesprops.json @@ -23,9 +23,7 @@ ], "onErrors": [ { - "error": "TimeoutError", - "code": "500", - "retryRef": "Test Retries", + "errorRef": "TimeoutError", "end": true } ], diff --git a/api/src/test/resources/features/retriesprops.yml b/api/src/test/resources/features/retriesprops.yml index bba18a47..a91614cd 100644 --- a/api/src/test/resources/features/retriesprops.yml +++ b/api/src/test/resources/features/retriesprops.yml @@ -16,8 +16,6 @@ states: type: operation actions: [] onErrors: - - error: TimeoutError - code: '500' - retryRef: Test Retries + - errorRef: TimeoutError end: true end: true diff --git a/api/src/test/resources/features/vetappointment.json b/api/src/test/resources/features/vetappointment.json index 17315d96..b8d39aa2 100644 --- a/api/src/test/resources/features/vetappointment.json +++ b/api/src/test/resources/features/vetappointment.json @@ -29,9 +29,7 @@ }, "onErrors": [ { - "error": "TimeoutError", - "code": "500", - "retryRef": "TimeoutRetryStrategy", + "errorRef": "TimeoutError", "end": true } ], diff --git a/api/src/test/resources/features/vetappointment.yml b/api/src/test/resources/features/vetappointment.yml index 2a1705ea..d56a13be 100644 --- a/api/src/test/resources/features/vetappointment.yml +++ b/api/src/test/resources/features/vetappointment.yml @@ -20,8 +20,6 @@ states: timeouts: actionExecTimeout: PT15M onErrors: - - error: TimeoutError - code: '500' - retryRef: TimeoutRetryStrategy + - errorRef: TimeoutError end: true end: true diff --git a/diagram/src/test/resources/examples/jobmonitoring.json b/diagram/src/test/resources/examples/jobmonitoring.json index 6951bce0..186eb2dc 100644 --- a/diagram/src/test/resources/examples/jobmonitoring.json +++ b/diagram/src/test/resources/examples/jobmonitoring.json @@ -43,7 +43,7 @@ ], "onErrors": [ { - "error": "*", + "errorRef": "AllErrors", "transition": "SubmitError" } ], diff --git a/diagram/src/test/resources/examples/jobmonitoring.yml b/diagram/src/test/resources/examples/jobmonitoring.yml index 4e0fcfc0..76dc4a7a 100644 --- a/diagram/src/test/resources/examples/jobmonitoring.yml +++ b/diagram/src/test/resources/examples/jobmonitoring.yml @@ -25,7 +25,7 @@ states: actionDataFilter: results: "${ .jobuid }" onErrors: - - error: "*" + - errorRef: "AllErrors" transition: SubmitError stateDataFilter: output: "${ .jobuid }" diff --git a/diagram/src/test/resources/examples/provisionorder.json b/diagram/src/test/resources/examples/provisionorder.json index b3839507..f6a8e446 100644 --- a/diagram/src/test/resources/examples/provisionorder.json +++ b/diagram/src/test/resources/examples/provisionorder.json @@ -32,15 +32,15 @@ "transition": "ApplyOrder", "onErrors": [ { - "error": "Missing order id", + "errorRef": "Missing order id", "transition": "MissingId" }, { - "error": "Missing order item", + "errorRef": "Missing order item", "transition": "MissingItem" }, { - "error": "Missing order quantity", + "errorRef": "Missing order quantity", "transition": "MissingQuantity" } ] diff --git a/diagram/src/test/resources/examples/provisionorder.yml b/diagram/src/test/resources/examples/provisionorder.yml index 431cab2c..37e5147d 100644 --- a/diagram/src/test/resources/examples/provisionorder.yml +++ b/diagram/src/test/resources/examples/provisionorder.yml @@ -20,11 +20,11 @@ states: output: "${ .exceptions }" transition: ApplyOrder onErrors: - - error: Missing order id + - errorRef: Missing order id transition: MissingId - - error: Missing order item + - errorRef: Missing order item transition: MissingItem - - error: Missing order quantity + - errorRef: Missing order quantity transition: MissingQuantity - name: MissingId type: operation diff --git a/validation/src/main/java/io/serverlessworkflow/validation/WorkflowValidatorImpl.java b/validation/src/main/java/io/serverlessworkflow/validation/WorkflowValidatorImpl.java index 29cb3ed3..84e0de4a 100644 --- a/validation/src/main/java/io/serverlessworkflow/validation/WorkflowValidatorImpl.java +++ b/validation/src/main/java/io/serverlessworkflow/validation/WorkflowValidatorImpl.java @@ -169,34 +169,6 @@ public List validate() { validation.addEndState(); } - if (workflow.getRetries() != null) { - List retryDefs = workflow.getRetries().getRetryDefs(); - if (s.getOnErrors() == null || s.getOnErrors().isEmpty()) { - addValidationError("No onErrors found for state" + s.getName() + " but retries is defined", - ValidationError.WORKFLOW_VALIDATION); - } else { - for (Error e : s.getOnErrors()) { - if (e.getRetryRef() == null || e.getRetryRef().isEmpty()) { - addValidationError("No retryRef found for onErrors" + e.getError(), - ValidationError.WORKFLOW_VALIDATION); - } else { - boolean validRetryDefinition = false; - for (RetryDefinition rd : retryDefs) { - if (rd.getName().equals(e.getRetryRef())) { - validRetryDefinition = true; - break; - } - } - if (!validRetryDefinition) { - addValidationError(e.getRetryRef() + " is not a valid retryRef", - ValidationError.WORKFLOW_VALIDATION); - } - } - } - } - } - - if (s instanceof OperationState) { OperationState operationState = (OperationState) s;