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"