This is an automated email from the ASF dual-hosted git repository.
potiuk 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 a1f1a491db1 Return null from Java SDK getVariable for missing
Variables (#75920)
a1f1a491db1 is described below
commit a1f1a491db1831aed7c82ae3a0f3aa74c58c30fe
Author: Aaron Chen <[email protected]>
AuthorDate: Sun Nov 1 15:34:07 2026 -0800
Return null from Java SDK getVariable for missing Variables (#75920)
* Return null from Java SDK getVariable for missing Variables
* Refactor error response handling in CommTest to use a unified
typedErrorFrame method
---
.../org/apache/airflow/e2e/TestBundleBuilder.java | 6 +-
.../main/kotlin/org/apache/airflow/sdk/Client.kt | 2 +-
.../org/apache/airflow/sdk/execution/Client.kt | 8 +-
.../org/apache/airflow/sdk/execution/CommTest.kt | 91 +++++++++++++---------
4 files changed, 65 insertions(+), 42 deletions(-)
diff --git
a/airflow-e2e-tests/java-test-bundle/src/java/org/apache/airflow/e2e/TestBundleBuilder.java
b/airflow-e2e-tests/java-test-bundle/src/java/org/apache/airflow/e2e/TestBundleBuilder.java
index 39c4bd46afc..342b1dd3354 100644
---
a/airflow-e2e-tests/java-test-bundle/src/java/org/apache/airflow/e2e/TestBundleBuilder.java
+++
b/airflow-e2e-tests/java-test-bundle/src/java/org/apache/airflow/e2e/TestBundleBuilder.java
@@ -51,7 +51,8 @@ public class TestBundleBuilder implements BundleBuilder {
/**
* Stores this run's id where the E2E test can read it back through the REST
API, then writes
- * and deletes a scratch variable to exercise the delete path.
+ * and deletes a scratch variable to exercise the delete path, and reads it
back to check that a
+ * missing variable reads as null.
*/
public static class WriteAndDeleteVariable implements Task {
public void execute(@NotNull Context context, Client client) {
@@ -59,6 +60,9 @@ public class TestBundleBuilder implements BundleBuilder {
"java_e2e_variable", context.dagRun.runId, "written by the Java SDK
e2e test");
client.setVariable("java_e2e_scratch", "scratch");
client.deleteVariable("java_e2e_scratch");
+ if (client.getVariable("java_e2e_scratch") != null) {
+ throw new IllegalStateException("java_e2e_scratch survived its
delete");
+ }
}
}
diff --git a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Client.kt
b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Client.kt
index d8cb35734fe..8b3311382b5 100644
--- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Client.kt
+++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/Client.kt
@@ -109,7 +109,7 @@ class Client internal constructor(
* @return The variable value, or `null` if the variable is not set.
* @throws ApiError if the API call fails.
*/
- fun getVariable(key: String): Any? = impl.getVariable(key).value
+ fun getVariable(key: String): Any? = impl.getVariable(key)?.value
/**
* Stores an Airflow variable, replacing any existing value.
diff --git
a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Client.kt
b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Client.kt
index 317dac7ebb0..7bcef266297 100644
--- a/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Client.kt
+++ b/java-sdk/sdk/src/main/kotlin/org/apache/airflow/sdk/execution/Client.kt
@@ -61,7 +61,8 @@ import java.util.UUID
interface Client {
fun getConnection(id: String): ConnectionResult
- fun getVariable(key: String): VariableResult
+ /** Returns `null` when the variable does not exist. */
+ fun getVariable(key: String): VariableResult?
fun setVariable(
key: String,
@@ -164,7 +165,10 @@ class CoordinatorClient(
override fun getVariable(key: String) =
runBlocking {
- exec.communicate<VariableResult>(GetVariable().also { it.key = key })
+ exec.communicateOrNullIf<VariableResult>(
+ GetVariable().also { it.key = key },
+ ErrorResponse.ErrorType.VARIABLE_NOT_FOUND,
+ )
}
override fun setVariable(
diff --git
a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/CommTest.kt
b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/CommTest.kt
index b6c12c36a57..68b4efa38dc 100644
--- a/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/CommTest.kt
+++ b/java-sdk/sdk/src/test/kotlin/org/apache/airflow/sdk/execution/CommTest.kt
@@ -183,24 +183,30 @@ class CommsTest {
return out.toByteArray()
}
- private fun notFoundResponseFrame(id: Int): ByteArray {
- val out = ByteArrayOutputStream()
- MessagePack.newDefaultPacker(out).use { packer ->
- packer.packArrayHeader(3)
- packer.packInt(id)
- packer.packNil()
- packer.packMapHeader(3)
- packer.packString("type")
- packer.packString("ErrorResponse")
- packer.packString("error")
- packer.packString("TASK_STORE_NOT_FOUND")
- packer.packString("detail")
- packer.packMapHeader(1)
- packer.packString("key")
- packer.packString("job_id")
+ // An ErrorResponse carrying a typed `error` code and a one-entry `detail`,
as the supervisor sends it.
+ private fun typedErrorFrame(
+ error: String,
+ detailKey: String,
+ detailValue: String,
+ ): (Int) -> ByteArray =
+ { id ->
+ val out = ByteArrayOutputStream()
+ MessagePack.newDefaultPacker(out).use { packer ->
+ packer.packArrayHeader(3)
+ packer.packInt(id)
+ packer.packNil()
+ packer.packMapHeader(3)
+ packer.packString("type")
+ packer.packString("ErrorResponse")
+ packer.packString("error")
+ packer.packString(error)
+ packer.packString("detail")
+ packer.packMapHeader(1)
+ packer.packString(detailKey)
+ packer.packString(detailValue)
+ }
+ out.toByteArray()
}
- return out.toByteArray()
- }
private fun taskStateStoreResultFrame(id: Int): ByteArray {
val out = ByteArrayOutputStream()
@@ -282,25 +288,6 @@ class CommsTest {
return Triple(requests.single().rawBody as Map<*, *>, outcome.getOrNull(),
outcome.exceptionOrNull())
}
- private fun dagRunExistsResponseFrame(id: Int): ByteArray {
- val out = ByteArrayOutputStream()
- MessagePack.newDefaultPacker(out).use { packer ->
- packer.packArrayHeader(3)
- packer.packInt(id)
- packer.packNil()
- packer.packMapHeader(3)
- packer.packString("type")
- packer.packString("ErrorResponse")
- packer.packString("error")
- packer.packString("DAGRUN_ALREADY_EXISTS")
- packer.packString("detail")
- packer.packMapHeader(1)
- packer.packString("dag_id")
- packer.packString("downstream")
- }
- return out.toByteArray()
- }
-
private fun dagRunStateResultFrame(id: Int): ByteArray {
val out = ByteArrayOutputStream()
MessagePack.newDefaultPacker(out).use { packer ->
@@ -491,7 +478,7 @@ class CommsTest {
@Timeout(value = 30, unit = TimeUnit.SECONDS)
fun triggerDagRunReportsAnExistingRun() {
val (_, alreadyExists, failure) =
- transportRoundTrip(::dagRunExistsResponseFrame) {
+ transportRoundTrip(typedErrorFrame("DAGRUN_ALREADY_EXISTS", "dag_id",
"downstream")) {
it.triggerDagRun("downstream", "run", null, null, null, false, null)
}
@@ -525,6 +512,31 @@ class CommsTest {
Assertions.assertEquals("downstream", body["dag_id"])
}
+ @Test
+ @DisplayName("getVariable returns null on VARIABLE_NOT_FOUND instead of
raising")
+ @Timeout(value = 30, unit = TimeUnit.SECONDS)
+ fun getVariableReturnsNullWhenNotFound() {
+ var value: Any? = "unset"
+ val (body, failure) =
+ roundTrip(typedErrorFrame("VARIABLE_NOT_FOUND", "key",
"optional_setting")) {
+ value = it.getVariable("optional_setting")
+ }
+
+ Assertions.assertNull(failure, "getVariable should return normally on
VARIABLE_NOT_FOUND, got $failure")
+ Assertions.assertEquals("GetVariable", body["type"])
+ Assertions.assertEquals("optional_setting", body["key"])
+ Assertions.assertNull(value)
+ }
+
+ @Test
+ @DisplayName("getVariable raises ApiError for any other error response")
+ @Timeout(value = 30, unit = TimeUnit.SECONDS)
+ fun getVariableRaisesApiErrorOnOtherErrors() {
+ val (_, failure) = roundTrip(::errorResponseFrame) {
it.getVariable("optional_setting") }
+
+ Assertions.assertInstanceOf(ApiError::class.java, failure)
+ }
+
@Test
@DisplayName("setVariable keeps a null description on the wire so the
supervisor accepts the request")
@Timeout(value = 30, unit = TimeUnit.SECONDS)
@@ -592,7 +604,10 @@ class CommsTest {
@Timeout(value = 30, unit = TimeUnit.SECONDS)
fun taskStateStoreGetReturnsNullWhenNotFound() {
var value: Any? = "unset"
- val (_, failure) = roundTrip(::notFoundResponseFrame,
currentTaskInstance()) { value = it.taskStateStore.get("job_id") }
+ val (_, failure) =
+ roundTrip(typedErrorFrame("TASK_STORE_NOT_FOUND", "key", "job_id"),
currentTaskInstance()) {
+ value = it.taskStateStore.get("job_id")
+ }
Assertions.assertNull(failure, "get should return normally on
TASK_STORE_NOT_FOUND, got $failure")
Assertions.assertNull(value)