Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,6 +298,9 @@ class DataflowTemplatedJobStartOperator(GoogleCloudBaseOperator):
https://cloud.google.com/dataflow/docs/templates/executing-templates

:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
"""

template_fields: Sequence[str] = (
Expand DownExpand Up@@ -334,11 +337,13 @@ def __init__(
append_job_name: bool = True,
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
expected_terminal_state: str | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)

self.template = template
self.cancel_on_kill = cancel_on_kill
self.job_name = job_name
self.options = options or {}
self.dataflow_default_options = dataflow_default_options or {}
Expand DownExpand Up@@ -437,6 +442,7 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -453,7 +459,10 @@ def execute_complete(self, context: Context, event: dict[str, Any]) -> str:
return job_id

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.log.info("Cancelling job %s", self.job_name)
self.hook.cancel_job(
Expand DownExpand Up@@ -525,6 +534,9 @@ class DataflowStartFlexTemplateOperator(GoogleCloudBaseOperator):
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: The expected final status of the operator on which the corresponding
Airflow task succeeds. When not specified, it will be determined by the hook.
:param append_job_name: True if unique suffix has to be appended to job name.
Expand All@@ -550,11 +562,13 @@ def __init__(
append_job_name: bool = True,
expected_terminal_state: str | None = None,
poll_sleep: int = 10,
cancel_on_kill: bool = True,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.body = body
self.cancel_on_kill = cancel_on_kill
self.location = location
self.project_id = project_id
self.gcp_conn_id = gcp_conn_id
Expand DownExpand Up@@ -633,6 +647,8 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
drain_pipeline=self.drain_pipeline,
Comment thread
steveahnahn marked this conversation as resolved.
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -658,7 +674,10 @@ def execute_complete(self, context: Context, event: dict) -> dict[str, str]:
return job

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.hook.cancel_job(
job_id=self.job.get("id"),
Expand DownExpand Up@@ -689,10 +708,13 @@ class DataflowStartYamlJobOperator(GoogleCloudBaseOperator):
:param append_job_name: Optional. Set to True if a unique suffix has to be appended to the `job_name`.
Defaults to True.
:param drain_pipeline: Optional. Set to True if you want to stop a streaming pipeline job by draining it
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs
or in the deferrable mode. Defaults to False.
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs.
Defaults to False.
For more info see: https://cloud.google.com/dataflow/docs/guides/stopping-a-pipeline
:param deferrable: Optional. Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: Optional. The expected terminal state of the Dataflow job at which the
operator task is set to succeed. Defaults to 'JOB_STATE_DONE' for the batch jobs and 'JOB_STATE_RUNNING'
for the streaming jobs.
Expand DownExpand Up@@ -748,10 +770,12 @@ def __init__(
jinja_variables: dict[str, str] | None = None,
options: dict[str, Any] | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.job_name = job_name
self.cancel_on_kill = cancel_on_kill
self.yaml_pipeline_file = yaml_pipeline_file
self.region = region
self.project_id = project_id
Expand DownExpand Up@@ -793,6 +817,8 @@ def execute(self, context: Context) -> dict[str, Any]:
cancel_timeout=self.cancel_timeout,
expected_terminal_state=self.expected_terminal_state,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_on_kill=self.cancel_on_kill,
),
Comment thread
steveahnahn marked this conversation as resolved.
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -818,10 +844,13 @@ def on_kill(self):
"""
Cancel the dataflow job if a task instance gets killed.

This method will not be called if a task instance is killed in a deferred
state.
This method is not called for a task instance killed in a deferred state;
in that case the trigger cancels the job instead, honoring cancel_on_kill
and drain_pipeline.
"""
self.log.info("On kill called.")
if not self.cancel_on_kill:
return
if self.job_id:
self.hook.cancel_job(
job_id=self.job_id,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any

from asgiref.sync import sync_to_async
from google.api_core.exceptions import ServiceUnavailable
from google.cloud.dataflow_v1beta3 import JobState
from google.cloud.dataflow_v1beta3.types import (
Expand All@@ -33,7 +34,11 @@
MetricUpdate,
)

from airflow.providers.google.cloud.hooks.dataflow import AsyncDataflowHook, DataflowJobStatus
from airflow.providers.google.cloud.hooks.dataflow import (
AsyncDataflowHook,
DataflowHook,
DataflowJobStatus,
)
from airflow.triggers.base import BaseTrigger, TriggerEvent

if TYPE_CHECKING:
Expand DownExpand Up@@ -62,6 +67,10 @@ class TemplateJobStartTrigger(BaseTrigger):
account from the list granting this role to the originating account (templated).
:param cancel_timeout: Optional. How long (in seconds) operator should wait for the pipeline to be
successfully cancelled when task is being killed.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark-failed, clear or mark-succeeded).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling, matching the operator's behaviour before deferral.
"""

