Skip to content

Commit 6b87155

Browse files
authored
feat(sdk.v2): Support Exit handler in v2 compiler. (#5784)
1 parent ef33b9e commit 6b87155

8 files changed

+294
-51
lines changed

sdk/python/kfp/v2/compiler/compiler.py

+53-4
Original file line numberDiff line numberDiff line change
@@ -647,10 +647,6 @@ def _group_to_dag_spec(
647647
raise NotImplementedError(
648648
'dsl.graph_component is not yet supported in KFP v2 compiler.')
649649

650-
if isinstance(subgroup, dsl.OpsGroup) and subgroup.type == 'exit_handler':
651-
raise NotImplementedError(
652-
'dsl.ExitHandler is not yet supported in KFP v2 compiler.')
653-
654650
if isinstance(subgroup, dsl.ContainerOp):
655651
if hasattr(subgroup, 'importer_spec'):
656652
importer_task_name = subgroup.task_spec.task_info.name
@@ -909,8 +905,60 @@ def _create_pipeline_spec(
909905
op_name_to_parent_groups,
910906
)
911907

908+
# Exit Handler
909+
if pipeline.groups[0].groups:
910+
first_group = pipeline.groups[0].groups[0]
911+
if first_group.type == 'exit_handler':
912+
exit_handler_op = first_group.exit_op
913+
914+
# Add exit op task spec
915+
task_name = exit_handler_op.task_spec.task_info.name
916+
exit_handler_op.task_spec.dependent_tasks.extend(
917+
pipeline_spec.root.dag.tasks.keys())
918+
exit_handler_op.task_spec.trigger_policy.strategy = (
919+
pipeline_spec_pb2.PipelineTaskSpec.TriggerPolicy.TriggerStrategy
920+
.ALL_UPSTREAM_TASKS_COMPLETED)
921+
pipeline_spec.root.dag.tasks[task_name].CopyFrom(
922+
exit_handler_op.task_spec)
923+
924+
# Add exit op component spec if it does not exist.
925+
component_name = exit_handler_op.task_spec.component_ref.name
926+
if component_name not in pipeline_spec.components:
927+
pipeline_spec.components[component_name].CopyFrom(
928+
exit_handler_op.component_spec)
929+
930+
# Add exit op executor spec if it does not exist.
931+
executor_label = exit_handler_op.component_spec.executor_label
932+
if executor_label not in deployment_config.executors:
933+
deployment_config.executors[executor_label].container.CopyFrom(
934+
exit_handler_op.container_spec)
935+
pipeline_spec.deployment_spec.update(
936+
json_format.MessageToDict(deployment_config))
937+
912938
return pipeline_spec
913939

940+
def _validate_exit_handler(self, pipeline):
941+
"""Makes sure there is only one global exit handler.
942+
943+
This is temporary to be compatible with KFP v1.
944+
"""
945+
946+
def _validate_exit_handler_helper(group, exiting_op_names, handler_exists):
947+
if group.type == 'exit_handler':
948+
if handler_exists or len(exiting_op_names) > 1:
949+
raise ValueError(
950+
'Only one global exit_handler is allowed and all ops need to be included.'
951+
)
952+
handler_exists = True
953+
954+
if group.ops:
955+
exiting_op_names.extend([x.name for x in group.ops])
956+
957+
for g in group.groups:
958+
_validate_exit_handler_helper(g, exiting_op_names, handler_exists)
959+
960+
return _validate_exit_handler_helper(pipeline.groups[0], [], False)
961+
914962
# TODO: Sanitizing beforehand, so that we don't need to sanitize here.
915963
def _sanitize_and_inject_artifact(self, pipeline: dsl.Pipeline) -> None:
916964
"""Sanitize operator/param names and inject pipeline artifact location. """
@@ -1006,6 +1054,7 @@ def _create_pipeline_v2(
10061054
with dsl.Pipeline(pipeline_name) as dsl_pipeline:
10071055
pipeline_func(*args_list)
10081056

1057+
self._validate_exit_handler(dsl_pipeline)
10091058
self._sanitize_and_inject_artifact(dsl_pipeline)
10101059

10111060
# Fill in the default values.

sdk/python/kfp/v2/compiler/compiler_test.py

-44
Original file line numberDiff line numberDiff line change
@@ -74,50 +74,6 @@ def simple_pipeline(pipeline_input: str = 'Hello KFP!'):
7474
finally:
7575
shutil.rmtree(tmpdir)
7676

77-
def test_compile_pipeline_with_dsl_exithandler_should_raise_error(self):
78-
79-
gcs_download_op = components.load_component_from_text("""
80-
name: GCS - Download
81-
inputs:
82-
- {name: url, type: String}
83-
outputs:
84-
- {name: result, type: String}
85-
implementation:
86-
container:
87-
image: gcr.io/my-project/my-image:tag
88-
args:
89-
- {inputValue: url}
90-
- {outputPath: result}
91-
""")
92-
93-
echo_op = components.load_component_from_text("""
94-
name: echo
95-
inputs:
96-
- {name: msg, type: String}
97-
implementation:
98-
container:
99-
image: gcr.io/my-project/my-image:tag
100-
args:
101-
- {inputValue: msg}
102-
""")
103-
104-
@dsl.pipeline(name='test-pipeline', pipeline_root='dummy_root')
105-
def download_and_print(
106-
url: str = 'gs://ml-pipeline/shakespeare/shakespeare1.txt'):
107-
"""A sample pipeline showing exit handler."""
108-
109-
exit_task = echo_op('exit!')
110-
111-
with dsl.ExitHandler(exit_task):
112-
download_task = gcs_download_op(url)
113-
echo_task = echo_op(download_task.outputs['result'])
114-
115-
with self.assertRaisesRegex(
116-
NotImplementedError,
117-
'dsl.ExitHandler is not yet supported in KFP v2 compiler.'):
118-
compiler.Compiler().compile(
119-
pipeline_func=download_and_print, package_path='output.json')
120-
12177
def test_compile_pipeline_with_dsl_graph_component_should_raise_error(self):
12278

12379
with self.assertRaisesRegex(

sdk/python/kfp/v2/compiler_cli_tests/compiler_cli_tests.py

+3
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ def test_pipeline_with_custom_job_spec(self):
127127
def test_pipeline_with_metrics_outputs(self):
128128
self._test_compile_py_to_json('pipeline_with_metrics_outputs')
129129

130+
def test_pipeline_with_exit_handler(self):
131+
self._test_compile_py_to_json('pipeline_with_exit_handler')
132+
130133

131134
if __name__ == '__main__':
132135
unittest.main()

sdk/python/kfp/v2/compiler_cli_tests/test_data/pipeline_with_exit_handler.json

+188
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright 2021 The Kubeflow Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Pipeline using ExitHandler."""
15+
16+
from kfp.v2 import dsl
17+
from kfp.v2 import compiler
18+
from kfp.v2.dsl import component
19+
20+
21+
@component
22+
def print_op(message: str):
23+
"""Prints a message."""
24+
print(message)
25+
26+
27+
@component
28+
def fail_op(message: str):
29+
"""Fails."""
30+
import sys
31+
print(message)
32+
sys.exit(1)
33+
34+
35+
@dsl.pipeline(name='pipeline-with-exit-handler', pipeline_root='dummy_root')
36+
def my_pipeline():
37+
38+
exit_task = print_op('Exit handler has worked!')
39+
40+
with dsl.ExitHandler(exit_task):
41+
print_op('Hello World!')
42+
fail_op('Task failed.')
43+
44+
45+
if __name__ == '__main__':
46+
compiler.Compiler().compile(
47+
pipeline_func=my_pipeline, package_path=__file__.replace('.py', '.json'))

sdk/python/requirements.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ click>=7.1.1,<8
2929

3030
# kfp.v2
3131
absl-py>=0.9,<=0.11
32-
kfp-pipeline-spec>=0.1.7,<0.2.0
32+
kfp-pipeline-spec>=0.1.8,<0.2.0
3333
fire>=0.3.1,<1
3434
google-api-python-client>=1.7.8,<2

sdk/python/requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ idna==2.10
6464
# via requests
6565
jsonschema==3.2.0
6666
# via -r requirements.in
67-
kfp-pipeline-spec==0.1.7
67+
kfp-pipeline-spec==0.1.8
6868
# via -r requirements.in
6969
kfp-server-api==1.3.0
7070
# via -r requirements.in

sdk/python/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
'Deprecated>=1.2.7,<2',
4747
'strip-hints>=0.1.8,<1',
4848
'docstring-parser>=0.7.3,<1',
49-
'kfp-pipeline-spec>=0.1.7,<0.2.0',
49+
'kfp-pipeline-spec>=0.1.8,<0.2.0',
5050
'fire>=0.3.1,<1',
5151
'protobuf>=3.13.0,<4'
5252
]

0 commit comments

Comments
 (0)