kaxil commented on code in PR #65993:
URL: https://github.com/apache/airflow/pull/65993#discussion_r3160934712


##########
airflow-core/src/airflow/api_fastapi/execution_api/app.py:
##########
@@ -19,6 +19,7 @@
 
 import json
 import time
+from concurrent.futures import TimeoutError

Review Comment:
   This shadows the builtin `TimeoutError`. In Python 3.11+ they're the same 
class, but Airflow still supports 3.10 where `concurrent.futures.TimeoutError` 
and the builtin are distinct. Anywhere later in this module that does `except 
TimeoutError` will now silently mean `concurrent.futures.TimeoutError`.
   
   Prefer `from concurrent.futures import TimeoutError as FutureTimeoutError`, 
or move the import inside `transport()` since that's the only use site.



##########
airflow-core/src/airflow/api_fastapi/execution_api/app.py:
##########
@@ -358,7 +359,12 @@ async def start_lifespan(cm: AsyncExitStack, app: FastAPI):
 
         self._cm = AsyncExitStack()
 
-        asyncio.run_coroutine_threadsafe(start_lifespan(self._cm, self.app), 
middleware.loop)
+        try:
+            asyncio.run_coroutine_threadsafe(start_lifespan(self._cm, 
self.app), middleware.loop).result(
+                timeout=30
+            )
+        except TimeoutError as err:
+            raise RuntimeError("Timed out while starting the in-process 
execution API lifespan") from err

Review Comment:
   This fix improves observability (loud failure instead of silent hang) but 
does not actually resolve #65945. The reporter's stack trace shows their custom 
`SlurmSharedFileTaskHandler.format` crashing inside `svcs.register_factory`'s 
`log.debug()` call -- the lifespan startup raises because of a buggy user log 
handler. After this PR merges they'll see a clear `RuntimeError` propagated 
from `transport`, but the triggerer will still fail to start; the user-side 
handler bug is the actual cause.
   
   Two follow-ups worth considering: (a) call this out explicitly in the PR 
description so the reporter understands what behavior to expect post-merge; (b) 
think about whether the `log.debug` call inside the svcs/lifespan setup should 
be made more defensive (or routed through a logger that bypasses the user task 
handler) so a single broken user handler doesn't take down triggerer startup.



##########
airflow-core/src/airflow/api_fastapi/execution_api/app.py:
##########
@@ -358,7 +359,12 @@ async def start_lifespan(cm: AsyncExitStack, app: FastAPI):
 
         self._cm = AsyncExitStack()

Review Comment:
   Pre-existing, but visible here: `self._cm` is set up to register the 
lifespan's exit handlers, yet I don't see anywhere in `InProcessExecutionAPI` 
that calls `self._cm.aclose()`. That means the lifespan's shutdown code (svcs 
registry teardown, anything yielded back from registered factories) never runs.
   
   Since you're already touching this block, worth confirming whether the 
missing teardown is intentional (process exits anyway) or a separate latent bug 
worth filing.



##########
airflow-core/tests/unit/api_fastapi/execution_api/test_app.py:
##########
@@ -60,6 +66,38 @@ def test_ti_self_routes_have_task_instance_id_param(client):
                 )
 
 
+class TestInProcessExecutionAPI:
+    def test_transport_waits_for_lifespan_startup(self):
+        entered_lifespan = threading.Event()
+
+        @asynccontextmanager
+        async def lifespan(app):
+            await asyncio.sleep(0.05)
+            app.state.lifespan_called = True
+            entered_lifespan.set()
+            yield
+
+        api = InProcessExecutionAPI()
+        api._app = FastAPI(lifespan=lifespan)
+
+        api.transport
+

Review Comment:
   Stylistic nit: accessing a property purely for its side effect reads like a 
typo to a future reader. Prefer `_ = api.transport` or `assert api.transport is 
not None` to make the intent explicit.



##########
airflow-core/src/airflow/api_fastapi/execution_api/app.py:
##########
@@ -358,7 +359,12 @@ async def start_lifespan(cm: AsyncExitStack, app: FastAPI):
 
         self._cm = AsyncExitStack()
 
-        asyncio.run_coroutine_threadsafe(start_lifespan(self._cm, self.app), 
middleware.loop)
+        try:
+            asyncio.run_coroutine_threadsafe(start_lifespan(self._cm, 
self.app), middleware.loop).result(
+                timeout=30

Review Comment:
   Hardcoded magic number. Lift to a module-level constant (e.g. 
`_LIFESPAN_STARTUP_TIMEOUT_SECONDS = 30`) so it's discoverable and tunable, and 
include the value in the `RuntimeError` message on line 367 so operators have a 
hint when a slow real-world startup gets clipped (`f"Timed out after 
{_LIFESPAN_STARTUP_TIMEOUT_SECONDS}s ..."`).
   
   Has 30s been sanity-checked against the slowest realistic deployment (cold 
filesystems, large dag_bag warmup, Postgres connection setup on a sleepy 
network)? If not, it might be worth bumping it or making it configurable.



##########
airflow-core/tests/unit/api_fastapi/execution_api/test_app.py:
##########
@@ -60,6 +66,38 @@ def test_ti_self_routes_have_task_instance_id_param(client):
                 )
 
 
+class TestInProcessExecutionAPI:
+    def test_transport_waits_for_lifespan_startup(self):
+        entered_lifespan = threading.Event()
+
+        @asynccontextmanager
+        async def lifespan(app):
+            await asyncio.sleep(0.05)
+            app.state.lifespan_called = True
+            entered_lifespan.set()
+            yield
+
+        api = InProcessExecutionAPI()
+        api._app = FastAPI(lifespan=lifespan)
+
+        api.transport
+
+        assert entered_lifespan.is_set()
+        assert api.app.state.lifespan_called
+
+    def test_transport_surfaces_lifespan_startup_errors(self):
+        @asynccontextmanager
+        async def lifespan(app):
+            raise RuntimeError("lifespan failed")
+            yield
+
+        api = InProcessExecutionAPI()
+        api._app = FastAPI(lifespan=lifespan)
+
+        with pytest.raises(RuntimeError, match="lifespan failed"):
+            api.transport
+

Review Comment:
   Both tests bypass `api.app` (the real `cached_property` that wires up 
`dependency_overrides`, `dag_bag`, and the actual Execution API routes at 
`app.py:325-342`) by assigning `api._app = FastAPI(lifespan=lifespan)` 
directly. That isolates the timeout/exception logic cleanly, which is fine for 
a unit test, but it doesn't exercise the *real* Execution API lifespan -- which 
is the thing that actually broke in #65945.
   
   Consider also adding an integration-style test that uses the real `api.app` 
and asserts startup completes, plus one that injects a slow lifespan to drive 
the 30s timeout path end-to-end.



-- 
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