-
Notifications
You must be signed in to change notification settings - Fork 367
feat(connectors): add Apache Airflow trigger sink #3716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
avirajkhare00
wants to merge
8
commits into
apache:master
Choose a base branch
from
avirajkhare00:feat/connectors-airflow-trigger-sink
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
f6d836a
feat(connectors): add Apache Airflow trigger sink
avirajkhare00 66b02f7
docs(connectors): fix markdownlint for airflow sink README
avirajkhare00 ad139d1
fix(connectors): drop unused tokio dep from airflow sink
avirajkhare00 27793f6
chore: update Cargo.lock after removing airflow sink tokio dep
avirajkhare00 daa2ec6
chore: retrigger CI after unrelated C++ e2e flake
avirajkhare00 734d54a
fix(connectors): trigger Airflow by poll batch, not per message
avirajkhare00 db2442e
Merge branch 'master' into feat/connectors-airflow-trigger-sink
avirajkhare00 2946a60
Merge branch 'master' into feat/connectors-airflow-trigger-sink
avirajkhare00 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
core/connectors/runtime/example_config/connectors/airflow_sink.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| type = "sink" | ||
| key = "airflow" | ||
| enabled = true | ||
| version = 0 | ||
| name = "Airflow trigger" | ||
| path = "target/release/libiggy_connector_airflow_sink" | ||
| verbose = false | ||
|
|
||
| [[streams]] | ||
| stream = "example_stream" | ||
| topics = ["example_topic"] | ||
| schema = "json" | ||
| batch_length = 50 | ||
| poll_interval = "100ms" | ||
| consumer_group = "airflow_sink_connector" | ||
|
|
||
| [plugin_config] | ||
| base_url = "http://localhost:8080" | ||
| dag_id = "example_dag" | ||
| api_prefix = "/api/v1" | ||
| auth = "basic" | ||
| username = "admin" | ||
| password = "admin" | ||
| # dag_id_header = "airflow_dag_id" # optional: group batch by header DAG id | ||
| conf_mode = "payload" | ||
| # One DAG run per poll batch; conf.messages holds the batch payloads. | ||
| include_iggy_metadata_in_conf = false | ||
| health_check_enabled = true | ||
| health_path = "/api/v1/version" | ||
| timeout = "30s" | ||
| max_retries = 3 | ||
| retry_delay = "1s" | ||
| retry_backoff_multiplier = 2 | ||
| max_retry_delay = "30s" | ||
| tls_danger_accept_invalid_certs = false | ||
| max_connections = 10 | ||
| verbose_logging = false | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
|
|
||
| [package] | ||
| name = "iggy_connector_airflow_sink" | ||
| version = "0.4.1-edge.1" | ||
| description = "Iggy Apache Airflow sink connector for triggering DAG runs from stream messages via the Airflow REST API." | ||
| edition = "2024" | ||
| license = "Apache-2.0" | ||
| keywords = ["iggy", "messaging", "streaming", "airflow", "sink"] | ||
| categories = ["command-line-utilities", "database", "network-programming"] | ||
| homepage = "https://iggy.apache.org" | ||
| documentation = "https://iggy.apache.org/docs" | ||
| repository = "https://github.com/apache/iggy" | ||
| readme = "../../README.md" | ||
| publish = false | ||
|
|
||
| [package.metadata.cargo-machete] | ||
| ignored = ["dashmap"] | ||
|
|
||
| [lib] | ||
| crate-type = ["cdylib", "lib"] | ||
|
|
||
| [dependencies] | ||
| async-trait = { workspace = true } | ||
| base64 = { workspace = true } | ||
| bytes = { workspace = true } | ||
| dashmap = { workspace = true } | ||
| humantime = { workspace = true } | ||
| iggy_common = { workspace = true } | ||
| iggy_connector_sdk = { workspace = true } | ||
| reqwest = { workspace = true } | ||
| reqwest-middleware = { workspace = true } | ||
| reqwest-retry = { workspace = true } | ||
| reqwest-tracing = { workspace = true } | ||
| secrecy = { workspace = true } | ||
| serde = { workspace = true } | ||
| serde_json = { workspace = true } | ||
| tracing = { workspace = true } | ||
|
|
||
| [dev-dependencies] | ||
| simd-json = { workspace = true } | ||
| toml = { workspace = true } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| # Apache Airflow Sink Connector | ||
|
|
||
| Consumes messages from Iggy streams and triggers Apache Airflow DAG runs via the | ||
| stable REST API (`POST /api/v1/dags/{dag_id}/dagRuns`). | ||
|
|
||
| ## Overview | ||
|
|
||
| | | | | ||
| | --- | --- | | ||
| | **Type** | Sink (batch trigger) | | ||
| | **Direction** | Iggy → Airflow | | ||
| | **API** | Airflow REST (`api_prefix` default `/api/v1`) | | ||
| | **Unit of work** | One DAG run per poll batch (not per message) | | ||
| | **Idempotency** | Deterministic batch `dag_run_id`; HTTP **409** treated as success | | ||
|
|
||
| Airflow runs are jobs, not stream events. Each connector poll becomes **one DAG | ||
| run** whose `conf.messages` holds the batch. Tune volume with stream | ||
| `batch_length` / `poll_interval`. Retries re-use the same `dag_run_id` so | ||
| redelivery does not create duplicate runs. | ||
|
|
||
| If `dag_id_header` is set and messages resolve to different DAG ids, the poll is | ||
| split into **one run per DAG id group** (still batches of messages, never one | ||
| run per message by default). | ||
|
|
||
| ## Configuration | ||
|
|
||
| ```toml | ||
| type = "sink" | ||
| key = "airflow" | ||
| enabled = true | ||
| version = 0 | ||
| name = "Airflow trigger" | ||
| path = "target/release/libiggy_connector_airflow_sink" | ||
|
|
||
| [[streams]] | ||
| stream = "events" | ||
| topics = ["orders"] | ||
| schema = "json" | ||
| batch_length = 50 | ||
| poll_interval = "100ms" | ||
| consumer_group = "airflow_sink_group" | ||
|
|
||
| [plugin_config] | ||
| base_url = "http://localhost:8080" | ||
| dag_id = "example_dag" | ||
| api_prefix = "/api/v1" | ||
| auth = "basic" # none | basic | bearer | ||
| username = "admin" | ||
| password = "admin" | ||
| # token = "..." # when auth = bearer | ||
| # dag_id_header = "airflow_dag_id" | ||
| conf_mode = "payload" | ||
| include_iggy_metadata_in_conf = false | ||
| health_check_enabled = true | ||
| health_path = "/api/v1/version" | ||
| timeout = "30s" | ||
| max_retries = 3 | ||
| retry_delay = "1s" | ||
| retry_backoff_multiplier = 2 | ||
| max_retry_delay = "30s" | ||
| ``` | ||
|
|
||
| ### Plugin fields | ||
|
|
||
| | Field | Required | Default | Description | | ||
| | --- | :---: | --- | --- | | ||
| | `base_url` | yes | — | Airflow webserver base URL | | ||
| | `dag_id` | yes* | — | Default DAG id (*optional if every message sets `dag_id_header`) | | ||
| | `api_prefix` | no | `/api/v1` | REST prefix for version differences | | ||
| | `auth` | no | `none` | `none`, `basic`, or `bearer` | | ||
| | `username` / `password` | basic | — | Basic auth credentials (`password` is secret) | | ||
| | `token` | bearer | — | Bearer/JWT token (secret) | | ||
| | `dag_id_header` | no | unset | Message header that overrides `dag_id` (groups batch by value) | | ||
| | `conf_mode` | no | `payload` | How each message body is placed under `conf.messages[].payload` | | ||
| | `include_iggy_metadata_in_conf` | no | `false` | Nest batch stream/topic/offset range under `conf.iggy` | | ||
| | `health_check_enabled` | no | `true` | `GET` health path in `open()` | | ||
| | `health_path` | no | `/api/v1/version` | Path relative to `base_url` | | ||
| | `timeout` | no | `30s` | HTTP timeout | | ||
| | `max_retries` | no | `3` | Total attempts including the first | | ||
| | `retry_delay` / `max_retry_delay` | no | `1s` / `30s` | Exponential backoff bounds | | ||
| | `tls_danger_accept_invalid_certs` | no | `false` | Dev only | | ||
| | `verbose_logging` | no | `false` | Extra per-trigger logs | | ||
|
|
||
| Credentials use `SecretString` and are redacted in logs and `/stats` serialization. | ||
|
|
||
| ## Request shape | ||
|
|
||
| ```http | ||
| POST {base_url}{api_prefix}/dags/{dag_id}/dagRuns | ||
| Content-Type: application/json | ||
|
|
||
| { | ||
| "dag_run_id": "iggy-{partition}-{first_offset}-{last_offset}-{message_count}", | ||
| "conf": { | ||
| "messages": [ | ||
| { | ||
| "offset": 0, | ||
| "id": "0000...0001", | ||
| "timestamp": 1700000000000000, | ||
| "payload": { "order_id": 1 } | ||
| }, | ||
| { | ||
| "offset": 1, | ||
| "id": "0000...0002", | ||
| "timestamp": 1700000000000001, | ||
| "payload": { "order_id": 2 } | ||
| } | ||
| ], | ||
| "iggy": { | ||
| "stream": "events", | ||
| "topic": "orders", | ||
| "partition_id": 0, | ||
| "first_offset": 0, | ||
| "last_offset": 1, | ||
| "message_count": 2 | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| `conf.iggy` is present only when `include_iggy_metadata_in_conf = true`. | ||
|
|
||
| The sink always sets `dag_run_id` explicitly. Airflow does **not** fall back to | ||
| `manual__timestamp` when the field is provided. Replaying the same batch yields | ||
| the same id; Airflow responds **409**, which this sink treats as success. | ||
|
|
||
| ### Status handling | ||
|
|
||
| | Status | Behavior | | ||
| | --- | --- | | ||
| | 2xx | Success | | ||
| | 409 | Success (run already exists — idempotent replay) | | ||
| | 400 / 401 / 403 / 404 / 422 | Permanent — fail the batch (do not advance as if triggered) | | ||
| | 429 / 5xx | Transient — retry with exponential backoff | | ||
| | Network errors | Transient — retry | | ||
|
|
||
| Permanent failures return an error for the whole batch so consumer offsets are | ||
| not committed past an untriggered poll. Fix config/auth/DAG availability and | ||
| retry. | ||
|
|
||
| ### Throughput guidance | ||
|
|
||
| - Prefer larger `batch_length` so one Airflow run processes many messages. | ||
| - Point this sink at **work-sized** streams (export ready, file landed, window | ||
| closed), not raw high-volume domain firehoses. | ||
| - For a single-message command topic, set `batch_length = 1` so each poll is | ||
| still one batch run with one entry in `conf.messages`. | ||
|
|
||
| ## Out of scope (v1) | ||
|
|
||
| - Waiting for DAG completion | ||
| - Airflow source (task/DAG state → Iggy) | ||
| - Airflow provider package on the Airflow side | ||
| - In-sink content filters (`apply_function`-style); use a dedicated topic or a | ||
| future transform if you need selective triggering | ||
|
|
||
| ## Build and test | ||
|
|
||
| ```bash | ||
| cargo build -p iggy_connector_airflow_sink | ||
| cargo test -p iggy_connector_airflow_sink | ||
| cargo clippy -p iggy_connector_airflow_sink --all-targets -- -D warnings | ||
| ``` | ||
|
|
||
| Integration (Docker + WireMock): | ||
|
|
||
| ```bash | ||
| cargo build -p iggy_connector_airflow_sink | ||
| cargo test -p integration -- connectors::airflow | ||
| ``` | ||
|
|
||
| ## Related | ||
|
|
||
| - Issue: [#3715](https://github.com/apache/iggy/issues/3715) | ||
| - Roadmap: [#2753](https://github.com/apache/iggy/issues/2753) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this sink is an anti-pattern because Airflow is meant for the batch processing usecase while Iggy is a streaming system. More so, because a lot of other Iggy sinks also operate on message batch level and not individual message level.
This sink creates a dag run for each Iggy message, so for a high-traffic topics, it will end up overwhelming Airflow.
Two solutions:
apply_functionlike the Kafka support in Airflow. That way the sink can be configured to trigger dag only when a message passes that filter.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed — one DAG run per message is the wrong unit of work for Airflow.
Pushed a redesign: one DAG run per consume poll batch (not per message).
conf.messagesis an array of{offset, id, timestamp, payload}for the batchconf.iggycarries stream/topic/partition + first/last offset + message_countbatch_length/poll_intervalcontrol volume;batch_length = 1covers low-rate command topicsdag_id_headeryields multiple DAG ids in one poll, we still do one run per DAG-id group, not one run per messageVerified with unit tests, WireMock integration (3 msgs → 1 POST), and real Airflow 2.10.5 e2e (5 msgs → 1 run
iggy-0-0-4-5).In-sink
apply_function-style filtering is still out of scope for this PR; selective triggering is better via a dedicated topic (or a later transform). Happy to discuss if you want that in a follow-up.