This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch sag in repository https://gitbox.apache.org/repos/asf/camel.git
commit 3cb16fd1cc7badd4063ea5675e3bddb4bbb959fc Author: Claus Ibsen <[email protected]> AuthorDate: Fri Feb 27 10:24:30 2026 +0100 CAMEL-23094: camel-core - Saga EIP fix model for completion and compensation --- .../org/apache/camel/catalog/models/saga.json | 4 +- .../apache/camel/catalog/schemas/camel-spring.xsd | 83 +++---- .../apache/camel/catalog/schemas/camel-xml-io.xsd | 83 +++---- .../org/apache/camel/spring/processor/saga.xml | 4 +- .../org/apache/camel/spring/processor/sagaRef.xml | 4 +- .../src/main/docs/modules/eips/pages/saga-eip.adoc | 260 +++++++++------------ .../META-INF/org/apache/camel/model/saga.json | 4 +- .../camel/model/SagaActionUriDefinition.java | 51 ---- .../org/apache/camel/model/SagaDefinition.java | 67 ++++-- .../java/org/apache/camel/reifier/SagaReifier.java | 46 +++- .../test/resources/org/apache/camel/model/saga.xml | 18 +- .../java/org/apache/camel/xml/in/ModelParser.java | 7 +- .../java/org/apache/camel/xml/out/ModelWriter.java | 9 +- core/camel-xml-io/src/test/resources/saga.xml | 4 +- .../org/apache/camel/yaml/out/ModelWriter.java | 9 +- .../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc | 110 +++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_19.adoc | 110 +++++++++ .../apache/camel/main/stub/StubBeanRepository.java | 6 + .../camel-yaml-dsl-deserializers/pom.xml | 4 - .../dsl/yaml/deserializers/ModelDeserializers.java | 81 +------ .../deserializers/ModelDeserializersResolver.java | 1 - .../generated/resources/schema/camelYamlDsl.json | 37 +-- .../org/apache/camel/dsl/yaml/SagaTest.groovy | 41 +--- 23 files changed, 535 insertions(+), 508 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/saga.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/saga.json index 1334df81a513..15543e203db7 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/saga.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/saga.json @@ -20,8 +20,8 @@ "propagation": { "index": 5, "kind": "attribute", "displayName": "Propagation", "group": "advanced", "label": "advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.SagaPropagation", "enum": [ "REQUIRED", "REQUIRES_NEW", "MANDATORY", "SUPPORTS", "NOT_SUPPORTED", "NEVER" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "REQUIRED", "description": "Set the Saga propagation mode (REQUIRED, REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPP [...] "completionMode": { "index": 6, "kind": "attribute", "displayName": "Completion Mode", "group": "advanced", "label": "advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.SagaCompletionMode", "enum": [ "AUTO", "MANUAL" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "AUTO", "description": "Determine how the saga should be considered complete. When set to AUTO, the saga is completed when the exchange that initiates the saga is [...] "timeout": { "index": 7, "kind": "attribute", "displayName": "Timeout", "group": "common", "required": false, "type": "duration", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Set the maximum amount of time for the Saga. After the timeout is expired, the saga will be compensated automatically (unless a different decision has been taken in the meantime)." }, - "compensation": { "index": 8, "kind": "element", "displayName": "Compensation", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.SagaActionUriDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding to the compensation URI must perform compensation and complete without error. If errors occur dur [...] - "completion": { "index": 9, "kind": "element", "displayName": "Completion", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.SagaActionUriDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the completion URI must perform completion tasks and terminate without error. If errors occur during com [...] + "compensation": { "index": 8, "kind": "attribute", "displayName": "Compensation", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the saga s [...] + "completion": { "index": 9, "kind": "attribute", "displayName": "Completion", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga service ma [...] "option": { "index": 10, "kind": "element", "displayName": "Option", "group": "advanced", "label": "advanced", "required": false, "type": "array", "javaType": "java.util.List<org.apache.camel.model.PropertyExpressionDefinition>", "deprecated": false, "autowired": false, "secret": false, "description": "Allows to save properties of the current exchange in order to re-use them in a compensation\/completion callback route. Options are usually helpful e.g. to store and retrieve identifie [...] "outputs": { "index": 11, "kind": "element", "displayName": "Outputs", "group": "common", "required": true, "type": "array", "javaType": "java.util.List", "oneOf": [ "aggregate", "bean", "choice", "circuitBreaker", "claimCheck", "convertBodyTo", "convertHeaderTo", "convertVariableTo", "delay", "doCatch", "doFinally", "doTry", "dynamicRouter", "enrich", "filter", "idempotentConsumer", "intercept", "interceptFrom", "interceptSendToEndpoint", "kamelet", "loadBalance", "log", "loop", "ma [...] } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd index 296d19fddb5b..fc03a837c852 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd @@ -13378,29 +13378,6 @@ However if there are a high degree of dynamic endpoints that have been used befo to reuse both producers and endpoints and therefore the cache size can be set accordingly or rely on the default size (1000). If there is a mix of unique and used before dynamic endpoints, then setting a reasonable cache size can help reduce memory usage to avoid storing too many non frequent used producers. -]]> - </xs:documentation> - </xs:annotation> - </xs:attribute> - </xs:extension> - </xs:complexContent> - </xs:complexType> - <xs:complexType name="sagaActionUriDefinition"> - <xs:complexContent> - <xs:extension base="tns:sendDefinition"> - <xs:sequence/> - </xs:extension> - </xs:complexContent> - </xs:complexType> - <xs:complexType abstract="true" name="sendDefinition"> - <xs:complexContent> - <xs:extension base="tns:noOutputDefinition"> - <xs:sequence/> - <xs:attribute name="uri" type="xs:string"> - <xs:annotation> - <xs:documentation xml:lang="en"> -<![CDATA[ -Sets the uri of the endpoint to send to. ]]> </xs:documentation> </xs:annotation> @@ -13412,28 +13389,6 @@ Sets the uri of the endpoint to send to. <xs:complexContent> <xs:extension base="tns:output"> <xs:sequence> - <xs:element minOccurs="0" name="compensation" type="tns:sagaActionUriDefinition"> - <xs:annotation> - <xs:documentation xml:lang="en"> -<![CDATA[ -The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding -to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the -saga service may call again the compensation URI to retry. -]]> - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element minOccurs="0" name="completion" type="tns:sagaActionUriDefinition"> - <xs:annotation> - <xs:documentation xml:lang="en"> -<![CDATA[ -The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the -completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga -service may call again the completion URI to retry. -]]> - </xs:documentation> - </xs:annotation> - </xs:element> <xs:element maxOccurs="unbounded" minOccurs="0" name="option" type="tns:propertyExpressionDefinition"> <xs:annotation> <xs:documentation xml:lang="en"> @@ -13555,6 +13510,28 @@ user must complete or compensate the saga using the saga:complete or saga:compen <![CDATA[ Set the maximum amount of time for the Saga. After the timeout is expired, the saga will be compensated automatically (unless a different decision has been taken in the meantime). +]]> + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="compensation" type="xs:string"> + <xs:annotation> + <xs:documentation xml:lang="en"> +<![CDATA[ +The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding +to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the +saga service may call again the compensation URI to retry. +]]> + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="completion" type="xs:string"> + <xs:annotation> + <xs:documentation xml:lang="en"> +<![CDATA[ +The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the +completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga +service may call again the completion URI to retry. ]]> </xs:documentation> </xs:annotation> @@ -13622,6 +13599,22 @@ Sets the sample message count which only a single Exchange will pass through aft </xs:extension> </xs:complexContent> </xs:complexType> + <xs:complexType abstract="true" name="sendDefinition"> + <xs:complexContent> + <xs:extension base="tns:noOutputDefinition"> + <xs:sequence/> + <xs:attribute name="uri" type="xs:string"> + <xs:annotation> + <xs:documentation xml:lang="en"> +<![CDATA[ +Sets the uri of the endpoint to send to. +]]> + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:extension> + </xs:complexContent> + </xs:complexType> <xs:complexType name="setBodyDefinition"> <xs:complexContent> <xs:extension base="tns:processorDefinition"> diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd index aae3eba9da82..a794e359f73b 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd @@ -12072,29 +12072,6 @@ However if there are a high degree of dynamic endpoints that have been used befo to reuse both producers and endpoints and therefore the cache size can be set accordingly or rely on the default size (1000). If there is a mix of unique and used before dynamic endpoints, then setting a reasonable cache size can help reduce memory usage to avoid storing too many non frequent used producers. -]]> - </xs:documentation> - </xs:annotation> - </xs:attribute> - </xs:extension> - </xs:complexContent> - </xs:complexType> - <xs:complexType name="sagaActionUriDefinition"> - <xs:complexContent> - <xs:extension base="tns:sendDefinition"> - <xs:sequence/> - </xs:extension> - </xs:complexContent> - </xs:complexType> - <xs:complexType abstract="true" name="sendDefinition"> - <xs:complexContent> - <xs:extension base="tns:noOutputDefinition"> - <xs:sequence/> - <xs:attribute name="uri" type="xs:string"> - <xs:annotation> - <xs:documentation xml:lang="en"> -<![CDATA[ -Sets the uri of the endpoint to send to. ]]> </xs:documentation> </xs:annotation> @@ -12106,28 +12083,6 @@ Sets the uri of the endpoint to send to. <xs:complexContent> <xs:extension base="tns:output"> <xs:sequence> - <xs:element minOccurs="0" name="compensation" type="tns:sagaActionUriDefinition"> - <xs:annotation> - <xs:documentation xml:lang="en"> -<![CDATA[ -The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding -to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the -saga service may call again the compensation URI to retry. -]]> - </xs:documentation> - </xs:annotation> - </xs:element> - <xs:element minOccurs="0" name="completion" type="tns:sagaActionUriDefinition"> - <xs:annotation> - <xs:documentation xml:lang="en"> -<![CDATA[ -The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the -completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga -service may call again the completion URI to retry. -]]> - </xs:documentation> - </xs:annotation> - </xs:element> <xs:element maxOccurs="unbounded" minOccurs="0" name="option" type="tns:propertyExpressionDefinition"> <xs:annotation> <xs:documentation xml:lang="en"> @@ -12249,6 +12204,28 @@ user must complete or compensate the saga using the saga:complete or saga:compen <![CDATA[ Set the maximum amount of time for the Saga. After the timeout is expired, the saga will be compensated automatically (unless a different decision has been taken in the meantime). +]]> + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="compensation" type="xs:string"> + <xs:annotation> + <xs:documentation xml:lang="en"> +<![CDATA[ +The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding +to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the +saga service may call again the compensation URI to retry. +]]> + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="completion" type="xs:string"> + <xs:annotation> + <xs:documentation xml:lang="en"> +<![CDATA[ +The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the +completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga +service may call again the completion URI to retry. ]]> </xs:documentation> </xs:annotation> @@ -12316,6 +12293,22 @@ Sets the sample message count which only a single Exchange will pass through aft </xs:extension> </xs:complexContent> </xs:complexType> + <xs:complexType abstract="true" name="sendDefinition"> + <xs:complexContent> + <xs:extension base="tns:noOutputDefinition"> + <xs:sequence/> + <xs:attribute name="uri" type="xs:string"> + <xs:annotation> + <xs:documentation xml:lang="en"> +<![CDATA[ +Sets the uri of the endpoint to send to. +]]> + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:extension> + </xs:complexContent> + </xs:complexType> <xs:complexType name="setBodyDefinition"> <xs:complexContent> <xs:extension base="tns:processorDefinition"> diff --git a/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/saga.xml b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/saga.xml index 25127c0c44f5..0e8c7724e310 100644 --- a/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/saga.xml +++ b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/saga.xml @@ -31,9 +31,7 @@ <jmxAgent id="jmx" disabled="true"/> <route> <from uri="direct:start"/> - <saga> - <compensation uri="mock:compensation" /> - <completion uri="mock:completion" /> + <saga compensation="mock:compensation" completion="mock:completion"> <option key="myOptionKey"> <constant>myOptionValue</constant> </option> diff --git a/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/sagaRef.xml b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/sagaRef.xml index c9f08253f123..2e12eddd504c 100644 --- a/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/sagaRef.xml +++ b/components/camel-spring-parent/camel-spring-xml/src/test/resources/org/apache/camel/spring/processor/sagaRef.xml @@ -32,9 +32,7 @@ <jmxAgent id="jmx" disabled="true"/> <route> <from uri="direct:start"/> - <saga sagaService="mySagaService"> - <compensation uri="mock:compensation"/> - <completion uri="mock:completion"/> + <saga sagaService="mySagaService" compensation="mock:compensation" completion="mock:completion"> <option key="myOptionKey"> <constant>myOptionValue</constant> </option> diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/saga-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/saga-eip.adoc index 0e67b73e965e..27eb8668d216 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/saga-eip.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/saga-eip.adoc @@ -256,8 +256,7 @@ XML:: ---- <route> <from uri="direct:newOrder"/> - <saga propagation="MANDATORY"> - <compensation uri="direct:cancelOrder"/> + <saga propagation="MANDATORY" compensation="direct:cancelOrder"> <transform> <header>Long-Running-Action</header> </transform> @@ -277,8 +276,7 @@ YAML:: steps: - saga: propagation: MANDATORY - compensation: - uri: direct:cancelOrder + compensation: direct:cancelOrder steps: - transform: expression: @@ -387,11 +385,11 @@ Java:: // action from("direct:reserveCredit") .saga() - .propagation(SagaPropagation.MANDATORY) - .compensation("direct:refundCredit") - .transform().header(Exchange.SAGA_LONG_RUNNING_ACTION) - .bean("creditService", "reserveCredit") - .log("Credit ${header.amount} reserved in action ${body}"); + .propagation(SagaPropagation.MANDATORY) + .compensation("direct:refundCredit") + .transform().header(Exchange.SAGA_LONG_RUNNING_ACTION) + .bean("creditService", "reserveCredit") + .log("Credit ${header.amount} reserved in action ${body}"); // compensation from("direct:refundCredit") @@ -406,14 +404,12 @@ XML:: ---- <route> <from uri="direct:reserveCredit"/> - <saga propagation="MANDATORY"> - <compensation uri="direct:refundCredit"/> - <transform> - <header>Long-Running-Action</header> - </transform> - <bean ref="creditService" method="reserveCredit"/> - <log message="Credit ${header.amount} reserved in action ${body}"/> - </saga> + <saga propagation="MANDATORY" compensation="direct:refundCredit"/> + <transform> + <header>Long-Running-Action</header> + </transform> + <bean ref="creditService" method="reserveCredit"/> + <log message="Credit ${header.amount} reserved in action ${body}"/> </route> <route> @@ -436,18 +432,16 @@ YAML:: steps: - saga: propagation: MANDATORY - compensation: - uri: direct:refundCredit - steps: - - transform: - expression: - header: - expression: Long-Running-Action - - bean: - ref: creditService - method: reserveCredit - - log: - message: "Credit ${header.amount} reserved in action ${body}" + compensation: direct:refundCredit + - transform: + expression: + header: + expression: Long-Running-Action + - bean: + ref: creditService + method: reserveCredit + - log: + message: "Credit ${header.amount} reserved in action ${body}" - route: from: uri: direct:refundCredit @@ -493,12 +487,12 @@ The compensation route is the same as in the previous example above ---- from("direct:newOrder") .saga() - .propagation(SagaPropagation.MANDATORY) - .compensation("direct:cancelOrder") - .completion("direct:completeOrder") // completion endpoint - .transform().header(Exchange.SAGA_LONG_RUNNING_ACTION) - .bean("orderManagerService", "newOrder") - .log("Order ${body} created"); + .propagation(SagaPropagation.MANDATORY) + .compensation("direct:cancelOrder") + .completion("direct:completeOrder") // completion endpoint + .transform().header(Exchange.SAGA_LONG_RUNNING_ACTION) + .bean("orderManagerService", "newOrder") + .log("Order ${body} created"); // direct:cancelOrder is the same as in the previous example @@ -518,15 +512,12 @@ The compensation route is the same as in the previous example above ---- <route> <from uri="direct:newOrder"/> - <saga propagation="MANDATORY"> - <completion uri="direct:completeOrder"/> - <compensation uri="direct:cancelOrder"/> - <transform> - <header>Long-Running-Action</header> - </transform> - <bean ref="orderManagerService" method="newOrder"/> - <log message="Order ${body} created"/> - </saga> + <saga propagation="MANDATORY" completion="direct:completeOrder" compensation="direct:cancelOrder"/> + <transform> + <header>Long-Running-Action</header> + </transform> + <bean ref="orderManagerService" method="newOrder"/> + <log message="Order ${body} created"/> </route> <route> @@ -552,20 +543,17 @@ The compensation route is the same as in the previous example above steps: - saga: propagation: MANDATORY - compensation: - uri: direct:cancelOrder - completion: - uri: direct:completeOrder - steps: - - transform: - expression: - header: - expression: Long-Running-Action - - bean: - ref: orderManagerService - method: newOrder - - log: - message: "Order ${body} created" + compensation: direct:cancelOrder + completion: direct:completeOrder + - transform: + expression: + header: + expression: Long-Running-Action + - bean: + ref: orderManagerService + method: newOrder + - log: + message: "Order ${body} created" - route: from: uri: direct:completeOrder @@ -617,11 +605,11 @@ from("direct:reserveCredit") // delegate action from("direct:creditReservation") .saga() - .propagation(SagaPropagation.SUPPORTS) - .option("CreditId", body()) // mark the current body as needed in the compensating action - .compensation("direct:creditRefund") - .bean("creditService", "reserveCredit") - .log("Credit ${header.amount} reserved. Custom Id used is ${body}"); + .propagation(SagaPropagation.SUPPORTS) + .option("CreditId", body()) // mark the current body as needed in the compensating action + .compensation("direct:creditRefund") + .bean("creditService", "reserveCredit") + .log("Credit ${header.amount} reserved. Custom Id used is ${body}"); // called only if the saga is canceled from("direct:creditRefund") @@ -644,14 +632,13 @@ Notice the listing below is not using the `Long-Running-Action` header at all. <route> <from uri="direct:creditReservation"/> - <saga propagation="SUPPORTS"> + <saga propagation="SUPPORTS" compensation="direct:creditRefund"> <option key="CreditId"> <simple>${body}</simple> </option> - <compensation uri="direct:creditRefund"/> - <bean ref="creditService" method="reserveCredit"/> - <log message="Credit ${header.amount} reserved. Custom Id used is ${body}"/> </saga> + <bean ref="creditService" method="reserveCredit"/> + <log message="Credit ${header.amount} reserved. Custom Id used is ${body}"/> </route> <route> @@ -690,14 +677,12 @@ Notice the listing below is not using the `Long-Running-Action` header at all. expression: simple: expression: "${body}" - compensation: - uri: direct:creditRefund - steps: - - bean: - ref: creditService - method: reserveCredit - - log: - message: "Credit ${header.amount} reserved. Custom Id used is ${body}" + compensation: direct:creditRefund + - bean: + ref: creditService + method: reserveCredit + - log: + message: "Credit ${header.amount} reserved. Custom Id used is ${body}" - route: from: uri: direct:creditRefund @@ -742,12 +727,12 @@ Java:: ---- from("direct:newOrder") .saga() - .timeout(1, TimeUnit.MINUTES) // newOrder requires that the saga is completed within 1 minute - .propagation(SagaPropagation.MANDATORY) - .compensation("direct:cancelOrder") - .completion("direct:completeOrder") + .timeout(1, TimeUnit.MINUTES) // newOrder requires that the saga is completed within 1 minute + .propagation(SagaPropagation.MANDATORY) + .compensation("direct:cancelOrder") + .completion("direct:completeOrder") // ... - .log("Order ${body} created"); + .log("Order ${body} created"); ---- XML:: @@ -756,11 +741,8 @@ XML:: ---- <route> <from uri="direct:newOrder"/> - <saga propagation="MANDATORY" timeout="1m"> - <completion uri="direct:completeOrder"/> - <compensation uri="direct:cancelOrder"/> - <log message="Order ${body} created"/> - </saga> + <saga propagation="MANDATORY" timeout="1m" completion="direct:completeOrder" compensation="direct:cancelOrder"/> + <log message="Order ${body} created"/> </route> ---- @@ -775,13 +757,10 @@ YAML:: - saga: propagation: MANDATORY timeout: 1m - compensation: - uri: direct:cancelOrder - completion: - uri: direct:completeOrder - steps: - - log: - message: "Order ${body} created" + compensation: direct:cancelOrder + completion: direct:completeOrder + - log: + message: "Order ${body} created" ---- ==== @@ -803,10 +782,9 @@ Java:: [source,java] ---- from("direct:buy") - .saga() - .timeout(5, TimeUnit.MINUTES) // timeout at saga level - .to("direct:newOrder") - .to("direct:reserveCredit"); + .saga().timeout(5, TimeUnit.MINUTES) // timeout at saga level + .to("direct:newOrder") + .to("direct:reserveCredit"); ---- XML:: @@ -815,10 +793,9 @@ XML:: ---- <route> <from uri="direct:buy"/> - <saga timeout="5m"> - <to uri="direct:newOrder"/> - <to uri="direct:reserveCredit"/> - </saga> + <saga timeout="5m"/> + <to uri="direct:newOrder"/> + <to uri="direct:reserveCredit"/> </route> ---- @@ -831,12 +808,11 @@ YAML:: uri: direct:buy steps: - saga: - timeout: 5m - steps: - - to: - uri: direct:newOrder - - to: - uri: direct:reserveCredit + timeout: "5m" + - to: + uri: direct:newOrder + - to: + uri: direct:reserveCredit ---- ==== @@ -876,22 +852,22 @@ Java:: ---- from("direct:mysaga") .saga() - .completionMode(SagaCompletionMode.MANUAL) - .completion("direct:finalize") - .timeout(2, TimeUnit.HOURS) - .to("seda:newOrder") - .to("seda:reserveCredit"); + .completionMode(SagaCompletionMode.MANUAL) + .completion("direct:finalize") + .timeout(2, TimeUnit.HOURS) + .to("seda:newOrder") + .to("seda:reserveCredit"); // Put here asynchronous processing for seda:newOrder and seda:reserveCredit // They will send asynchronous callbacks to seda:operationCompleted from("seda:operationCompleted") // an asynchronous callback .saga() - .propagation(SagaPropagation.MANDATORY) - .bean("controlService", "actionExecuted") - .filter(simple("${body} == 'ok'")) - .to("saga:complete") // complete the current saga manually (saga component) - .end(); + .propagation(SagaPropagation.MANDATORY) + .bean("controlService", "actionExecuted") + .filter(simple("${body} == 'ok'")) + .to("saga:complete") // complete the current saga manually (saga component) + .end(); // You can put here the direct:finalize endpoint to execute final actions ---- @@ -902,22 +878,19 @@ XML:: ---- <route> <from uri="direct:mysaga"/> - <saga completionMode="MANUAL" timeout="2h0m0s"> - <completion uri="direct:finalize"/> - <to uri="seda:newOrder"/> - <to uri="seda:reserveCredit"/> - </saga> + <saga completionMode="MANUAL" timeout="2h0m0s" completion="direct:finalize"/> + <to uri="seda:newOrder"/> + <to uri="seda:reserveCredit"/> </route> <route> <from uri="seda:operationCompleted"/> - <saga propagation="MANDATORY"> - <bean ref="controlService" method="actionExecuted"/> - <filter> - <simple>${body} == 'ok'</simple> - <to uri="saga:complete"/> - </filter> - </saga> + <saga propagation="MANDATORY"/> + <bean ref="controlService" method="actionExecuted"/> + <filter> + <simple>${body} == 'ok'</simple> + <to uri="saga:complete"/> + </filter> </route> ---- @@ -932,30 +905,27 @@ YAML:: - saga: completionMode: MANUAL timeout: 2h - completion: - uri: direct:finalize - steps: - - to: - uri: seda:newOrder - - to: - uri: seda:reserveCredit + completion: direct:finalize + - to: + uri: seda:newOrder + - to: + uri: seda:reserveCredit - route: from: uri: seda:operationCompleted steps: - saga: propagation: MANDATORY + - bean: + ref: controlService + method: actionExecuted + - filter: + expression: + simple: + expression: "${body} == 'ok'" steps: - - bean: - ref: controlService - method: actionExecuted - - filter: - expression: - simple: - expression: "${body} == 'ok'" - steps: - - to: - uri: saga:complete + - to: + uri: saga:complete ---- ==== diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/saga.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/saga.json index 1334df81a513..15543e203db7 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/saga.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/saga.json @@ -20,8 +20,8 @@ "propagation": { "index": 5, "kind": "attribute", "displayName": "Propagation", "group": "advanced", "label": "advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.SagaPropagation", "enum": [ "REQUIRED", "REQUIRES_NEW", "MANDATORY", "SUPPORTS", "NOT_SUPPORTED", "NEVER" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "REQUIRED", "description": "Set the Saga propagation mode (REQUIRED, REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPP [...] "completionMode": { "index": 6, "kind": "attribute", "displayName": "Completion Mode", "group": "advanced", "label": "advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.SagaCompletionMode", "enum": [ "AUTO", "MANUAL" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "AUTO", "description": "Determine how the saga should be considered complete. When set to AUTO, the saga is completed when the exchange that initiates the saga is [...] "timeout": { "index": 7, "kind": "attribute", "displayName": "Timeout", "group": "common", "required": false, "type": "duration", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Set the maximum amount of time for the Saga. After the timeout is expired, the saga will be compensated automatically (unless a different decision has been taken in the meantime)." }, - "compensation": { "index": 8, "kind": "element", "displayName": "Compensation", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.SagaActionUriDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding to the compensation URI must perform compensation and complete without error. If errors occur dur [...] - "completion": { "index": 9, "kind": "element", "displayName": "Completion", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.SagaActionUriDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the completion URI must perform completion tasks and terminate without error. If errors occur during com [...] + "compensation": { "index": 8, "kind": "attribute", "displayName": "Compensation", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the saga s [...] + "completion": { "index": 9, "kind": "attribute", "displayName": "Completion", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga service ma [...] "option": { "index": 10, "kind": "element", "displayName": "Option", "group": "advanced", "label": "advanced", "required": false, "type": "array", "javaType": "java.util.List<org.apache.camel.model.PropertyExpressionDefinition>", "deprecated": false, "autowired": false, "secret": false, "description": "Allows to save properties of the current exchange in order to re-use them in a compensation\/completion callback route. Options are usually helpful e.g. to store and retrieve identifie [...] "outputs": { "index": 11, "kind": "element", "displayName": "Outputs", "group": "common", "required": true, "type": "array", "javaType": "java.util.List", "oneOf": [ "aggregate", "bean", "choice", "circuitBreaker", "claimCheck", "convertBodyTo", "convertHeaderTo", "convertVariableTo", "delay", "doCatch", "doFinally", "doTry", "dynamicRouter", "enrich", "filter", "idempotentConsumer", "intercept", "interceptFrom", "interceptSendToEndpoint", "kamelet", "loadBalance", "log", "loop", "ma [...] } diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/SagaActionUriDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/SagaActionUriDefinition.java deleted file mode 100644 index cdcadbd87893..000000000000 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/SagaActionUriDefinition.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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 org.apache.camel.model; - -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; - -import org.apache.camel.spi.Metadata; - -/** - * Allows to declare saga actions to complete or compensate a saga - */ -@Metadata(label = "configuration") -@XmlAccessorType(XmlAccessType.FIELD) -public class SagaActionUriDefinition extends SendDefinition<SagaActionUriDefinition> { - - public SagaActionUriDefinition() { - } - - public SagaActionUriDefinition(SagaActionUriDefinition source) { - super(source); - } - - public SagaActionUriDefinition(String uri) { - super(uri); - } - - @Override - public SagaActionUriDefinition copyDefinition() { - return new SagaActionUriDefinition(this); - } - - @Override - public String getShortName() { - return "SagaAction"; - } -} diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/SagaDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/SagaDefinition.java index 248a29893ac3..92be490ff371 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/SagaDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/SagaDefinition.java @@ -30,7 +30,9 @@ import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.xml.bind.annotation.XmlTransient; import org.apache.camel.Expression; +import org.apache.camel.builder.EndpointProducerBuilder; import org.apache.camel.saga.CamelSagaService; +import org.apache.camel.spi.AsEndpointUri; import org.apache.camel.spi.Metadata; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.TimeUtils; @@ -43,6 +45,11 @@ import org.apache.camel.util.TimeUtils; @XmlAccessorType(XmlAccessType.FIELD) public class SagaDefinition extends OutputDefinition<SagaDefinition> { + @XmlTransient + private EndpointProducerBuilder compensationEndpointProducerBuilder; + @XmlTransient + private EndpointProducerBuilder completionEndpointProducerBuilder; + @XmlTransient private CamelSagaService sagaServiceBean; @XmlAttribute @@ -59,10 +66,12 @@ public class SagaDefinition extends OutputDefinition<SagaDefinition> { @XmlAttribute @Metadata(javaType = "java.time.Duration") private String timeout; - @XmlElement - private SagaActionUriDefinition compensation; - @XmlElement - private SagaActionUriDefinition completion; + @XmlAttribute + @Metadata + private String compensation; + @XmlAttribute + @Metadata + private String completion; @XmlElement(name = "option") @Metadata(label = "advanced") private List<PropertyExpressionDefinition> options; @@ -77,8 +86,10 @@ public class SagaDefinition extends OutputDefinition<SagaDefinition> { this.propagation = source.propagation; this.completionMode = source.completionMode; this.timeout = source.timeout; - this.compensation = source.compensation != null ? source.compensation.copyDefinition() : null; - this.completion = source.completion != null ? source.completion.copyDefinition() : null; + this.compensation = source.compensation; + this.completion = source.completion; + this.compensationEndpointProducerBuilder = source.compensationEndpointProducerBuilder; + this.completionEndpointProducerBuilder = source.completionEndpointProducerBuilder; this.options = ProcessorDefinitionHelper.deepCopyDefinitions(source.options); } @@ -135,6 +146,14 @@ public class SagaDefinition extends OutputDefinition<SagaDefinition> { // Properties + public EndpointProducerBuilder getCompensationEndpointProducerBuilder() { + return compensationEndpointProducerBuilder; + } + + public EndpointProducerBuilder getCompletionEndpointProducerBuilder() { + return completionEndpointProducerBuilder; + } + public CamelSagaService getSagaServiceBean() { return sagaServiceBean; } @@ -150,7 +169,7 @@ public class SagaDefinition extends OutputDefinition<SagaDefinition> { this.sagaService = sagaService; } - public SagaActionUriDefinition getCompensation() { + public String getCompensation() { return compensation; } @@ -159,11 +178,20 @@ public class SagaDefinition extends OutputDefinition<SagaDefinition> { * corresponding to the compensation URI must perform compensation and complete without error. If errors occur * during compensation, the saga service may call again the compensation URI to retry. */ - public void setCompensation(SagaActionUriDefinition compensation) { + public void setCompensation(@AsEndpointUri String compensation) { this.compensation = compensation; } - public SagaActionUriDefinition getCompletion() { + /** + * The compensation endpoint URI that must be called to compensate all changes done in the route. The route + * corresponding to the compensation URI must perform compensation and complete without error. If errors occur + * during compensation, the saga service may call again the compensation URI to retry. + */ + public void setCompensation(@AsEndpointUri EndpointProducerBuilder compensation) { + this.compensationEndpointProducerBuilder = compensation; + } + + public String getCompletion() { return completion; } @@ -172,10 +200,19 @@ public class SagaDefinition extends OutputDefinition<SagaDefinition> { * to the completion URI must perform completion tasks and terminate without error. If errors occur during * completion, the saga service may call again the completion URI to retry. */ - public void setCompletion(SagaActionUriDefinition completion) { + public void setCompletion(@AsEndpointUri String completion) { this.completion = completion; } + /** + * The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding + * to the completion URI must perform completion tasks and terminate without error. If errors occur during + * completion, the saga service may call again the completion URI to retry. + */ + public void setCompletion(@AsEndpointUri EndpointProducerBuilder completion) { + this.completionEndpointProducerBuilder = completion; + } + public String getPropagation() { return propagation; } @@ -236,18 +273,12 @@ public class SagaDefinition extends OutputDefinition<SagaDefinition> { // Builders public SagaDefinition compensation(String compensation) { - if (this.compensation != null) { - throw new IllegalStateException("Compensation has already been set"); - } - this.compensation = new SagaActionUriDefinition(compensation); + this.compensation = compensation; return this; } public SagaDefinition completion(String completion) { - if (this.completion != null) { - throw new IllegalStateException("Completion has already been set"); - } - this.completion = new SagaActionUriDefinition(completion); + this.completion = completion; return this; } diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SagaReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SagaReifier.java index cc7b8412ad90..75737ae210d5 100644 --- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SagaReifier.java +++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/SagaReifier.java @@ -17,16 +17,17 @@ package org.apache.camel.reifier; import java.util.Map; -import java.util.Optional; import java.util.TreeMap; +import org.apache.camel.CamelContextAware; import org.apache.camel.Endpoint; import org.apache.camel.Expression; import org.apache.camel.Processor; import org.apache.camel.Route; import org.apache.camel.model.ProcessorDefinition; +import org.apache.camel.model.ProcessorDefinitionHelper; import org.apache.camel.model.PropertyExpressionDefinition; -import org.apache.camel.model.SagaActionUriDefinition; +import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.SagaDefinition; import org.apache.camel.processor.saga.SagaCompletionMode; import org.apache.camel.processor.saga.SagaProcessor; @@ -34,6 +35,7 @@ import org.apache.camel.processor.saga.SagaProcessorBuilder; import org.apache.camel.processor.saga.SagaPropagation; import org.apache.camel.saga.CamelSagaService; import org.apache.camel.saga.CamelSagaStep; +import org.apache.camel.support.EndpointHelper; public class SagaReifier extends ProcessorReifier<SagaDefinition> { @@ -43,15 +45,38 @@ public class SagaReifier extends ProcessorReifier<SagaDefinition> { @Override public Processor createProcessor() throws Exception { - Endpoint compensationEndpoint = Optional.ofNullable(definition.getCompensation()) - .map(SagaActionUriDefinition::getUri) - .map(this::resolveEndpoint) - .orElse(null); - Endpoint completionEndpoint = Optional.ofNullable(definition.getCompletion()) - .map(SagaActionUriDefinition::getUri) - .map(this::resolveEndpoint) - .orElse(null); + // compensation + String uri; + if (definition.getCompensationEndpointProducerBuilder() != null) { + uri = definition.getCompensationEndpointProducerBuilder().getRawUri(); + } else { + uri = definition.getCompensation(); + } + // route templates should pre parse uri as they have dynamic values as part of their template parameters + RouteDefinition rd = ProcessorDefinitionHelper.getRoute(definition); + if (uri != null && rd != null && rd.isTemplate() != null && rd.isTemplate()) { + uri = EndpointHelper.resolveEndpointUriPropertyPlaceholders(camelContext, uri); + } + Endpoint compensationEndpoint = null; + if (uri != null) { + compensationEndpoint = camelContext.getEndpoint(uri); + } + + // completion + if (definition.getCompletionEndpointProducerBuilder() != null) { + uri = definition.getCompletionEndpointProducerBuilder().getRawUri(); + } else { + uri = definition.getCompletion(); + } + // route templates should pre parse uri as they have dynamic values as part of their template parameters + if (uri != null && rd != null && rd.isTemplate() != null && rd.isTemplate()) { + uri = EndpointHelper.resolveEndpointUriPropertyPlaceholders(camelContext, uri); + } + Endpoint completionEndpoint = null; + if (uri != null) { + completionEndpoint = camelContext.getEndpoint(uri); + } Map<String, Expression> optionsMap = new TreeMap<>(); if (definition.getOptions() != null) { @@ -81,6 +106,7 @@ public class SagaReifier extends ProcessorReifier<SagaDefinition> { Processor childProcessor = this.createChildProcessor(true); CamelSagaService camelSagaService = resolveSagaService(); + CamelContextAware.trySetCamelContext(camelSagaService, getCamelContext()); camelSagaService.registerStep(step); diff --git a/core/camel-core/src/test/resources/org/apache/camel/model/saga.xml b/core/camel-core/src/test/resources/org/apache/camel/model/saga.xml index 03412a85621b..23fc27f62011 100644 --- a/core/camel-core/src/test/resources/org/apache/camel/model/saga.xml +++ b/core/camel-core/src/test/resources/org/apache/camel/model/saga.xml @@ -18,14 +18,12 @@ --> <routes id="camel" xmlns="http://camel.apache.org/schema/spring"> - <route> - <from uri="direct:start"/> - <saga> - <compensation uri="mock:uri" /> - </saga> - <setBody> - <simple>${in.body} extra data!</simple> - </setBody> - <to uri="mock:end"/> - </route> + <route> + <from uri="direct:start"/> + <saga compensation="mock:uri"/> + <setBody> + <simple>${in.body} extra data!</simple> + </setBody> + <to uri="mock:end"/> + </route> </routes> diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java index 13920b6e1df8..598c58845a34 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java @@ -1061,21 +1061,18 @@ public class ModelParser extends BaseParser { } protected SagaDefinition doParseSagaDefinition() throws IOException, XmlPullParserException { return doParse(new SagaDefinition(), (def, key, val) -> switch (key) { + case "compensation": def.setCompensation(val); yield true; + case "completion": def.setCompletion(val); yield true; case "completionMode": def.setCompletionMode(val); yield true; case "propagation": def.setPropagation(val); yield true; case "sagaService": def.setSagaService(val); yield true; case "timeout": def.setTimeout(val); yield true; default: yield processorDefinitionAttributeHandler().accept(def, key, val); }, (def, key) -> switch (key) { - case "compensation": def.setCompensation(doParseSagaActionUriDefinition()); yield true; - case "completion": def.setCompletion(doParseSagaActionUriDefinition()); yield true; case "option": doAdd(doParsePropertyExpressionDefinition(), def.getOptions(), def::setOptions); yield true; default: yield outputDefinitionElementHandler().accept(def, key); }, noValueHandler()); } - protected SagaActionUriDefinition doParseSagaActionUriDefinition() throws IOException, XmlPullParserException { - return doParse(new SagaActionUriDefinition(), sendDefinitionAttributeHandler(), optionalIdentifiedDefinitionElementHandler(), noValueHandler()); - } protected SamplingDefinition doParseSamplingDefinition() throws IOException, XmlPullParserException { return doParse(new SamplingDefinition(), (def, key, val) -> switch (key) { case "messageFrequency": def.setMessageFrequency(val); yield true; diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java index 212452be88c4..b6bb134bac52 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java @@ -1712,11 +1712,6 @@ public class ModelWriter extends BaseWriter { doWriteExpressionNodeElements(def); endElement(name); } - protected void doWriteSagaActionUriDefinition(String name, SagaActionUriDefinition def) throws IOException { - startElement(name); - doWriteSendDefinitionAttributes(def); - endElement(name); - } protected void doWriteSagaDefinition(String name, SagaDefinition def) throws IOException { startElement(name); doWriteProcessorDefinitionAttributes(def); @@ -1724,9 +1719,9 @@ public class ModelWriter extends BaseWriter { doWriteAttribute("propagation", def.getPropagation(), "REQUIRED"); doWriteAttribute("completionMode", def.getCompletionMode(), "AUTO"); doWriteAttribute("timeout", def.getTimeout(), null); + doWriteAttribute("compensation", def.getCompensation(), null); + doWriteAttribute("completion", def.getCompletion(), null); doWriteList(null, "option", def.getOptions(), this::doWritePropertyExpressionDefinition); - doWriteElement("compensation", def.getCompensation(), this::doWriteSagaActionUriDefinition); - doWriteElement("completion", def.getCompletion(), this::doWriteSagaActionUriDefinition); doWriteList(null, null, def.getOutputs(), this::doWriteProcessorDefinitionRef); endElement(name); } diff --git a/core/camel-xml-io/src/test/resources/saga.xml b/core/camel-xml-io/src/test/resources/saga.xml index 0ac00028695e..6e24d3490d2a 100644 --- a/core/camel-xml-io/src/test/resources/saga.xml +++ b/core/camel-xml-io/src/test/resources/saga.xml @@ -20,9 +20,7 @@ <routes id="camel" xmlns="http://camel.apache.org/schema/xml-io"> <route> <from uri="direct:start"/> - <saga> - <compensation uri="mock:uri" /> - </saga> + <saga compensation="mock:uri"/> <setBody> <simple>${in.body} extra data!</simple> </setBody> diff --git a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java index 846612c5ee75..c4c310422894 100644 --- a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java +++ b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/ModelWriter.java @@ -1712,11 +1712,6 @@ public class ModelWriter extends BaseWriter { doWriteExpressionNodeElements(def); endElement(name); } - protected void doWriteSagaActionUriDefinition(String name, SagaActionUriDefinition def) throws IOException { - startElement(name); - doWriteSendDefinitionAttributes(def); - endElement(name); - } protected void doWriteSagaDefinition(String name, SagaDefinition def) throws IOException { startElement(name); doWriteProcessorDefinitionAttributes(def); @@ -1724,9 +1719,9 @@ public class ModelWriter extends BaseWriter { doWriteAttribute("propagation", def.getPropagation(), "REQUIRED"); doWriteAttribute("completionMode", def.getCompletionMode(), "AUTO"); doWriteAttribute("timeout", def.getTimeout(), null); + doWriteAttribute("compensation", def.getCompensation(), null); + doWriteAttribute("completion", def.getCompletion(), null); doWriteList(null, "option", def.getOptions(), this::doWritePropertyExpressionDefinition); - doWriteElement("compensation", def.getCompensation(), this::doWriteSagaActionUriDefinition); - doWriteElement("completion", def.getCompletion(), this::doWriteSagaActionUriDefinition); doWriteList(null, null, def.getOutputs(), this::doWriteProcessorDefinitionRef); endElement(name); } diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc index 590023ca05d9..6135543c315a 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc @@ -18,6 +18,116 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details. In the YAML DSL we have renamed `routePolicy` to `routePolicyRef` on the `route` node, as that is the correct name. +==== Saga EIP + +The Saga EIP has _fixed_ the model for how to configure completion and compensation URIs. + +For Java DSL there is no changes, but XML and YAML DSL is affected. Here the `<compensation>` and `<completion>` tags +has been changed to be an attribute on `<saga>` instead as shown below: + +Before: + +[source,xml] +---- +<route> + <from uri="direct:start"/> + <saga sagaService="mySagaService"> + <compensation uri="mock:compensation"/> + <completion uri="mock:completion"/> + <option key="myOptionKey"> + <constant>myOptionValue</constant> + </option> + <option key="myOptionKey2"> + <constant>myOptionValue2</constant> + </option> + </saga> + <choice> + <when> + <simple>${body} == 'fail'</simple> + <throwException exceptionType="java.lang.RuntimeException" message="fail"/> + </when> + </choice> + <to uri="mock:end"/> +</route> +---- + +In YAML DSL the changes are even simpler as the endpoint is moved from `uri` to the value of `completion` or `compensation`. + +[source,yaml] +---- +- route: + from: + uri: direct:start + steps: + - saga: + sagaService: mySagaService + compensation: + uri: mock:compensation + completion: + uri: mock:completion + key: myOptionKey2 + - choice: + when: + - expression: + simple: + expression: "${body} == 'fail'" + steps: + - throwException: + message: fail + exceptionType: java.lang.RuntimeException + - to: + uri: mock:end + +---- + +After: + +[source,xml] +---- +<route> + <from uri="direct:start"/> + <saga sagaService="mySagaService" compensation="mock:compensation" completion="mock:completion"> + <option key="myOptionKey"> + <constant>myOptionValue</constant> + </option> + <option key="myOptionKey2"> + <constant>myOptionValue2</constant> + </option> + </saga> + <choice> + <when> + <simple>${body} == 'fail'</simple> + <throwException exceptionType="java.lang.RuntimeException" message="fail"/> + </when> + </choice> + <to uri="mock:end"/> +</route> +---- + +[source,yaml] +---- +- route: + from: + uri: direct:start + steps: + - saga: + sagaService: mySagaService + compensation: mock:compensation + completion: mock:completion + key: myOptionKey2 + - choice: + when: + - expression: + simple: + expression: "${body} == 'fail'" + steps: + - throwException: + message: fail + exceptionType: java.lang.RuntimeException + - to: + uri: mock:end +---- + === camel-simple In the simple language then init blocks syntax has changed to require that each variable ends with a semicolon and new line (no trailing comments etc is allowed) diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_19.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_19.adoc index 3d9244a9d699..4732c687f8b8 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_19.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_19.adoc @@ -20,6 +20,116 @@ uses `InOnly` pattern. Removed 2 deprecated methods in Java DSL for `throttler` EIP. +==== Saga EIP + +The Saga EIP has _fixed_ the model for how to configure completion and compensation URIs. + +For Java DSL there is no changes, but XML and YAML DSL is affected. Here the `<compensation>` and `<completion>` tags +has been changed to be an attribute on `<saga>` instead as shown below: + +Before: + +[source,xml] +---- +<route> + <from uri="direct:start"/> + <saga sagaService="mySagaService"> + <compensation uri="mock:compensation"/> + <completion uri="mock:completion"/> + <option key="myOptionKey"> + <constant>myOptionValue</constant> + </option> + <option key="myOptionKey2"> + <constant>myOptionValue2</constant> + </option> + </saga> + <choice> + <when> + <simple>${body} == 'fail'</simple> + <throwException exceptionType="java.lang.RuntimeException" message="fail"/> + </when> + </choice> + <to uri="mock:end"/> +</route> +---- + +In YAML DSL the changes are even simpler as the endpoint is moved from `uri` to the value of `completion` or `compensation`. + +[source,yaml] +---- +- route: + from: + uri: direct:start + steps: + - saga: + sagaService: mySagaService + compensation: + uri: mock:compensation + completion: + uri: mock:completion + key: myOptionKey2 + - choice: + when: + - expression: + simple: + expression: "${body} == 'fail'" + steps: + - throwException: + message: fail + exceptionType: java.lang.RuntimeException + - to: + uri: mock:end + +---- + +After: + +[source,xml] +---- +<route> + <from uri="direct:start"/> + <saga sagaService="mySagaService" compensation="mock:compensation" completion="mock:completion"> + <option key="myOptionKey"> + <constant>myOptionValue</constant> + </option> + <option key="myOptionKey2"> + <constant>myOptionValue2</constant> + </option> + </saga> + <choice> + <when> + <simple>${body} == 'fail'</simple> + <throwException exceptionType="java.lang.RuntimeException" message="fail"/> + </when> + </choice> + <to uri="mock:end"/> +</route> +---- + +[source,yaml] +---- +- route: + from: + uri: direct:start + steps: + - saga: + sagaService: mySagaService + compensation: mock:compensation + completion: mock:completion + key: myOptionKey2 + - choice: + when: + - expression: + simple: + expression: "${body} == 'fail'" + steps: + - throwException: + message: fail + exceptionType: java.lang.RuntimeException + - to: + uri: mock:end +---- + === camel-simple In the simple language then init blocks syntax has changed to require that each variable ends with a semicolon and new line (no trailing comments etc is allowed) diff --git a/dsl/camel-kamelet-main-support/src/main/java/org/apache/camel/main/stub/StubBeanRepository.java b/dsl/camel-kamelet-main-support/src/main/java/org/apache/camel/main/stub/StubBeanRepository.java index a6a6f44cd370..b925b7850836 100644 --- a/dsl/camel-kamelet-main-support/src/main/java/org/apache/camel/main/stub/StubBeanRepository.java +++ b/dsl/camel-kamelet-main-support/src/main/java/org/apache/camel/main/stub/StubBeanRepository.java @@ -34,6 +34,8 @@ import org.apache.camel.processor.aggregate.GroupedBodyAggregationStrategy; import org.apache.camel.processor.aggregate.MemoryAggregationRepository; import org.apache.camel.processor.loadbalancer.LoadBalancer; import org.apache.camel.processor.loadbalancer.RoundRobinLoadBalancer; +import org.apache.camel.saga.CamelSagaService; +import org.apache.camel.saga.InMemorySagaService; import org.apache.camel.spi.AggregationRepository; import org.apache.camel.spi.BeanRepository; import org.apache.camel.spi.ClaimCheckRepository; @@ -62,6 +64,7 @@ public class StubBeanRepository implements BeanRepository { private final RoutePolicy service9 = new RoutePolicySupport() { }; private final ExecutorService service10 = Executors.newCachedThreadPool(); + private final CamelSagaService service11 = new InMemorySagaService(); private final String stubPattern; @@ -139,6 +142,9 @@ public class StubBeanRepository implements BeanRepository { if (ExecutorService.class.isAssignableFrom(type)) { return (T) service10; } + if (CamelSagaService.class.isAssignableFrom(type)) { + return (T) service11; + } if (Logger.class.isAssignableFrom(type)) { return (T) LOG; } diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml index 462412e678a5..a8f48fccea23 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml @@ -140,10 +140,6 @@ <bannedDefinition>org.apache.camel.model.app.BeansDefinition</bannedDefinition> <bannedDefinition>org.apache.camel.model.app.ApplicationDefinition</bannedDefinition> </bannedDefinitions> - <additionalDefinitions> - <!-- saga --> - <additionalDefinition>org.apache.camel.model.SagaActionUriDefinition</additionalDefinition> - </additionalDefinitions> </configuration> </execution> </executions> diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java index b858b853a32c..7f9da6c9d827 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java @@ -77,7 +77,6 @@ import org.apache.camel.model.RouteContextRefDefinition; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.RouteTemplateParameterDefinition; import org.apache.camel.model.RoutingSlipDefinition; -import org.apache.camel.model.SagaActionUriDefinition; import org.apache.camel.model.SagaDefinition; import org.apache.camel.model.SamplingDefinition; import org.apache.camel.model.ScriptDefinition; @@ -16239,78 +16238,6 @@ public final class ModelDeserializers extends YamlDeserializerSupport { } } - @YamlType( - inline = true, - types = org.apache.camel.model.SagaActionUriDefinition.class, - order = org.apache.camel.dsl.yaml.common.YamlDeserializerResolver.ORDER_LOWEST - 1, - properties = { - @YamlProperty(name = "description", type = "string"), - @YamlProperty(name = "disabled", type = "boolean"), - @YamlProperty(name = "id", type = "string"), - @YamlProperty(name = "note", type = "string"), - @YamlProperty(name = "parameters", type = "object"), - @YamlProperty(name = "uri", type = "string", required = true) - } - ) - public static class SagaActionUriDefinitionDeserializer extends YamlDeserializerEndpointAwareBase<SagaActionUriDefinition> { - public SagaActionUriDefinitionDeserializer() { - super(SagaActionUriDefinition.class); - } - - @Override - protected SagaActionUriDefinition newInstance() { - return new SagaActionUriDefinition(); - } - - @Override - protected SagaActionUriDefinition newInstance(String value) { - return new SagaActionUriDefinition(value); - } - - @Override - protected void setEndpointUri(CamelContext camelContext, Node node, - SagaActionUriDefinition target, Map<String, Object> parameters) { - target.setUri(org.apache.camel.dsl.yaml.common.YamlSupport.createEndpointUri(camelContext, node, target.getUri(), parameters)); - } - - @Override - protected boolean setProperty(SagaActionUriDefinition target, String propertyKey, - String propertyName, Node node) { - propertyKey = org.apache.camel.util.StringHelper.dashToCamelCase(propertyKey); - switch(propertyKey) { - case "disabled": { - String val = asText(node); - target.setDisabled(val); - break; - } - case "uri": { - String val = asText(node); - target.setUri(val); - break; - } - case "id": { - String val = asText(node); - target.setId(val); - break; - } - case "description": { - String val = asText(node); - target.setDescription(val); - break; - } - case "note": { - String val = asText(node); - target.setNote(val); - break; - } - default: { - return false; - } - } - return true; - } - } - @YamlType( nodes = "saga", types = org.apache.camel.model.SagaDefinition.class, @@ -16319,8 +16246,8 @@ public final class ModelDeserializers extends YamlDeserializerSupport { description = "Enables Sagas on the route", deprecated = false, properties = { - @YamlProperty(name = "compensation", type = "object:org.apache.camel.model.SagaActionUriDefinition", description = "The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the saga service may call again the compensation URI to retry.", displayName = "Compensation"), - @YamlProperty(name = "completion", type = "object:org.apache.camel.model.SagaActionUriDefinition", description = "The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga service may call again the completion URI to retry.", displayName = "Completion"), + @YamlProperty(name = "compensation", type = "string", description = "The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the saga service may call again the compensation URI to retry.", displayName = "Compensation"), + @YamlProperty(name = "completion", type = "string", description = "The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga service may call again the completion URI to retry.", displayName = "Completion"), @YamlProperty(name = "completionMode", type = "enum:AUTO,MANUAL", defaultValue = "AUTO", description = "Determine how the saga should be considered complete. When set to AUTO, the saga is completed when the exchange that initiates the saga is processed successfully, or compensated when it completes exceptionally. When set to MANUAL, the user must complete or compensate the saga using the saga:complete or saga:compensate endpoints.", displayName = "Completion Mode"), @YamlProperty(name = "description", type = "string", description = "Sets the description of this node", displayName = "Description"), @YamlProperty(name = "disabled", type = "boolean", defaultValue = "false", description = "Disables this EIP from the route.", displayName = "Disabled"), @@ -16349,12 +16276,12 @@ public final class ModelDeserializers extends YamlDeserializerSupport { propertyKey = org.apache.camel.util.StringHelper.dashToCamelCase(propertyKey); switch(propertyKey) { case "compensation": { - org.apache.camel.model.SagaActionUriDefinition val = asType(node, org.apache.camel.model.SagaActionUriDefinition.class); + String val = asText(node); target.setCompensation(val); break; } case "completion": { - org.apache.camel.model.SagaActionUriDefinition val = asType(node, org.apache.camel.model.SagaActionUriDefinition.class); + String val = asText(node); target.setCompletion(val); break; } diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializersResolver.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializersResolver.java index a500d5828274..09d84f8285d4 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializersResolver.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializersResolver.java @@ -367,7 +367,6 @@ public final class ModelDeserializersResolver implements YamlDeserializerResolve case "org.apache.camel.model.RoutingSlipDefinition": return new ModelDeserializers.RoutingSlipDefinitionDeserializer(); case "rss": return new ModelDeserializers.RssDataFormatDeserializer(); case "org.apache.camel.model.dataformat.RssDataFormat": return new ModelDeserializers.RssDataFormatDeserializer(); - case "org.apache.camel.model.SagaActionUriDefinition": return new ModelDeserializers.SagaActionUriDefinitionDeserializer(); case "saga": return new ModelDeserializers.SagaDefinitionDeserializer(); case "org.apache.camel.model.SagaDefinition": return new ModelDeserializers.SagaDefinitionDeserializer(); case "sample": return new ModelDeserializers.SamplingDefinitionDeserializer(); diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json index faa6977a189d..75c0a28249bd 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json @@ -6102,35 +6102,6 @@ } } ] }, - "org.apache.camel.model.SagaActionUriDefinition" : { - "oneOf" : [ { - "type" : "string" - }, { - "type" : "object", - "additionalProperties" : false, - "properties" : { - "description" : { - "type" : "string" - }, - "disabled" : { - "type" : "boolean" - }, - "id" : { - "type" : "string" - }, - "note" : { - "type" : "string" - }, - "parameters" : { - "type" : "object" - }, - "uri" : { - "type" : "string" - } - } - } ], - "required" : [ "uri" ] - }, "org.apache.camel.model.SagaDefinition" : { "title" : "Saga", "description" : "Enables Sagas on the route", @@ -6138,14 +6109,14 @@ "additionalProperties" : false, "properties" : { "compensation" : { + "type" : "string", "title" : "Compensation", - "description" : "The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the saga service may call again the compensation URI to retry.", - "$ref" : "#/items/definitions/org.apache.camel.model.SagaActionUriDefinition" + "description" : "The compensation endpoint URI that must be called to compensate all changes done in the route. The route corresponding to the compensation URI must perform compensation and complete without error. If errors occur during compensation, the saga service may call again the compensation URI to retry." }, "completion" : { + "type" : "string", "title" : "Completion", - "description" : "The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga service may call again the completion URI to retry.", - "$ref" : "#/items/definitions/org.apache.camel.model.SagaActionUriDefinition" + "description" : "The completion endpoint URI that will be called when the Saga is completed successfully. The route corresponding to the completion URI must perform completion tasks and terminate without error. If errors occur during completion, the saga service may call again the completion URI to retry." }, "completionMode" : { "type" : "string", diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/SagaTest.groovy b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/SagaTest.groovy index 84f3ca3d5858..a8c8dd6376f7 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/SagaTest.groovy +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/SagaTest.groovy @@ -17,7 +17,6 @@ package org.apache.camel.dsl.yaml import org.apache.camel.dsl.yaml.support.YamlTestSupport -import org.apache.camel.model.SagaActionUriDefinition import org.apache.camel.model.SagaDefinition import org.apache.camel.model.ToDefinition import org.apache.camel.model.language.JqExpression @@ -34,13 +33,8 @@ class SagaTest extends YamlTestSupport { with(context.routeDefinitions[0].outputs[0], SagaDefinition) { propagation == "MANDATORY" completionMode == "MANUAL" - - with(compensation, SagaActionUriDefinition) { - uri == "direct:compensation" - } - with(completion, SagaActionUriDefinition) { - uri == "direct:completion" - } + compensation == "direct:compensation" + completion == "direct:completion" // saga spans the entire route so steps are inserted before any saga specific step // https://issues.apache.org/jira/browse/CAMEL-17129 with(outputs[0], ToDefinition) { @@ -66,12 +60,8 @@ class SagaTest extends YamlTestSupport { - saga: propagation: "MANDATORY" completionMode: "MANUAL" - compensation: - uri: "direct" - parameters: - name: compensation - completion: - uri: "direct:completion" + compensation: direct:compensation + completion: direct:completion steps: - to: "direct:something" option: @@ -84,29 +74,6 @@ class SagaTest extends YamlTestSupport { expression: "${body}" - to: "mock:result" '''), - asResource('full-parameters-out-of-order)', ''' - - from: - uri: "direct:start" - steps: - - saga: - propagation: "MANDATORY" - completionMode: "MANUAL" - compensation: - parameters: - name: compensation - uri: "direct" - completion: - uri: "direct:completion" - steps: - - to: "direct:something" - option: - - key: o1 - jq: ".foo" - - key: o2 - expression: - simple: "${body}" - - to: "mock:result" - '''), asResource('short', ''' - from: uri: "direct:start"
