-
Notifications
You must be signed in to change notification settings - Fork 310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Eager cleanup #3148
base: master
Are you sure you want to change the base?
Eager cleanup #3148
Changes from all commits
f31e823
5770a5e
943ca5c
be37cd5
b6dbc73
3937e53
54a56e5
367a393
a7e1bdf
8f4cd24
da401fc
d5be81d
9d221fc
f12b1f4
cdb5189
6ac8010
3db4249
d310d66
c57ddb8
e183cbe
d8b8e73
2566714
b6ee2dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -20,6 +20,8 @@ | |||||||||||||||||||
import inspect | ||||||||||||||||||||
import os | ||||||||||||||||||||
import signal | ||||||||||||||||||||
import time | ||||||||||||||||||||
import typing | ||||||||||||||||||||
from abc import ABC | ||||||||||||||||||||
from collections import OrderedDict | ||||||||||||||||||||
from contextlib import suppress | ||||||||||||||||||||
|
@@ -32,7 +34,7 @@ | |||||||||||||||||||
from flytekit.core.constants import EAGER_ROOT_ENV_NAME | ||||||||||||||||||||
from flytekit.core.context_manager import ExecutionState, FlyteContext, FlyteContextManager | ||||||||||||||||||||
from flytekit.core.docstring import Docstring | ||||||||||||||||||||
from flytekit.core.interface import transform_function_to_interface | ||||||||||||||||||||
from flytekit.core.interface import Interface, transform_function_to_interface | ||||||||||||||||||||
from flytekit.core.promise import ( | ||||||||||||||||||||
Promise, | ||||||||||||||||||||
VoidPromise, | ||||||||||||||||||||
|
@@ -59,11 +61,17 @@ | |||||||||||||||||||
from flytekit.models import dynamic_job as _dynamic_job | ||||||||||||||||||||
from flytekit.models import literals as _literal_models | ||||||||||||||||||||
from flytekit.models import task as task_models | ||||||||||||||||||||
from flytekit.models.admin import common as admin_common_models | ||||||||||||||||||||
from flytekit.models.admin import workflow as admin_workflow_models | ||||||||||||||||||||
from flytekit.models.filters import ValueIn | ||||||||||||||||||||
from flytekit.models.literals import LiteralMap | ||||||||||||||||||||
from flytekit.models.security import Secret | ||||||||||||||||||||
from flytekit.utils.asyn import loop_manager | ||||||||||||||||||||
|
||||||||||||||||||||
T = TypeVar("T") | ||||||||||||||||||||
|
||||||||||||||||||||
CLEANUP_LOOP_DELAY_SECONDS = 1 | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
class PythonInstanceTask(PythonAutoContainerTask[T], ABC): # type: ignore | ||||||||||||||||||||
""" | ||||||||||||||||||||
|
@@ -549,6 +557,13 @@ def execute(self, **kwargs) -> Any: | |||||||||||||||||||
) | ||||||||||||||||||||
raw_output = ctx.user_space_params.raw_output_prefix if ctx.user_space_params else None | ||||||||||||||||||||
logger.info(f"Constructing default remote with no config and {project}, {domain}, {raw_output}") | ||||||||||||||||||||
import os | ||||||||||||||||||||
from union._config import _get_union_api_env_var | ||||||||||||||||||||
api_value_tuple = _get_union_api_env_var() | ||||||||||||||||||||
print(f"111!!!!!!!!!!!!!!!!??????????>>>>>>>>>>>>!!!!!!!!!!! {os.environ['_UNION_EAGER_API_KEY']}", | ||||||||||||||||||||
flush=True) | ||||||||||||||||||||
print(f"111!!!!!!!!!!!!!!!!??????????>>>>!!!!! {api_value_tuple}", flush=True) | ||||||||||||||||||||
|
||||||||||||||||||||
remote = get_plugin().get_remote( | ||||||||||||||||||||
config=None, project=project, domain=domain, data_upload_location=raw_output | ||||||||||||||||||||
) | ||||||||||||||||||||
|
@@ -636,3 +651,125 @@ def run(self, remote: "FlyteRemote", ss: SerializationSettings, **kwargs): # ty | |||||||||||||||||||
|
||||||||||||||||||||
with FlyteContextManager.with_context(builder): | ||||||||||||||||||||
return loop_manager.run_sync(self.async_execute, self, **kwargs) | ||||||||||||||||||||
|
||||||||||||||||||||
def get_as_workflow(self): | ||||||||||||||||||||
from flytekit.core.workflow import ImperativeWorkflow | ||||||||||||||||||||
|
||||||||||||||||||||
cleanup = EagerFailureHandlerTask(name=f"{self.name}-cleanup", inputs=self.python_interface.inputs) | ||||||||||||||||||||
# todo: remove this before merging | ||||||||||||||||||||
# this is actually bad, but useful for developing | ||||||||||||||||||||
cleanup._container_image = self._container_image | ||||||||||||||||||||
Comment on lines
+659
to
+661
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||||||||||||||||||||
wb = ImperativeWorkflow(name=self.name) | ||||||||||||||||||||
|
||||||||||||||||||||
input_kwargs = {} | ||||||||||||||||||||
for input_name, input_python_type in self.python_interface.inputs.items(): | ||||||||||||||||||||
wb.add_workflow_input(input_name, input_python_type) | ||||||||||||||||||||
input_kwargs[input_name] = wb.inputs[input_name] | ||||||||||||||||||||
|
||||||||||||||||||||
node = wb.add_entity(self, **input_kwargs) | ||||||||||||||||||||
for output_name, output_python_type in self.python_interface.outputs.items(): | ||||||||||||||||||||
wb.add_workflow_output(output_name, node.outputs[output_name]) | ||||||||||||||||||||
|
||||||||||||||||||||
wb.add_on_failure_handler(cleanup) | ||||||||||||||||||||
return wb | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
class EagerFailureTaskResolver(TaskResolverMixin): | ||||||||||||||||||||
@property | ||||||||||||||||||||
def location(self) -> str: | ||||||||||||||||||||
return f"{EagerFailureTaskResolver.__module__}.eager_failure_task_resolver" | ||||||||||||||||||||
|
||||||||||||||||||||
def name(self) -> str: | ||||||||||||||||||||
return "eager_failure_task_resolver" | ||||||||||||||||||||
|
||||||||||||||||||||
def load_task(self, loader_args: List[str]) -> Task: | ||||||||||||||||||||
""" | ||||||||||||||||||||
Given the set of identifier keys, should return one Python Task or raise an error if not found | ||||||||||||||||||||
""" | ||||||||||||||||||||
return EagerFailureHandlerTask(name="no_input_default_cleanup_task", inputs={}) | ||||||||||||||||||||
|
||||||||||||||||||||
def loader_args(self, settings: SerializationSettings, t: Task) -> List[str]: | ||||||||||||||||||||
""" | ||||||||||||||||||||
Return a list of strings that can help identify the parameter Task | ||||||||||||||||||||
""" | ||||||||||||||||||||
return ["eager", "failure", "handler"] | ||||||||||||||||||||
|
||||||||||||||||||||
def get_all_tasks(self) -> List[Task]: | ||||||||||||||||||||
""" | ||||||||||||||||||||
Future proof method. Just making it easy to access all tasks (Not required today as we auto register them) | ||||||||||||||||||||
""" | ||||||||||||||||||||
return [] | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
eager_failure_task_resolver = EagerFailureTaskResolver() | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
class EagerFailureHandlerTask(PythonAutoContainerTask, metaclass=FlyteTrackedABC): | ||||||||||||||||||||
_TASK_TYPE = "eager_failure_handler_task" | ||||||||||||||||||||
|
||||||||||||||||||||
def __init__(self, name: str, inputs: typing.Optional[typing.Dict[str, typing.Type]] = None, **kwargs): | ||||||||||||||||||||
""" """ | ||||||||||||||||||||
inputs = inputs or {} | ||||||||||||||||||||
super().__init__( | ||||||||||||||||||||
task_type=self._TASK_TYPE, | ||||||||||||||||||||
name=name, | ||||||||||||||||||||
interface=Interface(inputs=inputs, outputs=None), | ||||||||||||||||||||
task_config=None, | ||||||||||||||||||||
task_resolver=eager_failure_task_resolver, | ||||||||||||||||||||
**kwargs, | ||||||||||||||||||||
) | ||||||||||||||||||||
|
||||||||||||||||||||
def dispatch_execute(self, ctx: FlyteContext, input_literal_map: LiteralMap) -> LiteralMap: | ||||||||||||||||||||
""" | ||||||||||||||||||||
This task should only be called during remote execution. Because when rehydrating this task at execution | ||||||||||||||||||||
time, we don't have access to the python interface of the corresponding eager task/workflow, we don't | ||||||||||||||||||||
have the Python types to convert the input literal map, but nor do we need them. | ||||||||||||||||||||
This task is responsible only for ensuring that all executions are terminated. | ||||||||||||||||||||
""" | ||||||||||||||||||||
# Recursive imports | ||||||||||||||||||||
from flytekit import current_context | ||||||||||||||||||||
from flytekit.configuration.plugin import get_plugin | ||||||||||||||||||||
|
||||||||||||||||||||
most_recent = admin_common_models.Sort("created_at", admin_common_models.Sort.Direction.DESCENDING) | ||||||||||||||||||||
current_exec_id = current_context().execution_id | ||||||||||||||||||||
project = current_exec_id.project | ||||||||||||||||||||
domain = current_exec_id.domain | ||||||||||||||||||||
name = current_exec_id.name | ||||||||||||||||||||
logger.warning(f"Cleaning up potentially still running tasks for execution {name} in {project}/{domain}") | ||||||||||||||||||||
import os | ||||||||||||||||||||
from union._config import _get_union_api_env_var | ||||||||||||||||||||
api_value_tuple = _get_union_api_env_var() | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Invalid syntax with walrus operator
There's a syntax error on line 734 where the walrus operator Code suggestionCheck the AI-generated fix before applying
Suggested change
Code Review Run #5f941b Should Bito avoid suggestions like this for future reviews? (Manage Rules)
|
||||||||||||||||||||
print(f"!!!!!!!!!!!!!!!!??????????>>>>>>>>>>>>!!!!!!!!!!! {os.environ['_UNION_EAGER_API_KEY']}", flush=True) | ||||||||||||||||||||
print(f"!!!!!!!!!!!!!!!!??????????>>>>!!!!! {api_value_tuple}", flush=True) | ||||||||||||||||||||
try: | ||||||||||||||||||||
remote = get_plugin().get_remote(config=None, project=project, domain=domain) | ||||||||||||||||||||
except Exception as e: | ||||||||||||||||||||
print(e, flush=True) | ||||||||||||||||||||
import sys | ||||||||||||||||||||
sys.exit(1) | ||||||||||||||||||||
Comment on lines
+746
to
+749
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blind exception catch needs specificity
Catching a blind Code suggestionCheck the AI-generated fix before applying
Suggested change
Code Review Run #685a40 Should Bito avoid suggestions like this for future reviews? (Manage Rules)
|
||||||||||||||||||||
key_filter = ValueIn("execution_tag.key", ["eager-exec"]) | ||||||||||||||||||||
value_filter = ValueIn("execution_tag.value", [name]) | ||||||||||||||||||||
phase_filter = ValueIn("phase", ["UNDEFINED", "QUEUED", "RUNNING"]) | ||||||||||||||||||||
# This should be made more robust, currently lacking retries and exception handling | ||||||||||||||||||||
while True: | ||||||||||||||||||||
exec_models, _ = remote.client.list_executions_paginated( | ||||||||||||||||||||
project, | ||||||||||||||||||||
domain, | ||||||||||||||||||||
limit=100, | ||||||||||||||||||||
filters=[key_filter, value_filter, phase_filter], | ||||||||||||||||||||
sort_by=most_recent, | ||||||||||||||||||||
) | ||||||||||||||||||||
logger.warning(f"Found {len(exec_models)} executions this round for termination") | ||||||||||||||||||||
eapolinario marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
if not exec_models: | ||||||||||||||||||||
break | ||||||||||||||||||||
logger.warning(exec_models) | ||||||||||||||||||||
for exec_model in exec_models: | ||||||||||||||||||||
logger.warning(f"Terminating execution {exec_model.id}, phase {exec_model.closure.phase}") | ||||||||||||||||||||
remote.client.terminate_execution(exec_model.id, f"clean up by parent eager execution {name}") | ||||||||||||||||||||
time.sleep(CLEANUP_LOOP_DELAY_SECONDS) | ||||||||||||||||||||
|
||||||||||||||||||||
# Just echo back | ||||||||||||||||||||
return input_literal_map | ||||||||||||||||||||
|
||||||||||||||||||||
def execute(self, **kwargs) -> Any: | ||||||||||||||||||||
raise AssertionError("this task shouldn't need to call execute") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from collections import OrderedDict | ||
|
||
import flytekit.configuration | ||
from flytekit.configuration import Image, ImageConfig | ||
from flytekit.core.python_function_task import EagerFailureHandlerTask | ||
from flytekit.tools.translator import get_serializable | ||
|
||
default_img = Image(name="default", fqn="test", tag="tag") | ||
serialization_settings = flytekit.configuration.SerializationSettings( | ||
project="project", | ||
domain="domain", | ||
version="version", | ||
env=None, | ||
image_config=ImageConfig(default_image=default_img, images=[default_img]), | ||
) | ||
|
||
|
||
def test_failure(): | ||
t = EagerFailureHandlerTask(name="tester", inputs={"a": int}) | ||
|
||
spec = get_serializable(OrderedDict(), serialization_settings, t) | ||
print(spec) | ||
|
||
assert spec.template.container.args == ['pyflyte-execute', '--inputs', '{{.input}}', '--output-prefix', '{{.outputPrefix}}', '--raw-output-data-prefix', '{{.rawOutputDataPrefix}}', '--checkpoint-path', '{{.checkpointOutputPrefix}}', '--prev-checkpoint', '{{.prevCheckpointPrefix}}', '--resolver', 'flytekit.core.python_function_task.eager_failure_task_resolver', '--', 'eager', 'failure', 'handler'] | ||
|
||
|
||
def test_loading(): | ||
from flytekit.tools.module_loader import load_object_from_module | ||
|
||
resolver = load_object_from_module("flytekit.core.python_function_task.eager_failure_task_resolver") | ||
print(resolver) | ||
t = resolver.load_task([]) | ||
assert isinstance(t, EagerFailureHandlerTask) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider handling potential exceptions from
run_sync
when executing coroutine functions. The current implementation may silently fail if the async execution encounters issues.Code suggestion
Code Review Run #ce446d
Should Bito avoid suggestions like this for future reviews? (Manage Rules)