This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-jbang-examples.git


The following commit(s) were added to refs/heads/main by this push:
     new e3dc28c  route/dynamic-routing - recipient list, failover load 
balancer and throttle (#117)
e3dc28c is described below

commit e3dc28c4610ae408902220cafc0d8d978a7c51ba
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 16:31:29 2026 +0200

    route/dynamic-routing - recipient list, failover load balancer and throttle 
(#117)
    
    A recipient list computes the endpoints from each order: the warehouse of
    its country and the billing of its status. Billing a paid order fails over
    from a primary invoice service that is down to a backup one, and the US
    warehouse is throttled to one order per second. Sixth in the route group,
    with a Citrus YAML test in the CI matrix.
    
    Closes #122
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    # Conflicts:
    #       .github/workflows/build.yml
    #       README.md
    #       camel-jbang-example-catalog.json
    #       route/README.md
---
 .github/workflows/build.yml                        |   2 +
 README.md                                          |   1 +
 camel-jbang-example-catalog.json                   |  49 +++++++++
 route/README.md                                    |   1 +
 route/dynamic-routing/README.md                    |  82 +++++++++++++++
 route/dynamic-routing/application.properties       |  16 +++
 route/dynamic-routing/dynamic-routing.camel.yaml   | 111 +++++++++++++++++++++
 route/dynamic-routing/metadata.json                |  38 +++++++
 route/dynamic-routing/orders/order-1001.json       |   2 +
 route/dynamic-routing/orders/order-1002.json       |   2 +
 route/dynamic-routing/orders/order-1003.json       |   2 +
 .../test/dynamic-routing.citrus.it.yaml            |  35 +++++++
 12 files changed, 341 insertions(+)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 02a6e47..c76577e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -85,6 +85,8 @@ jobs:
             path: route/filter-and-multicast
           - name: Enrich and deduplicate
             path: route/enrich-and-dedupe
+          - name: Dynamic routing
+            path: route/dynamic-routing
           - name: Error handling
             path: fail-well/error-handling
           - name: Circuit breaker
diff --git a/README.md b/README.md
index b95b73e..5b33526 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,7 @@ The routing patterns: content-based router, splitter, 
aggregator, filter and mul
 | [Aggregator](route/aggregator/) | The warehouse reports each picked line on 
its own and the aggregator collects the lines of one order back into a 
shipment, correlated by the order id and complete when as many lines are in as 
the order had; each shipment is logged as JSON. | nothing |
 | [Filter and multicast](route/filter-and-multicast/) | Three orders are read 
from the orders directory; a filter lets only the paid ones through and a 
multicast sends each paid order to both the warehouse route and the invoicing 
route, which log their part; the pending order is logged as received and goes 
no further. | nothing |
 | [Enrich and deduplicate](route/enrich-and-dedupe/) | The three orders and a 
resent copy of ORD-1001 are read; an idempotent consumer accepts each order id 
once and logs the resent one as a duplicate, each accepted order is enriched 
with its customer's name and tier from customers/ with pollEnrich, and a wire 
tap writes a copy to audit/ without holding the order up. | nothing |
+| [Dynamic routing](route/dynamic-routing/) | Each of the three orders goes to 
the warehouse of its country and to the billing of its status through a 
recipient list computed from the order; billing a paid order fails over from a 
primary invoice service that is down to a backup one, the pending order gets no 
invoice, and the US warehouse is throttled to one order per second. | nothing |
 
 ### [Fail well](fail-well/)
 
diff --git a/camel-jbang-example-catalog.json b/camel-jbang-example-catalog.json
index b0ee5ab..601858b 100644
--- a/camel-jbang-example-catalog.json
+++ b/camel-jbang-example-catalog.json
@@ -1205,6 +1205,55 @@
         ],
         "order": 1
     },
