This is an automated email from the ASF dual-hosted git repository.

jason810496 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new ebca0c8edcf Java SDK: Register tasks as first-class TaskDef objects 
(#71057)
ebca0c8edcf is described below

commit ebca0c8edcf4d441381c3e66557363f88bef6b77
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Fri Aug 21 15:00:41 2026 +0800

    Java SDK: Register tasks as first-class TaskDef objects (#71057)
    
    * Java SDK: Register tasks as first-class TaskDef objects
    
    `dag.addTask("extract", Extract.class)` stored tasks as a plain
    `Map<String, Class<out Task>>`, which leaves nowhere to hang anything
    else a task needs: dependency edges, task-level configuration, and
    argument wiring all have to attach to a per-task object, and a map of
    classes cannot carry them. Introducing that object now keeps those
    follow-ups additive instead of forcing another break of the registration
    API later.
    
    The annotation surface keeps `Builder.Dag` / `Builder.Task`, and the
    interface users implement keeps the `Task` name, so the definition
    objects are `DagDef` and `TaskDef` -- a pairing that stays unambiguous
    next to `Task` at a use site. The SDK is pre-1.0, so the old
    string-keyed overload is removed outright rather than deprecated.
    
    The KubernetesExecutor lang-SDK example adopts the new names directly rather
    than pinning to the old ones: CI now builds those artifacts from the branch
    under test (#71527), so the example no longer has to compile against 
whatever
    the SDK on main happens to expose.
    
    * Java SDK: Clarify task ownership validation
    
    * Java SDK: Simplify manual task registration
    
    Manual interface-based bundles are authored directly by users, so requiring 
every call to wrap the task class in TaskDef adds noise to the common case 
while the SDK can retain first-class task definitions internally.
---
 .../language-sdks/java.rst                         | 12 +--
 .../airflow/example/ExampleBundleBuilder.java      |  2 +-
 .../airflow/example/InterfaceExampleBuilder.java   | 11 ++-
 .../org/apache/airflow/sdk/BuilderProcessor.kt     | 11 +--
 .../kotlin/org/apache/airflow/sdk/BuilderTest.kt   | 48 ++++++-----
 .../apache/airflow/example/ScalaSparkExample.scala |  8 +-
 .../main/kotlin/org/apache/airflow/sdk/Builder.kt  |  4 +-
 .../main/kotlin/org/apache/airflow/sdk/Bundle.kt   | 20 ++---
 .../org/apache/airflow/sdk/{Dag.kt => DagDef.kt}   | 67 ++++++++++++---
 .../org/apache/airflow/sdk/execution/Task.kt       |  6 +-
 .../kotlin/org/apache/airflow/sdk/BundleTest.kt    |  4 +-
 .../kotlin/org/apache/airflow/sdk/DagDefTest.kt    | 97 ++++++++++++++++++++++
 .../org/apache/airflow/sdk/execution/TaskTest.kt   |  6 +-
 .../airflow/k8sexample/K8sBundleBuilder.java       |  2 +-
 14 files changed, 221 insertions(+), 77 deletions(-)

diff --git a/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst 
b/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
index c8b73b4626a..a3e27f03132 100644
--- a/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
+++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/java.rst
@@ -115,7 +115,7 @@ Java entry point
 
     public class Main implements BundleBuilder {
       @Override
-      public Iterable<Dag> getDags() {
+      public Iterable<DagDef> getDags() {
         return List.of(SalesPipelineBuilder.build());  // SalesPipelineBuilder 
generated at compile time
       }
 
@@ -203,7 +203,7 @@ Interface-based API
 ~~~~~~~~~~~~~~~~~~~
 
 Implement the ``Task`` interface directly for full control over how tasks are 
registered and how XComs are
-read.
+read.  Each task is registered as a ``TaskDef`` on a ``DagDef``.
 
 .. code-block:: java
 
@@ -224,10 +224,10 @@ Register tasks manually in a ``BundleBuilder``:
 
     public class MyBundle implements BundleBuilder {
       @Override
-      public Iterable<Dag> getDags() {
-        var dag = new Dag("my_dag");
-        dag.addTask("fetch", FetchTask.class);
-        dag.addTask("process", ProcessTask.class);
+      public Iterable<DagDef> getDags() {
+        var dag = new DagDef("my_dag")
+            .addTask("fetch", FetchTask.class)
+            .addTask("process", ProcessTask.class);
         return List.of(dag);
       }
     }
diff --git 
a/java-sdk/example/src/java/org/apache/airflow/example/ExampleBundleBuilder.java
 
b/java-sdk/example/src/java/org/apache/airflow/example/ExampleBundleBuilder.java
index f63d6c4d743..fa1a8607558 100644
--- 
a/java-sdk/example/src/java/org/apache/airflow/example/ExampleBundleBuilder.java
+++ 
b/java-sdk/example/src/java/org/apache/airflow/example/ExampleBundleBuilder.java
@@ -26,7 +26,7 @@ import org.jetbrains.annotations.NotNull;
 public class ExampleBundleBuilder implements BundleBuilder {
   @NotNull
   @Override
-  public Iterable<Dag> getDags() {
+  public Iterable<DagDef> getDags() {
     return List.of(
         InterfaceExampleBuilder.build(),
         AnnotationExampleBuilder.build(),
diff --git 
a/java-sdk/example/src/java/org/apache/airflow/example/InterfaceExampleBuilder.java
 
b/java-sdk/example/src/java/org/apache/airflow/example/InterfaceExampleBuilder.java
index 1c536c3cbf2..78e78eed269 100644
--- 
a/java-sdk/example/src/java/org/apache/airflow/example/InterfaceExampleBuilder.java
+++ 
b/java-sdk/example/src/java/org/apache/airflow/example/InterfaceExampleBuilder.java
@@ -71,11 +71,10 @@ public class InterfaceExampleBuilder {
     }
   }
 
-  public static Dag build() {
-    var dag = new Dag("java_interface_example");
-    dag.addTask("extract", Extract.class);
-    dag.addTask("transform", Transform.class);
-    dag.addTask("load", Load.class);
-    return dag;
+  public static DagDef build() {
+    return new DagDef("java_interface_example")
+        .addTask("extract", Extract.class)
+        .addTask("transform", Transform.class)
+        .addTask("load", Load.class);
   }
 }
diff --git 
a/java-sdk/processor/src/main/kotlin/org/apache/airflow/sdk/BuilderProcessor.kt 
b/java-sdk/processor/src/main/kotlin/org/apache/airflow/sdk/BuilderProcessor.kt
index 11202ffcdb4..56cbf1e76ad 100644
--- 
a/java-sdk/processor/src/main/kotlin/org/apache/airflow/sdk/BuilderProcessor.kt
+++ 
b/java-sdk/processor/src/main/kotlin/org/apache/airflow/sdk/BuilderProcessor.kt
@@ -54,8 +54,8 @@ import javax.tools.Diagnostic
  * containing:
  *
  * - One inner class per [Builder.Task]-annotated method, implementing [Task].
- * - A static `build()` method that constructs the [Dag] and registers those
- *   inner classes as tasks.
+ * - A static `build()` method that constructs the [DagDef] and registers those
+ *   inner classes as [TaskDef]s.
  *
  * [Builder.XCom]-annotated parameters are resolved via `client.getXCom` in the
  * generated `execute` body, with the result cast to the parameter's declared
@@ -102,8 +102,8 @@ class BuilderProcessor : AbstractProcessor() {
       MethodSpec
         .methodBuilder("build")
         .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
-        .returns(ClassName.get(Dag::class.java))
-        .addStatement($$"var dag = new $T($S)", 
ClassName.get(Dag::class.java), ann.id.ifBlank { el.simpleName })
+        .returns(ClassName.get(DagDef::class.java))
+        .addStatement($$"var dag = new $T($S)", 
ClassName.get(DagDef::class.java), ann.id.ifBlank { el.simpleName })
 
     for (inner in el.enclosedElements) {
       if (inner !is ExecutableElement) continue
@@ -116,7 +116,8 @@ class BuilderProcessor : AbstractProcessor() {
       builderClass.addType(task.spec)
 
       buildMethod.addStatement(
-        $$"dag.addTask($S, $L.class)",
+        $$"dag.addTask(new $T($S, $L.class))",
+        ClassName.get(TaskDef::class.java),
         ann.id.ifBlank { inner.simpleName },
         innerName,
       )
diff --git 
a/java-sdk/processor/src/test/kotlin/org/apache/airflow/sdk/BuilderTest.kt 
b/java-sdk/processor/src/test/kotlin/org/apache/airflow/sdk/BuilderTest.kt
index 3e28e1b009a..6a08979c561 100644
--- a/java-sdk/processor/src/test/kotlin/org/apache/airflow/sdk/BuilderTest.kt
+++ b/java-sdk/processor/src/test/kotlin/org/apache/airflow/sdk/BuilderTest.kt
@@ -83,16 +83,17 @@ class BuilderTest {
          import java.util.Optional;
          import org.apache.airflow.sdk.Client;
          import org.apache.airflow.sdk.Context;
-         import org.apache.airflow.sdk.Dag;
+         import org.apache.airflow.sdk.DagDef;
          import org.apache.airflow.sdk.MissingXComException;
          import org.apache.airflow.sdk.Task;
+         import org.apache.airflow.sdk.TaskDef;
 
          public final class TestExampleBuilder {
-           public static Dag build() {
-             var dag = new Dag("TestExample");
-             dag.addTask("t1", T1.class);
-             dag.addTask("t2", T2.class);
-             dag.addTask("t3", T3.class);
+           public static DagDef build() {
+             var dag = new DagDef("TestExample");
+             dag.addTask(new TaskDef("t1", T1.class));
+             dag.addTask(new TaskDef("t2", T2.class));
+             dag.addTask(new TaskDef("t3", T3.class));
              return dag;
            }
            public static final class T1 implements Task {
@@ -157,14 +158,15 @@ class BuilderTest {
          import java.util.Optional;
          import org.apache.airflow.sdk.Client;
          import org.apache.airflow.sdk.Context;
-         import org.apache.airflow.sdk.Dag;
+         import org.apache.airflow.sdk.DagDef;
          import org.apache.airflow.sdk.MissingXComException;
          import org.apache.airflow.sdk.Task;
+         import org.apache.airflow.sdk.TaskDef;
 
          public final class TestExampleBuilder {
-           public static Dag build() {
-             var dag = new Dag("TestExample");
-             dag.addTask("t", T.class);
+           public static DagDef build() {
+             var dag = new DagDef("TestExample");
+             dag.addTask(new TaskDef("t", T.class));
              return dag;
            }
            public static final class T implements Task {
@@ -220,14 +222,15 @@ class BuilderTest {
          import java.util.Optional;
          import org.apache.airflow.sdk.Client;
          import org.apache.airflow.sdk.Context;
-         import org.apache.airflow.sdk.Dag;
+         import org.apache.airflow.sdk.DagDef;
          import org.apache.airflow.sdk.MissingXComException;
          import org.apache.airflow.sdk.Task;
+         import org.apache.airflow.sdk.TaskDef;
 
          public final class TestExampleBuilder {
-           public static Dag build() {
-             var dag = new Dag("TestExample");
-             dag.addTask("t", T.class);
+           public static DagDef build() {
+             var dag = new DagDef("TestExample");
+             dag.addTask(new TaskDef("t", T.class));
              return dag;
            }
            public static final class T implements Task {
@@ -261,8 +264,8 @@ class BuilderTest {
         "org.apache.airflow.example.TestExampleBuilder",
         """
          package org.apache.airflow.example;
-         import org.apache.airflow.sdk.Dag;
-         public final class TestExampleBuilder { public static Dag build() { 
var dag = new Dag("foo"); return dag; } }
+         import org.apache.airflow.sdk.DagDef;
+         public final class TestExampleBuilder { public static DagDef build() 
{ var dag = new DagDef("foo"); return dag; } }
         """,
       )
   }
@@ -284,8 +287,8 @@ class BuilderTest {
         "org.apache.airflow.example.Foo",
         """
          package org.apache.airflow.example;
-         import org.apache.airflow.sdk.Dag;
-         public final class Foo { public static Dag build() { var dag = new 
Dag("TestExample"); return dag; } }
+         import org.apache.airflow.sdk.DagDef;
+         public final class Foo { public static DagDef build() { var dag = new 
DagDef("TestExample"); return dag; } }
         """,
       )
   }
@@ -313,12 +316,13 @@ class BuilderTest {
          import java.lang.Override;
          import org.apache.airflow.sdk.Client;
          import org.apache.airflow.sdk.Context;
-         import org.apache.airflow.sdk.Dag;
+         import org.apache.airflow.sdk.DagDef;
          import org.apache.airflow.sdk.Task;
+         import org.apache.airflow.sdk.TaskDef;
          public final class TestExampleBuilder {
-           public static Dag build() {
-             var dag = new Dag("TestExample");
-             dag.addTask("foo", T1.class);
+           public static DagDef build() {
+             var dag = new DagDef("TestExample");
+             dag.addTask(new TaskDef("foo", T1.class));
              return dag;
            }
            public static final class T1 implements Task {
diff --git 
a/java-sdk/scala_spark_example/src/main/scala/org/apache/airflow/example/ScalaSparkExample.scala
 
b/java-sdk/scala_spark_example/src/main/scala/org/apache/airflow/example/ScalaSparkExample.scala
index ae779bf0213..f7f32bad8e2 100644
--- 
a/java-sdk/scala_spark_example/src/main/scala/org/apache/airflow/example/ScalaSparkExample.scala
+++ 
b/java-sdk/scala_spark_example/src/main/scala/org/apache/airflow/example/ScalaSparkExample.scala
@@ -19,7 +19,7 @@
 
 package org.apache.airflow.example
 
-import org.apache.airflow.sdk.{Bundle, BundleBuilder, Client, Context, Dag, 
Server, Task}
+import org.apache.airflow.sdk.{Bundle, BundleBuilder, Client, Context, DagDef, 
Server, Task}
 import org.apache.logging.log4j.{LogManager, Logger}
 import org.apache.spark.sql.SparkSession
 import org.apache.spark.sql.functions.sum
@@ -133,8 +133,8 @@ class SparkLoad extends Task {
 }
 
 object ScalaSparkExample {
-  def build(): Dag =
-    new Dag(SparkEtl.DagId)
+  def build(): DagDef =
+    new DagDef(SparkEtl.DagId)
       .addTask(SparkEtl.ExtractTaskId, classOf[SparkExtract])
       .addTask(SparkEtl.TransformTaskId, classOf[SparkTransform])
       .addTask(SparkEtl.LoadTaskId, classOf[SparkLoad])
@@ -142,7 +142,7 @@ object ScalaSparkExample {
 
 /** Bundle entry point served to Airflow's Java coordinator. */
 object ScalaSparkBundleBuilder extends BundleBuilder {
-  override def getDags(): java.lang.Iterable[Dag] = 
java.util.List.of(ScalaSparkExample.build())
+  override def getDags(): java.lang.Iterable[DagDef] = 
java.util.List.of(ScalaSparkExample.build())
 
   def main(args: Array[String]): Unit =
     Server.create(args).serve(new Bundle(getDags()))
diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt 
b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt
index 3a5b84d2daf..9dbfbfbefc3 100644
--- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt
+++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Builder.kt
@@ -24,7 +24,7 @@ package org.apache.airflow.sdk
  *
  * This class is not instantiated directly. Its nested annotations drive the
  * `BuilderProcessor` annotation processor in the :processor project,
- * which generates a `*Builder` class for each class annotated with [Dag].
+ * which generates a `*Builder` class for each class annotated with 
[Builder.Dag].
  *
  * Example:
  *
@@ -41,7 +41,7 @@ package org.apache.airflow.sdk
  * ```
  *
  * The processor generates `MyPipelineBuilder.build()`, which returns a
- * fully wired-up [Dag] ready to add to a [Bundle].
+ * fully wired-up [DagDef] ready to add to a [Bundle].
  */
 class Builder internal constructor() {
   /**
diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Bundle.kt 
b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Bundle.kt
index 677ec48eb93..6cd549270a8 100644
--- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Bundle.kt
+++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Bundle.kt
@@ -20,22 +20,22 @@
 package org.apache.airflow.sdk
 
 /**
- * An immutable snapshot of all [Dag]s that this JVM process can execute.
+ * An immutable snapshot of all [DagDef]s that this JVM process can execute.
  *
  * Build a [Bundle] by implementing [BundleBuilder], then pass it to
  * [Server.serve] to start accepting task-execution requests.
  *
- * @property dags All registered Dags keyed by [Dag.id].
+ * @property dags All registered Dags keyed by [DagDef.id].
  * @throws IllegalArgumentException if any two Dags share the same ID.
  */
 class Bundle(
-  dags: Iterable<Dag>,
+  dags: Iterable<DagDef>,
 ) {
-  internal val dags: Map<String, Dag> = dags.associateByDagId()
+  internal val dags: Map<String, DagDef> = dags.associateByDagId()
 }
 
-private fun Iterable<Dag>.associateByDagId(): Map<String, Dag> {
-  val dagMap = linkedMapOf<String, Dag>()
+private fun Iterable<DagDef>.associateByDagId(): Map<String, DagDef> {
+  val dagMap = linkedMapOf<String, DagDef>()
   for (dag in this) {
     require(dagMap.putIfAbsent(dag.id, dag) == null) {
       "Dags in bundle have duplicate ID: ${dag.id}"
@@ -45,14 +45,14 @@ private fun Iterable<Dag>.associateByDagId(): Map<String, 
Dag> {
 }
 
 /**
- * Entry point for declaring the [Dag]s that this bundle contains.
+ * Entry point for declaring the [DagDef]s that this bundle contains.
  *
  * Implement this interface to create a Dag bundle to be served by [Server].
  *
  * ```java
  * public class MyBundleBuilder implements BundleBuilder {
  *     @Override
- *     public Iterable<Dag> getDags() {
+ *     public Iterable<DagDef> getDags() {
  *         return List.of(MyDagBuilder.build());
  *     }
  *
@@ -64,14 +64,14 @@ private fun Iterable<Dag>.associateByDagId(): Map<String, 
Dag> {
  */
 interface BundleBuilder {
   /**
-   * Returns all [Dag]s that belong to this bundle.
+   * Returns all [DagDef]s that belong to this bundle.
    *
    * Called once during [build]; Dag IDs must be unique across the returned
    * collection.
    *
    * @throws IllegalArgumentException if any two Dags share the same ID.
    */
-  fun getDags(): Iterable<Dag>
+  fun getDags(): Iterable<DagDef>
 
   /**
    * Constructs a [Bundle] from the Dags returned by [getDags].
diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Dag.kt 
b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/DagDef.kt
similarity index 61%
rename from java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Dag.kt
rename to java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/DagDef.kt
index c9985803741..e3ba700a0fa 100644
--- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Dag.kt
+++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/DagDef.kt
@@ -24,46 +24,87 @@ import kotlin.Throws
 /**
  * A collection of tasks with directional dependencies.
  *
- * Create a [Dag] directly and register tasks with [addTask].
+ * Create a [DagDef] directly and register [TaskDef]s with [addTask].
  *
  * The [Builder.Dag] annotation should generally be preferred in user code,
  * where the annotation processor generates the wiring for you. Only use this
- * class directly if you need to do low-level plumbing.
+ * class directly if you need to do low-level plumbing:
+ *
+ * ```java
+ * var dag = new DagDef("java_etl")
+ *     .addTask("extract", Extract.class)
+ *     .addTask("load", Load.class);
+ * ```
  *
  * @param id Dag identifier. Must contain only ASCII alphanumeric characters,
  *    dashes, dots, or underscores; must be unique within a [Bundle].
  *
  * @see Builder.Dag
  */
-class Dag(
+class DagDef(
   val id: String, // TODO: charset check?
 ) {
-  internal var tasks = mutableMapOf<String, Class<out Task>>()
+  internal val tasks = linkedMapOf<String, TaskDef>()
 
   /**
-   * Registers a task with this Dag.
-   *
-   * The class must have a public no-argument constructor and implement [Task].
-   * Task IDs must be unique within a Dag.
+   * Registers a task from its ID and implementation class.
    *
    * @param id Task identifier, unique within this Dag.
    * @param definition Class that implements [Task]. Must have a public no-arg
    *    constructor.
    * @return This Dag, for chaining.
-   * @throws IllegalArgumentException if a task already exists in the Dag with
-   *    the same ID.
+   * @throws IllegalArgumentException if a task with the same ID is already
+   *    registered.
    */
   fun addTask(
     id: String,
     definition: Class<out Task>,
-  ): Dag {
-    require(tasks.putIfAbsent(id, definition) == null) {
-      "Tasks in Dag have duplicate ID: $id"
+  ): DagDef = addTask(TaskDef(id, definition))
+
+  /**
+   * Registers a task with this Dag.
+   *
+   * A [TaskDef] belongs to at most one [DagDef]; registering the same instance
+   * with a second Dag, or twice with the same one, fails. Task IDs must be
+   * unique within a Dag.
+   *
+   * @param task Task definition to register.
+   * @return This Dag, for chaining.
+   * @throws IllegalArgumentException if the task already belongs to a Dag or a
+   *    task with the same ID is already registered.
+   */
+  fun addTask(task: TaskDef): DagDef {
+    task.owner?.let { owner ->
+      throw IllegalArgumentException("Task '${task.id}' already belongs to Dag 
'${owner.id}'")
     }
+    require(tasks.putIfAbsent(task.id, task) == null) {
+      "Tasks in Dag have duplicate ID: ${task.id}"
+    }
+    task.owner = this
     return this
   }
 }
 
+/**
+ * One task definition: its ID and the class that implements it.
+ *
+ * ```java
+ * var extract = new TaskDef("extract", Extract.class);
+ * ```
+ *
+ * @param id Task identifier, unique within a [DagDef].
+ * @param definition Class that implements [Task]. Must have a public no-arg
+ *    constructor.
+ *
+ * @see Builder.Task
+ */
+class TaskDef(
+  val id: String,
+  val definition: Class<out Task>,
+) {
+  internal var owner: DagDef? = null
+}
+
 /**
  * A single unit of work executed by Airflow.
  *
diff --git 
a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Task.kt 
b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Task.kt
index 3685644c0b7..bddc5256f55 100644
--- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Task.kt
+++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Task.kt
@@ -71,9 +71,11 @@ internal object TaskRunner {
     request: StartupDetails,
     client: Client,
   ): Any {
-    val task = bundle.dags[request.ti.dagId]?.tasks[request.ti.taskId] ?: 
return TaskResult.of(TaskState.State.REMOVED)
+    val definition =
+      bundle.dags[request.ti.dagId]?.tasks[request.ti.taskId]?.definition
+        ?: return TaskResult.of(TaskState.State.REMOVED)
     return try {
-      
task.getDeclaredConstructor().newInstance().execute(Context.from(request), 
client)
+      
definition.getDeclaredConstructor().newInstance().execute(Context.from(request),
 client)
       TaskResult.success()
     } catch (e: CancellationException) {
       throw e // Let coroutine cancellation propagate so the task coroutine 
unwinds.
diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleTest.kt 
b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleTest.kt
index 0e4afb1894a..57050754e88 100644
--- a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleTest.kt
+++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/BundleTest.kt
@@ -27,7 +27,7 @@ internal class BundleTest {
   @Test
   @DisplayName("Should index dags by dagId")
   fun shouldIndexDagsByDagId() {
-    val dag = Dag("dag")
+    val dag = DagDef("dag")
 
     val bundle = Bundle(listOf(dag))
 
@@ -39,7 +39,7 @@ internal class BundleTest {
   fun shouldRejectDuplicateDagIds() {
     val error =
       Assertions.assertThrows(IllegalArgumentException::class.java) {
-        Bundle(listOf(Dag("dag"), Dag("dag")))
+        Bundle(listOf(DagDef("dag"), DagDef("dag")))
       }
 
     Assertions.assertEquals("Dags in bundle have duplicate ID: dag", 
error.message)
diff --git a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/DagDefTest.kt 
b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/DagDefTest.kt
new file mode 100644
index 00000000000..a23f76bad82
--- /dev/null
+++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/DagDefTest.kt
@@ -0,0 +1,97 @@
+/*
+ * 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 org.apache.airflow.sdk
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+
+internal class DagDefTest {
+  private class NoOp : Task {
+    override fun execute(
+      context: Context,
+      client: Client,
+    ) = Unit
+  }
+
+  @Test
+  @DisplayName("Should index tasks by taskId in registration order")
+  fun shouldIndexTasksByTaskId() {
+    val extract = TaskDef("extract", NoOp::class.java)
+    val load = TaskDef("load", NoOp::class.java)
+
+    val dag = DagDef("dag").addTask(extract).addTask(load)
+
+    Assertions.assertEquals(listOf("extract", "load"), dag.tasks.keys.toList())
+    Assertions.assertEquals(mapOf("extract" to extract, "load" to load), 
dag.tasks)
+  }
+
+  @Test
+  @DisplayName("Should register a task from its id and implementation class")
+  fun shouldRegisterTaskFromIdAndImplementationClass() {
+    val dag = DagDef("dag").addTask("extract", NoOp::class.java)
+
+    val task = dag.tasks.getValue("extract")
+    Assertions.assertEquals("extract", task.id)
+    Assertions.assertEquals(NoOp::class.java, task.definition)
+    Assertions.assertEquals(dag, task.owner)
+  }
+
+  @Test
+  @DisplayName("Should reject duplicate task ids")
+  fun shouldRejectDuplicateTaskIds() {
+    val dag = DagDef("dag").addTask(TaskDef("extract", NoOp::class.java))
+
+    val error =
+      Assertions.assertThrows(IllegalArgumentException::class.java) {
+        dag.addTask(TaskDef("extract", NoOp::class.java))
+      }
+
+    Assertions.assertEquals("Tasks in Dag have duplicate ID: extract", 
error.message)
+  }
+
+  @Test
+  @DisplayName("Should reject a task already registered with another dag")
+  fun shouldRejectTaskOwnedByAnotherDag() {
+    val extract = TaskDef("extract", NoOp::class.java)
+    DagDef("first").addTask(extract)
+
+    val error =
+      Assertions.assertThrows(IllegalArgumentException::class.java) {
+        DagDef("second").addTask(extract)
+      }
+
+    Assertions.assertEquals("Task 'extract' already belongs to Dag 'first'", 
error.message)
+  }
+
+  @Test
+  @DisplayName("Should reject the same task registered twice with one dag")
+  fun shouldRejectTaskRegisteredTwice() {
+    val extract = TaskDef("extract", NoOp::class.java)
+    val dag = DagDef("dag").addTask(extract)
+
+    val error =
+      Assertions.assertThrows(IllegalArgumentException::class.java) {
+        dag.addTask(extract)
+      }
+
+    Assertions.assertEquals("Task 'extract' already belongs to Dag 'dag'", 
error.message)
+  }
+}
diff --git 
a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/TaskTest.kt 
b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/TaskTest.kt
index 42a083b94aa..5d303a3dd81 100644
--- a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/TaskTest.kt
+++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/TaskTest.kt
@@ -22,8 +22,9 @@ package org.apache.airflow.sdk.execution
 import org.apache.airflow.sdk.Bundle
 import org.apache.airflow.sdk.Client
 import org.apache.airflow.sdk.Context
-import org.apache.airflow.sdk.Dag
+import org.apache.airflow.sdk.DagDef
 import org.apache.airflow.sdk.Task
+import org.apache.airflow.sdk.TaskDef
 import org.apache.airflow.sdk.execution.comm.BundleInfo
 import org.apache.airflow.sdk.execution.comm.DagRun
 import org.apache.airflow.sdk.execution.comm.RetryTask
@@ -88,8 +89,7 @@ class TaskTest {
     taskId: String,
     taskClass: Class<out Task>,
   ): Bundle {
-    val dag = Dag("test_dag")
-    dag.addTask(taskId, taskClass)
+    val dag = DagDef("test_dag").addTask(TaskDef(taskId, taskClass))
     return Bundle(listOf(dag))
   }
 
diff --git 
a/kubernetes-tests/lang_sdk/java_example/src/java/org/apache/airflow/k8sexample/K8sBundleBuilder.java
 
b/kubernetes-tests/lang_sdk/java_example/src/java/org/apache/airflow/k8sexample/K8sBundleBuilder.java
index 8333567da83..c3b511b898c 100644
--- 
a/kubernetes-tests/lang_sdk/java_example/src/java/org/apache/airflow/k8sexample/K8sBundleBuilder.java
+++ 
b/kubernetes-tests/lang_sdk/java_example/src/java/org/apache/airflow/k8sexample/K8sBundleBuilder.java
@@ -28,7 +28,7 @@ import org.jetbrains.annotations.NotNull;
 public class K8sBundleBuilder implements BundleBuilder {
   @NotNull
   @Override
-  public Iterable<Dag> getDags() {
+  public Iterable<DagDef> getDags() {
     return List.of(CombinedExampleBuilder.build());
   }
 

Reply via email to