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


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

Review Comment:
   Switching these pipes to non-blocking changes what `process_fd` receives. In 
blocking mode `fd.readline()` only returns a complete line (or `b""` at EOF). 
In non-blocking mode, `BufferedReader.readline()` returns whatever bytes are 
available when the raw read hits `EAGAIN`, so a line can arrive in two pieces. 
If the split falls inside a multi-byte UTF-8 character (any non-ASCII text in 
Beam/Dataflow output: user log messages, file paths, localized warnings), `line 
= line_raw.decode()` at L146 raises `UnicodeDecodeError` and the task fails. 
That's why the fix belongs in this PR even though L146 isn't in the diff:
   
   ```python
   # in process_fd (L146)
   line = line_raw.decode(errors="replace")
   ```
   
   (A split line can also make `process_line_callback` miss the job-id regex 
for that read. That's mostly harmless now that the name lookup is a fallback, 
but it's another reason to keep this to the Dataflow path, as you've done.)



##########
providers/google/src/airflow/providers/google/cloud/hooks/dataflow.py:
##########
@@ -339,6 +339,15 @@ def _get_current_jobs(self) -> list[dict]:
             jobs = self._fetch_jobs_by_prefix_name(self._job_name.lower())
             if len(jobs) == 1:
                 self._job_id = jobs[0]["id"]
+            elif len(jobs) > 1 and not self._multiple_jobs:
+                active_jobs = [
+                    job for job in jobs if job.get("currentState") not in 
DataflowJobStatus.TERMINAL_STATES
+                ]
+                if len(active_jobs) == 1:
+                    self._job_id = active_jobs[0]["id"]
+                else:
+                    jobs.sort(key=lambda j: j.get("createTime", ""), 
reverse=True)
+                    self._job_id = jobs[0]["id"]

Review Comment:
   `jobs` here comes from `_fetch_jobs_by_prefix_name`, so it holds every job 
whose name *starts with* `self._job_name`. Picking "the single active one, else 
the newest" can therefore select a different job: for `daily-etl` with 
`append_job_name=False`, an active `daily-etl-backfill` wins over this run's 
own job. If none is active, the newest terminal job is chosen, which may also 
be a sibling or a stale run.
   
   This method backs `wait_for_done(job_name=..., job_id=None)` for every 
Dataflow operator, not only beam, and the beam fix now goes through 
`fetch_job_id` instead. Could you drop this hunk? If it's needed for another 
case, please match exact names (`job.get("name") == self._job_name.lower()`) 
and explain that case in the PR.



##########
providers/apache/beam/src/airflow/providers/apache/beam/operators/beam.py:
##########
@@ -155,8 +155,20 @@ 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:
+                try:
+                    self.dataflow_job_id = 
self.dataflow_hook.get_job_id_of_running_job(
+                        job_name=self.dataflow_job_name,  # type: ignore
+                        project_id=self.dataflow_config.project_id,
+                        location=self.dataflow_config.location,
+                    )
+                except Exception:
+                    self.log.warning(  # type: ignore
+                        "Failed to get Dataflow job ID by job name '%s'",
+                        self.dataflow_job_name,  # type: ignore
+                    )

Review Comment:
   With beam installed alongside a google provider that predates 
`get_job_id_of_running_job`, this call raises `AttributeError`. The `except 
Exception` catches it, so the task no longer fails, but it logs "Failed to get 
Dataflow job ID by job name …" every 5 s of launcher silence with no indication 
why. Suggest:
   
   ```python
   if (
       checking_by_job_name
       and self.dataflow_hook
       and hasattr(self.dataflow_hook, "get_job_id_of_running_job")
   ):
       try:
           ...
       except Exception:
           self.log.warning("Failed to get Dataflow job ID by job name '%s'", 
self.dataflow_job_name, exc_info=True)
   ```
   
   The `hasattr` guard keeps the old behaviour on an older google provider (the 
same approach as #54982, so no hard dependency is needed), and `exc_info=True` 
shows the cause of a real failure, e.g. a missing `dataflow.jobs.list` 
permission.



##########
providers/google/src/airflow/providers/google/cloud/hooks/dataflow.py:
##########
@@ -434,13 +443,40 @@ def fetch_job_autoscaling_events_by_id(self, job_id: str) 
-> list[dict]:
             autoscaling_events.extend(response.get("autoscalingEvents", []))
         return autoscaling_events
 
-    def _fetch_all_jobs(self) -> list[dict]:
-        request = (
-            self._dataflow.projects()
-            .locations()
-            .jobs()
-            .list(projectId=self._project_number, location=self._job_location)
-        )
+    def fetch_job_id(self) -> str | None:
+        if self._job_id:
+            return self._job_id
+        if not self._job_name:
+            raise ValueError("Missing both dataflow job ID and name.")
+        jobs = self._fetch_all_jobs(jobs_filter="ACTIVE")
+        job_name = self._job_name.lower()
+        active_jobs = [
+            job
+            for job in jobs
+            if job.get("name") == job_name
+            and job.get("currentState") not in 
DataflowJobStatus.TERMINAL_STATES
+        ]

Review Comment:
   Minor, not blocking. With `append_job_name=False`, if an earlier try's job 
is still running under the same name (worker crash plus retry, or a streaming 
job), the first 5 s lookup returns *that* job, and the operator then monitors 
or defers on it. The new launcher, meanwhile, fails with 
`DataflowJobAlreadyExistsError`, and since we've stopped reading the subprocess 
nobody sees it; before this PR the task would fail. A `createTime` lower bound 
(task start time) would close this. Otherwise a sentence in the `.. warning::` 
docstring would do.



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