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,146 @@
|
|
|
1
|
+
# The country code table every integration ends up needing, built from Wikidata rather than typed.
|
|
2
|
+
#
|
|
3
|
+
# Wikidata's SPARQL endpoint is public and keyless. It answers SPARQL over the whole of
|
|
4
|
+
# Wikidata, and with `Accept: application/sparql-results+json` the answer is the W3C results
|
|
5
|
+
# shape: {"head": {"vars": [...]}, "results": {"bindings": [...]}}, where every binding is a
|
|
6
|
+
# map from variable name to {"type": ..., "value": ...} -- and every value is a *string*, even
|
|
7
|
+
# the numeric ones. Unwrapping that envelope is most of the work below.
|
|
8
|
+
#
|
|
9
|
+
# The query asks for sovereign states with an ISO 3166-1 alpha-3 code, plus the alpha-2 and
|
|
10
|
+
# numeric codes and the English label. The result is the reference table other pipelines join
|
|
11
|
+
# against: WHO GHO speaks alpha-3, health information systems often carry alpha-2, and national statistics
|
|
12
|
+
# offices publish the numeric code.
|
|
13
|
+
#
|
|
14
|
+
# On manners: the endpoint asks every client for a descriptive User-Agent that says who is
|
|
15
|
+
# calling and how to reach them, and it will refuse an anonymous one under load. That header is
|
|
16
|
+
# not optional politeness, it is the terms of use. A query heavier than this one belongs behind
|
|
17
|
+
# a longer timeout, or in the Query Service's own bulk export.
|
|
18
|
+
#
|
|
19
|
+
# What happens, hop by hop:
|
|
20
|
+
#
|
|
21
|
+
# fetch one GET with the query in the `query` parameter and the answer negotiated to
|
|
22
|
+
# JSON by the Accept header.
|
|
23
|
+
# table the unwrapping: `.results.bindings` mapped to flat rows, each `.value` pulled
|
|
24
|
+
# out of its little envelope, the numeric code turned into an actual number, and
|
|
25
|
+
# the Wikidata entity URI cut down to its Q-id.
|
|
26
|
+
# publish storage.write, the table as json. A reference table is a thing other runs read
|
|
27
|
+
# rather than a value this run carries onward, and a value leaves the run here.
|
|
28
|
+
# recalled storage.read of that object: the read another pipeline would do against the same
|
|
29
|
+
# path, which is the only way a value comes back in.
|
|
30
|
+
# summary what the run reports, counted off what came back rather than off what was held
|
|
31
|
+
# in memory upstream.
|
|
32
|
+
#
|
|
33
|
+
# To make it yours: change the query. Swapping wd:Q3624078 for wd:Q6256 widens it from
|
|
34
|
+
# sovereign states to countries, and adding `?item wdt:P1082 ?population` gives every row a
|
|
35
|
+
# population without another request. The table is written into the run's own scratch, which is
|
|
36
|
+
# deleted with the run; on an instance, writing to s3:// or to a stable prefix is what makes the
|
|
37
|
+
# table outlive the run that built it. A reference is not a parameter with a default, because a
|
|
38
|
+
# parameter default is literal text: ${run.scratch} means nothing until a step resolves it.
|
|
39
|
+
#
|
|
40
|
+
# dg run --local examples/open-data/wikidata-country-reference.yaml
|
|
41
|
+
|
|
42
|
+
format: dirigent/v1
|
|
43
|
+
kind: pipeline
|
|
44
|
+
code: wikidata-country-reference
|
|
45
|
+
name: Country code reference from Wikidata
|
|
46
|
+
description: |
|
|
47
|
+
ISO 3166-1 alpha-2, alpha-3 and numeric codes for every sovereign state, read from
|
|
48
|
+
**Wikidata** over SPARQL and saved as a json reference table other pipelines read.
|
|
49
|
+
|
|
50
|
+
tags: [open-data, http, storage, transform, sparql]
|
|
51
|
+
|
|
52
|
+
requires:
|
|
53
|
+
blocks:
|
|
54
|
+
- http.request
|
|
55
|
+
- transform.jq
|
|
56
|
+
- storage.write
|
|
57
|
+
- storage.read
|
|
58
|
+
|
|
59
|
+
params:
|
|
60
|
+
type: object
|
|
61
|
+
additionalProperties: false
|
|
62
|
+
properties:
|
|
63
|
+
language:
|
|
64
|
+
type: string
|
|
65
|
+
default: en
|
|
66
|
+
description: The label language, as the Wikidata label service names it.
|
|
67
|
+
user_agent:
|
|
68
|
+
type: string
|
|
69
|
+
default: dirigent-example/1.0 (https://github.com/winterop-com/dirigent)
|
|
70
|
+
description: Required by the endpoint's terms of use; say who you are and how to be reached.
|
|
71
|
+
|
|
72
|
+
steps:
|
|
73
|
+
fetch:
|
|
74
|
+
block: http.request
|
|
75
|
+
# The endpoint queues queries behind a shared budget and answers 429 when it is busy, so
|
|
76
|
+
# the wait between tries is longer than the query itself takes.
|
|
77
|
+
retry:
|
|
78
|
+
max_attempts: 3
|
|
79
|
+
backoff: 20s
|
|
80
|
+
config:
|
|
81
|
+
url: https://query.wikidata.org/sparql
|
|
82
|
+
headers:
|
|
83
|
+
# Without this the answer is XML, which is the endpoint's default.
|
|
84
|
+
Accept: application/sparql-results+json
|
|
85
|
+
User-Agent: ${params.user_agent}
|
|
86
|
+
query:
|
|
87
|
+
query: |
|
|
88
|
+
SELECT ?item ?itemLabel ?iso2 ?iso3 ?isoNumeric WHERE {
|
|
89
|
+
?item wdt:P31 wd:Q3624078 .
|
|
90
|
+
?item wdt:P298 ?iso3 .
|
|
91
|
+
OPTIONAL { ?item wdt:P297 ?iso2 }
|
|
92
|
+
OPTIONAL { ?item wdt:P299 ?isoNumeric }
|
|
93
|
+
SERVICE wikibase:label { bd:serviceParam wikibase:language "${params.language}". }
|
|
94
|
+
}
|
|
95
|
+
ORDER BY ?iso3
|
|
96
|
+
# A cold query against a busy endpoint can take most of a minute.
|
|
97
|
+
timeout: 90s
|
|
98
|
+
|
|
99
|
+
table:
|
|
100
|
+
block: transform.jq
|
|
101
|
+
depends_on: [fetch]
|
|
102
|
+
config:
|
|
103
|
+
input: ${steps.fetch.output.body}
|
|
104
|
+
# Every binding is {type, value} and every value is a string, so the numeric code is
|
|
105
|
+
# converted here rather than being carried as "454" for the rest of its life. An
|
|
106
|
+
# OPTIONAL that matched nothing has no binding at all, which is why each read is
|
|
107
|
+
# guarded rather than defaulted after the fact.
|
|
108
|
+
program: |
|
|
109
|
+
[.results.bindings[]
|
|
110
|
+
| {
|
|
111
|
+
wikidata_id: (.item.value | sub("^.*/"; "")),
|
|
112
|
+
name: .itemLabel.value,
|
|
113
|
+
iso2: (if .iso2 then .iso2.value else null end),
|
|
114
|
+
iso3: .iso3.value,
|
|
115
|
+
iso_numeric: (if .isoNumeric then (.isoNumeric.value | tonumber) else null end)
|
|
116
|
+
}]
|
|
117
|
+
| unique_by(.iso3)
|
|
118
|
+
|
|
119
|
+
publish:
|
|
120
|
+
block: storage.write
|
|
121
|
+
depends_on: [table]
|
|
122
|
+
config:
|
|
123
|
+
target: ${run.scratch}/countries.json
|
|
124
|
+
value: ${steps.table.output.value}
|
|
125
|
+
|
|
126
|
+
recalled:
|
|
127
|
+
block: storage.read
|
|
128
|
+
depends_on: [publish]
|
|
129
|
+
config:
|
|
130
|
+
# The read another pipeline would do: the table by its path, and the count below is off
|
|
131
|
+
# the object rather than off the value upstream.
|
|
132
|
+
source: ${steps.publish.output.uri}
|
|
133
|
+
max_size: 4mb
|
|
134
|
+
|
|
135
|
+
summary:
|
|
136
|
+
block: transform.jq
|
|
137
|
+
depends_on: [recalled]
|
|
138
|
+
config:
|
|
139
|
+
input: ${steps.recalled.output.value}
|
|
140
|
+
program: |
|
|
141
|
+
{
|
|
142
|
+
countries: length,
|
|
143
|
+
with_iso2: ([.[] | select(.iso2 != null)] | length),
|
|
144
|
+
with_numeric: ([.[] | select(.iso_numeric != null)] | length),
|
|
145
|
+
sample: (sort_by(.iso3) | .[0:3])
|
|
146
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Sixty years of population for one country, with year-on-year growth worked out on the way past.
|
|
2
|
+
#
|
|
3
|
+
# The World Bank's indicator API is keyless and answers a two-element array: the first element
|
|
4
|
+
# is the paging envelope, {page, pages, per_page, total, lastupdated}, and the second is the list
|
|
5
|
+
# of observations, newest year first. Nothing is nested inside anything else, so `.[0]` and
|
|
6
|
+
# `.[1]` are the whole access pattern -- and reading a page count out of `.[0]` is what makes
|
|
7
|
+
# paging visible rather than assumed.
|
|
8
|
+
#
|
|
9
|
+
# On paging: dirigent's format has no loop, so "keep fetching until the pages run out" is not a
|
|
10
|
+
# step. The honest single-hop version is the one below: ask for a page large enough to hold the
|
|
11
|
+
# whole series, then check the envelope and refuse the run if the answer says there was more.
|
|
12
|
+
# A truncated series that looks complete is the failure worth spending a step on.
|
|
13
|
+
#
|
|
14
|
+
# What happens, hop by hop:
|
|
15
|
+
#
|
|
16
|
+
# fetch one GET. per_page is a parameter because it is the thing you raise when the
|
|
17
|
+
# series is longer than the default guess.
|
|
18
|
+
# whole the paging check. jq's error() fails the step, so a series that did not fit
|
|
19
|
+
# stops here instead of reaching the gate looking well-formed.
|
|
20
|
+
# trend the arithmetic. The API answers newest-first, so the series is reversed into
|
|
21
|
+
# chronological order and each row is paired with the one before it. A missing
|
|
22
|
+
# year, or the first year, has no growth to report, and gets null rather than
|
|
23
|
+
# zero: zero would mean the population did not change.
|
|
24
|
+
# gate validate.schema against the shape written below. The gate passes the value
|
|
25
|
+
# through, so the step below reads the gate rather than the trend, and the document
|
|
26
|
+
# itself records that nothing reached the csv without being checked.
|
|
27
|
+
# checked storage.write, the checked rows as json. A converter reads one URI and writes
|
|
28
|
+
# another, so the only object the csv can be made from is the one the gate passed.
|
|
29
|
+
# report json to csv, from that object.
|
|
30
|
+
#
|
|
31
|
+
# To make it yours: change country to any ISO3 code and indicator to any World Bank series code
|
|
32
|
+
# -- SP.DYN.LE00.IN for life expectancy, NY.GDP.PCAP.CD for GDP per head -- and widen the
|
|
33
|
+
# schema's population bounds if the indicator you pick is not a count of people.
|
|
34
|
+
#
|
|
35
|
+
# dg run --local examples/open-data/world-bank-population-trend.yaml
|
|
36
|
+
# dg run --local examples/open-data/world-bank-population-trend.yaml -p country=NPL -p per_page=40
|
|
37
|
+
|
|
38
|
+
format: dirigent/v1
|
|
39
|
+
kind: pipeline
|
|
40
|
+
code: world-bank-population-trend
|
|
41
|
+
name: World Bank population trend
|
|
42
|
+
description: |
|
|
43
|
+
One country's population series from the **World Bank indicator API**, with year-on-year
|
|
44
|
+
growth computed in jq, checked against a carried schema and written as csv.
|
|
45
|
+
|
|
46
|
+
A server refuses a document that carries a schema, so this one is for a `--local` or
|
|
47
|
+
standalone run; on an instance, the same shape is created once with `dg schema create`.
|
|
48
|
+
|
|
49
|
+
tags: [open-data, http, storage, transform, validate, csv]
|
|
50
|
+
|
|
51
|
+
requires:
|
|
52
|
+
blocks:
|
|
53
|
+
- http.request
|
|
54
|
+
- transform.jq
|
|
55
|
+
- validate.schema
|
|
56
|
+
- storage.write
|
|
57
|
+
- convert.std
|
|
58
|
+
|
|
59
|
+
# Carried in the document so the gate needs nothing handed to it. The bounds are not
|
|
60
|
+
# decoration: a year outside them, or a negative population, means the API moved under us.
|
|
61
|
+
schemas:
|
|
62
|
+
population-trend-row:
|
|
63
|
+
type: array
|
|
64
|
+
minItems: 1
|
|
65
|
+
items:
|
|
66
|
+
type: object
|
|
67
|
+
required: [country, iso3, year, population, growth_pct]
|
|
68
|
+
additionalProperties: false
|
|
69
|
+
properties:
|
|
70
|
+
country: { type: string }
|
|
71
|
+
iso3: { type: string, minLength: 3, maxLength: 3 }
|
|
72
|
+
year: { type: integer, minimum: 1960, maximum: 2100 }
|
|
73
|
+
population: { type: [integer, "null"], minimum: 0 }
|
|
74
|
+
growth_pct: { type: [number, "null"] }
|
|
75
|
+
|
|
76
|
+
params:
|
|
77
|
+
type: object
|
|
78
|
+
additionalProperties: false
|
|
79
|
+
properties:
|
|
80
|
+
country:
|
|
81
|
+
type: string
|
|
82
|
+
default: MWI
|
|
83
|
+
description: ISO3 country code, as it appears in the request path.
|
|
84
|
+
indicator:
|
|
85
|
+
type: string
|
|
86
|
+
default: SP.POP.TOTL
|
|
87
|
+
description: A World Bank series code; the default is total population.
|
|
88
|
+
per_page:
|
|
89
|
+
type: integer
|
|
90
|
+
default: 100
|
|
91
|
+
minimum: 1
|
|
92
|
+
description: Raise this when the paging check refuses a series longer than one page.
|
|
93
|
+
|
|
94
|
+
steps:
|
|
95
|
+
fetch:
|
|
96
|
+
block: http.request
|
|
97
|
+
config:
|
|
98
|
+
url: https://api.worldbank.org/v2/country/${params.country}/indicator/${params.indicator}
|
|
99
|
+
query:
|
|
100
|
+
# Without format=json the API answers XML, which is its own default and nobody's
|
|
101
|
+
# convenience.
|
|
102
|
+
format: json
|
|
103
|
+
per_page: ${params.per_page}
|
|
104
|
+
page: 1
|
|
105
|
+
|
|
106
|
+
whole:
|
|
107
|
+
block: transform.jq
|
|
108
|
+
depends_on: [fetch]
|
|
109
|
+
config:
|
|
110
|
+
input: ${steps.fetch.output.body}
|
|
111
|
+
# The envelope, checked and then dropped: everything below works on the observations.
|
|
112
|
+
program: |
|
|
113
|
+
.[0] as $meta
|
|
114
|
+
| if $meta.pages > 1
|
|
115
|
+
then error("the series has \($meta.pages) pages of \($meta.per_page); raise per_page to at least \($meta.total)")
|
|
116
|
+
else .[1]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
trend:
|
|
120
|
+
block: transform.jq
|
|
121
|
+
depends_on: [whole]
|
|
122
|
+
config:
|
|
123
|
+
input: ${steps.whole.output.value}
|
|
124
|
+
# Chronological order first, because "the year before" is meaningless in the order the
|
|
125
|
+
# API answers in. The window is a pair, so index 0 has no predecessor and no growth.
|
|
126
|
+
program: |
|
|
127
|
+
[.[] | {country: .country.value,
|
|
128
|
+
iso3: .countryiso3code,
|
|
129
|
+
year: (.date | tonumber),
|
|
130
|
+
population: .value}]
|
|
131
|
+
| sort_by(.year)
|
|
132
|
+
| . as $series
|
|
133
|
+
| [range($series | length) as $i
|
|
134
|
+
| $series[$i]
|
|
135
|
+
| . + {growth_pct:
|
|
136
|
+
(if $i == 0 then null
|
|
137
|
+
else ($series[$i - 1].population) as $before
|
|
138
|
+
| if $before == null or .population == null or $before == 0 then null
|
|
139
|
+
else ((.population - $before) / $before * 1000 | round / 10)
|
|
140
|
+
end
|
|
141
|
+
end)}]
|
|
142
|
+
|
|
143
|
+
gate:
|
|
144
|
+
block: validate.schema
|
|
145
|
+
depends_on: [trend]
|
|
146
|
+
config:
|
|
147
|
+
input: ${steps.trend.output.value}
|
|
148
|
+
schema: population-trend-row
|
|
149
|
+
|
|
150
|
+
checked:
|
|
151
|
+
block: storage.write
|
|
152
|
+
depends_on: [gate]
|
|
153
|
+
config:
|
|
154
|
+
# Reading the gate rather than the trend is what proves, in the document, that only a
|
|
155
|
+
# checked value reaches the csv.
|
|
156
|
+
target: ${run.scratch}/${params.country}-${params.indicator}.json
|
|
157
|
+
value: ${steps.gate.output.value}
|
|
158
|
+
|
|
159
|
+
report:
|
|
160
|
+
block: convert.std
|
|
161
|
+
depends_on: [checked]
|
|
162
|
+
config:
|
|
163
|
+
source: ${steps.checked.output.uri}
|
|
164
|
+
target: ${run.scratch}/${params.country}-${params.indicator}.csv
|
|
165
|
+
from: json
|
|
166
|
+
to: csv
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Engine patterns
|
|
2
|
+
|
|
3
|
+
One engine behaviour per file, named for what it shows. Where the other shelves are organised
|
|
4
|
+
by what a pipeline is *for* -- moving data, reshaping it, waiting for it -- this one is
|
|
5
|
+
organised by what the engine *does*: the edge conditions, the retry policy, the two clocks, the
|
|
6
|
+
item policies, the trigger declarations, the concurrency policies, and the reference language.
|
|
7
|
+
|
|
8
|
+
Every document here runs for real. The HTTP ones call [Postman Echo](https://postman-echo.com),
|
|
9
|
+
a public request-and-response service, so nothing has to be stood up first; the rest are
|
|
10
|
+
offline. None of them needs `--enable-unsafe`: no file on this shelf runs code on the worker.
|
|
11
|
+
|
|
12
|
+
Each header says what the pipeline demonstrates end to end, what each hop hands on, what to
|
|
13
|
+
change, and **what outcome to expect** -- because several of these fail on purpose, and a file
|
|
14
|
+
whose lesson is a failure has to say so before you run it.
|
|
15
|
+
|
|
16
|
+
## Trigger rules
|
|
17
|
+
|
|
18
|
+
The edge condition on `depends_on`. Each of these fails a step deliberately, with
|
|
19
|
+
`/status/500`, so the rule has something to react to.
|
|
20
|
+
|
|
21
|
+
| File | What it shows | Ends as |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| [rule-all-success.yaml](rule-all-success.yaml) | The default: a failure skips everything below it | `failed` |
|
|
24
|
+
| [rule-all-done.yaml](rule-all-done.yaml) | Cleanup that runs whichever way the branch went | `failed` |
|
|
25
|
+
| [rule-one-failed.yaml](rule-one-failed.yaml) | The handler branch, and the success branch it excludes | `failed` |
|
|
26
|
+
| [rule-always.yaml](rule-always.yaml) | The widest edge, and the ordering it still respects | `failed` |
|
|
27
|
+
|
|
28
|
+
## Retries
|
|
29
|
+
|
|
30
|
+
Whether a failed attempt earns another one, and how long it waits first.
|
|
31
|
+
|
|
32
|
+
| File | What it shows | Ends as |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| [retry-exponential-backoff.yaml](retry-exponential-backoff.yaml) | The five fields, and the 1s, 2s, 4s they compute | `failed` |
|
|
35
|
+
| [retry-with-jitter.yaml](retry-with-jitter.yaml) | Why four items that failed together must not retry together | `completed_with_errors` |
|
|
36
|
+
| [retry-budget-exhausted.yaml](retry-budget-exhausted.yaml) | A budget spent in full on a hopeless failure | `failed` |
|
|
37
|
+
| [retry-only-transient.yaml](retry-only-transient.yaml) | The same policy on a 500 and a 404: three attempts and one | `completed_with_errors` |
|
|
38
|
+
|
|
39
|
+
## Timeouts and deadlines
|
|
40
|
+
|
|
41
|
+
Three clocks that stop a step, and they are not interchangeable.
|
|
42
|
+
|
|
43
|
+
| File | What it shows | Ends as |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| [timeout-fails-the-step.yaml](timeout-fails-the-step.yaml) | `timeout` bounds one block call, and expires as transient | `failed` |
|
|
46
|
+
| [timeout-skips-the-step.yaml](timeout-skips-the-step.yaml) | `on_timeout: skip` turns an expired deadline into a quiet day | `succeeded` |
|
|
47
|
+
| [deadline-on-a-sensor.yaml](deadline-on-a-sensor.yaml) | `poll`, `deadline` and the default `on_timeout: fail` | `failed` |
|
|
48
|
+
| [poll-cadence.yaml](poll-cadence.yaml) | The same wait polled two ways, with the overshoot measured | `succeeded` |
|
|
49
|
+
|
|
50
|
+
## Fan-out
|
|
51
|
+
|
|
52
|
+
One step definition, many run items.
|
|
53
|
+
|
|
54
|
+
| File | What it shows | Ends as |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| [fan-out-literal-list.yaml](fan-out-literal-list.yaml) | `for_each` written out in the document | `succeeded` |
|
|
57
|
+
| [fan-out-from-params.yaml](fan-out-from-params.yaml) | Width supplied by the caller, and the schema that guards it | `succeeded` |
|
|
58
|
+
| [fan-out-fail-fast.yaml](fan-out-fail-fast.yaml) | The default item policy: one bad element sinks the batch | `failed` |
|
|
59
|
+
| [fan-out-continue.yaml](fan-out-continue.yaml) | `items: continue`, and the failed item that is *absent* downstream | `completed_with_errors` |
|
|
60
|
+
| [fan-out-then-join.yaml](fan-out-then-join.yaml) | The join, which is just a step with no `for_each` of its own | `succeeded` |
|
|
61
|
+
| [fan-out-nested-objects.yaml](fan-out-nested-objects.yaml) | Elements that are objects, and `${item.limits.max_ms}` | `succeeded` |
|
|
62
|
+
| [fan-out-item-wise.yaml](fan-out-item-wise.yaml) | A second fan-out over the same grid, and the item it pairs with | `succeeded` |
|
|
63
|
+
|
|
64
|
+
## Parameters
|
|
65
|
+
|
|
66
|
+
| File | What it shows | Ends as |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| [params-every-type.yaml](params-every-type.yaml) | Every type and keyword, and why `format` needs a gate to assert | `succeeded` |
|
|
69
|
+
| [params-validation-refuses.yaml](params-validation-refuses.yaml) | What a bad run request is answered with, before a run exists | `succeeded` |
|
|
70
|
+
|
|
71
|
+
## Triggers
|
|
72
|
+
|
|
73
|
+
What starts a run on its own. The declarations travel with the document; the operational state
|
|
74
|
+
stays on the instance.
|
|
75
|
+
|
|
76
|
+
| File | What it shows | Ends as |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| [schedule-cron-timezone.yaml](schedule-cron-timezone.yaml) | Five columns, and the named zone that decides what they mean | `succeeded` |
|
|
79
|
+
| [schedule-interval.yaml](schedule-interval.yaml) | A cadence rather than a calendar, and the misfire grace | `succeeded` |
|
|
80
|
+
| [schedule-at-once.yaml](schedule-at-once.yaml) | One instant, anchored in the zone the schedule declares | `succeeded` |
|
|
81
|
+
| [schedule-window-half-open.yaml](schedule-window-half-open.yaml) | `${run.window.*}`, half-open; needs `--window` | `succeeded` |
|
|
82
|
+
| [webhook-mapping-nested-payload.yaml](webhook-mapping-nested-payload.yaml) | JSONPaths onto parameters, and the parameter left unreachable | `succeeded` |
|
|
83
|
+
| [webhook-signed.yaml](webhook-signed.yaml) | Outbound HMAC with `sign_with`, and where each secret lives | `succeeded` |
|
|
84
|
+
|
|
85
|
+
## Concurrency
|
|
86
|
+
|
|
87
|
+
What a second trigger does while a run is still going. Each header spells that out; a single
|
|
88
|
+
local run has nothing to collide with, so the policies are exercised against a server.
|
|
89
|
+
|
|
90
|
+
| File | What a second trigger does | Ends as |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| [concurrency-skip.yaml](concurrency-skip.yaml) | No run is created at all | `succeeded` |
|
|
93
|
+
| [concurrency-queue.yaml](concurrency-queue.yaml) | The run is created and held, with its parameters frozen | `succeeded` |
|
|
94
|
+
| [concurrency-replace.yaml](concurrency-replace.yaml) | The run in flight is cancelled, remotes told | `succeeded` |
|
|
95
|
+
|
|
96
|
+
## Composition
|
|
97
|
+
|
|
98
|
+
`pipeline.run`, and the four questions a call answers. All of them need the child, which a
|
|
99
|
+
local run is handed with `--also-apply examples/patterns/pipeline-run-child.yaml`.
|
|
100
|
+
|
|
101
|
+
| File | What it shows | Ends as |
|
|
102
|
+
| --- | --- | --- |
|
|
103
|
+
| [pipeline-run-child.yaml](pipeline-run-child.yaml) | The called pipeline, which knows nothing about parents | `succeeded` |
|
|
104
|
+
| [pipeline-run-wait.yaml](pipeline-run-wait.yaml) | `wait: true`: a parked row, not a held worker | `succeeded` |
|
|
105
|
+
| [pipeline-run-fire-and-forget.yaml](pipeline-run-fire-and-forget.yaml) | `wait: false`: a fork, and the run id that is the whole handoff | `succeeded` |
|
|
106
|
+
| [pipeline-run-strict.yaml](pipeline-run-strict.yaml) | `strict`, and the third status it is about | `failed` |
|
|
107
|
+
| [pipeline-run-with-params.yaml](pipeline-run-with-params.yaml) | A fan-out of children, checked against the child's schema | `succeeded` |
|
|
108
|
+
|
|
109
|
+
## The rest of the engine
|
|
110
|
+
|
|
111
|
+
| File | What it shows | Ends as |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| [step-names-and-keys.yaml](step-names-and-keys.yaml) | The map key addresses; `name` describes and identifies nothing | `succeeded` |
|
|
114
|
+
| [outputs-inline-vs-storage.yaml](outputs-inline-vs-storage.yaml) | The 16KB inline threshold the engine decides on its own, against `storage.write` and `storage.read` as the document's own choice | `succeeded` |
|
|
115
|
+
| [references-cheat-sheet.yaml](references-cheat-sheet.yaml) | Every `${...}` form, each used once; needs `--window` | `succeeded` |
|
|
116
|
+
| [sensor-http-ready.yaml](sensor-http-ready.yaml) | A 503 as "not yet" rather than as an error | `succeeded` |
|
|
117
|
+
| [sensor-storage-exists.yaml](sensor-storage-exists.yaml) | A glob, a size floor, and a drop that actually lands | `succeeded` |
|
|
118
|
+
| [log-levels.yaml](log-levels.yaml) | `--log-level PATTERN=LEVEL`, a run setting rather than a document one | `succeeded` |
|
|
119
|
+
| [priority-layered.yaml](priority-layered.yaml) | `priority` on the document, on a schedule, and on one ad hoc run | `succeeded` |
|
|
120
|
+
| [connections-referenced-vs-carried.yaml](connections-referenced-vs-carried.yaml) | Named, carried, and absolute; needs `--connections` | `succeeded` |
|
|
121
|
+
|
|
122
|
+
## Running them
|
|
123
|
+
|
|
124
|
+
Most take no flags at all:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
dg run --local examples/patterns/rule-one-failed.yaml
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Four need something handed to them, because what they demonstrate is exactly the thing a bare
|
|
131
|
+
local run does not have:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
dg run --local examples/patterns/references-cheat-sheet.yaml --window 2026-06-01..2026-06-02
|
|
135
|
+
dg run --local examples/patterns/schedule-window-half-open.yaml --window 2026-06-01..2026-06-02
|
|
136
|
+
dg run --local examples/patterns/connections-referenced-vs-carried.yaml \
|
|
137
|
+
--connections examples/connections.yaml
|
|
138
|
+
dg run --local examples/patterns/pipeline-run-wait.yaml \
|
|
139
|
+
--also-apply examples/patterns/pipeline-run-child.yaml
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Run the two without their flag once, on purpose: a run with no window refuses
|
|
143
|
+
`${run.window.start}` by name rather than resolving it to an empty string, which is the whole
|
|
144
|
+
argument for the reference language raising instead of defaulting.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# concurrency: queue -- a second trigger becomes a run that is created and held.
|
|
2
|
+
#
|
|
3
|
+
# WHAT A SECOND TRIGGER DOES HERE: the run IS created. It exists from the moment it is
|
|
4
|
+
# triggered, with its parameters and its window frozen as they were at that instant, and it
|
|
5
|
+
# simply does not advance -- no step is claimable while the slot is occupied. When the run
|
|
6
|
+
# ahead of it settles, the oldest held run is released and starts.
|
|
7
|
+
#
|
|
8
|
+
# That is the difference from skip, and it is the difference between losing work and delaying
|
|
9
|
+
# it. Use queue whenever a firing has work of its own that nothing later will redo: consuming
|
|
10
|
+
# a queue, appending a day's rows, advancing a cursor, sending a batch. The cost is that a
|
|
11
|
+
# backlog is real -- if the pipeline is consistently slower than its cadence, held runs
|
|
12
|
+
# accumulate, and that is a fact to look at rather than something the policy hides.
|
|
13
|
+
#
|
|
14
|
+
# THE PARAMETERS ARE FROZEN AT TRIGGER TIME, NOT AT RELEASE TIME. A run held for an hour still
|
|
15
|
+
# carries the parameters and the window it was created with, so an hourly increment released
|
|
16
|
+
# late covers the hour it was for, not the hour it happened to start in. That is the whole
|
|
17
|
+
# reason the run is created up front rather than the trigger being remembered.
|
|
18
|
+
#
|
|
19
|
+
# Everything about the ordering is FIFO: the oldest held run is the one released. A run
|
|
20
|
+
# cancelled while held simply never occupied the slot, and releasing does not double up.
|
|
21
|
+
#
|
|
22
|
+
# Hop by hop:
|
|
23
|
+
#
|
|
24
|
+
# claim_batch a sleep standing in for the work a firing owns -- the batch nothing else will
|
|
25
|
+
# pick up if this run never happens.
|
|
26
|
+
# commit records what the batch amounted to.
|
|
27
|
+
#
|
|
28
|
+
# EXPECT THIS RUN TO SUCCEED in about five seconds. A single local run has nothing to queue
|
|
29
|
+
# behind, so the policy is exercised against a server:
|
|
30
|
+
#
|
|
31
|
+
# dg run --local examples/patterns/concurrency-queue.yaml
|
|
32
|
+
# dg apply examples/patterns/concurrency-queue.yaml
|
|
33
|
+
# dg run concurrency-queue -p batch=1 &
|
|
34
|
+
# dg run concurrency-queue -p batch=2 # created, and held
|
|
35
|
+
# dg runs list concurrency-queue # two runs; the second starts when the first settles
|
|
36
|
+
|
|
37
|
+
format: dirigent/v1
|
|
38
|
+
kind: pipeline
|
|
39
|
+
code: concurrency-queue
|
|
40
|
+
name: A second run that waits its turn
|
|
41
|
+
description: |
|
|
42
|
+
`concurrency: queue` creates the second run and holds it: no step advances until the slot
|
|
43
|
+
is free, and then the oldest held run is released.
|
|
44
|
+
|
|
45
|
+
Its parameters and window are frozen at trigger time, so an increment released late still
|
|
46
|
+
covers the interval it was created for.
|
|
47
|
+
|
|
48
|
+
tags: [patterns, sensor, transform, concurrency]
|
|
49
|
+
|
|
50
|
+
# The one line this file is about.
|
|
51
|
+
concurrency: queue
|
|
52
|
+
|
|
53
|
+
requires:
|
|
54
|
+
blocks:
|
|
55
|
+
- time.sleep
|
|
56
|
+
- transform.jq
|
|
57
|
+
|
|
58
|
+
params:
|
|
59
|
+
type: object
|
|
60
|
+
properties:
|
|
61
|
+
batch:
|
|
62
|
+
type: integer
|
|
63
|
+
description: Which batch this firing owns; nothing else will pick it up.
|
|
64
|
+
default: 1
|
|
65
|
+
minimum: 1
|
|
66
|
+
seconds:
|
|
67
|
+
type: integer
|
|
68
|
+
description: How long the batch takes; long enough to queue a second run by hand.
|
|
69
|
+
default: 4
|
|
70
|
+
minimum: 1
|
|
71
|
+
maximum: 120
|
|
72
|
+
|
|
73
|
+
steps:
|
|
74
|
+
claim_batch:
|
|
75
|
+
block: time.sleep
|
|
76
|
+
poll: 1s
|
|
77
|
+
config:
|
|
78
|
+
for: "${params.seconds}s"
|
|
79
|
+
|
|
80
|
+
commit:
|
|
81
|
+
block: transform.jq
|
|
82
|
+
depends_on: [claim_batch]
|
|
83
|
+
# An append rather than a rebuild, which is what makes queue the right policy: a firing
|
|
84
|
+
# that never ran is a batch nobody ever committed.
|
|
85
|
+
config:
|
|
86
|
+
input:
|
|
87
|
+
batch: "${params.batch}"
|
|
88
|
+
program: |
|
|
89
|
+
{committed: .batch, incremental: true}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# concurrency: replace -- the newest trigger wins, and the run in flight is cancelled.
|
|
2
|
+
#
|
|
3
|
+
# WHAT A SECOND TRIGGER DOES HERE: every run currently in flight is CANCELLED, with the reason
|
|
4
|
+
# "replaced by a newer run", and the new run starts. Cancellation is not a suggestion -- what
|
|
5
|
+
# has not started is stopped, and an attempt holding a remote handle has its remote told, so a
|
|
6
|
+
# job submitted to something else is not left running behind a cancelled run.
|
|
7
|
+
#
|
|
8
|
+
# This is the "only the latest matters" policy, and it is the one to be most careful with,
|
|
9
|
+
# because it is the only one that destroys work already done. It fits a preview build, a
|
|
10
|
+
# dashboard refresh triggered by every commit, a re-render of a view where an older answer has
|
|
11
|
+
# no value the moment a newer request exists. It does not fit anything that writes: a load
|
|
12
|
+
# cancelled halfway is a load somebody has to reason about.
|
|
13
|
+
#
|
|
14
|
+
# THE ORDER OF OPERATIONS MATTERS AND IS WORTH KNOWING. A fan-out is expanded BEFORE the
|
|
15
|
+
# policy decides anything, so a document whose expansion would be refused is refused before
|
|
16
|
+
# any cancelling happens -- a refusal after the cancel would leave the old run dead and the
|
|
17
|
+
# new one non-existent.
|
|
18
|
+
#
|
|
19
|
+
# A cancelled run frees the slot exactly as a finished one does, and it settles as cancelled,
|
|
20
|
+
# which is neither succeeded nor failed. It stays in the run list with its attempts and logs:
|
|
21
|
+
# replaced is a thing to be able to see, not a thing to erase.
|
|
22
|
+
#
|
|
23
|
+
# Hop by hop:
|
|
24
|
+
#
|
|
25
|
+
# render a sleep standing in for a build whose older answer stops mattering the instant a
|
|
26
|
+
# newer request arrives.
|
|
27
|
+
# publish swaps the freshly rendered output in. It only ever runs for the newest request,
|
|
28
|
+
# which is the property this policy buys.
|
|
29
|
+
#
|
|
30
|
+
# EXPECT THIS RUN TO SUCCEED in about five seconds. A single local run has nothing to replace,
|
|
31
|
+
# so the policy is exercised against a server -- watch the first run settle as cancelled:
|
|
32
|
+
#
|
|
33
|
+
# dg run --local examples/patterns/concurrency-replace.yaml
|
|
34
|
+
# dg apply examples/patterns/concurrency-replace.yaml
|
|
35
|
+
# dg run concurrency-replace -p revision=abc123 &
|
|
36
|
+
# dg run concurrency-replace -p revision=def456 # the first is cancelled, this one starts
|
|
37
|
+
# dg runs list concurrency-replace # one cancelled, one succeeded
|
|
38
|
+
|
|
39
|
+
format: dirigent/v1
|
|
40
|
+
kind: pipeline
|
|
41
|
+
code: concurrency-replace
|
|
42
|
+
name: The newest trigger wins
|
|
43
|
+
description: |
|
|
44
|
+
`concurrency: replace` cancels every run in flight, telling any remote about the attempts
|
|
45
|
+
that had one, and starts the new run.
|
|
46
|
+
|
|
47
|
+
The only policy that destroys work already done. Right for a preview render whose older
|
|
48
|
+
answer is worthless; wrong for anything that writes.
|
|
49
|
+
|
|
50
|
+
tags: [patterns, sensor, transform, concurrency]
|
|
51
|
+
|
|
52
|
+
# The one line this file is about.
|
|
53
|
+
concurrency: replace
|
|
54
|
+
|
|
55
|
+
requires:
|
|
56
|
+
blocks:
|
|
57
|
+
- time.sleep
|
|
58
|
+
- transform.jq
|
|
59
|
+
|
|
60
|
+
params:
|
|
61
|
+
type: object
|
|
62
|
+
properties:
|
|
63
|
+
revision:
|
|
64
|
+
type: string
|
|
65
|
+
description: Which revision is being rendered; only the newest one matters.
|
|
66
|
+
default: abc123
|
|
67
|
+
minLength: 1
|
|
68
|
+
maxLength: 64
|
|
69
|
+
seconds:
|
|
70
|
+
type: integer
|
|
71
|
+
description: How long the render takes; long enough to replace it by hand.
|
|
72
|
+
default: 4
|
|
73
|
+
minimum: 1
|
|
74
|
+
maximum: 120
|
|
75
|
+
|
|
76
|
+
steps:
|
|
77
|
+
render:
|
|
78
|
+
block: time.sleep
|
|
79
|
+
poll: 1s
|
|
80
|
+
config:
|
|
81
|
+
for: "${params.seconds}s"
|
|
82
|
+
|
|
83
|
+
publish:
|
|
84
|
+
block: transform.jq
|
|
85
|
+
depends_on: [render]
|
|
86
|
+
# Nothing above this step writes anywhere, which is what makes cancelling it safe. A
|
|
87
|
+
# pipeline whose first step wrote to a real system would want queue and not this.
|
|
88
|
+
config:
|
|
89
|
+
input:
|
|
90
|
+
revision: "${params.revision}"
|
|
91
|
+
program: |
|
|
92
|
+
{published: .revision, superseded_anything_older: true}
|