Source code for airflow.providers.snowflake.triggers.snowpark_containers

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations

import asyncio
import time
from collections.abc import AsyncIterator
from typing import Any

from airflow.providers.common.sql.hooks.handlers import fetch_one_handler
from airflow.providers.snowflake.hooks.snowflake import SnowflakeHook
from airflow.providers.snowflake.utils.snowpark_containers import (
    NON_TERMINAL_STATUSES,
    TERMINAL_STATUSES,
)
from airflow.triggers.base import BaseTrigger, TriggerEvent


[docs] class SnowparkContainerJobTrigger(BaseTrigger): """ Poll a Snowpark Container Services job until it reaches a terminal status. :param job_name: name of the submitted job service to poll. :param snowflake_conn_id: reference to the Snowflake connection id. :param poll_interval: seconds to sleep between ``DESCRIBE SERVICE`` polls. :param end_time: epoch deadline (``time.time()`` seconds) after which a ``timeout`` event is emitted. :param execution_deadline: (Optional) absolute timestamp (in seconds since the epoch) after which the task is considered timed out. (default: None) :param database: (Optional) name of database. (default: None) :param schema: (Optional) name of schema. (default: None) :param role: (Optional) name of role. (default: None) :param warehouse: (Optional) name of warehouse. (default: None) """ def __init__( self, job_name: str, snowflake_conn_id: str, poll_interval: float, end_time: float, execution_deadline: float | None = None, database: str | None = None, schema: str | None = None, role: str | None = None, warehouse: str | None = None, ) -> None: super().__init__()
[docs] self.job_name = job_name
[docs] self.snowflake_conn_id = snowflake_conn_id
[docs] self.poll_interval = poll_interval
[docs] self.end_time = end_time
[docs] self.execution_deadline = execution_deadline
[docs] self.database = database
[docs] self.schema = schema
[docs] self.role = role
[docs] self.warehouse = warehouse
[docs] def serialize(self) -> tuple[str, dict[str, Any]]: """Serialize SnowparkContainerJobTrigger arguments and class path.""" return ( "airflow.providers.snowflake.triggers.snowpark_containers.SnowparkContainerJobTrigger", { "job_name": self.job_name, "snowflake_conn_id": self.snowflake_conn_id, "poll_interval": self.poll_interval, "end_time": self.end_time, "execution_deadline": self.execution_deadline, "database": self.database, "schema": self.schema, "role": self.role, "warehouse": self.warehouse, }, )
def _get_hook(self) -> SnowflakeHook: """Build a ``SnowflakeHook`` from the trigger's connection settings.""" return SnowflakeHook( snowflake_conn_id=self.snowflake_conn_id, warehouse=self.warehouse, database=self.database, schema=self.schema, role=self.role, ) async def _describe_status(self, hook: SnowflakeHook) -> str | None: """Return the job's current status via ``DESCRIBE SERVICE``, or ``None`` if absent.""" # SnowflakeHook is synchronous. Run the blocking poll off the event loop so a # single query does not stall every other trigger on this triggerer. response: Any = await asyncio.to_thread( hook.run, f"DESCRIBE SERVICE {self.job_name}", handler=fetch_one_handler, return_dictionaries=True, ) return response.get("status") if response else None
[docs] async def run(self) -> AsyncIterator[TriggerEvent]: """Poll the job status and yield exactly one terminal event.""" hook = self._get_hook() while True: now = time.time() if self.execution_deadline is not None and now >= self.execution_deadline: yield TriggerEvent( { "status": "timeout", "job_name": self.job_name, "message": f"Job {self.job_name} reached the execution timeout.", } ) return if now >= self.end_time: yield TriggerEvent( { "status": "timeout", "job_name": self.job_name, "message": f"Job {self.job_name} did not reach a terminal status before the timeout.", } ) return try: status = await self._describe_status(hook=hook) except Exception as e: yield TriggerEvent({"status": "error", "job_name": self.job_name, "message": str(e)}) return if status in TERMINAL_STATUSES: yield TriggerEvent({"status": status, "job_name": self.job_name}) return if status not in NON_TERMINAL_STATUSES: yield TriggerEvent( { "status": "error", "job_name": self.job_name, "message": f"Job {self.job_name} returned unexpected status: {status}", } ) return await asyncio.sleep(self.poll_interval)
[docs] async def on_kill(self) -> None: """Drop the job service when a deferred task is killed.""" hook = self._get_hook() try: await asyncio.to_thread(hook.run, f"DROP SERVICE IF EXISTS {self.job_name}") self.log.info("on_kill: dropped service %s", self.job_name) except Exception as e: self.log.error("on_kill: failed to drop service %s: %s", self.job_name, e)

Was this entry helpful?