+    {
+        "name": "route/dynamic-routing",
+        "title": "Dynamic routing",
+        "description": "Each of the three orders goes to the warehouse of its 
country and to the billing of its status through a recipient list computed from 
the order; billing a paid order fails over from a primary invoice service that 
is down to a backup one, the pending order gets no invoice, and the US 
warehouse is throttled to one order per second.",
+        "level": "route",
+        "teaches": {
+            "components": [
+                "file",
+                "direct",
+                "log"
+            ],
+            "eips": [
+                "recipientList",
+                "loadBalance",
+                "failoverLoadBalancer",
+                "throttle",
+                "throwException",
+                "noErrorHandler",
+                "unmarshal"
+            ],
+            "languages": [
+                "simple",
+                "constant"
+            ],
+            "dataformats": [
+                "json"
+            ]
+        },
+        "tags": [
+            "shop",
+            "eip",
+            "recipient-list",
+            "failover",
+            "load-balancer",
+            "throttle"
+        ],
+        "bundled": true,
+        "requiresDocker": false,
+        "hasCitrusTests": true,
+        "files": [
+            "README.md",
+            "application.properties",
+            "dynamic-routing.camel.yaml",
+            "orders/order-1001.json",
+            "orders/order-1002.json",
+            "orders/order-1003.json"
+        ],
+        "order": 6
+    },
     {
         "name": "route/enrich-and-dedupe",
         "title": "Enrich and deduplicate",
diff --git a/route/README.md b/route/README.md
index 4e1dffe..e957861 100644
--- a/route/README.md
+++ b/route/README.md
@@ -10,6 +10,7 @@ The routing patterns: content-based router, splitter, 
aggregator, filter and mul
 | [Aggregator](aggregator/) | The warehouse reports each picked line on its 
own and the aggregator collects the lines of one order back into a shipment, 
correlated by the order id and complete when as many lines are in as the order 
had; each shipment is logged as JSON. | nothing |
 | [Filter and multicast](filter-and-multicast/) | Three orders are read from 
the orders directory; a filter lets only the paid ones through and a multicast 
sends each paid order to both the warehouse route and the invoicing route, 
which log their part; the pending order is logged as received and goes no 
further. | nothing |
 | [Enrich and deduplicate](enrich-and-dedupe/) | The three orders and a resent 
copy of ORD-1001 are read; an idempotent consumer accepts each order id once 
and logs the resent one as a duplicate, each accepted order is enriched with 
its customer's name and tier from customers/ with pollEnrich, and a wire tap 
writes a copy to audit/ without holding the order up. | nothing |
+| [Dynamic routing](dynamic-routing/) | Each of the three orders goes to the 
warehouse of its country and to the billing of its status through a recipient 
list computed from the order; billing a paid order fails over from a primary 
invoice service that is down to a backup one, the pending order gets no 
invoice, and the US warehouse is throttled to one order per second. | nothing |
 
 Start with [Content-based router](content-based-router/); the examples read 
best in the order above, each one building on what the one before it set up.
 
diff --git a/route/dynamic-routing/README.md b/route/dynamic-routing/README.md
new file mode 100644
index 0000000..16e3c56
--- /dev/null
+++ b/route/dynamic-routing/README.md
@@ -0,0 +1,82 @@
+# Dynamic routing
+
+Where an order goes is computed from the order itself: a recipient list sends 
it to the warehouse of its country
+and to the billing of its status. Billing fails over to a backup invoice 
service when the primary one is down, and
+the US warehouse takes at most one order per second.
+
+## What you will see
+
+```text
+INFO ... dynamic-routing.camel.yaml:16 : Order ORD-1001 for DK, paid
+INFO ... dynamic-routing.camel.yaml:30 : Warehouse DK picks ORD-1001
+INFO ... dynamic-routing.camel.yaml:110 : Invoice for ORD-1001 via the backup 
service
+...
+INFO ... dynamic-routing.camel.yaml:16 : Order ORD-1003 for US, pending
+INFO ... dynamic-routing.camel.yaml:56 : Warehouse US picks ORD-1003
+INFO ... dynamic-routing.camel.yaml:86 : No invoice for ORD-1003 until it is 
paid
+```
+
+## Install Camel CLI
+
+Install [JBang](https://www.jbang.dev/download/) and the Camel CLI as 
described in the
+[root README](../../README.md#install-the-camel-cli); `camel --version` 
confirms the install.
+
+## Run it
+
+```shell
+camel run *
+```
+
+Stop the example with `ctrl` + `c`.
+
+## How it works
+
+- `recipientList` computes the endpoints for each order with Simple:
+  `direct:warehouse-${body[country]},direct:billing-${body[status]}`. ORD-1001 
goes to `direct:warehouse-DK` and
+  `direct:billing-paid`, ORD-1003 to `direct:warehouse-US` and 
`direct:billing-pending`. Each endpoint is a route.
+- `billing-paid` sends the order through a `failoverLoadBalancer`: the primary 
invoice service first, and the backup
+  when the primary fails with an `IllegalStateException`. 
`inheritErrorHandler: false` makes it fail over at once,
+  without the route's error handler logging the failure first.
+- `invoice-primary` is down: it throws the exception, and `noErrorHandler` 
hands the failure straight back to the
+  load balancer.
+- `throttle` lets at most one order per second into the US warehouse 
(`timePeriodMillis: 1000`); an order over the
+  limit waits.
+
+## Build it step by step
+
+Ask your assistant, or type it yourself, one step at a time, and run after 
each:
+
+1. A file route on `orders` that unmarshals each order and logs its country 
and status.
+2. A recipient list to `direct:warehouse-<country>` and 
`direct:billing-<status>`, with a route for each.
+3. Bill a paid order through a failover load balancer: a primary invoice 
service that fails, and a backup that
+   takes over.
+4. Throttle the US warehouse to one order per second.
+
+## Try changing
+
+- Copy `order-1003.json` to `order-1004.json` and `order-1005.json` with new 
order ids: the US warehouse picks
+  them one second apart.
+- Remove the `throwException` from `invoice-primary`: every invoice goes 
through the primary service.
+- Add an order for a country without a warehouse route: the recipient list 
fails on the unknown endpoint, unless
+  you set `ignoreInvalidEndpoints: true`.
+
+## Integration testing
+
+The example comes with a test in the [Citrus](https://citrusframework.org/) 
YAML DSL,
+`test/dynamic-routing.citrus.it.yaml`, which the Camel CLI runs:
+
+```shell
+camel test run test/dynamic-routing.citrus.it.yaml
+```
+
+The test runs the routes and verifies the warehouses, the failover and the 
pending order.
+
+## Help and contributions
+
+If you hit any problem using Camel or have some feedback, then please
+[let us know](https://camel.apache.org/community/support/).
+
+We also love contributors, so
+[get involved](https://camel.apache.org/community/contributing/) :-)
+
+The Camel riders!
diff --git a/route/dynamic-routing/application.properties 
b/route/dynamic-routing/application.properties
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/route/dynamic-routing/application.properties
@@ -0,0 +1,16 @@
+# 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.
diff --git a/route/dynamic-routing/dynamic-routing.camel.yaml 
b/route/dynamic-routing/dynamic-routing.camel.yaml
new file mode 100644
index 0000000..e9f5f7e
--- /dev/null
+++ b/route/dynamic-routing/dynamic-routing.camel.yaml
@@ -0,0 +1,111 @@
+# Each order goes to the warehouse of its country and to the billing of its 
status: a recipient list computes both
+# endpoints from the order. Billing a paid order fails over to a backup 
invoice service when the primary one is down,
+# and the US warehouse takes at most one order per second.
+- route:
+    id: incoming-orders
+    from:
+      uri: file
+      parameters:
+        directoryName: orders
+        noop: true
+        sortBy: "file:name"
+      steps:
+        - unmarshal:
+            json:
+              library: Jackson
+        - log:
+            message: "Order ${body[orderId]} for ${body[country]}, 
${body[status]}"
+        - recipientList:
+            expression:
+              simple:
+                expression: 
"direct:warehouse-${body[country]},direct:billing-${body[status]}"
+
+- route:
+    id: warehouse-DK
+    from:
+      uri: direct
+      parameters:
+        name: warehouse-DK
+      steps:
+        - log:
+            message: "Warehouse DK picks ${body[orderId]}"
+
+- route:
+    id: warehouse-DE
+    from:
+      uri: direct
+      parameters:
+        name: warehouse-DE
+      steps:
+        - log:
+            message: "Warehouse DE picks ${body[orderId]}"
+
+# At most one order per second: an order over the limit waits
+- route:
+    id: warehouse-US
+    from:
+      uri: direct
+      parameters:
+        name: warehouse-US
+      steps:
+        - throttle:
+            expression:
+              constant:
+                expression: "1"
+            timePeriodMillis: 1000
+        - log:
+            message: "Warehouse US picks ${body[orderId]}"
+
+# The failover load balancer tries the primary invoice service first, and the 
backup when it fails
+- route:
+    id: billing-paid
+    from:
+      uri: direct
+      parameters:
+        name: billing-paid
+      steps:
+        - loadBalance:
+            failoverLoadBalancer:
+              # fail over at once, without the route's error handler logging 
the failure first
+              inheritErrorHandler: false
+              exception:
+                - java.lang.IllegalStateException
+            steps:
+              - to:
+                  uri: direct:invoice-primary
+              - to:
+                  uri: direct:invoice-backup
+
+- route:
+    id: billing-pending
+    from:
+      uri: direct
+      parameters:
+        name: billing-pending
+      steps:
+        - log:
+            message: "No invoice for ${body[orderId]} until it is paid"
+
+# The primary invoice service is down; noErrorHandler hands the failure 
straight back to the load balancer
+- route:
+    id: invoice-primary
+    errorHandler:
+      noErrorHandler: {}
+    from:
+      uri: direct
+      parameters:
+        name: invoice-primary
+      steps:
+        - throwException:
+            exceptionType: java.lang.IllegalStateException
+            message: "The invoice service is down"
+
+- route:
+    id: invoice-backup
+    from:
+      uri: direct
+      parameters:
+        name: invoice-backup
+      steps:
+        - log:
+            message: "Invoice for ${body[orderId]} via the backup service"
diff --git a/route/dynamic-routing/metadata.json 
b/route/dynamic-routing/metadata.json
new file mode 100644
index 0000000..ecb6801
--- /dev/null
+++ b/route/dynamic-routing/metadata.json
@@ -0,0 +1,38 @@
+{
+    "title": "Dynamic routing",
+    "description": "Each of the three orders goes to the warehouse of its 
country and to the billing of its status through a recipient list computed from 
the order; billing a paid order fails over from a primary invoice service that 
is down to a backup one, the pending order gets no invoice, and the US 
warehouse is throttled to one order per second.",
+    "level": "route",
+    "order": 6,
+    "teaches": {
+        "components": [
+            "file",
+            "direct",
+            "log"
+        ],
+        "eips": [
+            "recipientList",
+            "loadBalance",
+            "failoverLoadBalancer",
+            "throttle",
+            "throwException",
+            "noErrorHandler",
+            "unmarshal"
+        ],
+        "languages": [
+            "simple",
+            "constant"
+        ],
+        "dataformats": [
+            "json"
+        ]
+    },
+    "tags": [
+        "shop",
+        "eip",
+        "recipient-list",
+        "failover",
+        "load-balancer",
+        "throttle"
+    ],
+    "bundled": true
+}
diff --git a/route/dynamic-routing/orders/order-1001.json 
b/route/dynamic-routing/orders/order-1001.json
new file mode 100644
index 0000000..3eb5f03
--- /dev/null
+++ b/route/dynamic-routing/orders/order-1001.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1001", "customer": "C-482", "country": "DK", "status": "paid",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 2, "price": 19.95}, {"sku": 
"CAMEL-MUG", "qty": 1, "price": 9.50}]}
diff --git a/route/dynamic-routing/orders/order-1002.json 
b/route/dynamic-routing/orders/order-1002.json
new file mode 100644
index 0000000..bb7aa0e
--- /dev/null
+++ b/route/dynamic-routing/orders/order-1002.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1002", "customer": "C-207", "country": "DE", "status": "paid",
+ "lines": [{"sku": "CAMEL-MUG", "qty": 3, "price": 9.50}]}
diff --git a/route/dynamic-routing/orders/order-1003.json 
b/route/dynamic-routing/orders/order-1003.json
new file mode 100644
index 0000000..9edab76
--- /dev/null
+++ b/route/dynamic-routing/orders/order-1003.json
@@ -0,0 +1,2 @@
+{"orderId": "ORD-1003", "customer": "C-134", "country": "US", "status": 
"pending",
+ "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1, "price": 19.95}, {"sku": 
"CAMEL-CAP", "qty": 1, "price": 14.00}, {"sku": "CAMEL-MUG", "qty": 2, "price": 
9.50}]}
diff --git a/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml 
b/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
new file mode 100644
index 0000000..4794852
--- /dev/null
+++ b/route/dynamic-routing/test/dynamic-routing.citrus.it.yaml
@@ -0,0 +1,35 @@
+name: dynamic-routing-test
+description: The dynamic-routing example logs what its README promises
+actions:
+  - camel:
+      jbang:
+        run:
+          integration:
+            name: "dynamic-routing"
+            file: "../dynamic-routing.camel.yaml"
+            systemProperties:
+              file: "../application.properties"
+          resources:
+            - "orders/order-1001.json"
+            - "orders/order-1002.json"
+            - "orders/order-1003.json"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "Warehouse DK picks ORD-1001"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "Invoice for ORD-1001 via the backup service"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "Warehouse US picks ORD-1003"
+  - camel:
+      jbang:
+        verify:
+          integration: "dynamic-routing"
+          logMessage: "No invoice for ORD-1003 until it is paid"

Reply via email to