Repository navigation
Record the Go SDK's Mixed Lang Task and Native Dag task interfaces #72043
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
Merged
jason810496
merged 16 commits into
apache:main
from
jason810496:docs/go-sdk/adr-interface-design
Sep 16, 2026
+551
−0
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
3207a1b
Document the Go SDK's Dag interface design decisions
jason810496 0a9f706
Record the reviewed Go SDK task handler and native Dag interfaces
jason810496 16c07f2
Move Lang-SDK control flow and provider DSL decisions to airflow-core
jason810496 da1e1d7
Hand Go tasks one Airflow context instead of injected parameters
jason810496 59ad9db
Rework the Go SDK ADRs around one registration verb and one context type
jason810496 b78cfcc
Lead the Lang-SDK control-flow and provider ADRs with their decisions
jason810496 8251bd1
Let a Go bundle register itself and serve without a provider callback
jason810496 8ab3514
Name the Mixed Lang registration after the task, not a handler
jason810496 1496f22
Name the Mixed Lang registration for the handler relationship
jason810496 cd43315
Address some comments myself
jason810496 8e86450
Record the Go SDK interface signatures and the reasoning behind them
jason810496 e1c044b
Self review with comments
jason810496 ae6dacd
Spell out task_id and label edges at the endpoint
jason810496 9cd41fd
Record how the generated specs reach the airflow package
jason810496 2c5d6af
Move the bundle rename into the decision it belongs to
jason810496 990ab2f
Derive task_ids from Go functions and branch by reference
jason810496 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
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,96 @@ | ||
| <!-- | ||
| 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. | ||
| --> | ||
|
|
||
| # ADR-0008: Control-Flow Constructs in Lang SDKs | ||
|
|
||
| ## Status | ||
|
|
||
| Proposed. | ||
|
|
||
| ## Decision | ||
|
|
||
| 1. **Name each construct after the host language's control flow**, Conditional skipping and branching are `if`/`else` and `switch`/`case` in most languages; the `Operator` suffix and the `Python` infix carry nothing an author of another language needs. | ||
| `If`, with or without `Else`, and `Switch` all serialize as a branch operator. | ||
| 2. **A branch selects a task, not a string.** A case is the task reference the SDK already handed back, and the value on the wire is that task's task_id, so the compiler checks the candidate exists and no label has to be kept in step with it. | ||
| 3. **No default case.** `BranchPythonOperator` has none to serialize, and a one-sided `If` whose condition is false follows nothing at all. | ||
| A decider returning a ref that is not one of the declared cases is a run-time error the SDK raises, a narrower check than `skip_all_except`, which only rejects a task_id missing from the whole Dag. | ||
| 4. **Triggering a Dag run is an ordinary DSL task**, it is pure DSL purpose instead of a new runtime. | ||
| 5. **Grouping keeps Python's semantics**: a scope offering the same task and nesting methods as the Dag, prefixing each task_id with the group id (`prefix_group_id`), | ||
| and can be ordered against a task or another group, as `group1 >> group2` does in Python. | ||
|
|
||
| ## Context | ||
|
|
||
| A native Dag interface starts with "register a task, declare an edge". | ||
| Four constructs follow immediately, because Python Dags use them everywhere: | ||
|
|
||
| - grouping (`TaskGroup`) | ||
| - conditional skipping (`ShortCircuitOperator`, `@task.short_circuit`) | ||
| - branching (`BranchPythonOperator`, `@task.branch`) | ||
| - triggering another Dag's run (`TriggerDagRunOperator`) | ||
|
|
||
| Every Lang SDK has to decide how to spell them, and copying Python's class names is the tempting default but the existing convention of Python SDK might not be straightforward for other Lang SDKs. | ||
| For example `dag.ShortCircuitOperator(...)` asks an author who has never seen Airflow to learn Python's operator taxonomy in order to write an `if`. | ||
| Feeling native to its own language matters more than the SDKs looking alike. | ||
|
|
||
| ## Example | ||
|
|
||
| The Go SDK spelling of all four. Every callable takes `airflow.Context` first, and a decider returns the case it picked, which the SDK sends as that task's task_id: | ||
|
|
||
| ```go | ||
| func hasRows(actx airflow.Context, rows Rows) (bool, error) | ||
| func pickPath(actx airflow.Context) (*airflow.TaskRef, error) | ||
| ``` | ||
|
|
||
| ```go | ||
| group := dag.TaskGroup("transform") | ||
| cleaned := group.Task(cleanRows) | ||
| validated := group.Task(validateRows, airflow.Inputs(cleaned)) | ||
|
|
||
| loadIfReadyRef := dag.Task(loadIfReady) | ||
| loadFallbackRef := dag.Task(loadFallback) | ||
| handleLongRef := dag.Task(handleLong) | ||
| handleShortRef := dag.Task(handleShort) | ||
|
|
||
| gate := dag.If(hasRows, airflow.Inputs(validated)) // BranchOperator: skips the side not taken | ||
| gate.Then(loadIfReadyRef) | ||
| gate.Else(loadFallbackRef) | ||
|
|
||
| pick := dag.Switch(pickPath) // task_id pickPath, from the function name | ||
| pick.Case(handleLongRef). | ||
| Case(handleShortRef) | ||
|
|
||
| dag.Task(airflow.TriggerDagRun(airflow.TriggerDagRunSpec{DagId: "downstream_etl"}), airflow.TaskSpec{TaskId: "trigger_downstream"}).After(gate) | ||
| ``` | ||
|
|
||
| A decider has to see the refs it returns, so either they are package-level or it is a closure where the Dag is built. | ||
|
|
||
| A one-sided `If` is a branch with one candidate rather than a `ShortCircuitOperator`. | ||
| That operator defaults to skipping every task in its downstream closure and ignoring their trigger rules, where a branch skips only the immediate downstream it did not take and keeps a task that several branches converge on running. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - **A branch selects exactly one task.** Python's branch callable may return a list of task_ids, and `skip_all_except` handles it; | ||
| a single-ref return cannot express that, and no Lang SDK offers it for now. | ||
| An author needing several paths together puts them behind one task, or gates each with its own condition. | ||
| This limitation is accepted rather than open. | ||
| - **No Lang SDK needs a deferral mechanism for now** to offer `deferrable` or `wait_for_completion`, because the trigger task runs in Python. | ||
| - **Nothing extra reaches the Dag JSON**, which carries no branch-candidate field at all. A ref is a task_id by the time the decision is sent, so each SDK stores its cases and nothing else. | ||
| - **A group edge needs one base type per SDK** that both a task and a group satisfy, since either can sit at the end of an edge. | ||
| Python already has it: `TaskGroup(TaskGroupMixin, DAGNode)` (`task-sdk/src/airflow/sdk/definitions/taskgroup.py:96`) and every operator inherit `DependencyMixin` (`.../definitions/_internal/mixins.py:35`), where `set_upstream` and `set_downstream` live. | ||
| The Go shape is `airflow.Node` ([`go-sdk/adr/0007`](../../../go-sdk/adr/0007-native-dag-interface.md)). |
82 changes: 82 additions & 0 deletions
82
airflow-core/adr/lang-sdk/0009-provider-operators-as-generated-dsl.md
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,82 @@ | ||
| <!-- | ||
| 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. | ||
| --> | ||
|
|
||
| # ADR-0009: Provider Operators as Generated Lang-SDK DSL | ||
|
|
||
| ## Status | ||
|
|
||
| Proposed. | ||
|
|
||
| ## Decision | ||
|
|
||
| 1. **Provider operators reach every Lang SDK as generated, serialization-only bindings.** | ||
| Only a task wrapping a host-language function executes in that language; a generated operator carries no body. | ||
| Both go through the SDK's ordinary task registration. | ||
| 2. **A provider DSL task runs on a Python worker**, so it must not inherit the SDK's queue, and the deployment must have that provider installed. | ||
| 3. **Generate from the Python constructors, commit the output, and guard it with a prek hook.** | ||
| 4. **Generate an operator only when every required constructor parameter is JSON-serializable** | ||
| (primitive, list, dict, or a nested spec of those); omit optional parameters that are not; | ||
| skip entirely any operator requiring a callable or a live object. | ||
| 5. **The namespace mirrors `providers/`, adapted to each language's naming rules.** | ||
| 6. **Templated fields pass through untouched.** The SDK writes the Jinja string; rendering stays server-side, where it already happens. | ||
| 7. **Version skew warns at Dag parsing time and never blocks execution.** Warning on SDK provider DSL version and the server-side Python provider runtime version mismatch, shouldn't be a fatal error. | ||
| 8. **Bindings ship as one package per provider from day one**, on that provider's release cadence, with an aggregate pin published alongside. | ||
|
|
||
| ## Context | ||
|
|
||
| The design review on #72043 asked whether authoring a Dag in Go means giving up Python provider operators. | ||
|
|
||
| **It must not**, for any language: Airflow's ~100 provider distributions are what a native Lang-SDK Dag cannot afford to lose, and what no workflow engine outside Airflow's ecosystem can offer. | ||
| A native Dag serializes into the same Dag JSON a Python Dag produces, so any operator whose | ||
| constructor arguments are JSON-representable can be expressed from another language as a DSL that | ||
| emits serialization and nothing else. | ||
|
|
||
| ## Example | ||
|
|
||
| The Go SDK spelling, mixing a native task with two generated operators: | ||
|
|
||
| ```go | ||
| import ( | ||
| "github.com/apache/airflow/go-sdk/airflowprovider/amazon" | ||
| "github.com/apache/airflow/go-sdk/airflowprovider/cncf/kubernetes" | ||
| ) | ||
|
|
||
| extracted := dag.Task(extract) // native Go: runs on a Go worker | ||
|
|
||
| staged := dag.Task(amazon.S3ToRedshiftOperator{ | ||
| SchemaName: "public", TableName: "events", S3Bucket: "raw", S3Key: "events/{{ ds }}", | ||
| }, airflow.TaskSpec{TaskId: "stage"}).After(extracted) // DSL only: runs on a Python worker | ||
|
|
||
| dag.Task(kubernetes.KubernetesPodOperator{ | ||
| Namespace: "airflow", Image: "report:latest", Name: "report", | ||
| }, airflow.TaskSpec{TaskId: "report"}).After(staged) | ||
| ``` | ||
|
|
||
| `airflow.TriggerDagRun` ([ADR-0008](0008-control-flow-constructs.md)) follows the same concept but it is the hand-written. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - **Authors should pin the DSL to the provider version their deployment has installed.** The bindings | ||
| describe one provider version's constructors, the operator that actually runs is whatever the | ||
| Python worker imports, and skew between them only warns. Matching the two is what keeps that | ||
| warning from becoming a surprise at run time. | ||
| - **Most skew is harmless, so it must not be fatal.** Python already fails loudly and precisely when | ||
| a class or argument genuinely is not there, and a parse-time hard failure would take a whole Dag | ||
| out over a version difference its tasks may not even touch. The hook also emits a coverage report, | ||
| so which operators each SDK can reach is reviewable. | ||
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
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.
Uh oh!
There was an error while loading. Please reload this page.