Skip to content

Commit d48a002

Browse files
authored
Add pattern matching features on allow and block lists (GoogleCloudPlatform#956)
* Add DAG item rules to manage allow and block lists. Update readme. Migrate allow_list.txt * refactore * refactor for review
1 parent 8ece1e9 commit d48a002

3 files changed

Lines changed: 91 additions & 46 deletions

File tree

plugins/README.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,36 @@ By default, this plugin is **disabled** for all DAGs.
1010

1111
##### Enabling the Plugin for a DAG
1212

13-
If you want your DAG to **trigger this plugin**, add your DAG ID as a new line in **plugins/allow_list.txt**.
13+
If you want your DAG to **trigger this plugin**, add your DAG item as a new line in **plugins/allow_list.txt**.
1414

1515
##### Disabling the Plugin for a DAG
1616

17-
To prevent your DAG from ever triggering this plugin, add your DAG ID as a new line in **plugins/block_list.txt**. This list is intended to avoid repeated issue postings for DAGs that are not currently resolvable.
18-
19-
> **Note:** Each DAG ID should be placed on its own line, without extra spaces or quotes.
17+
To prevent your DAG from ever triggering this plugin, add your DAG item as a new line in **plugins/block_list.txt**. This list is intended to avoid repeated issue postings for DAGs that are not currently resolvable.
2018

19+
> **Note:** Each item should be placed on its own line, without extra spaces or quotes.
20+
##### DAG item rules
21+
##### 1. id
22+
```
23+
id:<DAG_ID>
24+
```
25+
<DAG_ID> should be the same as the DAG ID you want to track.
26+
##### 2. tag
27+
```
28+
tag:<DAG_TAG>
29+
```
30+
The DAG will be tracked if <DAG_TAG> matches one of its tags. An empty tag ('') is invalid and will be ignored.
31+
##### 3. pattern
32+
```
33+
pattern:<DAG_ID_PATTERN>
34+
```
35+
<DAG_ID_PATTERN> follows the same rules as UNIX shell globbing, not regular expression.
2136
##### Example
22-
2337
```
24-
<DAG_ID 1>
25-
<DAG_ID 2>
38+
id:on_failure_action_trigger
39+
tag:xlml
40+
pattern:on_failure_action_*
2641
...
2742
```
28-
2943
##### Plugin Default Toggle
3044
The `enable_plugin_by_default` attribute in `config.json` should be set to true to have all completed DAGs trigger this plugin, unless those DAGs are explicitly listed in the `allow_list.txt` or `block_list.txt`.
3145
If you don't want this plugin to be triggered by default, please set `enable_plugin_by_default` to false.

plugins/allow_list.txt

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,11 @@
1-
on_failure_actions_trigger
2-
maxtext_sft_trainer
3-
maxtext_configs_aot
4-
maxtext_convergence
5-
maxtext_end_to_end
6-
maxtext_profiling
7-
gke_node_pool_status
8-
multi-host-availability-rollback
9-
validate_interruption_count_gce_bare_metal_preemption
10-
validate_interruption_count_gce_defragmentation
11-
validate_interruption_count_gce_eviction
12-
validate_interruption_count_gce_host_error
13-
validate_interruption_count_gce_hwsw_maintenance
14-
validate_interruption_count_gce_migrate_on_hwsw_maintenance
15-
validate_interruption_count_gce_other
16-
validate_interruption_count_gke_bare_metal_preemption
17-
validate_interruption_count_gke_defragmentation
18-
validate_interruption_count_gke_eviction
19-
validate_interruption_count_gke_host_error
20-
validate_interruption_count_gke_hwsw_maintenance
21-
validate_interruption_count_gke_migrate_on_hwsw_maintenance
22-
validate_interruption_count_gke_other
23-
maxtext_emc_and_mtc_orbax_save_local
24-
maxtext_mtc_orbax_save_gcs
25-
maxtext_emc_orbax_res_local
26-
maxtext_mtc_orbax_res_local
1+
id:on_failure_actions_trigger
2+
id:maxtext_sft_trainer
3+
id:maxtext_configs_aot
4+
id:maxtext_convergence
5+
id:maxtext_end_to_end
6+
id:maxtext_profiling
7+
id:gke_node_pool_status
8+
id:multi-host-availability-rollback
9+
pattern:validate_interruption_*
10+
pattern:maxtext_mtc_*
11+
pattern:maxtext_emc_*

plugins/on_failure_actions.py

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import fnmatch
12
import json
23
import logging
34
import os
45
import time
5-
from typing import List, Dict, Any
6+
from typing import List, Dict, Any, Set
67

78
import google.auth.transport.requests
89
import jwt
@@ -135,7 +136,7 @@ def create_comment(issue, body):
135136
issue.create_comment(body=body)
136137

137138

138-
def read_list_from_gcs(bucket_name: str, blob_name: str) -> set[str]:
139+
def read_items_from_gcs(bucket_name: str, blob_name: str) -> set[str]:
139140
storage_client = storage.Client()
140141
bucket = storage_client.bucket(bucket_name)
141142
blob = bucket.blob(blob_name)
@@ -190,18 +191,19 @@ def on_dag_finished(self, dag_run: DagRun, msg: str):
190191
try:
191192
# A DAG would trigger the following logic if the DAG ID is in allow list and not in block list.
192193
enable_plugin = None
193-
if DagRunListener.is_in_allow_list(dag_run.dag_id):
194+
if DagRunListener.is_in_allow_list(dag_run):
194195
enable_plugin = True
195196
logging.info(
196197
f"[{self.log_prefix}] DAG {dag_run.dag_id} is in allow_list.txt"
197198
)
198-
if DagRunListener.is_in_block_list(dag_run.dag_id):
199+
200+
if DagRunListener.is_in_block_list(dag_run):
199201
enable_plugin = False
200202
logging.info(
201203
f"[{self.log_prefix}] DAG {dag_run.dag_id} is in block_list.txt"
202204
)
203205

204-
default_enabled = DagRunListener.enable_plugin_by_default(dag_run.dag_id)
206+
default_enabled = DagRunListener.enable_plugin_by_default()
205207

206208
if enable_plugin is False or (
207209
enable_plugin is None and not default_enabled
@@ -331,27 +333,71 @@ def get_airflow_url(project: str, region: str, env: str) -> str:
331333
return configs["config"]["airflowUri"]
332334

333335
@staticmethod
334-
def is_in_allow_list(dag_id: str) -> bool:
335-
allow_list = read_list_from_gcs(
336+
def is_in_allow_list(dag_run: DagRun) -> bool:
337+
allow_items = read_items_from_gcs(
336338
os.environ.get("GCS_BUCKET"), ALLOW_LIST_PATH
337339
)
338-
return True if dag_id in allow_list else False
340+
return DagRunListener.is_in_list(allow_items, dag_run)
339341

340342
@staticmethod
341-
def is_in_block_list(dag_id: str) -> bool:
342-
block_list = read_list_from_gcs(
343+
def is_in_block_list(dag_run: DagRun) -> bool:
344+
block_items = read_items_from_gcs(
343345
os.environ.get("GCS_BUCKET"), BLOCK_LIST_PATH
344346
)
345-
return True if dag_id in block_list else False
347+
return DagRunListener.is_in_list(block_items, dag_run)
346348

347349
@staticmethod
348-
def enable_plugin_by_default(dag_id: str) -> bool:
350+
def is_in_list(items: Set[str], dag_run: DagRun):
351+
return (
352+
DagRunListener.contains_id(items, dag_run.dag_id)
353+
or DagRunListener.has_any_tag(items, dag_run.dag.tags)
354+
or DagRunListener.matches_pattern(items, dag_run.dag_id)
355+
)
356+
357+
@staticmethod
358+
def enable_plugin_by_default() -> bool:
349359
config = read_json_from_gcs(os.environ.get("GCS_BUCKET"), CONFIG_PATH)
350360
logging.info(
351361
f"[DagRunListener] Enable plugin by default: {config['enable_plugin_by_default']}"
352362
)
353363
return config["enable_plugin_by_default"]
354364

365+
@staticmethod
366+
def contains_id(items: Set[str], target_id: str):
367+
id_set = set()
368+
for item in items:
369+
parts = item.split(":", 1)
370+
if len(parts) == 2:
371+
key = parts[0].lower()
372+
value = parts[1].strip()
373+
if key == "id":
374+
id_set.add(value)
375+
return target_id in id_set
376+
377+
@staticmethod
378+
def has_any_tag(items: Set[str], target_tags: List[str]):
379+
tag_set = set()
380+
for item in items:
381+
parts = item.split(":", 1)
382+
if len(parts) == 2:
383+
key = parts[0].lower()
384+
value = parts[1].strip()
385+
if key == "tag" and value:
386+
tag_set.add(value)
387+
return not tag_set.isdisjoint(target_tags)
388+
389+
@staticmethod
390+
def matches_pattern(items: Set[str], target_id: str):
391+
pattern_set = set()
392+
for item in items:
393+
parts = item.split(":", 1)
394+
if len(parts) == 2:
395+
key = parts[0].lower()
396+
value = parts[1].strip()
397+
if key == "pattern" and value:
398+
pattern_set.add(value)
399+
return any(fnmatch.fnmatch(target_id, pattern) for pattern in pattern_set)
400+
355401

356402
class ListenerPlugins(AirflowPlugin):
357403
name = "listener_plugins"

0 commit comments

Comments
 (0)