-
Notifications
You must be signed in to change notification settings - Fork 17.4k
Add Deferrable switch to SnowflakeSqlApiOperator #31596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
34a1be2
52f73e0
0063f65
a6add08
9ffd1ef
96baa30
fc40089
566ffe8
cb6a7c1
1149913
d1f7cf2
4b5037e
e8fc37a
64c72e6
f3b13fc
c5626f3
b39b57b
f201020
bd50387
cc3787f
23033b1
d70a22d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| import aiohttp | ||
| import requests | ||
| from cryptography.hazmat.backends import default_backend | ||
| from cryptography.hazmat.primitives import serialization | ||
|
|
@@ -60,6 +61,7 @@ class SnowflakeSqlApiHook(SnowflakeHook): | |
| the time you connect to Snowflake | ||
| :param token_life_time: lifetime of the JWT Token in timedelta | ||
| :param token_renewal_delta: Renewal time of the JWT Token in timedelta | ||
| :param deferrable: Run operator in the deferrable mode. | ||
| """ | ||
|
|
||
| LIFETIME = timedelta(minutes=59) # The tokens will have a 59 minute lifetime | ||
|
|
@@ -225,17 +227,7 @@ def check_query_output(self, query_ids: list[str]) -> None: | |
| f"Response: {e.response.content}, Status Code: {e.response.status_code}" | ||
| ) | ||
|
|
||
| def get_sql_api_query_status(self, query_id: str) -> dict[str, str | list[str]]: | ||
| """ | ||
| Based on the query id async HTTP request is made to snowflake SQL API and return response. | ||
|
|
||
| :param query_id: statement handle id for the individual statements. | ||
| """ | ||
| self.log.info("Retrieving status for query id %s", {query_id}) | ||
| header, params, url = self.get_request_url_header_params(query_id) | ||
| response = requests.get(url, params=params, headers=header) | ||
| status_code = response.status_code | ||
| resp = response.json() | ||
| def _process_response(self, status_code, resp): | ||
| self.log.info("Snowflake SQL GET statements status API response: %s", resp) | ||
| if status_code == 202: | ||
| return {"status": "running", "message": "Query statements are still running"} | ||
|
|
@@ -254,3 +246,30 @@ def get_sql_api_query_status(self, query_id: str) -> dict[str, str | list[str]]: | |
| } | ||
| else: | ||
| return {"status": "error", "message": resp["message"]} | ||
|
|
||
| def get_sql_api_query_status(self, query_id: str) -> dict[str, str | list[str]]: | ||
| """ | ||
| Based on the query id async HTTP request is made to snowflake SQL API and return response. | ||
|
|
||
| :param query_id: statement handle id for the individual statements. | ||
| """ | ||
| self.log.info("Retrieving status for query id %s", query_id) | ||
| header, params, url = self.get_request_url_header_params(query_id) | ||
| response = requests.get(url, params=params, headers=header) | ||
| status_code = response.status_code | ||
| resp = response.json() | ||
| return self._process_response(status_code, resp) | ||
|
|
||
| async def get_sql_api_query_status_async(self, query_id: str) -> dict[str, str | list[str]]: | ||
| """ | ||
| Based on the query id async HTTP request is made to snowflake SQL API and return response. | ||
|
|
||
| :param query_id: statement handle id for the individual statements. | ||
| """ | ||
| self.log.info("Retrieving status for query id %s", {query_id}) | ||
|
utkarsharma2 marked this conversation as resolved.
Outdated
|
||
| header, params, url = self.get_request_url_header_params(query_id) | ||
| async with aiohttp.ClientSession(headers=header) as session: | ||
| async with session.get(url, params=params) as response: | ||
| status_code = response.status | ||
| resp = await response.json() | ||
| return self._process_response(status_code, resp) | ||
|
Comment on lines
+274
to
+275
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should make the response body be fetched lazily, what could take a while (and might actually be the part worth deferring—not sure)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @uranusjr I'm not sure I understand you completely, but based on what I got. The function |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # 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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| # 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 | ||
| from datetime import timedelta | ||
| from typing import Any, AsyncIterator | ||
|
|
||
| from airflow.providers.snowflake.hooks.snowflake_sql_api import ( | ||
| SnowflakeSqlApiHook, | ||
| ) | ||
|
utkarsharma2 marked this conversation as resolved.
Outdated
|
||
| from airflow.triggers.base import BaseTrigger, TriggerEvent | ||
|
|
||
|
|
||
| class SnowflakeSqlApiTrigger(BaseTrigger): | ||
| """ | ||
| SnowflakeSqlApi Trigger inherits from the BaseTrigger,it is fired as | ||
| deferred class with params to run the task in trigger worker and | ||
| fetch the status for the query ids passed. | ||
|
utkarsharma2 marked this conversation as resolved.
Outdated
|
||
|
|
||
| :param task_id: Reference to task id of the Dag | ||
| :param poll_interval: polling period in seconds to check for the status | ||
| :param query_ids: List of Query ids to run and poll for the status | ||
| :param snowflake_conn_id: Reference to Snowflake connection id | ||
|
utkarsharma2 marked this conversation as resolved.
Outdated
|
||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| poll_interval: float, | ||
| query_ids: list[str], | ||
| snowflake_conn_id: str, | ||
| token_life_time: timedelta, | ||
| token_renewal_delta: timedelta, | ||
| ): | ||
| super().__init__() | ||
| self.poll_interval = poll_interval | ||
| self.query_ids = query_ids | ||
| self.snowflake_conn_id = snowflake_conn_id | ||
| self.token_life_time = token_life_time | ||
| self.token_renewal_delta = token_renewal_delta | ||
|
|
||
| def serialize(self) -> tuple[str, dict[str, Any]]: | ||
| """Serializes SnowflakeSqlApiTrigger arguments and classpath.""" | ||
| return ( | ||
| "airflow.providers.snowflake.triggers.snowflake_trigger.SnowflakeSqlApiTrigger", | ||
| { | ||
| "poll_interval": self.poll_interval, | ||
| "query_ids": self.query_ids, | ||
| "snowflake_conn_id": self.snowflake_conn_id, | ||
| "token_life_time": self.token_life_time, | ||
| "token_renewal_delta": self.token_renewal_delta, | ||
| }, | ||
| ) | ||
|
|
||
| async def run(self) -> AsyncIterator[TriggerEvent]: | ||
| """ | ||
| Makes a GET API request to snowflake with query_id to get the status of the query | ||
| by get_sql_api_query_status async function. | ||
| """ | ||
|
utkarsharma2 marked this conversation as resolved.
Outdated
|
||
| hook = SnowflakeSqlApiHook( | ||
| self.snowflake_conn_id, | ||
| self.token_life_time, | ||
| self.token_renewal_delta, | ||
| ) | ||
| try: | ||
| statement_query_ids: list[str] = [] | ||
| for query_id in self.query_ids: | ||
| while await self.is_still_running(query_id): | ||
| await asyncio.sleep(self.poll_interval) | ||
| statement_status = await hook.get_sql_api_query_status_async(query_id) | ||
|
utkarsharma2 marked this conversation as resolved.
Outdated
|
||
| if statement_status["status"] == "error": | ||
| yield TriggerEvent(statement_status) | ||
| if statement_status["status"] == "success": | ||
| statement_query_ids.extend(statement_status["statement_handles"]) | ||
| yield TriggerEvent( | ||
| { | ||
| "status": "success", | ||
| "statement_query_ids": statement_query_ids, | ||
| } | ||
| ) | ||
| except Exception as e: | ||
| yield TriggerEvent({"status": "error", "message": str(e)}) | ||
|
|
||
| async def is_still_running(self, query_id: str) -> bool: | ||
| """ | ||
| Async function to check whether the query statement submitted via SQL API is still | ||
| running state and returns True if it is still running else | ||
| return False. | ||
| """ | ||
| hook = SnowflakeSqlApiHook( | ||
| self.snowflake_conn_id, | ||
| self.token_life_time, | ||
| self.token_renewal_delta, | ||
| ) | ||
| statement_status = await hook.get_sql_api_query_status_async(query_id) | ||
| if statement_status["status"] in ["running"]: | ||
| return True | ||
| return False | ||
|
|
||
| def _set_context(self, context): | ||
| pass | ||
Uh oh!
There was an error while loading. Please reload this page.