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

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 9a28876d514 Validate Gemini batch results_folder in the worker that 
writes results (#70393)
9a28876d514 is described below

commit 9a28876d51496f5119b825515934694018c25c76
Author: Shahar Epstein <[email protected]>
AuthorDate: Sat Aug 1 07:42:20 2026 +0300

    Validate Gemini batch results_folder in the worker that writes results 
(#70393)
    
    In deferrable mode the results file is written by execute_complete in a
    resume worker where execute() -- and its results_folder pre-flight check
    -- never ran. A folder missing there (heterogeneous workers, relative
    path against a different CWD) surfaced as a raw FileNotFoundError after
    the billable batch job already succeeded, instead of the operator's
    clear validation error.
---
 .../providers/google/cloud/operators/gen_ai.py     | 18 ++++++--
 .../unit/google/cloud/operators/test_gen_ai.py     | 50 ++++++++++++++++++++++
 2 files changed, 64 insertions(+), 4 deletions(-)

diff --git 
a/providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py 
b/providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py
index 0801bb24a75..65d8a943374 100644
--- a/providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py
+++ b/providers/google/src/airflow/providers/google/cloud/operators/gen_ai.py
@@ -499,6 +499,11 @@ class 
GenAIGeminiCreateBatchJobOperator(GoogleCloudBaseOperator):
             raise AirflowException("Something went wrong during waiting of the 
batch job.")
         return job
 
+    def _validate_results_folder(self):
+        # Re-checked where the results file is written: deferral resume skips 
execute().
+        if self.results_folder and not 
os.path.exists(os.path.abspath(self.results_folder)):
+            raise AirflowException("path to results_folder does not exist, 
please provide correct path")
+
     def _prepare_results_for_xcom(self, job):
         results = []
         if job.dest and job.dest.inlined_responses:
@@ -514,6 +519,7 @@ class 
GenAIGeminiCreateBatchJobOperator(GoogleCloudBaseOperator):
                     self.log.warning("Error found in the inline result")
                     results.append(inline_response.error)
         elif job.dest and job.dest.file_name:
+            self._validate_results_folder()
             file_content_bytes = 
self.hook.download_file(file_name=job.dest.file_name)
             file_content = file_content_bytes.decode("utf-8")
             file_name = job.display_name or job.name.replace("/", "-")
@@ -541,8 +547,7 @@ class 
GenAIGeminiCreateBatchJobOperator(GoogleCloudBaseOperator):
         if self.results_folder and not isinstance(self.input_source, str):
             raise AirflowException("results_folder works only when 
input_source is file name")
 
-        if self.results_folder and not 
os.path.exists(os.path.abspath(self.results_folder)):
-            raise AirflowException("path to results_folder does not exist, 
please provide correct path")
+        self._validate_results_folder()
 
         if self.deferrable:
             self.defer(
@@ -944,6 +949,11 @@ class 
GenAIGeminiCreateEmbeddingsBatchJobOperator(GoogleCloudBaseOperator):
             raise AirflowException("Something went wrong during waiting of the 
batch job: %s", e)
         return job
 
+    def _validate_results_folder(self):
+        # Re-checked where the results file is written: deferral resume skips 
execute().
+        if self.results_folder and not 
os.path.exists(os.path.abspath(self.results_folder)):
+            raise AirflowException("path to results_folder does not exist, 
please provide correct path")
+
     def _prepare_results_for_xcom(self, job):
         results = []
         if job.dest and job.dest.inlined_embed_content_responses:
@@ -959,6 +969,7 @@ class 
GenAIGeminiCreateEmbeddingsBatchJobOperator(GoogleCloudBaseOperator):
                     self.log.warning("Error found in the inline result")
                     results.append(inline_embed_response.error)
         elif job.dest and job.dest.file_name:
+            self._validate_results_folder()
             file_content_bytes = 
self.hook.download_file(file_name=job.dest.file_name)
             file_content = file_content_bytes.decode("utf-8")
             file_name = job.display_name or job.name.replace("/", "-")
@@ -986,8 +997,7 @@ class 
GenAIGeminiCreateEmbeddingsBatchJobOperator(GoogleCloudBaseOperator):
         if self.results_folder and not isinstance(self.input_source, str):
             raise AirflowException("results_folder works only when 
input_source is file name")
 
-        if self.results_folder and not 
os.path.exists(os.path.abspath(self.results_folder)):
-            raise AirflowException("path to results_folder does not exist, 
please provide correct path")
+        self._validate_results_folder()
         if self.deferrable:
             self.defer(
                 trigger=GenAIGeminiCreateEmbeddingsBatchJobTrigger(
diff --git a/providers/google/tests/unit/google/cloud/operators/test_gen_ai.py 
b/providers/google/tests/unit/google/cloud/operators/test_gen_ai.py
index 3e814f9bd9a..1071f2de829 100644
--- a/providers/google/tests/unit/google/cloud/operators/test_gen_ai.py
+++ b/providers/google/tests/unit/google/cloud/operators/test_gen_ai.py
@@ -457,6 +457,31 @@ class TestGenAIGeminiCreateBatchJobOperator:
         ):
             op.execute(context={"ti": mock.MagicMock()})
 
+    @mock.patch(GEN_AI_PATH.format("GenAIGeminiAPIHook"))
+    def 
test_prepare_results_for_xcom_results_folder_not_exists_raises_airflow_exception(self,
 mock_hook):
+        op = GenAIGeminiCreateBatchJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            model=TEST_GEMINI_MODEL,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            input_source=TEST_FILE_NAME,
+            gemini_api_key=TEST_GEMINI_API_KEY,
+            results_folder=TEST_FILE_PATH,
+        )
+        mock_job = mock.MagicMock()
+        mock_job.dest.inlined_responses = None
+        mock_job.dest.file_name = "results-file"
+
+        with pytest.raises(
+            AirflowException,
+            match="path to results_folder does not exist, please provide 
correct path",
+        ):
+            op._prepare_results_for_xcom(mock_job)
+
+        mock_hook.return_value.download_file.assert_not_called()
+
     @mock.patch(GEN_AI_PATH.format("GenAIGeminiAPIHook"))
     def test__wait_until_complete_exception_raises_airflow_exception(self, 
mock_hook):
         op = GenAIGeminiCreateBatchJobOperator(
@@ -832,6 +857,31 @@ class TestGenAIGeminiCreateEmbeddingsBatchJobOperator:
         ):
             op.execute(context={"ti": mock.MagicMock()})
 
+    @mock.patch(GEN_AI_PATH.format("GenAIGeminiAPIHook"))
+    def 
test_prepare_results_for_xcom_results_folder_not_exists_raises_airflow_exception(self,
 mock_hook):
+        op = GenAIGeminiCreateEmbeddingsBatchJobOperator(
+            task_id=TASK_ID,
+            project_id=GCP_PROJECT,
+            location=GCP_LOCATION,
+            input_source=TEST_FILE_NAME,
+            model=EMBEDDING_MODEL,
+            gemini_api_key=TEST_GEMINI_API_KEY,
+            gcp_conn_id=GCP_CONN_ID,
+            impersonation_chain=IMPERSONATION_CHAIN,
+            results_folder=TEST_FILE_PATH,
+        )
+        mock_job = mock.MagicMock()
+        mock_job.dest.inlined_embed_content_responses = None
+        mock_job.dest.file_name = "results-file"
+
+        with pytest.raises(
+            AirflowException,
+            match="path to results_folder does not exist, please provide 
correct path",
+        ):
+            op._prepare_results_for_xcom(mock_job)
+
+        mock_hook.return_value.download_file.assert_not_called()
+
     @mock.patch(GEN_AI_PATH.format("GenAIGeminiAPIHook"))
     def test__wait_until_complete_exception_raises_airflow_exception(self, 
mock_hook):
         op = GenAIGeminiCreateEmbeddingsBatchJobOperator(

Reply via email to