dirigent-examples 0.15.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dirigent_examples/__init__.py +22 -0
- dirigent_examples/py.typed +0 -0
- dirigent_examples/shelves/README.md +299 -0
- dirigent_examples/shelves/composition/README.md +18 -0
- dirigent_examples/shelves/composition/chained-instances.yaml +97 -0
- dirigent_examples/shelves/composition/composition-child.yaml +64 -0
- dirigent_examples/shelves/composition/composition-parent.yaml +89 -0
- dirigent_examples/shelves/connections.yaml +52 -0
- dirigent_examples/shelves/demo/README.md +19 -0
- dirigent_examples/shelves/demo/markdown-showcase.yaml +117 -0
- dirigent_examples/shelves/demo/params-showcase.yaml +92 -0
- dirigent_examples/shelves/demo/requires.yaml +65 -0
- dirigent_examples/shelves/demo/weekly-import-malawi.yaml +54 -0
- dirigent_examples/shelves/demo/weekly-import-nepal.yaml +75 -0
- dirigent_examples/shelves/docker/README.md +29 -0
- dirigent_examples/shelves/docker/docker-build-push.yaml +110 -0
- dirigent_examples/shelves/docker/docker-build-run.yaml +106 -0
- dirigent_examples/shelves/docker/docker-compose-database.yaml +124 -0
- dirigent_examples/shelves/docker/docker-compose-failing-up.yaml +83 -0
- dirigent_examples/shelves/docker/docker-compose-file.yaml +117 -0
- dirigent_examples/shelves/docker/docker-compose-profiles-env.yaml +133 -0
- dirigent_examples/shelves/docker/docker-compose-stack.yaml +70 -0
- dirigent_examples/shelves/docker/docker-hello.yaml +53 -0
- dirigent_examples/shelves/docker/docker-remote-daemon.yaml +92 -0
- dirigent_examples/shelves/docker/docker-run-failing-teardown.yaml +94 -0
- dirigent_examples/shelves/docker/docker-ticker.yaml +49 -0
- dirigent_examples/shelves/execute/README.md +16 -0
- dirigent_examples/shelves/execute/long-log.yaml +89 -0
- dirigent_examples/shelves/failure/README.md +20 -0
- dirigent_examples/shelves/failure/error-handler.yaml +89 -0
- dirigent_examples/shelves/failure/optional-step.yaml +82 -0
- dirigent_examples/shelves/failure/retries.yaml +75 -0
- dirigent_examples/shelves/failure/retry-budget.yaml +82 -0
- dirigent_examples/shelves/failure/step-timeout.yaml +96 -0
- dirigent_examples/shelves/git/README.md +32 -0
- dirigent_examples/shelves/git/git-checkout-build.yaml +125 -0
- dirigent_examples/shelves/git/git-checkout-compose.yaml +138 -0
- dirigent_examples/shelves/git/git-checkout-public.yaml +84 -0
- dirigent_examples/shelves/graph/README.md +22 -0
- dirigent_examples/shelves/graph/deep-chain.yaml +119 -0
- dirigent_examples/shelves/graph/fan-in.yaml +80 -0
- dirigent_examples/shelves/graph/fan-out.yaml +66 -0
- dirigent_examples/shelves/graph/linear.yaml +66 -0
- dirigent_examples/shelves/graph/parallel-branches.yaml +57 -0
- dirigent_examples/shelves/graph/parallel-sleep.yaml +55 -0
- dirigent_examples/shelves/graph/skip-diamond.yaml +105 -0
- dirigent_examples/shelves/graph/wide-fan.yaml +147 -0
- dirigent_examples/shelves/hello-world.yaml +30 -0
- dirigent_examples/shelves/open-data/README.md +67 -0
- dirigent_examples/shelves/open-data/feeds-composition.yaml +120 -0
- dirigent_examples/shelves/open-data/gdacs-disaster-updates.yaml +230 -0
- dirigent_examples/shelves/open-data/github-releases-relay.yaml +225 -0
- dirigent_examples/shelves/open-data/hdx-dataset-watch.yaml +211 -0
- dirigent_examples/shelves/open-data/kobo-submissions-to-csv.yaml +149 -0
- dirigent_examples/shelves/open-data/nominatim-geocode-facilities.yaml +169 -0
- dirigent_examples/shelves/open-data/odk-central-submissions.yaml +146 -0
- dirigent_examples/shelves/open-data/open-meteo-weekly-report.yaml +142 -0
- dirigent_examples/shelves/open-data/overpass-health-facilities.yaml +154 -0
- dirigent_examples/shelves/open-data/usgs-earthquakes-alert.yaml +208 -0
- dirigent_examples/shelves/open-data/who-gho-indicators-to-parquet.yaml +163 -0
- dirigent_examples/shelves/open-data/wikidata-country-reference.yaml +146 -0
- dirigent_examples/shelves/open-data/world-bank-population-trend.yaml +166 -0
- dirigent_examples/shelves/patterns/README.md +144 -0
- dirigent_examples/shelves/patterns/concurrency-queue.yaml +89 -0
- dirigent_examples/shelves/patterns/concurrency-replace.yaml +92 -0
- dirigent_examples/shelves/patterns/concurrency-skip.yaml +97 -0
- dirigent_examples/shelves/patterns/connections-referenced-vs-carried.yaml +140 -0
- dirigent_examples/shelves/patterns/deadline-on-a-sensor.yaml +106 -0
- dirigent_examples/shelves/patterns/fan-out-continue.yaml +88 -0
- dirigent_examples/shelves/patterns/fan-out-fail-fast.yaml +80 -0
- dirigent_examples/shelves/patterns/fan-out-from-params.yaml +84 -0
- dirigent_examples/shelves/patterns/fan-out-item-wise.yaml +111 -0
- dirigent_examples/shelves/patterns/fan-out-literal-list.yaml +81 -0
- dirigent_examples/shelves/patterns/fan-out-nested-objects.yaml +107 -0
- dirigent_examples/shelves/patterns/fan-out-then-join.yaml +86 -0
- dirigent_examples/shelves/patterns/log-levels.yaml +119 -0
- dirigent_examples/shelves/patterns/outputs-inline-vs-storage.yaml +140 -0
- dirigent_examples/shelves/patterns/params-every-type.yaml +259 -0
- dirigent_examples/shelves/patterns/params-validation-refuses.yaml +131 -0
- dirigent_examples/shelves/patterns/pipeline-run-child.yaml +96 -0
- dirigent_examples/shelves/patterns/pipeline-run-fire-and-forget.yaml +99 -0
- dirigent_examples/shelves/patterns/pipeline-run-strict.yaml +103 -0
- dirigent_examples/shelves/patterns/pipeline-run-wait.yaml +107 -0
- dirigent_examples/shelves/patterns/pipeline-run-with-params.yaml +124 -0
- dirigent_examples/shelves/patterns/poll-cadence.yaml +102 -0
- dirigent_examples/shelves/patterns/priority-layered.yaml +120 -0
- dirigent_examples/shelves/patterns/references-cheat-sheet.yaml +186 -0
- dirigent_examples/shelves/patterns/retry-budget-exhausted.yaml +92 -0
- dirigent_examples/shelves/patterns/retry-exponential-backoff.yaml +88 -0
- dirigent_examples/shelves/patterns/retry-only-transient.yaml +108 -0
- dirigent_examples/shelves/patterns/retry-with-jitter.yaml +102 -0
- dirigent_examples/shelves/patterns/rule-all-done.yaml +83 -0
- dirigent_examples/shelves/patterns/rule-all-success.yaml +79 -0
- dirigent_examples/shelves/patterns/rule-always.yaml +92 -0
- dirigent_examples/shelves/patterns/rule-one-failed.yaml +86 -0
- dirigent_examples/shelves/patterns/schedule-at-once.yaml +110 -0
- dirigent_examples/shelves/patterns/schedule-cron-timezone.yaml +121 -0
- dirigent_examples/shelves/patterns/schedule-interval.yaml +109 -0
- dirigent_examples/shelves/patterns/schedule-window-half-open.yaml +105 -0
- dirigent_examples/shelves/patterns/sensor-http-ready.yaml +121 -0
- dirigent_examples/shelves/patterns/sensor-storage-exists.yaml +134 -0
- dirigent_examples/shelves/patterns/step-names-and-keys.yaml +99 -0
- dirigent_examples/shelves/patterns/timeout-fails-the-step.yaml +94 -0
- dirigent_examples/shelves/patterns/timeout-skips-the-step.yaml +102 -0
- dirigent_examples/shelves/patterns/webhook-mapping-nested-payload.yaml +125 -0
- dirigent_examples/shelves/patterns/webhook-signed.yaml +144 -0
- dirigent_examples/shelves/preview/s3-parquet-to-ingestion.yaml +92 -0
- dirigent_examples/shelves/python/README.md +31 -0
- dirigent_examples/shelves/python/apply_and_run.py +52 -0
- dirigent_examples/shelves/python/ci_gate.py +76 -0
- dirigent_examples/shelves/python/connections.py +61 -0
- dirigent_examples/shelves/python/error_handling.py +84 -0
- dirigent_examples/shelves/python/follow_logs.py +39 -0
- dirigent_examples/shelves/python/list_and_filter.py +52 -0
- dirigent_examples/shelves/queues/README.md +59 -0
- dirigent_examples/shelves/queues/kafka-consume-then-transform.yaml +105 -0
- dirigent_examples/shelves/queues/kafka-produce-then-consume.yaml +124 -0
- dirigent_examples/shelves/queues/rabbitmq-consume-ack-on-success.yaml +102 -0
- dirigent_examples/shelves/queues/report-to-kafka.yaml +105 -0
- dirigent_examples/shelves/queues/report-to-rabbitmq.yaml +105 -0
- dirigent_examples/shelves/recipes/README.md +130 -0
- dirigent_examples/shelves/recipes/csv-header-rules.yaml +147 -0
- dirigent_examples/shelves/recipes/csv-to-ndjson.yaml +107 -0
- dirigent_examples/shelves/recipes/etl-csv-clean-validate-parquet.yaml +207 -0
- dirigent_examples/shelves/recipes/filter-by-predicate.yaml +116 -0
- dirigent_examples/shelves/recipes/filter-then-map-then-reduce.yaml +109 -0
- dirigent_examples/shelves/recipes/http-fetch-validate-post.yaml +142 -0
- dirigent_examples/shelves/recipes/http-follow-redirects.yaml +100 -0
- dirigent_examples/shelves/recipes/http-get-with-query.yaml +96 -0
- dirigent_examples/shelves/recipes/http-headers-and-auth-connection.yaml +110 -0
- dirigent_examples/shelves/recipes/http-post-file-from-storage.yaml +117 -0
- dirigent_examples/shelves/recipes/http-post-json-echo.yaml +105 -0
- dirigent_examples/shelves/recipes/http-post-report.yaml +183 -0
- dirigent_examples/shelves/recipes/http-save-body-to-storage.yaml +106 -0
- dirigent_examples/shelves/recipes/http-success-status-list.yaml +80 -0
- dirigent_examples/shelves/recipes/http-timeout-override.yaml +104 -0
- dirigent_examples/shelves/recipes/jq-dedupe-by-key.yaml +78 -0
- dirigent_examples/shelves/recipes/jq-defaults-and-nulls.yaml +91 -0
- dirigent_examples/shelves/recipes/jq-group-by-and-sum.yaml +74 -0
- dirigent_examples/shelves/recipes/jq-join-two-lists.yaml +77 -0
- dirigent_examples/shelves/recipes/jq-long-to-wide.yaml +76 -0
- dirigent_examples/shelves/recipes/jq-nested-to-flat.yaml +89 -0
- dirigent_examples/shelves/recipes/jq-pivot-wide-to-long.yaml +65 -0
- dirigent_examples/shelves/recipes/jq-running-totals.yaml +82 -0
- dirigent_examples/shelves/recipes/jq-string-cleaning.yaml +88 -0
- dirigent_examples/shelves/recipes/jq-top-n.yaml +85 -0
- dirigent_examples/shelves/recipes/jq-validate-in-jq-vs-schema.yaml +124 -0
- dirigent_examples/shelves/recipes/jq-window-dates.yaml +82 -0
- dirigent_examples/shelves/recipes/json-to-csv-flattening.yaml +141 -0
- dirigent_examples/shelves/recipes/large-output-to-storage.yaml +134 -0
- dirigent_examples/shelves/recipes/map-enrich-with-lookup.yaml +96 -0
- dirigent_examples/shelves/recipes/ndjson-to-parquet.yaml +130 -0
- dirigent_examples/shelves/recipes/pagination-by-fan-out.yaml +115 -0
- dirigent_examples/shelves/recipes/parquet-round-trip-types.yaml +163 -0
- dirigent_examples/shelves/recipes/reconcile-two-sources.yaml +159 -0
- dirigent_examples/shelves/recipes/report-built-in.yaml +72 -0
- dirigent_examples/shelves/recipes/report-daily-digest.yaml +186 -0
- dirigent_examples/shelves/recipes/report-to-file.yaml +131 -0
- dirigent_examples/shelves/recipes/report-to-webhook.yaml +136 -0
- dirigent_examples/shelves/recipes/schema-carried.yaml +112 -0
- dirigent_examples/shelves/recipes/schema-formats.yaml +107 -0
- dirigent_examples/shelves/recipes/schema-referenced.yaml +86 -0
- dirigent_examples/shelves/recipes/schema-refuses-then-rule.yaml +127 -0
- dirigent_examples/shelves/recipes/storage-copy-dated-archive.yaml +114 -0
- dirigent_examples/shelves/recipes/storage-exists-gate.yaml +127 -0
- dirigent_examples/shelves/recipes/storage-manifest-of-a-fan-out.yaml +104 -0
- dirigent_examples/shelves/recipes/storage-write-then-read.yaml +119 -0
- dirigent_examples/shelves/recipes/webhook-post-hmac.yaml +132 -0
- dirigent_examples/shelves/recipes/webhook-post-summary.yaml +142 -0
- dirigent_examples/shelves/s3/README.md +34 -0
- dirigent_examples/shelves/s3/report-to-s3.yaml +93 -0
- dirigent_examples/shelves/s3/s3-copy-and-verify.yaml +105 -0
- dirigent_examples/shelves/s3/s3-csv-report.yaml +87 -0
- dirigent_examples/shelves/s3/s3-parquet-report.yaml +77 -0
- dirigent_examples/shelves/s3/s3-round-trip.yaml +125 -0
- dirigent_examples/shelves/schemas/README.md +36 -0
- dirigent_examples/shelves/schemas/echo-reading.json +18 -0
- dirigent_examples/shelves/schemas/ou-record.json +13 -0
- dirigent_examples/shelves/schemas/station-reading.json +13 -0
- dirigent_examples/shelves/sensors/README.md +16 -0
- dirigent_examples/shelves/sensors/sensor-gate.yaml +65 -0
- dirigent_examples/shelves/sensors/time-window.yaml +61 -0
- dirigent_examples/shelves/sql/README.md +52 -0
- dirigent_examples/shelves/sql/duckdb-parquet-to-report.yaml +146 -0
- dirigent_examples/shelves/sql/sql-postgres-readonly.yaml +111 -0
- dirigent_examples/shelves/sql/sql-query-to-storage.yaml +85 -0
- dirigent_examples/shelves/sql/sql-sqlite-roundtrip.yaml +114 -0
- dirigent_examples/shelves/sql/warehouse.sql +42 -0
- dirigent_examples/shelves/transform/README.md +36 -0
- dirigent_examples/shelves/transform/csv-report.yaml +55 -0
- dirigent_examples/shelves/transform/jq-filter-and-map.yaml +70 -0
- dirigent_examples/shelves/transform/jq-group-and-aggregate.yaml +70 -0
- dirigent_examples/shelves/transform/jq-join-two-sources.yaml +98 -0
- dirigent_examples/shelves/transform/jq-reshape.yaml +91 -0
- dirigent_examples/shelves/transform/jq-stream-through-storage.yaml +112 -0
- dirigent_examples/shelves/transform/ndjson-round-trip.yaml +56 -0
- dirigent_examples/shelves/transform/parquet-round-trip.yaml +68 -0
- dirigent_examples/shelves/transform/std-convert-fan-out.yaml +142 -0
- dirigent_examples/shelves/transform/xml-feed-to-ndjson.yaml +116 -0
- dirigent_examples/shelves/transform/yaml-config-to-json.yaml +104 -0
- dirigent_examples/shelves/triggers/README.md +45 -0
- dirigent_examples/shelves/triggers/at-one-time.yaml +78 -0
- dirigent_examples/shelves/triggers/cron-nightly.yaml +79 -0
- dirigent_examples/shelves/triggers/cron-windowed.yaml +86 -0
- dirigent_examples/shelves/triggers/document-nightly.yaml +80 -0
- dirigent_examples/shelves/triggers/interval-rolling.yaml +88 -0
- dirigent_examples/shelves/triggers/managed-and-manual.yaml +109 -0
- dirigent_examples/shelves/triggers/webhook-trigger.yaml +75 -0
- dirigent_examples/shelves/validate/README.md +31 -0
- dirigent_examples/shelves/validate/expects-a-shape.yaml +56 -0
- dirigent_examples/shelves/validate/the-shape-is-wrong.yaml +46 -0
- dirigent_examples-0.15.0.dist-info/METADATA +21 -0
- dirigent_examples-0.15.0.dist-info/RECORD +216 -0
- dirigent_examples-0.15.0.dist-info/WHEEL +4 -0
- dirigent_examples-0.15.0.dist-info/entry_points.txt +3 -0
- dirigent_examples-0.15.0.dist-info/licenses/LICENSE +18 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Queue examples
|
|
2
|
+
|
|
3
|
+
A queue sensor starts a run from a message instead of a clock or a webhook. Each poke reads
|
|
4
|
+
what has arrived since the last one and succeeds the moment there is enough, so the batch
|
|
5
|
+
becomes ordinary step output and the rest of the DAG never learns where it came from. A publish
|
|
6
|
+
goes the other way: a step puts records on a topic and succeeds once the broker has them.
|
|
7
|
+
[docs/queues.md](../../docs/queues.md) is the family's home.
|
|
8
|
+
|
|
9
|
+
Every block here is **ordinary**: each reaches only the brokers its connection names, so no id
|
|
10
|
+
has to be allowlisted to run one.
|
|
11
|
+
|
|
12
|
+
## Bring the brokers up first
|
|
13
|
+
|
|
14
|
+
No document here can run without a broker. `infra/compose.queues.yaml` is a
|
|
15
|
+
single-node Redpanda (which speaks the Kafka protocol) and a RabbitMQ, both unprotected,
|
|
16
|
+
for a developer's machine:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
make queues-up # docker compose -f infra/compose.queues.yaml up -d --wait
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then create the topic or the queue, put something on it, and create the connection each
|
|
23
|
+
document names. Each document's header spells out its own three commands, and the produce
|
|
24
|
+
document needs nothing put on the topic first: it publishes what it reads back.
|
|
25
|
+
|
|
26
|
+
Every document **names** its connection under `requires.connections` rather than carrying one,
|
|
27
|
+
which is the shape to copy: bootstrap servers, an AMQP URL and a sealed password belong to an
|
|
28
|
+
environment, not to a pipeline. On an instance they are created once:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
dg connection create kafka orders-topic --set bootstrap_servers='["127.0.0.1:9092"]'
|
|
32
|
+
dg connection create rabbitmq shop-queue \
|
|
33
|
+
--set url=amqp://dirigent@127.0.0.1:5672/ --set password=dirigent
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
On the compose stack the brokers are the `infra/compose.brokers.yaml` overlay instead
|
|
37
|
+
(`make docker-run-queues`), and the same two connections name `redpanda:9092` and
|
|
38
|
+
`amqp://dirigent@rabbitmq:5672/`, which is what a container resolves.
|
|
39
|
+
|
|
40
|
+
## Pipelines
|
|
41
|
+
|
|
42
|
+
| File | What it teaches |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| [kafka-consume-then-transform.yaml](kafka-consume-then-transform.yaml) | Waiting on a topic: `min_messages` and `max_messages`, why the cursor rather than a consumer group holds the offsets, and the batch read by a downstream transform. |
|
|
45
|
+
| [kafka-produce-then-consume.yaml](kafka-produce-then-consume.yaml) | Publishing: the two shapes an element of `records` may take, where a message key comes from, what `acks: all` and an idempotent producer do and do not promise, and why this one consumes from `earliest`. |
|
|
46
|
+
| [report-to-kafka.yaml](report-to-kafka.yaml) | Publishing a rendered page: `report.render` hands its text on, and `kafka.produce` puts it on the topic as one keyed record. |
|
|
47
|
+
| [rabbitmq-consume-ack-on-success.yaml](rabbitmq-consume-ack-on-success.yaml) | When a message is acknowledged: why `ack: on_success` is the default, what a poke that parks does with the messages it took, and what `always` gives up in exchange. |
|
|
48
|
+
| [report-to-rabbitmq.yaml](report-to-rabbitmq.yaml) | Publishing one message: the default exchange, where a routing key is a queue name, and what `persistent` buys. |
|
|
49
|
+
|
|
50
|
+
## The cursor, in one paragraph
|
|
51
|
+
|
|
52
|
+
A sensor's poke keeps its place in a **cursor**, a JSON map the poke returns with a `NotYet`
|
|
53
|
+
and the next poke receives as `ctx.cursor`. `kafka.consume` keeps the offsets it has read to in
|
|
54
|
+
it; `rabbitmq.consume` keeps only a count and the last delivery tag, because the broker holds
|
|
55
|
+
the state there. It replaces rather than merges, it is stored by the transaction that parks the
|
|
56
|
+
attempt -- which makes advancing it at-least-once, so a poke may read the same ground twice --
|
|
57
|
+
and it lives only as long as the waiting attempt, because a poke that succeeds ends the step.
|
|
58
|
+
Anything a later step needs is in the output, which is why `kafka.consume` reports the offsets
|
|
59
|
+
it ended at there.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# A run that starts from what is on a Kafka topic, and reshapes the batch it took.
|
|
2
|
+
#
|
|
3
|
+
# NEEDS A BROKER, and a topic to read:
|
|
4
|
+
# docker compose -f infra/compose.queues.yaml up -d --wait
|
|
5
|
+
# docker exec -it dirigent-queues-redpanda-1 rpk topic create orders
|
|
6
|
+
# dg connection create kafka orders-topic --set bootstrap_servers='["127.0.0.1:9092"]'
|
|
7
|
+
# dg run examples/queues/kafka-consume-then-transform.yaml
|
|
8
|
+
# Then produce to the topic in another shell, whenever -- start: latest means the run takes
|
|
9
|
+
# what arrives after it began rather than what was already there, and the first poke writes
|
|
10
|
+
# that place down, so a message landing between two pokes is read by the next one:
|
|
11
|
+
# docker exec -it dirigent-queues-redpanda-1 \
|
|
12
|
+
# rpk topic produce orders --format '%v\n' # then type a JSON line and press ctrl-d
|
|
13
|
+
# On the compose stack the broker is the infra/compose.brokers.yaml overlay and the connection
|
|
14
|
+
# names it as `redpanda:9092`; docs/queues.md is the family's home.
|
|
15
|
+
#
|
|
16
|
+
# Two hops, and what each one hands on:
|
|
17
|
+
# wait kafka.consume, parked until at least one message is on the topic. Output:
|
|
18
|
+
# messages, count, and the offsets it ended at.
|
|
19
|
+
# summarise a jq transform over that batch, which is the only thing this example does with
|
|
20
|
+
# the messages -- the point is that they are ordinary step output from here on.
|
|
21
|
+
#
|
|
22
|
+
# THE CONNECTION IS NAMED, NOT CARRIED. requires.connections says the instance must already
|
|
23
|
+
# hold `orders-topic`, and refuses the document at apply if it does not. That is the shape to
|
|
24
|
+
# copy: bootstrap servers, a security setting and a sealed password belong to an environment,
|
|
25
|
+
# not to a pipeline, so pointing this at staging instead of production is an edit to one
|
|
26
|
+
# connection and to no document.
|
|
27
|
+
#
|
|
28
|
+
# WHY min_messages: 1. A poke succeeds as soon as there is one message, which is what "a run
|
|
29
|
+
# per batch, as soon as there is a batch" means. Raise it and the sensor waits for a fuller
|
|
30
|
+
# batch -- and a poke that finds fewer than that parks, keeping the offsets it read, so the
|
|
31
|
+
# next poke carries on rather than starting over. max_messages: 50 is the other half: it
|
|
32
|
+
# bounds what one run carries, so a topic that has been quiet for a week does not hand a
|
|
33
|
+
# single step ten thousand messages.
|
|
34
|
+
#
|
|
35
|
+
# NO group_id, DELIBERATELY. Without one the sensor tracks offsets itself, in the attempt's
|
|
36
|
+
# cursor, and commits nothing back to the cluster. Nothing on the broker has to be arranged
|
|
37
|
+
# for this to work, and a second pipeline over the same topic sees every message rather than
|
|
38
|
+
# racing this one for it. Set a group_id when the offsets should outlive the run -- then the
|
|
39
|
+
# commit happens in the poke that succeeded, and never in one that parked.
|
|
40
|
+
#
|
|
41
|
+
# READING THE SAME GROUND TWICE. Advancing the cursor is at-least-once: it is written by the
|
|
42
|
+
# transaction that parks the attempt, so a worker that dies before that commit leaves the
|
|
43
|
+
# older offsets behind and the next poke reads those messages again. A step that must not act
|
|
44
|
+
# twice on a message makes itself idempotent; this one only reshapes, so it does not care.
|
|
45
|
+
#
|
|
46
|
+
# TO MAKE IT YOURS: point the connection at your own cluster, change the topic, and replace
|
|
47
|
+
# the summarise step with whatever actually consumes the batch.
|
|
48
|
+
|
|
49
|
+
format: dirigent/v1
|
|
50
|
+
kind: pipeline
|
|
51
|
+
code: kafka-consume-then-transform
|
|
52
|
+
name: Consume a Kafka topic, then transform the batch
|
|
53
|
+
description: Wait for messages on a Kafka topic and reshape the batch the sensor handed on.
|
|
54
|
+
|
|
55
|
+
tags: [queues, kafka, sensor, transform, starter]
|
|
56
|
+
|
|
57
|
+
requires:
|
|
58
|
+
blocks:
|
|
59
|
+
- kafka.consume
|
|
60
|
+
- transform.jq
|
|
61
|
+
connections:
|
|
62
|
+
- orders-topic
|
|
63
|
+
|
|
64
|
+
steps:
|
|
65
|
+
wait:
|
|
66
|
+
block: kafka.consume
|
|
67
|
+
# A sensor's poll is a promise about how often the world is checked, and this one is
|
|
68
|
+
# checked every ten seconds. Between pokes the attempt is a row with a due time, not a
|
|
69
|
+
# worker: waiting on a quiet topic for the whole deadline costs nothing.
|
|
70
|
+
poll: 10s
|
|
71
|
+
deadline: 1h
|
|
72
|
+
# A wait that ran out is not a failure here: no messages arrived inside the hour, so the
|
|
73
|
+
# run is skipped and the next trigger tries again.
|
|
74
|
+
on_timeout: skip
|
|
75
|
+
config:
|
|
76
|
+
connection: orders-topic
|
|
77
|
+
topic: orders
|
|
78
|
+
min_messages: 1
|
|
79
|
+
max_messages: 50
|
|
80
|
+
# How long one poke waits on the broker before answering with what it has. It bounds the
|
|
81
|
+
# poke, never the wait.
|
|
82
|
+
poll_timeout: 5s
|
|
83
|
+
# latest begins at whatever arrives next, which is what a pipeline started by a message
|
|
84
|
+
# wants: earliest would replay the whole topic on the first poke of every run.
|
|
85
|
+
start: latest
|
|
86
|
+
# A value is JSON here because whatever produces to this topic writes JSON. text or
|
|
87
|
+
# base64 are the other two, and base64 is the default for a key because a key is often
|
|
88
|
+
# not text at all.
|
|
89
|
+
value_format: json
|
|
90
|
+
|
|
91
|
+
summarise:
|
|
92
|
+
block: transform.jq
|
|
93
|
+
depends_on: [wait]
|
|
94
|
+
config:
|
|
95
|
+
# The batch is ordinary step output from here on: a list of objects, each with the
|
|
96
|
+
# topic, partition, offset, key, value, timestamp and headers the broker recorded.
|
|
97
|
+
input: "${steps.wait.output.messages}"
|
|
98
|
+
program: >-
|
|
99
|
+
{
|
|
100
|
+
count: length,
|
|
101
|
+
partitions: (map(.partition) | unique),
|
|
102
|
+
first_offset: (map(.offset) | min),
|
|
103
|
+
last_offset: (map(.offset) | max),
|
|
104
|
+
values: map(.value)
|
|
105
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# A run that puts records on a Kafka topic and then reads its own batch back off it.
|
|
2
|
+
#
|
|
3
|
+
# NEEDS A BROKER, and a topic to write to:
|
|
4
|
+
# docker compose -f infra/compose.queues.yaml up -d --wait
|
|
5
|
+
# docker exec -it dirigent-queues-redpanda-1 rpk topic create orders
|
|
6
|
+
# dg connection create kafka orders-topic --set bootstrap_servers='["127.0.0.1:9092"]'
|
|
7
|
+
# dg run examples/queues/kafka-produce-then-consume.yaml
|
|
8
|
+
# Nothing else has to produce for this one: it publishes what it later reads, so the whole
|
|
9
|
+
# round trip is in the document.
|
|
10
|
+
#
|
|
11
|
+
# ON THE COMPOSE STACK the broker is the infra/compose.brokers.yaml overlay
|
|
12
|
+
# (`make docker-run-queues`), where the connection names it `redpanda:9092` rather than
|
|
13
|
+
# `127.0.0.1:9092` -- a container resolves the service, not the host. Create the topic on that
|
|
14
|
+
# stack with `docker compose ... exec redpanda rpk topic create orders`.
|
|
15
|
+
# docs/queues.md is the family's home.
|
|
16
|
+
#
|
|
17
|
+
# Three hops, and what each one hands on:
|
|
18
|
+
# publish kafka.produce, three records onto the topic. Output: produced, the topic, the
|
|
19
|
+
# last offset per partition, duration_ms and sent_bytes.
|
|
20
|
+
# wait kafka.consume, reading the topic from the beginning, so it takes what the step
|
|
21
|
+
# before it just wrote.
|
|
22
|
+
# summarise a jq transform over that batch, so the messages end as ordinary step output.
|
|
23
|
+
#
|
|
24
|
+
# THE CONNECTION IS NAMED, NOT CARRIED. requires.connections says the instance must already
|
|
25
|
+
# hold `orders-topic`, and the document is refused at apply if it does not. Bootstrap servers,
|
|
26
|
+
# a security setting and a sealed password belong to an environment, not to a pipeline.
|
|
27
|
+
#
|
|
28
|
+
# TWO SHAPES OF RECORD. An element of `records` is read as an envelope when it is an object
|
|
29
|
+
# carrying `value` and nothing besides `key`, `value` and `headers`; every other element is
|
|
30
|
+
# itself the value. So a list of documents from an earlier step publishes unchanged, and a
|
|
31
|
+
# record that needs a key or a header says so in an envelope. An object that means to be a
|
|
32
|
+
# value and would read as an envelope is written `{"value": {...}}`.
|
|
33
|
+
#
|
|
34
|
+
# WHERE THE KEY COMES FROM. `key: region` takes each record's key from that field of its
|
|
35
|
+
# value, which is what puts every record of one region on one partition and keeps them in
|
|
36
|
+
# order relative to each other. A record whose envelope names its own `key` keeps that one, and
|
|
37
|
+
# a record with no such field fails the step rather than being published unkeyed.
|
|
38
|
+
#
|
|
39
|
+
# ACKS AND WHAT IDEMPOTENCE ACTUALLY BUYS. `acks: all` is the default and the only setting the
|
|
40
|
+
# idempotent producer runs under: the broker recognises a record the client itself retried and
|
|
41
|
+
# writes it once, so a connection that stumbles inside this step does not double-publish. A
|
|
42
|
+
# RETRY OF THE STEP is a different matter -- it opens a new producer session, and all three
|
|
43
|
+
# records go again. This example does not retry the publish, which is the simplest way to mean
|
|
44
|
+
# it; a pipeline that must not publish twice either does the same or lets a compacted topic or
|
|
45
|
+
# the reader settle the duplicates.
|
|
46
|
+
#
|
|
47
|
+
# WHY start: earliest HERE. The consume sensor's usual setting is `latest`, because a pipeline
|
|
48
|
+
# started by a message wants what arrives next. This one reads a batch its own earlier step
|
|
49
|
+
# wrote, which is already on the topic by the time the sensor pokes, so it starts at the oldest
|
|
50
|
+
# record instead. On a topic something else is also writing to, that would replay the lot.
|
|
51
|
+
#
|
|
52
|
+
# TO MAKE IT YOURS: point the connection at your own cluster, change the topic, and feed
|
|
53
|
+
# `records` from an earlier step instead of writing them inline -- an export in storage reaches
|
|
54
|
+
# the publish through a storage.read, which is the one door a value comes in by.
|
|
55
|
+
|
|
56
|
+
format: dirigent/v1
|
|
57
|
+
kind: pipeline
|
|
58
|
+
code: kafka-produce-then-consume
|
|
59
|
+
name: Publish to a Kafka topic, then consume the batch
|
|
60
|
+
description: Publish records to a Kafka topic and read the same batch back off it.
|
|
61
|
+
|
|
62
|
+
tags: [queues, kafka, sensor, transform]
|
|
63
|
+
|
|
64
|
+
requires:
|
|
65
|
+
blocks:
|
|
66
|
+
- kafka.produce
|
|
67
|
+
- kafka.consume
|
|
68
|
+
- transform.jq
|
|
69
|
+
connections:
|
|
70
|
+
- orders-topic
|
|
71
|
+
|
|
72
|
+
steps:
|
|
73
|
+
publish:
|
|
74
|
+
block: kafka.produce
|
|
75
|
+
config:
|
|
76
|
+
connection: orders-topic
|
|
77
|
+
topic: orders
|
|
78
|
+
# Each record's key is this field of its value, so one region's records share a
|
|
79
|
+
# partition and keep their order.
|
|
80
|
+
key: region
|
|
81
|
+
records:
|
|
82
|
+
# A bare element is the value.
|
|
83
|
+
- {order: 1, region: north, total: 240}
|
|
84
|
+
- {order: 2, region: south, total: 95}
|
|
85
|
+
# An envelope, because this record carries headers as well as a value.
|
|
86
|
+
- value: {order: 3, region: north, total: 610}
|
|
87
|
+
headers:
|
|
88
|
+
source: till-4
|
|
89
|
+
# The default, and the only setting the idempotent producer runs under.
|
|
90
|
+
acks: all
|
|
91
|
+
# The whole publish, from the first send to the last acknowledgement.
|
|
92
|
+
timeout: 30s
|
|
93
|
+
|
|
94
|
+
wait:
|
|
95
|
+
block: kafka.consume
|
|
96
|
+
depends_on: [publish]
|
|
97
|
+
poll: 5s
|
|
98
|
+
deadline: 5m
|
|
99
|
+
config:
|
|
100
|
+
connection: orders-topic
|
|
101
|
+
topic: orders
|
|
102
|
+
# The batch the step before this one wrote is already on the topic, so this poke reads
|
|
103
|
+
# the whole topic rather than waiting for what arrives next.
|
|
104
|
+
start: earliest
|
|
105
|
+
min_messages: 3
|
|
106
|
+
max_messages: 50
|
|
107
|
+
poll_timeout: 5s
|
|
108
|
+
# The keys were written as text, so they are read back as text; base64 is the default,
|
|
109
|
+
# because a key is often not text at all.
|
|
110
|
+
key_format: text
|
|
111
|
+
value_format: json
|
|
112
|
+
|
|
113
|
+
summarise:
|
|
114
|
+
block: transform.jq
|
|
115
|
+
depends_on: [wait]
|
|
116
|
+
config:
|
|
117
|
+
input: "${steps.wait.output.messages}"
|
|
118
|
+
program: >-
|
|
119
|
+
{
|
|
120
|
+
count: length,
|
|
121
|
+
keys: (map(.key) | unique),
|
|
122
|
+
total: (map(.value.total) | add),
|
|
123
|
+
headers: (map(.headers) | map(select(length > 0)))
|
|
124
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# A run that starts from what is on a RabbitMQ queue, and why the default ack mode is the one
|
|
2
|
+
# that hands a short batch back.
|
|
3
|
+
#
|
|
4
|
+
# NEEDS A BROKER. Bring one up and put something on the queue first:
|
|
5
|
+
# docker compose -f infra/compose.queues.yaml up -d --wait
|
|
6
|
+
# open http://127.0.0.1:15672 # dirigent / dirigent; declare a queue named `shop-orders`
|
|
7
|
+
# # and publish a JSON body to it from the management UI
|
|
8
|
+
# dg connection create rabbitmq shop-queue \
|
|
9
|
+
# --set url=amqp://dirigent@127.0.0.1:5672/ --set password=dirigent
|
|
10
|
+
# dg run examples/queues/rabbitmq-consume-ack-on-success.yaml
|
|
11
|
+
# On the compose stack the broker is the infra/compose.brokers.yaml overlay and the connection
|
|
12
|
+
# names it as `amqp://dirigent@rabbitmq:5672/`; docs/queues.md is the family's home.
|
|
13
|
+
#
|
|
14
|
+
# Two hops, and what each one hands on:
|
|
15
|
+
# wait rabbitmq.consume, parked until at least one message is on the queue. Output:
|
|
16
|
+
# messages and count.
|
|
17
|
+
# summarise a jq transform over that batch, which is the only thing this example does with
|
|
18
|
+
# the messages.
|
|
19
|
+
#
|
|
20
|
+
# THE CONNECTION IS NAMED, NOT CARRIED. requires.connections says the instance must already
|
|
21
|
+
# hold `shop-queue`. The password is never written into the url: a plain field is neither
|
|
22
|
+
# encrypted at rest nor redacted in an API response, so the secret goes in the connection's
|
|
23
|
+
# sealed password field, which is both, and is merged into the url when a connection is opened
|
|
24
|
+
# and nowhere else. A url written amqp://user:secret@host is refused, the same way the sql
|
|
25
|
+
# kind refuses one.
|
|
26
|
+
#
|
|
27
|
+
# WHY ack: on_success IS THE DEFAULT. RabbitMQ, unlike Kafka, keeps its own place: a message is
|
|
28
|
+
# held unacknowledged until it is acked or nacked. So the question is not where to read but
|
|
29
|
+
# when to acknowledge, and the safe answer is "only in the poke that succeeded".
|
|
30
|
+
#
|
|
31
|
+
# Work through what a poke does with min_messages: 3 and two messages on the queue. It takes
|
|
32
|
+
# both, finds the batch too small, and nacks both back onto the queue with requeue -- so they
|
|
33
|
+
# are still there for the next poke, or for another consumer, and nothing was lost to a batch
|
|
34
|
+
# that was never acted on. Then a third message arrives, the next poke takes all three, hands
|
|
35
|
+
# them to the transform, and acknowledges them.
|
|
36
|
+
#
|
|
37
|
+
# ack: always is the other choice, and it is a real one with a real cost: every message is
|
|
38
|
+
# acknowledged the moment it is taken, park or no park, so a batch that parks is gone. It
|
|
39
|
+
# suits a queue nothing else reads and a step that would rather drop a partial batch than see
|
|
40
|
+
# a message twice.
|
|
41
|
+
#
|
|
42
|
+
# ON_SUCCESS IS STILL AT-LEAST-ONCE. Acknowledging happens inside the poke, and the attempt's
|
|
43
|
+
# outcome commits after it: a worker that dies in between leaves the messages acked and the
|
|
44
|
+
# step unfinished, and a worker that dies before the ack leaves them on the queue to be taken
|
|
45
|
+
# again. Neither ordering gives exactly-once -- that would need the broker and this instance's
|
|
46
|
+
# database in one transaction -- so a step that must not act twice makes itself idempotent.
|
|
47
|
+
#
|
|
48
|
+
# THE QUEUE MUST ALREADY EXIST. This block declares nothing: a queue a pipeline invented is a
|
|
49
|
+
# typo that reads as a working pipeline, so a name nobody created is rejected, with the name in
|
|
50
|
+
# the message, rather than waiting forever on a queue that will never have anything in it.
|
|
51
|
+
#
|
|
52
|
+
# TO MAKE IT YOURS: point the connection at your own broker, change the queue, and replace the
|
|
53
|
+
# summarise step with whatever actually consumes the batch.
|
|
54
|
+
|
|
55
|
+
format: dirigent/v1
|
|
56
|
+
kind: pipeline
|
|
57
|
+
code: rabbitmq-consume-ack-on-success
|
|
58
|
+
name: Consume a RabbitMQ queue, acknowledging only on success
|
|
59
|
+
description: Wait for messages on a RabbitMQ queue and reshape the batch, handing a short batch back to the queue.
|
|
60
|
+
|
|
61
|
+
tags: [queues, rabbitmq, sensor, transform, starter]
|
|
62
|
+
|
|
63
|
+
requires:
|
|
64
|
+
blocks:
|
|
65
|
+
- rabbitmq.consume
|
|
66
|
+
- transform.jq
|
|
67
|
+
connections:
|
|
68
|
+
- shop-queue
|
|
69
|
+
|
|
70
|
+
steps:
|
|
71
|
+
wait:
|
|
72
|
+
block: rabbitmq.consume
|
|
73
|
+
poll: 10s
|
|
74
|
+
deadline: 1h
|
|
75
|
+
# A wait that ran out is not a failure: nothing arrived inside the hour, so the run is
|
|
76
|
+
# skipped and the next trigger tries again.
|
|
77
|
+
on_timeout: skip
|
|
78
|
+
config:
|
|
79
|
+
connection: shop-queue
|
|
80
|
+
queue: shop-orders
|
|
81
|
+
min_messages: 1
|
|
82
|
+
max_messages: 50
|
|
83
|
+
# How long one poke waits on the broker before answering with what it has.
|
|
84
|
+
poll_timeout: 5s
|
|
85
|
+
# Written out although it is the default, because it is the decision this example is
|
|
86
|
+
# about. Change it to always and a batch below min_messages is acknowledged and dropped.
|
|
87
|
+
ack: on_success
|
|
88
|
+
value_format: json
|
|
89
|
+
|
|
90
|
+
summarise:
|
|
91
|
+
block: transform.jq
|
|
92
|
+
depends_on: [wait]
|
|
93
|
+
config:
|
|
94
|
+
# Each message carries the routing key, the exchange, the delivery tag, the decoded
|
|
95
|
+
# body, the headers, and the publisher's timestamp when it stamped one.
|
|
96
|
+
input: "${steps.wait.output.messages}"
|
|
97
|
+
program: >-
|
|
98
|
+
{
|
|
99
|
+
count: length,
|
|
100
|
+
routing_keys: (map(.routing_key) | unique),
|
|
101
|
+
bodies: map(.body)
|
|
102
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# A rendered page published to a Kafka topic, for whatever reads the topic to deliver.
|
|
2
|
+
#
|
|
3
|
+
# NEEDS A BROKER, and a topic to write to:
|
|
4
|
+
# docker compose -f infra/compose.queues.yaml up -d --wait
|
|
5
|
+
# docker exec -it dirigent-queues-redpanda-1 rpk topic create orders
|
|
6
|
+
# dg connection create kafka orders-topic --set bootstrap_servers='["127.0.0.1:9092"]'
|
|
7
|
+
# dg run examples/queues/report-to-kafka.yaml
|
|
8
|
+
# On the compose stack the broker is the infra/compose.brokers.yaml overlay and the
|
|
9
|
+
# connection names it `redpanda:9092`; docs/queues.md is the family's home.
|
|
10
|
+
#
|
|
11
|
+
# Two hops, and what each one hands on:
|
|
12
|
+
# page report.render, the markdown. Output: text, content_type, text_bytes.
|
|
13
|
+
# publish kafka.produce, one record carrying the page. Output: produced, the topic, the
|
|
14
|
+
# last offset per partition, duration_ms and sent_bytes.
|
|
15
|
+
#
|
|
16
|
+
# THE RENDER IS A STEP, THE SENDING IS ANOTHER. report.render has no destination of its own:
|
|
17
|
+
# it renders text and hands it on, and the step below it decides where the text goes. The
|
|
18
|
+
# same template writes a file in examples/recipes/report-to-file.yaml, an object in
|
|
19
|
+
# examples/s3/report-to-s3.yaml, and a queue message in report-to-rabbitmq.yaml.
|
|
20
|
+
#
|
|
21
|
+
# WHY THE RECORD CARRIES A CODE. A consumer reading this topic gets one JSON record whose
|
|
22
|
+
# `text` is the whole page, so `code` is what tells it which report arrived without parsing
|
|
23
|
+
# the markdown. It is the record's key too, so every day's page for one report lands on one
|
|
24
|
+
# partition and the days stay in order relative to each other.
|
|
25
|
+
#
|
|
26
|
+
# A PUBLISH IS NOT IDEMPOTENT ACROSS A RETRY: a retried step opens a new producer session and
|
|
27
|
+
# publishes the record again. This document does not retry it, which is the simplest way to
|
|
28
|
+
# mean that.
|
|
29
|
+
#
|
|
30
|
+
# TO MAKE IT YOURS: change the template, change the topic, and give the record whatever
|
|
31
|
+
# envelope the consumer on the other side expects.
|
|
32
|
+
|
|
33
|
+
format: dirigent/v1
|
|
34
|
+
kind: pipeline
|
|
35
|
+
code: report-to-kafka
|
|
36
|
+
name: Publish a rendered report to a Kafka topic
|
|
37
|
+
description: Render a markdown page from a Jinja template and publish it as one record on a Kafka topic.
|
|
38
|
+
|
|
39
|
+
tags: [queues, report, kafka, transform]
|
|
40
|
+
|
|
41
|
+
requires:
|
|
42
|
+
blocks:
|
|
43
|
+
- transform.jq
|
|
44
|
+
- report.render
|
|
45
|
+
- kafka.produce
|
|
46
|
+
connections:
|
|
47
|
+
- orders-topic
|
|
48
|
+
|
|
49
|
+
params:
|
|
50
|
+
type: object
|
|
51
|
+
properties:
|
|
52
|
+
day:
|
|
53
|
+
type: string
|
|
54
|
+
format: date
|
|
55
|
+
default: "2026-01-01"
|
|
56
|
+
description: The day the report is about.
|
|
57
|
+
|
|
58
|
+
steps:
|
|
59
|
+
summary:
|
|
60
|
+
block: transform.jq
|
|
61
|
+
config:
|
|
62
|
+
input:
|
|
63
|
+
day: ${params.day}
|
|
64
|
+
orders:
|
|
65
|
+
- {order: 1, region: north, total: 240}
|
|
66
|
+
- {order: 2, region: south, total: 95}
|
|
67
|
+
- {order: 3, region: north, total: 610}
|
|
68
|
+
program: |
|
|
69
|
+
{day: .day,
|
|
70
|
+
orders: (.orders | length),
|
|
71
|
+
total: (.orders | map(.total) | add),
|
|
72
|
+
by_region: (.orders | group_by(.region)
|
|
73
|
+
| map({region: .[0].region, total: map(.total) | add}))}
|
|
74
|
+
|
|
75
|
+
page:
|
|
76
|
+
block: report.render
|
|
77
|
+
depends_on: [summary]
|
|
78
|
+
config:
|
|
79
|
+
values:
|
|
80
|
+
summary: ${steps.summary.output.value}
|
|
81
|
+
content_type: text/markdown
|
|
82
|
+
template: |
|
|
83
|
+
# Orders for {{ summary.day }}
|
|
84
|
+
|
|
85
|
+
{{ summary.orders }} orders, {{ summary.total }} in total.
|
|
86
|
+
|
|
87
|
+
{% for row in summary.by_region %}
|
|
88
|
+
- {{ row.region }}: {{ row.total }}
|
|
89
|
+
{% endfor %}
|
|
90
|
+
|
|
91
|
+
publish:
|
|
92
|
+
block: kafka.produce
|
|
93
|
+
depends_on: [page]
|
|
94
|
+
config:
|
|
95
|
+
connection: orders-topic
|
|
96
|
+
topic: orders
|
|
97
|
+
# The record's key, so one report's days share a partition and keep their order.
|
|
98
|
+
key: code
|
|
99
|
+
records:
|
|
100
|
+
- code: daily-orders
|
|
101
|
+
day: ${params.day}
|
|
102
|
+
text: ${steps.page.output.text}
|
|
103
|
+
content_type: ${steps.page.output.content_type}
|
|
104
|
+
acks: all
|
|
105
|
+
timeout: 30s
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# A rendered page published to a RabbitMQ queue, for whatever consumes the queue to deliver.
|
|
2
|
+
#
|
|
3
|
+
# NEEDS A BROKER, and a queue to publish to:
|
|
4
|
+
# docker compose -f infra/compose.queues.yaml up -d --wait
|
|
5
|
+
# open http://127.0.0.1:15672 # dirigent / dirigent; declare a queue named `shop-reports`
|
|
6
|
+
# dg connection create rabbitmq shop-queue \
|
|
7
|
+
# --set url=amqp://dirigent@127.0.0.1:5672/ --set password=dirigent
|
|
8
|
+
# dg run examples/queues/report-to-rabbitmq.yaml
|
|
9
|
+
# On the compose stack the broker is the infra/compose.brokers.yaml overlay and the
|
|
10
|
+
# connection names it `amqp://dirigent@rabbitmq:5672/`; docs/queues.md is the family's home.
|
|
11
|
+
#
|
|
12
|
+
# Two hops, and what each one hands on:
|
|
13
|
+
# page report.render, the markdown. Output: text, content_type, text_bytes.
|
|
14
|
+
# publish rabbitmq.publish, one message carrying that text. Output: published, which is
|
|
15
|
+
# one, and message_bytes.
|
|
16
|
+
#
|
|
17
|
+
# THE DEFAULT EXCHANGE IS THE ONE WITH NO NAME, and on it a routing key is a queue name. So
|
|
18
|
+
# `exchange: ""` with `routing_key: shop-reports` puts the message on that queue and nowhere
|
|
19
|
+
# else. The queue has to exist: the publish is mandatory, so a routing key nothing takes is
|
|
20
|
+
# refused by the broker and fails the step, rather than dropped without a word. Naming an
|
|
21
|
+
# exchange instead routes by whatever bindings the broker holds, and an exchange the broker
|
|
22
|
+
# does not have is rejected by name the same way.
|
|
23
|
+
#
|
|
24
|
+
# WHAT THE MESSAGE CARRIES. A string message is sent as UTF-8 text and anything else as
|
|
25
|
+
# canonical JSON, and the content type follows -- `text/plain` or `application/json` unless
|
|
26
|
+
# the step names one. This one sends the page itself and says it is markdown, so a consumer
|
|
27
|
+
# reads the body rather than unwrapping an envelope first.
|
|
28
|
+
#
|
|
29
|
+
# PERSISTENT: TRUE is the default, and it is what makes a durable queue keep the message
|
|
30
|
+
# across a broker restart. A publish is not idempotent either way: a retry of this step puts
|
|
31
|
+
# a second copy on the queue, so a pipeline that must not do that either does not retry it or
|
|
32
|
+
# gives the consumer something to settle duplicates by.
|
|
33
|
+
#
|
|
34
|
+
# TO MAKE IT YOURS: change the template, change the queue, and point the connection at your
|
|
35
|
+
# own broker.
|
|
36
|
+
|
|
37
|
+
format: dirigent/v1
|
|
38
|
+
kind: pipeline
|
|
39
|
+
code: report-to-rabbitmq
|
|
40
|
+
name: Publish a rendered report to a RabbitMQ queue
|
|
41
|
+
description: Render a markdown page from a Jinja template and publish it as one message on a RabbitMQ queue.
|
|
42
|
+
|
|
43
|
+
tags: [queues, report, rabbitmq, transform]
|
|
44
|
+
|
|
45
|
+
requires:
|
|
46
|
+
blocks:
|
|
47
|
+
- transform.jq
|
|
48
|
+
- report.render
|
|
49
|
+
- rabbitmq.publish
|
|
50
|
+
connections:
|
|
51
|
+
- shop-queue
|
|
52
|
+
|
|
53
|
+
params:
|
|
54
|
+
type: object
|
|
55
|
+
properties:
|
|
56
|
+
day:
|
|
57
|
+
type: string
|
|
58
|
+
format: date
|
|
59
|
+
default: "2026-01-01"
|
|
60
|
+
description: The day the report is about.
|
|
61
|
+
|
|
62
|
+
steps:
|
|
63
|
+
summary:
|
|
64
|
+
block: transform.jq
|
|
65
|
+
config:
|
|
66
|
+
input:
|
|
67
|
+
day: ${params.day}
|
|
68
|
+
orders:
|
|
69
|
+
- {order: 1, status: shipped}
|
|
70
|
+
- {order: 2, status: held}
|
|
71
|
+
- {order: 3, status: shipped}
|
|
72
|
+
program: |
|
|
73
|
+
{day: .day,
|
|
74
|
+
orders: (.orders | length),
|
|
75
|
+
held: [.orders[] | select(.status == "held") | .order]}
|
|
76
|
+
|
|
77
|
+
page:
|
|
78
|
+
block: report.render
|
|
79
|
+
depends_on: [summary]
|
|
80
|
+
config:
|
|
81
|
+
values:
|
|
82
|
+
summary: ${steps.summary.output.value}
|
|
83
|
+
content_type: text/markdown
|
|
84
|
+
template: |
|
|
85
|
+
# Shop report for {{ summary.day }}
|
|
86
|
+
|
|
87
|
+
{{ summary.orders }} orders, {{ summary.held | length }} held.
|
|
88
|
+
|
|
89
|
+
{% for order in summary.held %}
|
|
90
|
+
- order {{ order }} is held
|
|
91
|
+
{% endfor %}
|
|
92
|
+
|
|
93
|
+
publish:
|
|
94
|
+
block: rabbitmq.publish
|
|
95
|
+
depends_on: [page]
|
|
96
|
+
config:
|
|
97
|
+
connection: shop-queue
|
|
98
|
+
# The default exchange, where the routing key is the queue's own name.
|
|
99
|
+
exchange: ""
|
|
100
|
+
routing_key: shop-reports
|
|
101
|
+
message: ${steps.page.output.text}
|
|
102
|
+
content_type: ${steps.page.output.content_type}
|
|
103
|
+
# The default, written out because it is what a durable queue needs to survive a restart.
|
|
104
|
+
persistent: true
|
|
105
|
+
timeout: 30s
|