Skip to content

Commit 7a07fa3

Browse files
committed
Java SDK: Resolve a task's arguments from the Dag's own wiring
A Dag authored in Java has no Python call site, so the supervisor sends no argument bindings for it and a task's data parameters had nothing to resolve against. Where the Dag wired inputs for a task, those stand in; a stub-backed task keeps reading its bindings, including when the call site bound none. The authoring surface that records those inputs lands in the next commit, so nothing declares them yet.
1 parent 571e95f commit 7a07fa3

14 files changed

Lines changed: 423 additions & 16 deletions

File tree

‎java-sdk/processor/src/main/kotlin/org/apache/airflow/sdk/BuilderProcessor.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ class BuilderProcessor : AbstractProcessor() {
361361
val paramType = TypeName.get(param.type)
362362
if (param.isTaskInput) {
363363
executeSpec.addStatement(
364-
$$"$T $L = $T.bindInput(client, $T.class)",
364+
$$"$T $L = $T.bindInput(context, client, $T.class)",
365365
paramType,
366366
param.local,
367367
ARG_VALUES_TYPE,

‎java-sdk/processor/src/test/kotlin/org/apache/airflow/sdk/BuilderTest.kt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ class BuilderTest {
480480
public static final class Named implements Task {
481481
@Override
482482
public void execute(Context context, Client client) throws Exception {
483-
TestExample.ScoreInput context_ = ArgValues.bindInput(client, TestExample.ScoreInput.class);
483+
TestExample.ScoreInput context_ = ArgValues.bindInput(context, client, TestExample.ScoreInput.class);
484484
new TestExample().named(context_);
485485
}
486486
}
@@ -873,7 +873,7 @@ class BuilderTest {
873873
public static final class Score implements Task {
874874
@Override
875875
public void execute(Context context, Client client) throws Exception {
876-
TestExample.ScoreInput input = ArgValues.bindInput(client, TestExample.ScoreInput.class);
876+
TestExample.ScoreInput input = ArgValues.bindInput(context, client, TestExample.ScoreInput.class);
877877
client.setXCom(new TestExample().score(client, input));
878878
}
879879
}

‎java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Arg.kt‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,20 @@
2020
package org.apache.airflow.sdk
2121

2222
/**
23-
* A value a task can be given, of which [TaskRef] — the output of an upstream
24-
* task — is the only form so far.
23+
* A value a task can be given: the output of an upstream task, carried by the
24+
* [TaskRef] that task's registration returned, or an inline constant.
25+
*
26+
* A constant has to be wrapped because a bare `Double` cannot implement this
27+
* type: boxed types only, no primitives.
2528
*
2629
* @param T Type of the value.
2730
*/
2831
sealed class Arg<T>
2932

33+
internal class LiteralArg<T>(
34+
internal val value: T?,
35+
) : Arg<T>()
36+
3037
/**
3138
* The output of a registered task, and the task's place in the flow.
3239
*

‎java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Context.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ data class Context(
124124
@JvmField val dagRun: DagRun,
125125
@JvmField val ti: TaskInstance,
126126
) {
127+
/** Registration of the executing task; resolves wired data inputs. */
128+
internal var taskDef: TaskDef? = null
129+
127130
internal companion object {
128131
fun from(request: StartupDetails) =
129132
Context(

‎java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/DagDef.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ class TaskDef(
171171
}
172172

173173
internal val configValues = linkedMapOf<String, Any>()
174+
internal val inputs = mutableListOf<Arg<*>>()
174175
internal val upstreams = linkedSetOf<TaskDef>()
175176
internal var owner: DagDef? = null
176177

‎java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/InputTask.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ interface InputTask<I : TaskInput> : Task {
5757
override fun execute(
5858
context: Context,
5959
client: Client,
60-
) = execute(context, client, ArgValues.bindInput(client, inputType()))
60+
) = execute(context, client, ArgValues.bindInput(context, client, inputType()))
6161

6262
/**
6363
* Executes this task.

‎java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Task.kt‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ internal object TaskRunner {
7474
request: StartupDetails,
7575
client: Client,
7676
): Any {
77-
val definition =
78-
bundle.taskDef(request.ti.dagId, request.ti.taskId)?.definition
77+
val taskDef =
78+
bundle.taskDef(request.ti.dagId, request.ti.taskId)
7979
?: return TaskResult.of(TaskState.State.REMOVED)
80+
val definition = taskDef.definition
8081
val instance =
8182
try {
8283
definition.getDeclaredConstructor().newInstance()
@@ -103,7 +104,7 @@ internal object TaskRunner {
103104
return TaskResult.failure(request.tiContext.shouldRetry)
104105
}
105106
return try {
106-
instance.execute(Context.from(request), client)
107+
instance.execute(Context.from(request).also { it.taskDef = taskDef }, client)
107108
TaskResult.success()
108109
} catch (e: CancellationException) {
109110
throw e // Let coroutine cancellation propagate so the task coroutine unwinds.

‎java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/internal/ArgValues.kt‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ package org.apache.airflow.sdk.internal
2323

2424
import com.fasterxml.jackson.databind.ObjectMapper
2525
import com.fasterxml.jackson.databind.json.JsonMapper
26+
import org.apache.airflow.sdk.Arg
2627
import org.apache.airflow.sdk.Client
28+
import org.apache.airflow.sdk.Context
29+
import org.apache.airflow.sdk.LiteralArg
2730
import org.apache.airflow.sdk.MissingXComException
2831
import org.apache.airflow.sdk.TaskInput
32+
import org.apache.airflow.sdk.TaskRef
2933
import org.apache.airflow.sdk.execution.ArgBinding
3034
import org.apache.airflow.sdk.execution.Logger
3135
import java.lang.reflect.Field
@@ -42,6 +46,11 @@ import java.lang.reflect.Type
4246
* graph the scheduler ordered the run by. Flat data parameters resolve the
4347
* binding at their position (through [TaskArgs]); [TaskInput] fields resolve
4448
* bindings by name.
49+
*
50+
* A natively authored Dag has no stub call site, so the supervisor sends no
51+
* bindings for it and the inputs the Dag itself wired stand in. When the
52+
* supervisor sends bindings they are used for every parameter; the Dag's own
53+
* inputs are read only when it sends none.
4554
*/
4655
object ArgValues {
4756
private val mapper: ObjectMapper = JsonMapper.builder().build().findAndRegisterModules()
@@ -68,9 +77,17 @@ object ArgValues {
6877
*/
6978
@JvmStatic
7079
fun <I : TaskInput> bindInput(
80+
context: Context,
7181
client: Client,
7282
type: Class<I>,
7383
): I {
84+
// Runtime bindings carry argument names to match fields against. A wired
85+
// input carries none, so it decodes into the whole input at once -- which
86+
// is well defined because a TaskInput is a task's only data parameter.
87+
wiredInputs(context, client)?.let { wired ->
88+
return decode(resolveWired(wired[0], client), type) as I?
89+
?: throw missingInput(wired[0], type.simpleName)
90+
}
7491
val input = newInput(type)
7592
val arguments = ArgIndex(client.argBindings)
7693
val unfilled = mutableListOf<String>()
@@ -147,6 +164,66 @@ object ArgValues {
147164
type: Type,
148165
): Any? = decode(client.resolveBinding(binding), type)
149166

167+
/**
168+
* Resolves the wired input at a data parameter's position into [type],
169+
* passing null through.
170+
*/
171+
internal fun valueWired(
172+
input: Arg<*>,
173+
client: Client,
174+
type: Type,
175+
): Any? = decode(resolveWired(input, client), type)
176+
177+
/**
178+
* The inputs the Dag wired for this task, or null when the run's arguments
179+
* come from the stub call site. A task with no wired inputs reads the
180+
* bindings, so a stub call that bound nothing keeps its own diagnostics.
181+
*/
182+
internal fun wiredInputs(
183+
context: Context,
184+
client: Client,
185+
): List<Arg<*>>? = if (client.argBindings.isEmpty()) context.taskDef?.inputs?.takeIf { it.isNotEmpty() } else null
186+
187+
/**
188+
* The failure for a wired argument that resolved to nothing where a value is
189+
* required, naming [target] — the position of the parameter it feeds.
190+
*/
191+
internal fun missingWired(
192+
input: Arg<*>,
193+
target: String,
194+
): MissingXComException =
195+
when (input) {
196+
is TaskRef<*> -> MissingXComException(input.def.id, target)
197+
else ->
198+
MissingXComException(
199+
"Task parameter '$target' is wired to a null literal, but has a primitive type that cannot " +
200+
"be null; declare a boxed type (e.g. Integer instead of int) to receive null.",
201+
)
202+
}
203+
204+
/** The failure for a wired input that resolved to nothing for a [TaskInput]. */
205+
private fun missingInput(
206+
input: Arg<*>,
207+
target: String,
208+
): MissingXComException =
209+
when (input) {
210+
is TaskRef<*> ->
211+
MissingXComException(
212+
"Input '$target' requires an XCom from task '${input.def.id}', but none was pushed.",
213+
)
214+
else -> MissingXComException("Input '$target' is wired to a null literal, so there is nothing to bind.")
215+
}
216+
217+
private fun resolveWired(
218+
input: Arg<*>,
219+
client: Client,
220+
): Any? =
221+
when (input) {
222+
is TaskRef<*> -> client.getXCom(taskId = input.def.id)
223+
is LiteralArg<*> -> input.value
224+
else -> null
225+
}
226+
150227
/**
151228
* Builds the failure for a binding that resolved to nothing where a value is
152229
* required, naming [target] — the stub argument, or the [TaskInput] field

‎java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/internal/TaskArgs.kt‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919

2020
package org.apache.airflow.sdk.internal
2121

22+
import org.apache.airflow.sdk.Arg
2223
import org.apache.airflow.sdk.Client
2324
import org.apache.airflow.sdk.Context
2425
import org.apache.airflow.sdk.MissingXComException
2526
import org.apache.airflow.sdk.execution.ArgBinding
27+
import java.lang.reflect.Type
2628

2729
/**
2830
* @suppress
@@ -46,6 +48,7 @@ class TaskArgs private constructor(
4648
private val context: Context,
4749
private val client: Client,
4850
private val arguments: List<ArgBinding>,
51+
private val wired: List<Arg<*>>?,
4952
) {
5053
companion object {
5154
/**
@@ -71,6 +74,13 @@ class TaskArgs private constructor(
7174
client: Client,
7275
declared: Int,
7376
): TaskArgs {
77+
ArgValues.wiredInputs(context, client)?.let { wired ->
78+
check(wired.size == declared) {
79+
"Task '${context.ti.taskId}' declares $declared data parameter(s) " +
80+
"but the Dag wired ${wired.size} argument(s)"
81+
}
82+
return TaskArgs(context, client, emptyList(), wired)
83+
}
7484
val bound = client.argBindings
7585
val arguments = if (bound.size == declared) bound else bound.filterNot { it.fromDefault }
7686
check(arguments.size == declared) {
@@ -83,7 +93,7 @@ class TaskArgs private constructor(
8393
"Task '${context.ti.taskId}' declares $declared data parameter(s) " +
8494
"but the stub call bound ${bound.size} argument(s)$defaults"
8595
}
86-
return TaskArgs(context, client, arguments)
96+
return TaskArgs(context, client, arguments, null)
8797
}
8898
}
8999

@@ -96,7 +106,7 @@ class TaskArgs private constructor(
96106
fun <T : Any> get(
97107
position: Int,
98108
type: Class<T>,
99-
): T? = type.cast(ArgValues.valueAt(client, arguments[position], type))
109+
): T? = type.cast(valueAt(position, type))
100110

101111
/**
102112
* Resolves the argument bound at [position] into the generic [type], passing
@@ -108,7 +118,7 @@ class TaskArgs private constructor(
108118
fun <T : Any> get(
109119
position: Int,
110120
type: TypeRef<T>,
111-
): T? = ArgValues.valueAt(client, arguments[position], type.type) as T?
121+
): T? = valueAt(position, type.type) as T?
112122

113123
/**
114124
* Resolves the argument bound at [position] into [type], which must not be
@@ -136,7 +146,23 @@ class TaskArgs private constructor(
136146
type: TypeRef<T>,
137147
): T = get(position, type) ?: throw missingAt(position)
138148

139-
// The stub signature's own parameter name is the clearest label for a failure
140-
// here: it is what the Dag author has to change.
141-
private fun missingAt(at: Int) = ArgValues.missing(arguments[at], context.ti.taskId)
149+
private fun valueAt(
150+
position: Int,
151+
type: Type,
152+
): Any? =
153+
if (wired != null) {
154+
ArgValues.valueWired(wired[position], client, type)
155+
} else {
156+
ArgValues.valueAt(client, arguments[position], type)
157+
}
158+
159+
// A bound argument is labelled with the stub signature's own parameter name,
160+
// which is what the Dag author has to change. A wired one has no such name,
161+
// so it is labelled with the position it feeds.
162+
private fun missingAt(at: Int): MissingXComException =
163+
if (wired != null) {
164+
ArgValues.missingWired(wired[at], "position $at")
165+
} else {
166+
ArgValues.missing(arguments[at], context.ti.taskId)
167+
}
142168
}

‎java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/ArgTestSupport.kt‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,18 @@ internal fun taskContext(): Context =
9494
dagRun = DagRun("d", "r", null, null, null, null, null, emptyMap()),
9595
ti = TaskInstance("d", "r", "t", null, 1),
9696
)
97+
98+
internal class NoopTask : Task {
99+
override fun execute(
100+
context: Context,
101+
client: Client,
102+
) = Unit
103+
}
104+
105+
/** A context whose task was wired by its Dag with the given inputs. */
106+
internal fun contextWiredWith(inputs: List<Arg<*>>): Context {
107+
val def = TaskDef("t", NoopTask::class.java)
108+
DagDef("d").addTask(def)
109+
def.inputs += inputs
110+
return taskContext().also { it.taskDef = def }
111+
}

0 commit comments

Comments
 (0)