mirror of
https://github.com/apache/superset.git
synced 2026-04-13 13:18:25 +00:00
185 lines
6.8 KiB
Python
185 lines
6.8 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
"""Internal task update commands for GTF internal use only.
|
|
|
|
These commands perform zero-read updates using targeted SQL UPDATE statements.
|
|
They're designed for use by TaskContext and executor code where the framework
|
|
owns the authoritative state and doesn't need to read before writing.
|
|
|
|
Unlike UpdateTaskCommand, these commands:
|
|
- Do NOT fetch the task entity before updating
|
|
- Do NOT check permissions (internal use only)
|
|
- Use targeted SQL UPDATE for efficiency
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from functools import partial
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
from superset_core.tasks.types import TaskProperties, TaskStatus
|
|
|
|
from superset.commands.base import BaseCommand
|
|
from superset.commands.tasks.exceptions import TaskUpdateFailedError
|
|
from superset.daos.tasks import TaskDAO
|
|
from superset.utils.decorators import on_error, transaction
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class InternalUpdateTaskCommand(BaseCommand):
|
|
"""
|
|
Zero-read task update command for properties/payload.
|
|
|
|
This command directly writes properties and/or payload to the database
|
|
without reading the current values first. The caller (TaskContext)
|
|
maintains the authoritative cached state and passes complete merged
|
|
values to write.
|
|
|
|
This is an optimization for task execution where:
|
|
1. The executor owns the properties/payload state
|
|
2. No permission checks are needed (internal framework code)
|
|
3. Status column should not be touched (use InternalStatusTransitionCommand)
|
|
|
|
WARNING: This command should ONLY be used by TaskContext and similar
|
|
internal framework code. External callers should use UpdateTaskCommand.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
task_uuid: UUID,
|
|
properties: TaskProperties | None = None,
|
|
payload: dict[str, Any] | None = None,
|
|
):
|
|
"""
|
|
Initialize internal update command.
|
|
|
|
:param task_uuid: UUID of the task to update
|
|
:param properties: Complete properties dict to write (replaces existing)
|
|
:param payload: Complete payload dict to write (replaces existing)
|
|
"""
|
|
self._task_uuid = task_uuid
|
|
self._properties = properties
|
|
self._payload = payload
|
|
|
|
def validate(self) -> None:
|
|
"""No validation needed for internal command."""
|
|
pass
|
|
|
|
@transaction(on_error=partial(on_error, reraise=TaskUpdateFailedError))
|
|
def run(self) -> bool:
|
|
"""
|
|
Execute zero-read update.
|
|
|
|
:returns: True if task was updated, False if not found or nothing to update
|
|
"""
|
|
if self._properties is None and self._payload is None:
|
|
return False
|
|
|
|
updated = TaskDAO.set_properties_and_payload(
|
|
task_uuid=self._task_uuid,
|
|
properties=self._properties,
|
|
payload=self._payload,
|
|
)
|
|
|
|
if updated:
|
|
logger.debug(
|
|
"Internal update for task %s: properties=%s, payload=%s",
|
|
self._task_uuid,
|
|
self._properties is not None,
|
|
self._payload is not None,
|
|
)
|
|
|
|
return updated
|
|
|
|
|
|
class InternalStatusTransitionCommand(BaseCommand):
|
|
"""
|
|
Atomic conditional status transition command for executor use.
|
|
|
|
This command provides race-safe status transitions by using atomic
|
|
compare-and-swap semantics. The status is only updated if the current
|
|
status matches the expected value(s).
|
|
|
|
Use cases:
|
|
- PENDING → IN_PROGRESS: Task pickup (executor starting)
|
|
- IN_PROGRESS → SUCCESS: Normal completion (only if not ABORTING)
|
|
- IN_PROGRESS → FAILURE: Task exception (only if not ABORTING)
|
|
- ABORTING → ABORTED: Abort handlers completed successfully
|
|
- ABORTING → TIMED_OUT: Timeout handlers completed successfully
|
|
- ABORTING → FAILURE: Abort/cleanup handlers failed
|
|
|
|
The atomic nature prevents race conditions where:
|
|
- Executor tries to set SUCCESS but task was concurrently aborted
|
|
- Multiple executors try to pick up the same task
|
|
|
|
WARNING: This command should ONLY be used by executor code (decorators.py,
|
|
scheduler.py). External callers should use UpdateTaskCommand.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
task_uuid: UUID,
|
|
new_status: TaskStatus | str,
|
|
expected_status: TaskStatus | str | list[TaskStatus | str],
|
|
properties: TaskProperties | None = None,
|
|
set_started_at: bool = False,
|
|
set_ended_at: bool = False,
|
|
):
|
|
"""
|
|
Initialize status transition command.
|
|
|
|
:param task_uuid: UUID of the task to update
|
|
:param new_status: Target status to set
|
|
:param expected_status: Current status(es) required for update to succeed.
|
|
Can be a single status or list of acceptable current statuses.
|
|
:param properties: Optional properties to update atomically with status
|
|
(e.g., error_message on FAILURE)
|
|
:param set_started_at: If True, also set started_at to current timestamp.
|
|
Should be True for PENDING → IN_PROGRESS transitions.
|
|
:param set_ended_at: If True, also set ended_at to current timestamp.
|
|
Should be True for terminal status transitions.
|
|
"""
|
|
self._task_uuid = task_uuid
|
|
self._new_status = new_status
|
|
self._expected_status = expected_status
|
|
self._properties = properties
|
|
self._set_started_at = set_started_at
|
|
self._set_ended_at = set_ended_at
|
|
|
|
def validate(self) -> None:
|
|
"""No validation needed for internal command."""
|
|
pass
|
|
|
|
@transaction(on_error=partial(on_error, reraise=TaskUpdateFailedError))
|
|
def run(self) -> bool:
|
|
"""
|
|
Execute atomic conditional status update.
|
|
|
|
:returns: True if status was updated (expected matched), False otherwise
|
|
"""
|
|
return TaskDAO.conditional_status_update(
|
|
task_uuid=self._task_uuid,
|
|
new_status=self._new_status,
|
|
expected_status=self._expected_status,
|
|
properties=self._properties,
|
|
set_started_at=self._set_started_at,
|
|
set_ended_at=self._set_ended_at,
|
|
)
|