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


##########
providers/snowflake/src/airflow/providers/snowflake/operators/snowpark_containers.py:
##########
@@ -203,12 +191,16 @@ def _submit_job(self) -> str:
 
     def _poll_for_status(self) -> str:
         """Poll until the job reaches a terminal state."""
+        end_time = time.time() + self.timeout
         while True:
+            if time.time() >= end_time:
+                self._drop_service()

Review Comment:
   On timeout the service is dropped before any container logs are fetched, and 
a job that hung is exactly the case where those logs are wanted — dropping the 
service destroys them. Consider `self._log_container_output(status)` before the 
drop here and in the `"timeout"` branch of `execute_complete`. Separately: this 
path drops unconditionally, ignoring `drop_on_completion=False`. It's 
documented on the `timeout` param so it may well be deliberate, but it's worth 
a second look, since a *failed* job is left in place while a *timed-out* one is 
not.



##########
providers/snowflake/src/airflow/providers/snowflake/hooks/snowflake.py:
##########
@@ -64,6 +65,39 @@
     from airflow.providers.openlineage.sqlparser import DatabaseInfo
 
 
+class SnowparkContainerJobStatus(str, Enum):

Review Comment:
   These Snowpark Container Services statuses now live in the SQL hook module, 
which is otherwise `SnowflakeHook` plus its OpenLineage surface. I take it the 
move is to break the operator↔trigger import cycle — 
`triggers/snowpark_containers.py` would do that equally well, since the 
operator already imports the trigger and the trigger imports nothing from the 
operator. Non-blocking, just a placement thought.



##########
providers/snowflake/src/airflow/providers/snowflake/operators/snowpark_containers.py:
##########
@@ -242,10 +246,47 @@ def execute(self, context: Context) -> str:
             raise RuntimeError("Job name was not returned")
         if not self.wait_for_completion:
             return self.job_name
+        if self.deferrable:
+            # timeout and execution_timeout give the trigger two separate 
deadlines. timeout caps
+            # how long the job is polled, and execution_timeout, when set, 
enforces the task-level
+            # limit. The trigger times out on whichever is reached first.
+            now = time.time()
+            poll_buffer = timedelta(seconds=self.poll_interval + 60)
+            execution_deadline = None
+            defer_timeout = timedelta(seconds=self.timeout) + poll_buffer
+            if self.execution_timeout is not None:
+                # Hand the execution deadline to the trigger so it emits a 
timeout event that drops the
+                # service. The framework's defer timeout would otherwise kill 
the task with no cleanup.
+                execution_deadline = now + 
self.execution_timeout.total_seconds()

Review Comment:
   `now` is `time.time()` at the moment of deferral, so everything before this 
point — connection setup, `_submit_job`, the `DESCRIBE` round trip — falls 
outside the window. `execution_timeout` is a task-level limit measured from 
task start, so a task with `execution_timeout=120` that spent 30s submitting 
gets 150s of wall clock. The comment two lines up says this "enforces the 
task-level limit", which is what makes it worth fixing rather than documenting. 
Deriving the deadline from the task's start instead would be exact:
   
   ```suggestion
                   execution_deadline = (
                       context["ti"].start_date.timestamp() + 
self.execution_timeout.total_seconds()
                   )
   ```



##########
providers/snowflake/src/airflow/providers/snowflake/operators/snowpark_containers.py:
##########
@@ -19,51 +19,25 @@
 
 import time
 from collections.abc import Sequence
-from enum import Enum
+from datetime import timedelta
 from functools import cached_property
 from typing import TYPE_CHECKING, Any
 
+from airflow.providers.common.compat.sdk import conf
 from airflow.providers.common.compat.standard.operators import BaseOperator
 from airflow.providers.common.sql.hooks.handlers import fetch_one_handler
-from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
+from airflow.providers.snowflake.hooks.snowflake import (
+    CONTAINER_JOB_NON_TERMINAL_STATUSES,

Review Comment:
   `TERMINAL_STATUSES` and `NON_TERMINAL_STATUSES` were public module-level 
names in this module and shipped in 6.15.0 (`Add SnowparkContainerJobOperator 
(#68259)`), still present in 6.16.1. Renaming them to `CONTAINER_JOB_*` and 
moving them to the hook turns `from 
airflow.providers.snowflake.operators.snowpark_containers import 
TERMINAL_STATUSES` into an `ImportError` on what is otherwise a feature 
release. `SnowparkContainerJobStatus` is unaffected — it's re-imported here — 
so it's just these two. Two aliases after the import block would keep the old 
spelling working.



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