This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-25764-dynamic-routing in repository https://gitbox.apache.org/repos/asf/camel-jbang-examples.git
commit 89fc4183e766178ded6cb701ba34b6948a997a6d Author: Claus Ibsen <[email protected]> AuthorDate: Sun Oct 18 15:01:52 2026 +0200 CAMEL-25764: route/dynamic-routing - recipient list, failover load balancer and throttle 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. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .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 05cc0cd..a9fe25d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -74,6 +74,8 @@ jobs: path: route/aggregator - name: Filter and multicast path: route/filter-and-multicast + - 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 290139b..b4cb071 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ The routing patterns: content-based router, splitter, aggregator, filter and mul | [Order lines](route/order-lines/) | Each order read from the orders directory is split into one message per line, the order id travels along in a header, and the log shows the order, one pick line per line, and the parent's confirmation that all lines went to picking. | nothing | | [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 | +| [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 0be9e36..30dcdd2 100644 --- a/camel-jbang-example-catalog.json +++ b/camel-jbang-example-catalog.json @@ -1047,6 +1047,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/filter-and-multicast", "title": "Filter and multicast", diff --git a/route/README.md b/route/README.md index d881b2e..7ce8648 100644 --- a/route/README.md +++ b/route/README.md @@ -9,6 +9,7 @@ The routing patterns: content-based router, splitter, aggregator, filter and mul | [Order lines](order-lines/) | Each order read from the orders directory is split into one message per line, the order id travels along in a header, and the log shows the order, one pick line per line, and the parent's confirmation that all lines went to picking. | nothing | | [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 | +| [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"