def __init__(
Expand All@@ -73,6 +82,8 @@ def __init__(
poll_sleep: int = 10,
impersonation_chain: str | Sequence[str] | None = None,
cancel_timeout: int | None = 5 * 60,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -82,6 +93,8 @@ def __init__(
self.poll_sleep = poll_sleep
self.impersonation_chain = impersonation_chain
self.cancel_timeout = cancel_timeout
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -95,9 +108,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"poll_sleep": self.poll_sleep,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job status or yield certain Events.
Expand DownExpand Up@@ -300,6 +352,10 @@ class DataflowStartYamlJobTrigger(BaseTrigger):
If set as a sequence, the identities from the list must grant
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark failed, clear, or mark success).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling when the task is killed.
"""

def __init__(
Expand All@@ -312,6 +368,8 @@ def __init__(
cancel_timeout: int | None = 5 * 60,
expected_terminal_state: str | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -322,6 +380,8 @@ def __init__(
self.cancel_timeout = cancel_timeout
self.expected_terminal_state = expected_terminal_state
self.impersonation_chain = impersonation_chain
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -336,9 +396,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"expected_terminal_state": self.expected_terminal_state,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job and yield events depending on the job's type and state.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Cancel the Dataflow job when a user kills the deferred task by steveahnahn · Pull Request #69586 · apache/airflow · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,6 +298,9 @@ class DataflowTemplatedJobStartOperator(GoogleCloudBaseOperator):
https://cloud.google.com/dataflow/docs/templates/executing-templates

:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
"""

template_fields: Sequence[str] = (
Expand DownExpand Up@@ -334,11 +337,13 @@ def __init__(
append_job_name: bool = True,
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
expected_terminal_state: str | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)

self.template = template
self.cancel_on_kill = cancel_on_kill
self.job_name = job_name
self.options = options or {}
self.dataflow_default_options = dataflow_default_options or {}
Expand DownExpand Up@@ -437,6 +442,7 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -453,7 +459,10 @@ def execute_complete(self, context: Context, event: dict[str, Any]) -> str:
return job_id

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.log.info("Cancelling job %s", self.job_name)
self.hook.cancel_job(
Expand DownExpand Up@@ -525,6 +534,9 @@ class DataflowStartFlexTemplateOperator(GoogleCloudBaseOperator):
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: The expected final status of the operator on which the corresponding
Airflow task succeeds. When not specified, it will be determined by the hook.
:param append_job_name: True if unique suffix has to be appended to job name.
Expand All@@ -550,11 +562,13 @@ def __init__(
append_job_name: bool = True,
expected_terminal_state: str | None = None,
poll_sleep: int = 10,
cancel_on_kill: bool = True,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.body = body
self.cancel_on_kill = cancel_on_kill
self.location = location
self.project_id = project_id
self.gcp_conn_id = gcp_conn_id
Expand DownExpand Up@@ -633,6 +647,8 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
drain_pipeline=self.drain_pipeline,
Comment thread
steveahnahn marked this conversation as resolved.
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -658,7 +674,10 @@ def execute_complete(self, context: Context, event: dict) -> dict[str, str]:
return job

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.hook.cancel_job(
job_id=self.job.get("id"),
Expand DownExpand Up@@ -689,10 +708,13 @@ class DataflowStartYamlJobOperator(GoogleCloudBaseOperator):
:param append_job_name: Optional. Set to True if a unique suffix has to be appended to the `job_name`.
Defaults to True.
:param drain_pipeline: Optional. Set to True if you want to stop a streaming pipeline job by draining it
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs
or in the deferrable mode. Defaults to False.
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs.
Defaults to False.
For more info see: https://cloud.google.com/dataflow/docs/guides/stopping-a-pipeline
:param deferrable: Optional. Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: Optional. The expected terminal state of the Dataflow job at which the
operator task is set to succeed. Defaults to 'JOB_STATE_DONE' for the batch jobs and 'JOB_STATE_RUNNING'
for the streaming jobs.
Expand DownExpand Up@@ -748,10 +770,12 @@ def __init__(
jinja_variables: dict[str, str] | None = None,
options: dict[str, Any] | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.job_name = job_name
self.cancel_on_kill = cancel_on_kill
self.yaml_pipeline_file = yaml_pipeline_file
self.region = region
self.project_id = project_id
Expand DownExpand Up@@ -793,6 +817,8 @@ def execute(self, context: Context) -> dict[str, Any]:
cancel_timeout=self.cancel_timeout,
expected_terminal_state=self.expected_terminal_state,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_on_kill=self.cancel_on_kill,
),
Comment thread
steveahnahn marked this conversation as resolved.
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -818,10 +844,13 @@ def on_kill(self):
"""
Cancel the dataflow job if a task instance gets killed.

This method will not be called if a task instance is killed in a deferred
state.
This method is not called for a task instance killed in a deferred state;
in that case the trigger cancels the job instead, honoring cancel_on_kill
and drain_pipeline.
"""
self.log.info("On kill called.")
if not self.cancel_on_kill:
return
if self.job_id:
self.hook.cancel_job(
job_id=self.job_id,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any

from asgiref.sync import sync_to_async
from google.api_core.exceptions import ServiceUnavailable
from google.cloud.dataflow_v1beta3 import JobState
from google.cloud.dataflow_v1beta3.types import (
Expand All@@ -33,7 +34,11 @@
MetricUpdate,
)

from airflow.providers.google.cloud.hooks.dataflow import AsyncDataflowHook, DataflowJobStatus
from airflow.providers.google.cloud.hooks.dataflow import (
AsyncDataflowHook,
DataflowHook,
DataflowJobStatus,
)
from airflow.triggers.base import BaseTrigger, TriggerEvent

if TYPE_CHECKING:
Expand DownExpand Up@@ -62,6 +67,10 @@ class TemplateJobStartTrigger(BaseTrigger):
account from the list granting this role to the originating account (templated).
:param cancel_timeout: Optional. How long (in seconds) operator should wait for the pipeline to be
successfully cancelled when task is being killed.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark-failed, clear or mark-succeeded).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling, matching the operator's behaviour before deferral.
"""

def __init__(
Expand All@@ -73,6 +82,8 @@ def __init__(
poll_sleep: int = 10,
impersonation_chain: str | Sequence[str] | None = None,
cancel_timeout: int | None = 5 * 60,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -82,6 +93,8 @@ def __init__(
self.poll_sleep = poll_sleep
self.impersonation_chain = impersonation_chain
self.cancel_timeout = cancel_timeout
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -95,9 +108,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"poll_sleep": self.poll_sleep,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job status or yield certain Events.
Expand DownExpand Up@@ -300,6 +352,10 @@ class DataflowStartYamlJobTrigger(BaseTrigger):
If set as a sequence, the identities from the list must grant
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark failed, clear, or mark success).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling when the task is killed.
"""

def __init__(
Expand All@@ -312,6 +368,8 @@ def __init__(
cancel_timeout: int | None = 5 * 60,
expected_terminal_state: str | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -322,6 +380,8 @@ def __init__(
self.cancel_timeout = cancel_timeout
self.expected_terminal_state = expected_terminal_state
self.impersonation_chain = impersonation_chain
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -336,9 +396,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"expected_terminal_state": self.expected_terminal_state,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job and yield events depending on the job's type and state.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Cancel the Dataflow job when a user kills the deferred task by steveahnahn · Pull Request #69586 · apache/airflow · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,6 +298,9 @@ class DataflowTemplatedJobStartOperator(GoogleCloudBaseOperator):
https://cloud.google.com/dataflow/docs/templates/executing-templates

:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
"""

template_fields: Sequence[str] = (
Expand DownExpand Up@@ -334,11 +337,13 @@ def __init__(
append_job_name: bool = True,
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
expected_terminal_state: str | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)

self.template = template
self.cancel_on_kill = cancel_on_kill
self.job_name = job_name
self.options = options or {}
self.dataflow_default_options = dataflow_default_options or {}
Expand DownExpand Up@@ -437,6 +442,7 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -453,7 +459,10 @@ def execute_complete(self, context: Context, event: dict[str, Any]) -> str:
return job_id

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.log.info("Cancelling job %s", self.job_name)
self.hook.cancel_job(
Expand DownExpand Up@@ -525,6 +534,9 @@ class DataflowStartFlexTemplateOperator(GoogleCloudBaseOperator):
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: The expected final status of the operator on which the corresponding
Airflow task succeeds. When not specified, it will be determined by the hook.
:param append_job_name: True if unique suffix has to be appended to job name.
Expand All@@ -550,11 +562,13 @@ def __init__(
append_job_name: bool = True,
expected_terminal_state: str | None = None,
poll_sleep: int = 10,
cancel_on_kill: bool = True,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.body = body
self.cancel_on_kill = cancel_on_kill
self.location = location
self.project_id = project_id
self.gcp_conn_id = gcp_conn_id
Expand DownExpand Up@@ -633,6 +647,8 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
drain_pipeline=self.drain_pipeline,
Comment thread
steveahnahn marked this conversation as resolved.
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -658,7 +674,10 @@ def execute_complete(self, context: Context, event: dict) -> dict[str, str]:
return job

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.hook.cancel_job(
job_id=self.job.get("id"),
Expand DownExpand Up@@ -689,10 +708,13 @@ class DataflowStartYamlJobOperator(GoogleCloudBaseOperator):
:param append_job_name: Optional. Set to True if a unique suffix has to be appended to the `job_name`.
Defaults to True.
:param drain_pipeline: Optional. Set to True if you want to stop a streaming pipeline job by draining it
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs
or in the deferrable mode. Defaults to False.
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs.
Defaults to False.
For more info see: https://cloud.google.com/dataflow/docs/guides/stopping-a-pipeline
:param deferrable: Optional. Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: Optional. The expected terminal state of the Dataflow job at which the
operator task is set to succeed. Defaults to 'JOB_STATE_DONE' for the batch jobs and 'JOB_STATE_RUNNING'
for the streaming jobs.
Expand DownExpand Up@@ -748,10 +770,12 @@ def __init__(
jinja_variables: dict[str, str] | None = None,
options: dict[str, Any] | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.job_name = job_name
self.cancel_on_kill = cancel_on_kill
self.yaml_pipeline_file = yaml_pipeline_file
self.region = region
self.project_id = project_id
Expand DownExpand Up@@ -793,6 +817,8 @@ def execute(self, context: Context) -> dict[str, Any]:
cancel_timeout=self.cancel_timeout,
expected_terminal_state=self.expected_terminal_state,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_on_kill=self.cancel_on_kill,
),
Comment thread
steveahnahn marked this conversation as resolved.
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -818,10 +844,13 @@ def on_kill(self):
"""
Cancel the dataflow job if a task instance gets killed.

This method will not be called if a task instance is killed in a deferred
state.
This method is not called for a task instance killed in a deferred state;
in that case the trigger cancels the job instead, honoring cancel_on_kill
and drain_pipeline.
"""
self.log.info("On kill called.")
if not self.cancel_on_kill:
return
if self.job_id:
self.hook.cancel_job(
job_id=self.job_id,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any

from asgiref.sync import sync_to_async
from google.api_core.exceptions import ServiceUnavailable
from google.cloud.dataflow_v1beta3 import JobState
from google.cloud.dataflow_v1beta3.types import (
Expand All@@ -33,7 +34,11 @@
MetricUpdate,
)

from airflow.providers.google.cloud.hooks.dataflow import AsyncDataflowHook, DataflowJobStatus
from airflow.providers.google.cloud.hooks.dataflow import (
AsyncDataflowHook,
DataflowHook,
DataflowJobStatus,
)
from airflow.triggers.base import BaseTrigger, TriggerEvent

if TYPE_CHECKING:
Expand DownExpand Up@@ -62,6 +67,10 @@ class TemplateJobStartTrigger(BaseTrigger):
account from the list granting this role to the originating account (templated).
:param cancel_timeout: Optional. How long (in seconds) operator should wait for the pipeline to be
successfully cancelled when task is being killed.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark-failed, clear or mark-succeeded).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling, matching the operator's behaviour before deferral.
"""

def __init__(
Expand All@@ -73,6 +82,8 @@ def __init__(
poll_sleep: int = 10,
impersonation_chain: str | Sequence[str] | None = None,
cancel_timeout: int | None = 5 * 60,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -82,6 +93,8 @@ def __init__(
self.poll_sleep = poll_sleep
self.impersonation_chain = impersonation_chain
self.cancel_timeout = cancel_timeout
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -95,9 +108,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"poll_sleep": self.poll_sleep,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job status or yield certain Events.
Expand DownExpand Up@@ -300,6 +352,10 @@ class DataflowStartYamlJobTrigger(BaseTrigger):
If set as a sequence, the identities from the list must grant
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark failed, clear, or mark success).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling when the task is killed.
"""

def __init__(
Expand All@@ -312,6 +368,8 @@ def __init__(
cancel_timeout: int | None = 5 * 60,
expected_terminal_state: str | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -322,6 +380,8 @@ def __init__(
self.cancel_timeout = cancel_timeout
self.expected_terminal_state = expected_terminal_state
self.impersonation_chain = impersonation_chain
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -336,9 +396,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"expected_terminal_state": self.expected_terminal_state,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job and yield events depending on the job's type and state.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Cancel the Dataflow job when a user kills the deferred task by steveahnahn · Pull Request #69586 · apache/airflow · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,6 +298,9 @@ class DataflowTemplatedJobStartOperator(GoogleCloudBaseOperator):
https://cloud.google.com/dataflow/docs/templates/executing-templates

:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
"""

template_fields: Sequence[str] = (
Expand DownExpand Up@@ -334,11 +337,13 @@ def __init__(
append_job_name: bool = True,
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
expected_terminal_state: str | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)

self.template = template
self.cancel_on_kill = cancel_on_kill
self.job_name = job_name
self.options = options or {}
self.dataflow_default_options = dataflow_default_options or {}
Expand DownExpand Up@@ -437,6 +442,7 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -453,7 +459,10 @@ def execute_complete(self, context: Context, event: dict[str, Any]) -> str:
return job_id

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.log.info("Cancelling job %s", self.job_name)
self.hook.cancel_job(
Expand DownExpand Up@@ -525,6 +534,9 @@ class DataflowStartFlexTemplateOperator(GoogleCloudBaseOperator):
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: The expected final status of the operator on which the corresponding
Airflow task succeeds. When not specified, it will be determined by the hook.
:param append_job_name: True if unique suffix has to be appended to job name.
Expand All@@ -550,11 +562,13 @@ def __init__(
append_job_name: bool = True,
expected_terminal_state: str | None = None,
poll_sleep: int = 10,
cancel_on_kill: bool = True,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.body = body
self.cancel_on_kill = cancel_on_kill
self.location = location
self.project_id = project_id
self.gcp_conn_id = gcp_conn_id
Expand DownExpand Up@@ -633,6 +647,8 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
drain_pipeline=self.drain_pipeline,
Comment thread
steveahnahn marked this conversation as resolved.
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -658,7 +674,10 @@ def execute_complete(self, context: Context, event: dict) -> dict[str, str]:
return job

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.hook.cancel_job(
job_id=self.job.get("id"),
Expand DownExpand Up@@ -689,10 +708,13 @@ class DataflowStartYamlJobOperator(GoogleCloudBaseOperator):
:param append_job_name: Optional. Set to True if a unique suffix has to be appended to the `job_name`.
Defaults to True.
:param drain_pipeline: Optional. Set to True if you want to stop a streaming pipeline job by draining it
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs
or in the deferrable mode. Defaults to False.
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs.
Defaults to False.
For more info see: https://cloud.google.com/dataflow/docs/guides/stopping-a-pipeline
:param deferrable: Optional. Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: Optional. The expected terminal state of the Dataflow job at which the
operator task is set to succeed. Defaults to 'JOB_STATE_DONE' for the batch jobs and 'JOB_STATE_RUNNING'
for the streaming jobs.
Expand DownExpand Up@@ -748,10 +770,12 @@ def __init__(
jinja_variables: dict[str, str] | None = None,
options: dict[str, Any] | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.job_name = job_name
self.cancel_on_kill = cancel_on_kill
self.yaml_pipeline_file = yaml_pipeline_file
self.region = region
self.project_id = project_id
Expand DownExpand Up@@ -793,6 +817,8 @@ def execute(self, context: Context) -> dict[str, Any]:
cancel_timeout=self.cancel_timeout,
expected_terminal_state=self.expected_terminal_state,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_on_kill=self.cancel_on_kill,
),
Comment thread
steveahnahn marked this conversation as resolved.
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -818,10 +844,13 @@ def on_kill(self):
"""
Cancel the dataflow job if a task instance gets killed.

This method will not be called if a task instance is killed in a deferred
state.
This method is not called for a task instance killed in a deferred state;
in that case the trigger cancels the job instead, honoring cancel_on_kill
and drain_pipeline.
"""
self.log.info("On kill called.")
if not self.cancel_on_kill:
return
if self.job_id:
self.hook.cancel_job(
job_id=self.job_id,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any

from asgiref.sync import sync_to_async
from google.api_core.exceptions import ServiceUnavailable
from google.cloud.dataflow_v1beta3 import JobState
from google.cloud.dataflow_v1beta3.types import (
Expand All@@ -33,7 +34,11 @@
MetricUpdate,
)

from airflow.providers.google.cloud.hooks.dataflow import AsyncDataflowHook, DataflowJobStatus
from airflow.providers.google.cloud.hooks.dataflow import (
AsyncDataflowHook,
DataflowHook,
DataflowJobStatus,
)
from airflow.triggers.base import BaseTrigger, TriggerEvent

if TYPE_CHECKING:
Expand DownExpand Up@@ -62,6 +67,10 @@ class TemplateJobStartTrigger(BaseTrigger):
account from the list granting this role to the originating account (templated).
:param cancel_timeout: Optional. How long (in seconds) operator should wait for the pipeline to be
successfully cancelled when task is being killed.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark-failed, clear or mark-succeeded).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling, matching the operator's behaviour before deferral.
"""

def __init__(
Expand All@@ -73,6 +82,8 @@ def __init__(
poll_sleep: int = 10,
impersonation_chain: str | Sequence[str] | None = None,
cancel_timeout: int | None = 5 * 60,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -82,6 +93,8 @@ def __init__(
self.poll_sleep = poll_sleep
self.impersonation_chain = impersonation_chain
self.cancel_timeout = cancel_timeout
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -95,9 +108,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"poll_sleep": self.poll_sleep,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job status or yield certain Events.
Expand DownExpand Up@@ -300,6 +352,10 @@ class DataflowStartYamlJobTrigger(BaseTrigger):
If set as a sequence, the identities from the list must grant
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark failed, clear, or mark success).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling when the task is killed.
"""

def __init__(
Expand All@@ -312,6 +368,8 @@ def __init__(
cancel_timeout: int | None = 5 * 60,
expected_terminal_state: str | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -322,6 +380,8 @@ def __init__(
self.cancel_timeout = cancel_timeout
self.expected_terminal_state = expected_terminal_state
self.impersonation_chain = impersonation_chain
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -336,9 +396,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"expected_terminal_state": self.expected_terminal_state,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job and yield events depending on the job's type and state.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Cancel the Dataflow job when a user kills the deferred task by steveahnahn · Pull Request #69586 · apache/airflow · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,6 +298,9 @@ class DataflowTemplatedJobStartOperator(GoogleCloudBaseOperator):
https://cloud.google.com/dataflow/docs/templates/executing-templates

:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
"""

template_fields: Sequence[str] = (
Expand DownExpand Up@@ -334,11 +337,13 @@ def __init__(
append_job_name: bool = True,
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
expected_terminal_state: str | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)

self.template = template
self.cancel_on_kill = cancel_on_kill
self.job_name = job_name
self.options = options or {}
self.dataflow_default_options = dataflow_default_options or {}
Expand DownExpand Up@@ -437,6 +442,7 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -453,7 +459,10 @@ def execute_complete(self, context: Context, event: dict[str, Any]) -> str:
return job_id

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.log.info("Cancelling job %s", self.job_name)
self.hook.cancel_job(
Expand DownExpand Up@@ -525,6 +534,9 @@ class DataflowStartFlexTemplateOperator(GoogleCloudBaseOperator):
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: The expected final status of the operator on which the corresponding
Airflow task succeeds. When not specified, it will be determined by the hook.
:param append_job_name: True if unique suffix has to be appended to job name.
Expand All@@ -550,11 +562,13 @@ def __init__(
append_job_name: bool = True,
expected_terminal_state: str | None = None,
poll_sleep: int = 10,
cancel_on_kill: bool = True,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.body = body
self.cancel_on_kill = cancel_on_kill
self.location = location
self.project_id = project_id
self.gcp_conn_id = gcp_conn_id
Expand DownExpand Up@@ -633,6 +647,8 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
drain_pipeline=self.drain_pipeline,
Comment thread
steveahnahn marked this conversation as resolved.
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -658,7 +674,10 @@ def execute_complete(self, context: Context, event: dict) -> dict[str, str]:
return job

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.hook.cancel_job(
job_id=self.job.get("id"),
Expand DownExpand Up@@ -689,10 +708,13 @@ class DataflowStartYamlJobOperator(GoogleCloudBaseOperator):
:param append_job_name: Optional. Set to True if a unique suffix has to be appended to the `job_name`.
Defaults to True.
:param drain_pipeline: Optional. Set to True if you want to stop a streaming pipeline job by draining it
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs
or in the deferrable mode. Defaults to False.
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs.
Defaults to False.
For more info see: https://cloud.google.com/dataflow/docs/guides/stopping-a-pipeline
:param deferrable: Optional. Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: Optional. The expected terminal state of the Dataflow job at which the
operator task is set to succeed. Defaults to 'JOB_STATE_DONE' for the batch jobs and 'JOB_STATE_RUNNING'
for the streaming jobs.
Expand DownExpand Up@@ -748,10 +770,12 @@ def __init__(
jinja_variables: dict[str, str] | None = None,
options: dict[str, Any] | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.job_name = job_name
self.cancel_on_kill = cancel_on_kill
self.yaml_pipeline_file = yaml_pipeline_file
self.region = region
self.project_id = project_id
Expand DownExpand Up@@ -793,6 +817,8 @@ def execute(self, context: Context) -> dict[str, Any]:
cancel_timeout=self.cancel_timeout,
expected_terminal_state=self.expected_terminal_state,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_on_kill=self.cancel_on_kill,
),
Comment thread
steveahnahn marked this conversation as resolved.
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -818,10 +844,13 @@ def on_kill(self):
"""
Cancel the dataflow job if a task instance gets killed.

This method will not be called if a task instance is killed in a deferred
state.
This method is not called for a task instance killed in a deferred state;
in that case the trigger cancels the job instead, honoring cancel_on_kill
and drain_pipeline.
"""
self.log.info("On kill called.")
if not self.cancel_on_kill:
return
if self.job_id:
self.hook.cancel_job(
job_id=self.job_id,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any

from asgiref.sync import sync_to_async
from google.api_core.exceptions import ServiceUnavailable
from google.cloud.dataflow_v1beta3 import JobState
from google.cloud.dataflow_v1beta3.types import (
Expand All@@ -33,7 +34,11 @@
MetricUpdate,
)

from airflow.providers.google.cloud.hooks.dataflow import AsyncDataflowHook, DataflowJobStatus
from airflow.providers.google.cloud.hooks.dataflow import (
AsyncDataflowHook,
DataflowHook,
DataflowJobStatus,
)
from airflow.triggers.base import BaseTrigger, TriggerEvent

if TYPE_CHECKING:
Expand DownExpand Up@@ -62,6 +67,10 @@ class TemplateJobStartTrigger(BaseTrigger):
account from the list granting this role to the originating account (templated).
:param cancel_timeout: Optional. How long (in seconds) operator should wait for the pipeline to be
successfully cancelled when task is being killed.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark-failed, clear or mark-succeeded).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling, matching the operator's behaviour before deferral.
"""

def __init__(
Expand All@@ -73,6 +82,8 @@ def __init__(
poll_sleep: int = 10,
impersonation_chain: str | Sequence[str] | None = None,
cancel_timeout: int | None = 5 * 60,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -82,6 +93,8 @@ def __init__(
self.poll_sleep = poll_sleep
self.impersonation_chain = impersonation_chain
self.cancel_timeout = cancel_timeout
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -95,9 +108,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"poll_sleep": self.poll_sleep,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job status or yield certain Events.
Expand DownExpand Up@@ -300,6 +352,10 @@ class DataflowStartYamlJobTrigger(BaseTrigger):
If set as a sequence, the identities from the list must grant
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark failed, clear, or mark success).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling when the task is killed.
"""

def __init__(
Expand All@@ -312,6 +368,8 @@ def __init__(
cancel_timeout: int | None = 5 * 60,
expected_terminal_state: str | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -322,6 +380,8 @@ def __init__(
self.cancel_timeout = cancel_timeout
self.expected_terminal_state = expected_terminal_state
self.impersonation_chain = impersonation_chain
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -336,9 +396,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"expected_terminal_state": self.expected_terminal_state,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job and yield events depending on the job's type and state.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Cancel the Dataflow job when a user kills the deferred task by steveahnahn · Pull Request #69586 · apache/airflow · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,6 +298,9 @@ class DataflowTemplatedJobStartOperator(GoogleCloudBaseOperator):
https://cloud.google.com/dataflow/docs/templates/executing-templates

:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
"""

template_fields: Sequence[str] = (
Expand DownExpand Up@@ -334,11 +337,13 @@ def __init__(
append_job_name: bool = True,
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
expected_terminal_state: str | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)

self.template = template
self.cancel_on_kill = cancel_on_kill
self.job_name = job_name
self.options = options or {}
self.dataflow_default_options = dataflow_default_options or {}
Expand DownExpand Up@@ -437,6 +442,7 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -453,7 +459,10 @@ def execute_complete(self, context: Context, event: dict[str, Any]) -> str:
return job_id

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.log.info("Cancelling job %s", self.job_name)
self.hook.cancel_job(
Expand DownExpand Up@@ -525,6 +534,9 @@ class DataflowStartFlexTemplateOperator(GoogleCloudBaseOperator):
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: The expected final status of the operator on which the corresponding
Airflow task succeeds. When not specified, it will be determined by the hook.
:param append_job_name: True if unique suffix has to be appended to job name.
Expand All@@ -550,11 +562,13 @@ def __init__(
append_job_name: bool = True,
expected_terminal_state: str | None = None,
poll_sleep: int = 10,
cancel_on_kill: bool = True,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.body = body
self.cancel_on_kill = cancel_on_kill
self.location = location
self.project_id = project_id
self.gcp_conn_id = gcp_conn_id
Expand DownExpand Up@@ -633,6 +647,8 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
drain_pipeline=self.drain_pipeline,
Comment thread
steveahnahn marked this conversation as resolved.
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -658,7 +674,10 @@ def execute_complete(self, context: Context, event: dict) -> dict[str, str]:
return job

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.hook.cancel_job(
job_id=self.job.get("id"),
Expand DownExpand Up@@ -689,10 +708,13 @@ class DataflowStartYamlJobOperator(GoogleCloudBaseOperator):
:param append_job_name: Optional. Set to True if a unique suffix has to be appended to the `job_name`.
Defaults to True.
:param drain_pipeline: Optional. Set to True if you want to stop a streaming pipeline job by draining it
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs
or in the deferrable mode. Defaults to False.
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs.
Defaults to False.
For more info see: https://cloud.google.com/dataflow/docs/guides/stopping-a-pipeline
:param deferrable: Optional. Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: Optional. The expected terminal state of the Dataflow job at which the
operator task is set to succeed. Defaults to 'JOB_STATE_DONE' for the batch jobs and 'JOB_STATE_RUNNING'
for the streaming jobs.
Expand DownExpand Up@@ -748,10 +770,12 @@ def __init__(
jinja_variables: dict[str, str] | None = None,
options: dict[str, Any] | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.job_name = job_name
self.cancel_on_kill = cancel_on_kill
self.yaml_pipeline_file = yaml_pipeline_file
self.region = region
self.project_id = project_id
Expand DownExpand Up@@ -793,6 +817,8 @@ def execute(self, context: Context) -> dict[str, Any]:
cancel_timeout=self.cancel_timeout,
expected_terminal_state=self.expected_terminal_state,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_on_kill=self.cancel_on_kill,
),
Comment thread
steveahnahn marked this conversation as resolved.
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -818,10 +844,13 @@ def on_kill(self):
"""
Cancel the dataflow job if a task instance gets killed.

This method will not be called if a task instance is killed in a deferred
state.
This method is not called for a task instance killed in a deferred state;
in that case the trigger cancels the job instead, honoring cancel_on_kill
and drain_pipeline.
"""
self.log.info("On kill called.")
if not self.cancel_on_kill:
return
if self.job_id:
self.hook.cancel_job(
job_id=self.job_id,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any

from asgiref.sync import sync_to_async
from google.api_core.exceptions import ServiceUnavailable
from google.cloud.dataflow_v1beta3 import JobState
from google.cloud.dataflow_v1beta3.types import (
Expand All@@ -33,7 +34,11 @@
MetricUpdate,
)

from airflow.providers.google.cloud.hooks.dataflow import AsyncDataflowHook, DataflowJobStatus
from airflow.providers.google.cloud.hooks.dataflow import (
AsyncDataflowHook,
DataflowHook,
DataflowJobStatus,
)
from airflow.triggers.base import BaseTrigger, TriggerEvent

if TYPE_CHECKING:
Expand DownExpand Up@@ -62,6 +67,10 @@ class TemplateJobStartTrigger(BaseTrigger):
account from the list granting this role to the originating account (templated).
:param cancel_timeout: Optional. How long (in seconds) operator should wait for the pipeline to be
successfully cancelled when task is being killed.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark-failed, clear or mark-succeeded).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling, matching the operator's behaviour before deferral.
"""

def __init__(
Expand All@@ -73,6 +82,8 @@ def __init__(
poll_sleep: int = 10,
impersonation_chain: str | Sequence[str] | None = None,
cancel_timeout: int | None = 5 * 60,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -82,6 +93,8 @@ def __init__(
self.poll_sleep = poll_sleep
self.impersonation_chain = impersonation_chain
self.cancel_timeout = cancel_timeout
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -95,9 +108,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"poll_sleep": self.poll_sleep,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job status or yield certain Events.
Expand DownExpand Up@@ -300,6 +352,10 @@ class DataflowStartYamlJobTrigger(BaseTrigger):
If set as a sequence, the identities from the list must grant
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark failed, clear, or mark success).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling when the task is killed.
"""

def __init__(
Expand All@@ -312,6 +368,8 @@ def __init__(
cancel_timeout: int | None = 5 * 60,
expected_terminal_state: str | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -322,6 +380,8 @@ def __init__(
self.cancel_timeout = cancel_timeout
self.expected_terminal_state = expected_terminal_state
self.impersonation_chain = impersonation_chain
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -336,9 +396,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"expected_terminal_state": self.expected_terminal_state,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job and yield events depending on the job's type and state.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Cancel the Dataflow job when a user kills the deferred task by steveahnahn · Pull Request #69586 · apache/airflow · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,6 +298,9 @@ class DataflowTemplatedJobStartOperator(GoogleCloudBaseOperator):
https://cloud.google.com/dataflow/docs/templates/executing-templates

:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
"""

template_fields: Sequence[str] = (
Expand DownExpand Up@@ -334,11 +337,13 @@ def __init__(
append_job_name: bool = True,
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
expected_terminal_state: str | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)

self.template = template
self.cancel_on_kill = cancel_on_kill
self.job_name = job_name
self.options = options or {}
self.dataflow_default_options = dataflow_default_options or {}
Expand DownExpand Up@@ -437,6 +442,7 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -453,7 +459,10 @@ def execute_complete(self, context: Context, event: dict[str, Any]) -> str:
return job_id

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.log.info("Cancelling job %s", self.job_name)
self.hook.cancel_job(
Expand DownExpand Up@@ -525,6 +534,9 @@ class DataflowStartFlexTemplateOperator(GoogleCloudBaseOperator):
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: The expected final status of the operator on which the corresponding
Airflow task succeeds. When not specified, it will be determined by the hook.
:param append_job_name: True if unique suffix has to be appended to job name.
Expand All@@ -550,11 +562,13 @@ def __init__(
append_job_name: bool = True,
expected_terminal_state: str | None = None,
poll_sleep: int = 10,
cancel_on_kill: bool = True,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.body = body
self.cancel_on_kill = cancel_on_kill
self.location = location
self.project_id = project_id
self.gcp_conn_id = gcp_conn_id
Expand DownExpand Up@@ -633,6 +647,8 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
drain_pipeline=self.drain_pipeline,
Comment thread
steveahnahn marked this conversation as resolved.
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -658,7 +674,10 @@ def execute_complete(self, context: Context, event: dict) -> dict[str, str]:
return job

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.hook.cancel_job(
job_id=self.job.get("id"),
Expand DownExpand Up@@ -689,10 +708,13 @@ class DataflowStartYamlJobOperator(GoogleCloudBaseOperator):
:param append_job_name: Optional. Set to True if a unique suffix has to be appended to the `job_name`.
Defaults to True.
:param drain_pipeline: Optional. Set to True if you want to stop a streaming pipeline job by draining it
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs
or in the deferrable mode. Defaults to False.
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs.
Defaults to False.
For more info see: https://cloud.google.com/dataflow/docs/guides/stopping-a-pipeline
:param deferrable: Optional. Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: Optional. The expected terminal state of the Dataflow job at which the
operator task is set to succeed. Defaults to 'JOB_STATE_DONE' for the batch jobs and 'JOB_STATE_RUNNING'
for the streaming jobs.
Expand DownExpand Up@@ -748,10 +770,12 @@ def __init__(
jinja_variables: dict[str, str] | None = None,
options: dict[str, Any] | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.job_name = job_name
self.cancel_on_kill = cancel_on_kill
self.yaml_pipeline_file = yaml_pipeline_file
self.region = region
self.project_id = project_id
Expand DownExpand Up@@ -793,6 +817,8 @@ def execute(self, context: Context) -> dict[str, Any]:
cancel_timeout=self.cancel_timeout,
expected_terminal_state=self.expected_terminal_state,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_on_kill=self.cancel_on_kill,
),
Comment thread
steveahnahn marked this conversation as resolved.
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -818,10 +844,13 @@ def on_kill(self):
"""
Cancel the dataflow job if a task instance gets killed.

This method will not be called if a task instance is killed in a deferred
state.
This method is not called for a task instance killed in a deferred state;
in that case the trigger cancels the job instead, honoring cancel_on_kill
and drain_pipeline.
"""
self.log.info("On kill called.")
if not self.cancel_on_kill:
return
if self.job_id:
self.hook.cancel_job(
job_id=self.job_id,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any

from asgiref.sync import sync_to_async
from google.api_core.exceptions import ServiceUnavailable
from google.cloud.dataflow_v1beta3 import JobState
from google.cloud.dataflow_v1beta3.types import (
Expand All@@ -33,7 +34,11 @@
MetricUpdate,
)

from airflow.providers.google.cloud.hooks.dataflow import AsyncDataflowHook, DataflowJobStatus
from airflow.providers.google.cloud.hooks.dataflow import (
AsyncDataflowHook,
DataflowHook,
DataflowJobStatus,
)
from airflow.triggers.base import BaseTrigger, TriggerEvent

if TYPE_CHECKING:
Expand DownExpand Up@@ -62,6 +67,10 @@ class TemplateJobStartTrigger(BaseTrigger):
account from the list granting this role to the originating account (templated).
:param cancel_timeout: Optional. How long (in seconds) operator should wait for the pipeline to be
successfully cancelled when task is being killed.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark-failed, clear or mark-succeeded).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling, matching the operator's behaviour before deferral.
"""

def __init__(
Expand All@@ -73,6 +82,8 @@ def __init__(
poll_sleep: int = 10,
impersonation_chain: str | Sequence[str] | None = None,
cancel_timeout: int | None = 5 * 60,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -82,6 +93,8 @@ def __init__(
self.poll_sleep = poll_sleep
self.impersonation_chain = impersonation_chain
self.cancel_timeout = cancel_timeout
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -95,9 +108,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"poll_sleep": self.poll_sleep,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job status or yield certain Events.
Expand DownExpand Up@@ -300,6 +352,10 @@ class DataflowStartYamlJobTrigger(BaseTrigger):
If set as a sequence, the identities from the list must grant
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark failed, clear, or mark success).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling when the task is killed.
"""

def __init__(
Expand All@@ -312,6 +368,8 @@ def __init__(
cancel_timeout: int | None = 5 * 60,
expected_terminal_state: str | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -322,6 +380,8 @@ def __init__(
self.cancel_timeout = cancel_timeout
self.expected_terminal_state = expected_terminal_state
self.impersonation_chain = impersonation_chain
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -336,9 +396,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"expected_terminal_state": self.expected_terminal_state,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job and yield events depending on the job's type and state.
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Cancel the Dataflow job when a user kills the deferred task by steveahnahn · Pull Request #69586 · apache/airflow · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -298,6 +298,9 @@ class DataflowTemplatedJobStartOperator(GoogleCloudBaseOperator):
https://cloud.google.com/dataflow/docs/templates/executing-templates

:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
"""

template_fields: Sequence[str] = (
Expand DownExpand Up@@ -334,11 +337,13 @@ def __init__(
append_job_name: bool = True,
deferrable: bool = conf.getboolean("operators", "default_deferrable", fallback=False),
expected_terminal_state: str | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)

self.template = template
self.cancel_on_kill = cancel_on_kill
self.job_name = job_name
self.options = options or {}
self.dataflow_default_options = dataflow_default_options or {}
Expand DownExpand Up@@ -437,6 +442,7 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -453,7 +459,10 @@ def execute_complete(self, context: Context, event: dict[str, Any]) -> str:
return job_id

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.log.info("Cancelling job %s", self.job_name)
self.hook.cancel_job(
Expand DownExpand Up@@ -525,6 +534,9 @@ class DataflowStartFlexTemplateOperator(GoogleCloudBaseOperator):
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param deferrable: Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: The expected final status of the operator on which the corresponding
Airflow task succeeds. When not specified, it will be determined by the hook.
:param append_job_name: True if unique suffix has to be appended to job name.
Expand All@@ -550,11 +562,13 @@ def __init__(
append_job_name: bool = True,
expected_terminal_state: str | None = None,
poll_sleep: int = 10,
cancel_on_kill: bool = True,
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.body = body
self.cancel_on_kill = cancel_on_kill
self.location = location
self.project_id = project_id
self.gcp_conn_id = gcp_conn_id
Expand DownExpand Up@@ -633,6 +647,8 @@ def set_current_job(current_job):
poll_sleep=self.poll_sleep,
impersonation_chain=self.impersonation_chain,
cancel_timeout=self.cancel_timeout,
drain_pipeline=self.drain_pipeline,
Comment thread
steveahnahn marked this conversation as resolved.
cancel_on_kill=self.cancel_on_kill,
),
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -658,7 +674,10 @@ def execute_complete(self, context: Context, event: dict) -> dict[str, str]:
return job

def on_kill(self) -> None:
"""Cancel the running job; a kill of a deferred task cancels through the trigger instead."""
self.log.info("On kill.")
if not self.cancel_on_kill:
return
if self.job is not None:
self.hook.cancel_job(
job_id=self.job.get("id"),
Expand DownExpand Up@@ -689,10 +708,13 @@ class DataflowStartYamlJobOperator(GoogleCloudBaseOperator):
:param append_job_name: Optional. Set to True if a unique suffix has to be appended to the `job_name`.
Defaults to True.
:param drain_pipeline: Optional. Set to True if you want to stop a streaming pipeline job by draining it
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs
or in the deferrable mode. Defaults to False.
instead of canceling when killing the task instance. Note that this does not work for batch pipeline jobs.
Defaults to False.
For more info see: https://cloud.google.com/dataflow/docs/guides/stopping-a-pipeline
:param deferrable: Optional. Run operator in the deferrable mode.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the task is killed,
both while the operator is running and, for a deferred task, while it waits in the
triggerer.
:param expected_terminal_state: Optional. The expected terminal state of the Dataflow job at which the
operator task is set to succeed. Defaults to 'JOB_STATE_DONE' for the batch jobs and 'JOB_STATE_RUNNING'
for the streaming jobs.
Expand DownExpand Up@@ -748,10 +770,12 @@ def __init__(
jinja_variables: dict[str, str] | None = None,
options: dict[str, Any] | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.job_name = job_name
self.cancel_on_kill = cancel_on_kill
self.yaml_pipeline_file = yaml_pipeline_file
self.region = region
self.project_id = project_id
Expand DownExpand Up@@ -793,6 +817,8 @@ def execute(self, context: Context) -> dict[str, Any]:
cancel_timeout=self.cancel_timeout,
expected_terminal_state=self.expected_terminal_state,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_on_kill=self.cancel_on_kill,
),
Comment thread
steveahnahn marked this conversation as resolved.
method_name=GOOGLE_DEFAULT_DEFERRABLE_METHOD_NAME,
)
Expand All@@ -818,10 +844,13 @@ def on_kill(self):
"""
Cancel the dataflow job if a task instance gets killed.

This method will not be called if a task instance is killed in a deferred
state.
This method is not called for a task instance killed in a deferred state;
in that case the trigger cancels the job instead, honoring cancel_on_kill
and drain_pipeline.
"""
self.log.info("On kill called.")
if not self.cancel_on_kill:
return
if self.job_id:
self.hook.cancel_job(
job_id=self.job_id,
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any

from asgiref.sync import sync_to_async
from google.api_core.exceptions import ServiceUnavailable
from google.cloud.dataflow_v1beta3 import JobState
from google.cloud.dataflow_v1beta3.types import (
Expand All@@ -33,7 +34,11 @@
MetricUpdate,
)

from airflow.providers.google.cloud.hooks.dataflow import AsyncDataflowHook, DataflowJobStatus
from airflow.providers.google.cloud.hooks.dataflow import (
AsyncDataflowHook,
DataflowHook,
DataflowJobStatus,
)
from airflow.triggers.base import BaseTrigger, TriggerEvent

if TYPE_CHECKING:
Expand DownExpand Up@@ -62,6 +67,10 @@ class TemplateJobStartTrigger(BaseTrigger):
account from the list granting this role to the originating account (templated).
:param cancel_timeout: Optional. How long (in seconds) operator should wait for the pipeline to be
successfully cancelled when task is being killed.
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark-failed, clear or mark-succeeded).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling, matching the operator's behaviour before deferral.
"""

def __init__(
Expand All@@ -73,6 +82,8 @@ def __init__(
poll_sleep: int = 10,
impersonation_chain: str | Sequence[str] | None = None,
cancel_timeout: int | None = 5 * 60,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -82,6 +93,8 @@ def __init__(
self.poll_sleep = poll_sleep
self.impersonation_chain = impersonation_chain
self.cancel_timeout = cancel_timeout
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -95,9 +108,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"poll_sleep": self.poll_sleep,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job status or yield certain Events.
Expand DownExpand Up@@ -300,6 +352,10 @@ class DataflowStartYamlJobTrigger(BaseTrigger):
If set as a sequence, the identities from the list must grant
Service Account Token Creator IAM role to the directly preceding identity, with first
account from the list granting this role to the originating account (templated).
:param cancel_on_kill: If True (default), cancel the Dataflow job when the user acts on the
deferred task (mark failed, clear, or mark success).
:param drain_pipeline: Optional. Set to True if you want a streaming job to be stopped by
draining it instead of cancelling when the task is killed.
"""

def __init__(
Expand All@@ -312,6 +368,8 @@ def __init__(
cancel_timeout: int | None = 5 * 60,
expected_terminal_state: str | None = None,
impersonation_chain: str | Sequence[str] | None = None,
cancel_on_kill: bool = True,
drain_pipeline: bool = False,
):
super().__init__()
self.project_id = project_id
Expand All@@ -322,6 +380,8 @@ def __init__(
self.cancel_timeout = cancel_timeout
self.expected_terminal_state = expected_terminal_state
self.impersonation_chain = impersonation_chain
self.cancel_on_kill = cancel_on_kill
self.drain_pipeline = drain_pipeline

def serialize(self) -> tuple[str, dict[str, Any]]:
"""Serialize class arguments and classpath."""
Expand All@@ -336,9 +396,48 @@ def serialize(self) -> tuple[str, dict[str, Any]]:
"expected_terminal_state": self.expected_terminal_state,
"impersonation_chain": self.impersonation_chain,
"cancel_timeout": self.cancel_timeout,
"cancel_on_kill": self.cancel_on_kill,
"drain_pipeline": self.drain_pipeline,
},
)

async def on_kill(self) -> None:
"""Stop the Dataflow job when the user acts on the deferred task."""
if not self.cancel_on_kill or not self.job_id or not self.project_id:
return
self.log.info(
"Stopping Dataflow job. Project ID: %s, Location: %s, Job ID: %s, drain: %s",
self.project_id,
self.location,
self.job_id,
self.drain_pipeline,
)
try:
# Build the synchronous hook and cancel inside the worker thread: the hook resolves the
# connection eagerly during construction, which must not run in the triggerer's event loop.
await sync_to_async(self._stop_job)()
self.log.info("Dataflow job %s stopped.", self.job_id)
except Exception:
self.log.exception(
"Failed to stop Dataflow job %s. The job may still be running.",
self.job_id,
)

def _stop_job(self) -> None:
"""Cancel or drain the Dataflow job through the synchronous hook (runs off the event loop)."""
hook = DataflowHook(
gcp_conn_id=self.gcp_conn_id,
impersonation_chain=self.impersonation_chain,
drain_pipeline=self.drain_pipeline,
cancel_timeout=self.cancel_timeout,
poll_sleep=self.poll_sleep,
)
hook.cancel_job(
job_id=self.job_id,
project_id=self.project_id,
location=self.location,
)

async def run(self):
"""
Fetch job and yield events depending on the job's type and state.
Expand Down
Loading