Skip to content

feat(connectors): add Apache Airflow trigger sink#3716

Open
avirajkhare00 wants to merge 7 commits into
apache:masterfrom
avirajkhare00:feat/connectors-airflow-trigger-sink
Open

feat(connectors): add Apache Airflow trigger sink#3716
avirajkhare00 wants to merge 7 commits into
apache:masterfrom
avirajkhare00:feat/connectors-airflow-trigger-sink

Conversation

@avirajkhare00

@avirajkhare00 avirajkhare00 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add iggy_connector_airflow_sink that triggers Airflow DAG runs (POST /api/v1/dags/{dag_id}/dagRuns) from Iggy poll batches (one run per batch, not per message)
  • conf.messages carries the batch; optional conf.iggy metadata; deterministic batch dag_run_id; HTTP 409 treated as success
  • Auth modes none / basic / bearer; optional dag_id_header groups a poll into one run per DAG id
  • Permanent 4xx fails the batch (no silent drop); WireMock + real Airflow e2e; tracked by feat(connectors): add Apache Airflow trigger connector #3715

Design note

Airflow runs are jobs, not stream events. High-traffic 1:1 message→run would overwhelm the scheduler. Volume is controlled via stream batch_length / poll_interval.

Test plan

  • cargo fmt / cargo sort --no-format --workspace / clippy on the sink crate
  • cargo test -p iggy_connector_airflow_sink (12 unit tests)
  • cargo test -p integration -- connectors::airflow (WireMock: 3 messages → 1 DAG-run POST)
  • Local e2e: Iggy server + connectors + Airflow 2.10.5 standalone (5 messages → 1 run iggy-0-0-4-5, re-POST 409)

Review follow-up

Addressed @kriti-sc feedback: batch trigger default, explicit idempotent dag_run_id, fail batch on permanent HTTP errors.

Add a sink plugin that consumes Iggy messages and creates Airflow DAG
runs via the stable REST API, with basic/bearer auth, deterministic
dag_run_id for idempotent retries, and WireMock integration tests.

Closes apache#3715
@github-actions

Copy link
Copy Markdown

Thanks for the PR. It is labeled S-waiting-on-review and queued for review.

Slash commands (own line, regular comment) move it around the queue:

  • /ready - back to S-waiting-on-review after addressing feedback
  • /author - flip to S-waiting-on-author while you finish changes
  • /request-review @user-or-team - request a reviewer

See CONTRIBUTING.md for details.

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Jul 21, 2026
Adjust table separators and link bare URLs so MD060/MD034 pass.
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.91%. Comparing base (4c30a88) to head (db2442e).

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3716      +/-   ##
============================================
- Coverage     74.66%   72.91%   -1.75%     
  Complexity      950      950              
============================================
  Files          1304     1301       -3     
  Lines        149553   148290    -1263     
  Branches     125064   123956    -1108     
============================================
- Hits         111663   108132    -3531     
- Misses        34393    36766    +2373     
+ Partials       3497     3392     -105     
Components Coverage Δ
Rust Core 74.62% <ø> (-0.33%) ⬇️
Java SDK 62.64% <ø> (ø)
C# SDK 72.26% <ø> (ø)
Python SDK 92.27% <ø> (ø)
PHP SDK 84.52% <ø> (ø)
Node SDK 81.37% <ø> (-11.47%) ⬇️
Go SDK 18.18% <ø> (-24.91%) ⬇️
see 117 files with indirect coverage changes
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

cargo-machete failed CI because tokio was declared but not used.
finalize_pr failed solely because LowLevelE2E_Client.GetClientsReflectsSessionRemovalAfterDisconnect failed (278 passed, 1 failed). No C++ changes in this PR.
@@ -0,0 +1,53 @@
# Licensed to the Apache Software Foundation (ASF) under one

Copy link
Copy Markdown
Contributor

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:

  1. implement an apply_function like the Kafka support in Airflow. That way the sink can be configured to trigger dag only when a message passes that filter.
  2. Consider triggering Airflow on the batch and not individual messages.

Copy link
Copy Markdown
Contributor Author

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.messages is an array of {offset, id, timestamp, payload} for the batch
  • Optional conf.iggy carries stream/topic/partition + first/last offset + message_count
  • Stream batch_length / poll_interval control volume; batch_length = 1 covers low-rate command topics
  • If dag_id_header yields multiple DAG ids in one poll, we still do one run per DAG-id group, not one run per message

Verified 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.

| **Type** | Sink (trigger) |
| **Direction** | Iggy → Airflow |
| **API** | Airflow REST (`api_prefix` default `/api/v1`) |
| **Idempotency** | Deterministic `dag_run_id`; HTTP **409** treated as success |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My Airflow knowledge maybe outdated, but afaik dag_run_id would be manual__timestamp which wouldn't be idempotant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the default Airflow form — that would only apply if we omitted dag_run_id.

We always set an explicit deterministic id, so Airflow does not generate manual__timestamp:

  • Before (per-message): iggy-{partition}-{offset}-{message_id_hex}
  • Now (batch): iggy-{partition}-{first_offset}-{last_offset}-{message_count}

Same poll range → same id on redelivery; Airflow returns 409, which we treat as success. Documented in the README with the request shape and a note that we never rely on auto-generated run ids.

let skipped = (total as u64).saturating_sub(processed);
error!(
"{CONNECTOR_NAME} ID: {} aborting batch after {} consecutive failures \
({} remaining messages skipped)",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about skipping messages here.
Classifying errors into 3 parts-

  1. Transient Airflow related - Might not apply to the remainder of messages in the batch.
  2. Malformed Iggy message - Same as above.
  3. Permanent error such as auth or Airflow down - Might be better to fail the sink.

Feel free to add your reasoning or cases I might have missed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed the old per-message skip path was too loose for permanent failures.

With batch triggering the consume path is simpler:

  1. Transient (429/5xx/network): middleware retries, then Err so the runtime can redeliver the same batch (same dag_run_id → 409 if already created).
  2. Malformed single payload: skip that entry when building conf.messages, count an error; if every entry fails conversion, return error for the batch.
  3. Permanent HTTP (401/403/404/400/422, auth/missing DAG, etc.): return PermanentHttpError for the whole batch — do not silent-drop and advance as if the run was created.

Auth / Airflow-down style permanent failures no longer drop messages and continue.

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Jul 23, 2026
Airflow runs are jobs; one DAG run per message overwhelms the
scheduler on busy topics. Create one run per consume batch with
conf.messages, a deterministic batch dag_run_id, and fail the batch
on permanent HTTP errors instead of silent drops.

Addresses review feedback on apache#3716.
@avirajkhare00

Copy link
Copy Markdown
Contributor Author

/ready

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review PR is waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants