shahar1 commented on code in PR #72809:
URL: https://github.com/apache/airflow/pull/72809#discussion_r4477173294


##########
providers/apache/beam/src/airflow/providers/apache/beam/hooks/beam.py:
##########
@@ -176,13 +176,22 @@ def run_beam_command(
         stderr=subprocess.PIPE,
         close_fds=True,
     )
+
+    if is_dataflow_job_id_exist_callback and hasattr(os, "set_blocking") and 
proc.stdout and proc.stderr:
+        os.set_blocking(proc.stdout.fileno(), False)
+        os.set_blocking(proc.stderr.fileno(), False)
+
     # Waits for Apache Beam pipeline to complete.
     log.info("Start waiting for Apache Beam process to complete.")
     reads = [fd for fd in (proc.stderr, proc.stdout) if fd is not None]
     while True:
         # Wait for at least one available fd.
         readable_fds, _, _ = select.select(reads, [], [], 5)
-        if readable_fds is None:
+        if not readable_fds:
+            if is_dataflow_job_id_exist_callback and 
is_dataflow_job_id_exist_callback(

Review Comment:
   **major.** The by-name lookup only runs on a 5 s `select` timeout, so a 
launcher that exits before the next timeout leaves `dataflow_job_id` as `None`. 
A pipeline that calls `p.run()` without `wait_until_finish()`, at the default 
WARNING log level, does this: the job is created just before the launcher 
exits, and the EOF makes `select` return readable, so no timeout follows the 
job's creation.
   
   I ran this function with a silent child that creates the "job" at 6.5 s and 
exits 0, and a stub lookup that finds it from then on:
   
   ```text
   outcome: returned normally after 6.5s
   job_id: None  lookups: [None]
   ```
   
   With `deferrable=True`, `execute_on_dataflow` then defers with 
`job_id=None`, which is what produces `400 Request must contain a job and 
project id.` in #68279. This is also the earlier question on this PR that 
wasn't answered: "If the launcher exits without logging a job ID before the 
timeout, when would the API lookup run before deferring?"
   
   Two small changes would close it:
   
   1. One last lookup after the launcher exits cleanly, after the final drain:
   
      ```python
      if proc.returncode == 0 and is_dataflow_job_id_exist_callback:
          is_dataflow_job_id_exist_callback(checking_by_job_name=True)
      ```
   
   2. `if self.deferrable and self.dataflow_job_id:` in both 
`execute_on_dataflow` methods. If the job still isn't found (it already 
finished, so `filter="ACTIVE"` misses it, or the lookup failed, or the google 
provider is older), the task then falls through to the by-name `wait_for_done` 
that the sync path already uses, instead of deferring and failing.



##########
providers/apache/beam/src/airflow/providers/apache/beam/hooks/beam.py:
##########
@@ -142,7 +142,7 @@ def process_fd(
     func_log = fd_to_log[fd]
 
     for line_raw in iter(fd.readline, b""):
-        line = line_raw.decode()
+        line = line_raw.decode(errors="replace")

Review Comment:
   **minor.** The earlier review called a split line's missed regex mostly 
harmless, but the Java alternative of `JOB_ID_PATTERN` (`Submitted job: 
(?P<job_id_java>[^\"\n\s]*)`) has no closing delimiter, so a split line can 
match a truncated ID instead of missing. A child writing `Submitted job: 
2026-11-0`, pausing, then writing the rest gives:
   
   ```text
   outcome: returned normally after 0.0s
   job_id: '2026-11-0'
   ```
   
   The task then monitors an ID that doesn't exist. This needs the launcher to 
emit the line in more than one write, and I haven't checked whether the Beam 
Java launcher ever does, so it's likely rare. Passing only complete lines to 
the callback (`if process_line_callback and line.endswith("\n"):`) would leave 
this case to the by-name lookup.



##########
providers/apache/beam/src/airflow/providers/apache/beam/operators/beam.py:
##########
@@ -155,8 +155,25 @@ def set_current_dataflow_job_id(job_id):
             on_new_job_id_callback=set_current_dataflow_job_id
         )
 
-    def __is_dataflow_job_id_exist_callback(self) -> Callable[[], bool]:
-        def is_dataflow_job_id_exist() -> bool:
+    def __is_dataflow_job_id_exist_callback(self) -> Callable[..., bool]:
+        def is_dataflow_job_id_exist(checking_by_job_name: bool = False) -> 
bool:
+            if (
+                checking_by_job_name
+                and self.dataflow_hook
+                and hasattr(self.dataflow_hook, "get_job_id_of_running_job")
+            ):
+                try:
+                    self.dataflow_job_id = 
self.dataflow_hook.get_job_id_of_running_job(

Review Comment:
   **minor.** On the open thread about `DataflowJobAlreadyExistsError`: I think 
both descriptions are right, and which one you get depends on timing. With a 
stub lookup that returns another run's active job:
   
   ```text
   launcher fails at t=2s: raised AirflowException: Apache Beam process failed 
with return code 1
   launcher fails at t=7s: returned normally after 5.0s, job_id 'job-other-run'
   ```
   
   So the task fails, matching what you reported, when the launcher reaches the 
error before 5 s of silence. When staging stays silent for longer, the lookup 
wins: the task follows the other run's job and reports its result, and 
`on_kill` would cancel that job. This needs `append_job_name=False`. The 
earlier suggestion still applies: add a sentence to the `.. warning::`, or 
bound the lookup by `createTime`.



##########
providers/apache/beam/src/airflow/providers/apache/beam/hooks/beam.py:
##########
@@ -176,13 +176,22 @@ def run_beam_command(
         stderr=subprocess.PIPE,
         close_fds=True,
     )
+
+    if is_dataflow_job_id_exist_callback and hasattr(os, "set_blocking") and 
proc.stdout and proc.stderr:
+        os.set_blocking(proc.stdout.fileno(), False)
+        os.set_blocking(proc.stderr.fileno(), False)
+
     # Waits for Apache Beam pipeline to complete.
     log.info("Start waiting for Apache Beam process to complete.")
     reads = [fd for fd in (proc.stderr, proc.stdout) if fd is not None]
     while True:
         # Wait for at least one available fd.
         readable_fds, _, _ = select.select(reads, [], [], 5)
-        if readable_fds is None:
+        if not readable_fds:
+            if is_dataflow_job_id_exist_callback and 
is_dataflow_job_id_exist_callback(
+                checking_by_job_name=True

Review Comment:
   **minor.** `is_dataflow_job_id_exist_callback` is a public parameter of 
`BeamHook.start_python_pipeline` and `start_java_pipeline`, previously typed 
`Callable[[], bool]`. A caller passing its own zero-argument callback now fails 
at the first 5 s of silence:
   
   ```text
   TypeError: zero_arg_callback() got an unexpected keyword argument 
'checking_by_job_name'
   ```
   
   Nothing in this repository passes one, so this only affects outside callers. 
A separate optional callback for the lookup would keep the existing signature 
and let the type go back from `Callable[..., bool]`.



##########
providers/apache/beam/tests/unit/apache/beam/hooks/test_beam.py:
##########
@@ -441,6 +443,42 @@ def test_beam_wait_for_done_logging(self, mock_select, 
mock_popen, caplog):
         assert "apache-beam-stderr-3" in warn_messages
         assert "apache-beam-other-stderr" in warn_messages
 
+    @mock.patch("os.set_blocking")
+    @mock.patch("subprocess.Popen")
+    @mock.patch("select.select")
+    def test_run_beam_command_checks_job_id_by_name_on_timeout(
+        self, mock_select, mock_popen, mock_set_blocking
+    ):
+        fake_logger = MagicMock()

Review Comment:
   **minor.** From `.github/instructions/code-review.instructions.md`:
   
   > **Flag any changed or added behaviour without a corresponding test, and 
flag tests that a reviewer cannot fail by reverting the PR's change.**
   
   Two changes have no test yet: `errors="replace"` in `process_fd` (no test 
feeds it undecodable bytes), and the false branch of the `hasattr` guard (a 
`MagicMock` hook always has the attribute).
   
   > **Flag any `mock.Mock()` or `mock.MagicMock()` without `spec` or 
`autospec`.**
   
   This applies to the new `fake_logger`, `mock_proc`, `fake_stderr_fd`, 
`fake_stdout_fd` and `is_job_id_exist_callback` here.



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