vatsrahul1001 commented on code in PR #74842:
URL: https://github.com/apache/airflow/pull/74842#discussion_r4306143502


##########
airflow-core/newsfragments/74842.improvement.rst:
##########
@@ -0,0 +1 @@
+The triggerer now validates that a triggerer callback path resolves to a 
coroutine function or an awaitable callable (for example an async notifier 
class) before running it, the same way trigger classpaths are validated. Other 
targets fail the callback without being called.

Review Comment:
   Optional: per our newsfragment guidelines these are for major or breaking 
changes. This is hardening that still accepts every documented use, so it can 
probably be dropped.



##########
airflow-core/tests/unit/triggers/test_callback.py:
##########
@@ -151,3 +205,42 @@ async def test_run_failure(self, trigger, 
mock_import_string):
         mock_callback.assert_called_once_with(**TEST_CALLBACK_KWARGS)
         assert failure_event.payload[PAYLOAD_STATUS_KEY] == 
CallbackState.FAILED
         assert all(s in failure_event.payload[PAYLOAD_BODY_KEY] for s in 
["raise", "RuntimeError", exc_msg])
+
+    @pytest.mark.asyncio
+    async def test_run_with_real_coroutine_function_path(self):
+        trigger = CallbackTrigger(
+            callback_path=f"{__name__}.example_async_callback",
+            callback_kwargs={"message": TEST_MESSAGE},
+        )
+        trigger_gen = trigger.run()
+
+        assert (await anext(trigger_gen)).payload[PAYLOAD_STATUS_KEY] == 
CallbackState.RUNNING
+        success_event = await anext(trigger_gen)
+        assert success_event.payload[PAYLOAD_STATUS_KEY] == 
CallbackState.SUCCESS
+        assert success_event.payload[PAYLOAD_BODY_KEY] == f"Async callback: 
{TEST_MESSAGE}"
+
+    @pytest.mark.asyncio
+    @pytest.mark.parametrize(
+        "callback_path",
+        [
+            pytest.param("os.path", id="module"),
+            pytest.param("os.sep", id="non-callable-attribute"),
+            pytest.param("os.getcwd", id="sync-callable"),
+            pytest.param(f"{__name__}.ExamplePlainClass", id="plain-class"),
+        ],
+    )
+    async def test_run_fails_for_invalid_callback_path(self, callback_path):
+        trigger = CallbackTrigger(callback_path=callback_path, 
callback_kwargs={})
+        trigger_gen = trigger.run()
+
+        with mock.patch("os.getcwd") as mock_getcwd:

Review Comment:
   Nit: the `os.getcwd` patch only matters for the `sync-callable` case, where 
`import_string("os.getcwd")` returns the mock because the patch is active 
before the first `anext`. That is subtle. Patching only for that case, or using 
a local sync function with a spy, would make the intent clearer.



##########
airflow-core/src/airflow/triggers/callback.py:
##########
@@ -32,6 +33,33 @@
 PAYLOAD_BODY_KEY = "body"
 
 
+class InvalidCallbackPathError(TypeError):
+    """Raised when a callback path does not resolve to a usable triggerer 
callback."""
+
+
+def _validate_callback(callback: Any, callback_path: str) -> None:
+    """
+    Check that the object resolved from ``callback_path`` can be used as a 
triggerer callback.
+
+    Triggerer callbacks (``AsyncCallback``) are coroutine functions or 
awaitable callables such as
+    notifier classes, which is also what ``AsyncCallback`` checks when the 
callback is defined.
+    Callable objects with an ``async def __call__`` are accepted as well.
+    The callback path is validated here before the callable is invoked, the 
same way trigger
+    classpaths are validated before a trigger is instantiated.
+    """
+    if not callable(callback):
+        raise InvalidCallbackPathError(f"The callback path {callback_path!r} 
does not resolve to a callable.")
+    if not (
+        inspect.iscoroutinefunction(callback)
+        or hasattr(callback, "__await__")
+        or inspect.iscoroutinefunction(type(callback).__call__)

Review Comment:
   Optional: this duplicates `AsyncCallback.verify_callable` and extends it 
with the `async def __call__` case, which the SDK check rejects. A path to an 
async callable object would be refused when the callback is defined but 
accepted here. Either add that case to the SDK check too, or drop it here, so 
there is a single source of truth.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to