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
1 change: 0 additions & 1 deletion .github/workflows/dag-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name: DAG Check

on:
pull_request:
branches: [master]
types: [opened, synchronize, edited]

push:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/pyink-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ name: Formatter

on:
pull_request:
branches: [master]
types: [opened, synchronize, edited]
push:
branches: [master]

workflow_dispatch: {}

jobs:
format_check:
runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/pylint-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ name: Linter

on:
pull_request:
branches: [master]
types: [opened, synchronize, edited]

push:
branches: [master]

workflow_dispatch: {}

jobs:
linting_check:
runs-on: ubuntu-latest
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/require-checklist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ name: Require Checklist
on:
pull_request:
types: [opened, edited, synchronize]

workflow_dispatch: {}

jobs:
check_pr_body:
runs-on: ubuntu-latest
steps:
- uses: mheap/require-checklist-action@v2
with:
requireChecklist: false # If this is true and there are no checklists detected, the action will fail
requireChecklist: false # If this is true and there are no checklists detected, the action will fail
1 change: 0 additions & 1 deletion .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name: Unit Test

on:
pull_request:
branches: [master]
types: [opened, synchronize, edited]

push:
Expand Down
31 changes: 23 additions & 8 deletions dags/common/quarantined_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,36 @@ def match_quarantine_patterns(
return False


def is_ci_environment() -> bool:
"""Checks if running in a CI environment.

Identifies environments where framework-specific runtime dependencies do not
exist. For example, GitHub Actions lacks the actual Airflow environment and
its components (such as database context or cloud credentials), requiring code
to bypass these dependencies and use mocks instead.

Returns:
bool: True if in CI (GitHub Actions), False otherwise.
"""
return os.getenv("GITHUB_ACTIONS", "false").lower() == "true"


def safe_get_from_variable(key: str, default_var: str):
"""
Check whether the current runtime is GitHub Actions. Skip retrieving variables in GitHub Actions to avoid excessive log output.
Check whether the current runtime is GitHub Actions. Skip retrieving variables
in GitHub Actions to avoid excessive log output.
"""
value = default_var
is_ci_env = os.getenv("GITHUB_ACTIONS", "false").lower() == "true"
if is_ci_env:
if is_ci_environment():
logging.info("In GitHub Actions, skip getting variables")
else:
value = Variable.get(key, default_var=default_var)
return value


"""
The quarantine list is defined by a set of UNIX Shell Glob Patterns.
These patterns are used to match and quarantine tests.
The quarantine list is defined by a set of UNIX Shell Glob Patterns.
These patterns are used to match and quarantine tests.

The patterns are stored in the Airflow Variable named 'quarantine_patterns'.

Expand All @@ -90,9 +104,10 @@ def is_quarantined(test_name) -> bool:
"""
Checks if a test is quarantined using both legacy and current methods.

The legacy method checks if `test_name` is present in `QuarantineTests.tests`.
The current method checks against a runtime quarantine list fetched from Airflow Variables
(key: 'quarantine_list') using `is_in_runtime_quarantine_list()`.
The legacy method checks if `test_name` is present in
`QuarantineTests.tests`. The current method checks against a runtime
quarantine list fetched from Airflow Variables (key: 'quarantine_list')
using `is_in_runtime_quarantine_list()`.

The test is considered quarantined if it's found by either method.
"""
Expand Down
1 change: 1 addition & 0 deletions dags/common/scheduling_helper/scheduling_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class DayOfWeek(enum.Enum):
"jobset_ttr_drain_restart": DefaultTimeout,
"tpu_info_metrics_verification": DefaultTimeout,
"jobset_ttr_node_reboot": dt.timedelta(minutes=90),
"test_task_group_with_timeout": DefaultTimeout,
},
TPU_INTERRUPTION_MOCK_CLUSTER.name: {
"validate_interruption_count_gce_bare_metal_preemption": DefaultTimeout,
Expand Down
168 changes: 168 additions & 0 deletions dags/common/task_group_with_timeout/test_task_group_with_timeout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""A integration DAG to test the behavior of TaskGroupWithTimeout."""

import datetime
from datetime import timedelta
import logging
import time

from airflow import models
from airflow.models.baseoperator import chain
from airflow.operators.python import PythonOperator
from airflow.exceptions import AirflowException
from airflow.utils.task_group import TaskGroup
from airflow.utils.trigger_rule import TriggerRule

from dags import composer_env
from dags.common.scheduling_helper.scheduling_helper import (
SchedulingHelper,
get_dag_timeout,
)
from dags.common.task_group_with_timeout.task_group_with_timeout import (
TaskGroupWithTimeout,
)

DAG_ID = "test_task_group_with_timeout"
DAGRUN_TIMEOUT = get_dag_timeout(DAG_ID)
SCHEDULE = SchedulingHelper.arrange_schedule_time(DAG_ID)


def simulate_workload(sleep_seconds: int):
"""Simulates a long-running workload with explicit sleep."""
logging.info(f"Executing workload... Sleeping for {sleep_seconds}s.")
time.sleep(sleep_seconds)
logging.info("Workload completed successfully.")


def verify_static_defenses():
"""Verifies that TaskGroupWithTimeout structural restrictions are active."""
# Mock a minimal dummy DAG context to allow TaskGroup initialization
mock_dag = models.DAG(
dag_id="mock_static_defense_dag", start_date=datetime.datetime(2026, 6, 1)
)

# Test 1: Active enforcement against nested TaskGroups
try:
tg_timeout = TaskGroupWithTimeout(
group_id="parent_timeout_group",
timeout=timedelta(seconds=60),
dag=mock_dag,
)
nested_native = TaskGroup(group_id="nested_native_group", dag=mock_dag)
# Explicitly trigger the component's add constraint logic
tg_timeout.add(nested_native)
except AirflowException as e:
print(f"Static Parsing Check - Caught expected nested error: {e}")

# Test 2: Active enforcement against Dynamic Task Mapping inside the group
try:
tg_timeout_map = TaskGroupWithTimeout(
group_id="parent_mapping_group",
timeout=timedelta(seconds=60),
dag=mock_dag,
)
dummy_mapped_task = PythonOperator.partial(
task_id="dummy_map", python_callable=lambda: None, dag=mock_dag
).expand(op_args=[[]])
# Explicitly trigger the component's add constraint logic
tg_timeout_map.add(dummy_mapped_task)
except AirflowException as e:
print(f"Static Parsing Check - Caught expected mapping error: {e}")


# Runtime Timeout Tests
with models.DAG(
dag_id=DAG_ID,
start_date=datetime.datetime(2026, 6, 1),
schedule=SCHEDULE if composer_env.is_prod_env() else None,
dagrun_timeout=DAGRUN_TIMEOUT,
catchup=False,
tags=["integration-test", "timeout-validation"],
description=(
"Validates continuous time-budget pool depletion across consecutive "
"TaskGroups and tests teardown resource protection safeguards."
),
doc_md="""
# TaskGroupWithTimeout Integration Test Suite

### Description
This DAG serves as a production-grade sandbox to verify the dynamic runtime
capabilities of the custom `TaskGroupWithTimeout` infrastructure component.

### Prerequisites
The custom TaskGroupWithTimeout component must be available under dags.common.

### Procedures
1. **Static Validation:** Pre-checks blocking of nested groups and dynamic mapping.
2. **Case 1 (Normal Flow):** Consumes 60 seconds from the global pool and succeeds.
3. **Case 2 (Timeout Flow):** Dynamically inherits the remaining budget. Forces `t3`
to fail due to budget exhaustion.
4. **Teardown & Final Status:** Ensures external cleanup runs and forces the final
DAG status to SUCCESS via an ALL_DONE end-node.
""",
) as dag:
verify_static_defenses()

# Case 1: Normal Flow
with TaskGroupWithTimeout(
group_id="normal_flow", timeout=timedelta(seconds=100)
) as case1:
normal_flow_task_1 = PythonOperator(
task_id="normal_flow_task_1",
python_callable=simulate_workload,
op_args=[20],
)
normal_flow_task_2 = PythonOperator(
task_id="normal_flow_task_2",
python_callable=simulate_workload,
op_args=[20],
)
normal_flow_task_3 = PythonOperator(
task_id="normal_flow_task_3",
python_callable=simulate_workload,
op_args=[20],
)

chain(normal_flow_task_1, normal_flow_task_2, normal_flow_task_3)

# Case 2: Timeout Block & Teardown Bypass
with TaskGroupWithTimeout(
group_id="timeout_flow", timeout=timedelta(seconds=100)
) as case2:
timeout_flow_task_1 = PythonOperator(
task_id="timeout_flow_task_1",
python_callable=simulate_workload,
op_args=[20],
)
timeout_flow_task_2 = PythonOperator(
task_id="timeout_flow_task_2",
python_callable=simulate_workload,
op_args=[40],
)
timeout_flow_task_3 = PythonOperator(
task_id="timeout_flow_task_3",
python_callable=simulate_workload,
op_args=[60],
retries=0,
)

env_cleanup_teardown = PythonOperator(
task_id="env_cleanup_teardown",
python_callable=simulate_workload,
op_args=[5],
).as_teardown()

dag_status_override = PythonOperator(
task_id="dag_status_override",
python_callable=lambda: logging.info(
"Test pipeline finished. Overriding final status to SUCCESS."
),
trigger_rule=TriggerRule.ALL_DONE,
)

chain(
timeout_flow_task_1,
timeout_flow_task_2,
timeout_flow_task_3,
env_cleanup_teardown,
)
chain(case1, case2, dag_status_override)
24 changes: 12 additions & 12 deletions dags/tpu_observability/utils/jobset_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,18 +282,18 @@ class JobSet:
recreating pods. Defaults to False.
"""

namespace: str
max_restarts: int
replicated_job_name: str
replicas: int
backoff_limit: int
completions: int
parallelism: int
tpu_accelerator_type: str
tpu_topology: str
container_name: str
image: str
tpu_cores_per_pod: int
namespace: str = ""
max_restarts: int = -1
replicated_job_name: str = ""
replicas: int = -1
backoff_limit: int = -1
completions: int = -1
parallelism: int = -1
tpu_accelerator_type: str = ""
tpu_topology: str = ""
container_name: str = ""
image: str = ""
tpu_cores_per_pod: int = -1
privileged: bool = False
dag_id_prefix: str = ""
delay_recovery: bool = False
Expand Down
41 changes: 32 additions & 9 deletions scripts/code-style.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,37 @@ set -e

FOLDERS_TO_FORMAT=("dags" "xlml")

for folder in "${FOLDERS_TO_FORMAT[@]}"
do
pyink "$folder" --pyink-indentation=2 --pyink-use-majority-quotes --line-length=80 --check --diff
done

for folder in "${FOLDERS_TO_FORMAT[@]}"
do
pylint "./$folder" --fail-under=9.6
done
HEAD_SHA="$(git rev-parse HEAD)"
BASE_BRANCH="dev"

if ! git rev-parse --verify "$BASE_BRANCH" >/dev/null 2>&1; then
git fetch origin "$BASE_BRANCH":"$BASE_BRANCH" || {
echo "[code-style] base branch '$BASE_BRANCH' not found, skip diff-based check."
exit 0
}
fi

CHANGED_PY_FILES="$(
git diff --name-only --diff-filter=ACM "${BASE_BRANCH}" "${HEAD_SHA}" \
| grep '\.py$' \
| while read -r f; do
for folder in "${FOLDERS_TO_FORMAT[@]}"; do
if [[ "$f" == "$folder/"* ]]; then
echo "$f"
break
fi
done
done \
| sort -u
)"

if [[ -z "${CHANGED_PY_FILES}" ]]; then
echo "[pre-push hook] no changed files detected between ${HEAD_SHA} and ${BASE_BRANCH}"
exit 1
fi

pyink ${CHANGED_PY_FILES} --pyink-indentation=2 --pyink-use-majority-quotes --line-length=80 --check --diff

pylint ${CHANGED_PY_FILES} --fail-under=9.6 --disable=E1123

echo "Successfully clean up all codes."
Loading
Loading