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

potiuk pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new 3ad7b64c39f Fix trigger kwargs decoding for asset watcher triggers 
(#65992) (#70750)
3ad7b64c39f is described below

commit 3ad7b64c39fe28e2beef0cfbdc37c8b178930698
Author: Rahul Vats <[email protected]>
AuthorDate: Thu Jul 30 22:20:14 2026 +0530

    Fix trigger kwargs decoding for asset watcher triggers (#65992) (#70750)
    
    (cherry picked from commit 4f6186db09f40e026b68a0af77a6c2fd8e8f1343)
    
    Co-authored-by: Shae Alhusayni <[email protected]>
---
 airflow-core/src/airflow/serialization/decoders.py |  23 ++++-
 airflow-core/tests/unit/jobs/test_triggerer_job.py | 102 ++++++++++++++++++++-
 2 files changed, 122 insertions(+), 3 deletions(-)

diff --git a/airflow-core/src/airflow/serialization/decoders.py 
b/airflow-core/src/airflow/serialization/decoders.py
index 890a1296a4a..78ae241813c 100644
--- a/airflow-core/src/airflow/serialization/decoders.py
+++ b/airflow-core/src/airflow/serialization/decoders.py
@@ -90,9 +90,28 @@ def smart_decode_trigger_kwargs(d):
     """
     from airflow.serialization.serialized_objects import BaseSerialization
 
-    if not isinstance(d, dict) or Encoding.TYPE not in d:
+    d = _normalize_stringified_encoding_keys(d)
+    if not isinstance(d, dict):
         return d
-    return BaseSerialization.deserialize(d)
+    if Encoding.TYPE in d and Encoding.VAR in d:
+        return BaseSerialization.deserialize(d)
+    return {k: smart_decode_trigger_kwargs(v) for k, v in d.items()}
+
+
+def _normalize_stringified_encoding_keys(value):
+    if isinstance(value, list):
+        return [_normalize_stringified_encoding_keys(v) for v in value]
+    if not isinstance(value, dict):
+        return value
+
+    if str(Encoding.TYPE) in value and str(Encoding.VAR) in value:
+        return {
+            Encoding.TYPE if k == str(Encoding.TYPE) else Encoding.VAR if k == 
str(Encoding.VAR) else k: (
+                _normalize_stringified_encoding_keys(v)
+            )
+            for k, v in value.items()
+        }
+    return {k: _normalize_stringified_encoding_keys(v) for k, v in 
value.items()}
 
 
 def _decode_asset(var: dict[str, Any]):
diff --git a/airflow-core/tests/unit/jobs/test_triggerer_job.py 
b/airflow-core/tests/unit/jobs/test_triggerer_job.py
index 9ad31b4e5c2..ff3dde7df59 100644
--- a/airflow-core/tests/unit/jobs/test_triggerer_job.py
+++ b/airflow-core/tests/unit/jobs/test_triggerer_job.py
@@ -30,7 +30,7 @@ import typing
 import uuid
 from collections.abc import AsyncIterator
 from socket import socket, socketpair
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, ClassVar
 from unittest import mock
 from unittest.mock import ANY, AsyncMock, MagicMock, patch
 
@@ -123,6 +123,33 @@ if TYPE_CHECKING:
 pytestmark = pytest.mark.db_test
 
 
+class SerializedKwargsTrigger(BaseTrigger):
+    constructed: ClassVar[list[SerializedKwargsTrigger]] = []
+
+    def __init__(
+        self,
+        *,
+        apply_function_args: tuple[Any, ...],
+        apply_function_kwargs: dict[str, Any],
+    ) -> None:
+        super().__init__()
+        self.apply_function_args = apply_function_args
+        self.apply_function_kwargs = apply_function_kwargs
+        self.constructed.append(self)
+
+    def serialize(self) -> tuple[str, dict[str, Any]]:
+        return (
+            "tests.unit.jobs.test_triggerer_job.SerializedKwargsTrigger",
+            {
+                "apply_function_args": self.apply_function_args,
+                "apply_function_kwargs": self.apply_function_kwargs,
+            },
+        )
+
+    async def run(self) -> AsyncIterator[TriggerEvent]:
+        yield TriggerEvent(True)
+
+
 @pytest.fixture(autouse=True)
 def clean_database():
     """Fixture that cleans the database before and after every test."""
@@ -1512,6 +1539,79 @@ class TestTriggerRunner:
         trigger_instance.cancel()
         await runner.cleanup_finished_triggers()
 
+    @pytest.mark.asyncio
+    @patch(
+        
"airflow.jobs.triggerer_job_runner.TriggerRunner.get_trigger_by_classpath",
+        return_value=SerializedKwargsTrigger,
+    )
+    async def test_trigger_kwargs_cleanup_decodes_stringified_encoding_keys(
+        self, mock_get_trigger_by_classpath, session
+    ):
+        import json
+
+        from airflow.models.crypto import get_fernet
+        from airflow.sdk.serde import serialize
+
+        SerializedKwargsTrigger.constructed.clear()
+        trigger_orm = Trigger(
+            
classpath="tests.unit.jobs.test_triggerer_job.SerializedKwargsTrigger",
+            kwargs={},
+        )
+        legacy_stringified_kwargs = {
+            "apply_function_args": {"Encoding.TYPE": "tuple", "Encoding.VAR": 
[]},
+            "apply_function_kwargs": {
+                "Encoding.TYPE": "dict",
+                "Encoding.VAR": {
+                    "action_filter": ["create"],
+                    "data_filter": {
+                        "Encoding.TYPE": "dict",
+                        "Encoding.VAR": {"BatchId": None},
+                    },
+                },
+            },
+        }
+        trigger_orm.encrypted_kwargs = (
+            
get_fernet().encrypt(json.dumps(serialize(legacy_stringified_kwargs)).encode()).decode()
+        )
+        session.add(trigger_orm)
+        session.commit()
+
+        stored_kwargs = trigger_orm.kwargs
+        assert stored_kwargs == {
+            "apply_function_args": {"Encoding.TYPE": "tuple", "Encoding.VAR": 
[]},
+            "apply_function_kwargs": {
+                "Encoding.TYPE": "dict",
+                "Encoding.VAR": {
+                    "action_filter": ["create"],
+                    "data_filter": {
+                        "Encoding.TYPE": "dict",
+                        "Encoding.VAR": {"BatchId": None},
+                    },
+                },
+            },
+        }
+
+        runner = TriggerRunner()
+        runner.to_create.append(
+            workloads.RunTrigger.model_construct(
+                id=trigger_orm.id,
+                ti=None,
+                classpath=trigger_orm.classpath,
+                encrypted_kwargs=trigger_orm.encrypted_kwargs,
+            )
+        )
+
+        await runner.create_triggers()
+
+        trigger_instance = SerializedKwargsTrigger.constructed[-1]
+        assert trigger_instance.apply_function_args == ()
+        assert trigger_instance.apply_function_kwargs == {
+            "action_filter": ["create"],
+            "data_filter": {"BatchId": None},
+        }
+        runner.triggers[trigger_orm.id]["task"].cancel()
+        await runner.cleanup_finished_triggers()
+
     @pytest.mark.asyncio
     @patch("airflow.sdk.execution_time.task_runner.SUPERVISOR_COMMS", 
create=True)
     async def test_sync_state_to_supervisor(self, supervisor_builder):

Reply via email to