Skip to content

Commit ca59398

Browse files
authored
[Feat] add sleep plugins and examples (#3446)
Signed-off-by: machichima <nary12321@gmail.com>
1 parent 124d99c commit ca59398

9 files changed

Lines changed: 358 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Sleep Plugin Example
3+
====================
4+
5+
The ``core-sleep`` plugin executes entirely in the backend — no task pod is created.
6+
The sleep duration is a normal task input, so it can be a dynamic workflow value.
7+
"""
8+
9+
import os
10+
from datetime import timedelta
11+
12+
from flytekitplugins.sleep import Sleep
13+
14+
from flytekit import task, workflow
15+
16+
17+
@task(cache_version="2", task_config=Sleep())
18+
def sleep_for(duration: timedelta) -> None:
19+
# This body only runs during local execution.
20+
# On the cluster, the backend sleeps for `duration` without running this.
21+
print(f"[local] sleeping for {duration}")
22+
23+
24+
@workflow
25+
def wf(duration: timedelta) -> None:
26+
sleep_for(duration=duration)
27+
28+
29+
if __name__ == "__main__":
30+
from click.testing import CliRunner
31+
32+
from flytekit.clis.sdk_in_container import pyflyte
33+
34+
runner = CliRunner()
35+
path = os.path.realpath(__file__)
36+
result = runner.invoke(
37+
pyflyte.main,
38+
[
39+
"--config",
40+
os.path.expanduser("~/.flyte/config-sandbox.yaml"),
41+
"run",
42+
"--remote",
43+
path,
44+
"wf",
45+
"--duration",
46+
"10s",
47+
],
48+
)
49+
print("Remote Execution: ", result.output)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
Sleep Fanout Example
3+
====================
4+
5+
Fan out N core-sleep leaves in parallel using map_task.
6+
No task pods are created for the leaves — the backend handles sleep directly.
7+
8+
Usage (remote):
9+
python sleep_fanout.py
10+
"""
11+
12+
import os
13+
from datetime import timedelta
14+
from typing import List
15+
16+
from flytekitplugins.sleep import Sleep
17+
18+
import flytekit as fl
19+
from flytekit import map_task, task, workflow
20+
21+
sleep_image = fl.ImageSpec(
22+
registry="ghcr.io/machichima",
23+
name="sleep-fanout",
24+
apt_packages=["git"],
25+
packages=["git+https://github.com/machichima/flytekit.git@add-sleep-plugin#subdirectory=plugins/flytekit-sleep"],
26+
env={"REBUILD": "1"},
27+
)
28+
29+
30+
@task(container_image=sleep_image)
31+
def make_durations(duration: timedelta, n: int) -> List[timedelta]:
32+
return [duration] * n
33+
34+
35+
@task(task_config=Sleep(), container_image=sleep_image)
36+
def sleep_leaf(duration: timedelta) -> None:
37+
pass
38+
39+
40+
@workflow
41+
def wf(sleep_duration: timedelta = timedelta(seconds=10), n_children: int = 400) -> None:
42+
durations = make_durations(duration=sleep_duration, n=n_children)
43+
map_task(sleep_leaf)(duration=durations)
44+
45+
46+
if __name__ == "__main__":
47+
from click.testing import CliRunner
48+
49+
from flytekit.clis.sdk_in_container import pyflyte
50+
51+
runner = CliRunner()
52+
path = os.path.realpath(__file__)
53+
54+
result = runner.invoke(
55+
pyflyte.main,
56+
[
57+
"--config",
58+
os.path.expanduser("~/.flyte/config-sandbox.yaml"),
59+
"run",
60+
"--remote",
61+
path,
62+
"wf",
63+
"--sleep_duration",
64+
"10s",
65+
"--n_children",
66+
"400",
67+
],
68+
)
69+
print(result.output)
70+
if result.exception:
71+
raise result.exception
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
Sleep Fanout (Dynamic) Example
3+
===============================
4+
5+
Fan out N core-sleep leaves in parallel using @dynamic.
6+
n_children is a runtime input — the @dynamic task runs in a container to
7+
expand the subworkflow, then each leaf runs as core-sleep (no pod).
8+
9+
Requires flytekitplugins-sleep in the container image. The ImageSpec below
10+
installs it from the local wheel via copy + pip install.
11+
12+
Usage (remote):
13+
python sleep_fanout_dynamic.py
14+
"""
15+
16+
import os
17+
from datetime import timedelta
18+
19+
from flytekitplugins.sleep import Sleep
20+
21+
import flytekit as fl
22+
from flytekit import dynamic, task, workflow
23+
24+
fanout_image = fl.ImageSpec(
25+
registry="ghcr.io/machichima",
26+
name="sleep-fanout",
27+
apt_packages=["git"],
28+
packages=["git+https://github.com/machichima/flytekit.git@add-sleep-plugin#subdirectory=plugins/flytekit-sleep"],
29+
)
30+
31+
32+
@task(task_config=Sleep())
33+
def sleep_leaf(duration: timedelta) -> None:
34+
pass
35+
36+
37+
@dynamic(container_image=fanout_image)
38+
def sleep_fanout(n_children: int, sleep_duration: timedelta) -> None:
39+
for _ in range(n_children):
40+
sleep_leaf(duration=sleep_duration)
41+
42+
43+
@workflow
44+
def wf(
45+
n_children: int = 10,
46+
sleep_duration: timedelta = timedelta(seconds=10),
47+
) -> None:
48+
sleep_fanout(n_children=n_children, sleep_duration=sleep_duration)
49+
50+
51+
if __name__ == "__main__":
52+
from click.testing import CliRunner
53+
54+
from flytekit.clis.sdk_in_container import pyflyte
55+
56+
runner = CliRunner()
57+
path = os.path.realpath(__file__)
58+
59+
result = runner.invoke(
60+
pyflyte.main,
61+
[
62+
"--config",
63+
os.path.expanduser("~/.flyte/config-sandbox.yaml"),
64+
"run",
65+
"--remote",
66+
path,
67+
"wf",
68+
"--n_children",
69+
"400",
70+
"--sleep_duration",
71+
"10s",
72+
],
73+
)
74+
print(result.output)
75+
if result.exception:
76+
raise result.exception

plugins/flytekit-sleep/flytekitplugins/__init__.py

Whitespace-only changes.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""
2+
.. currentmodule:: flytekitplugins.sleep
3+
4+
.. autosummary::
5+
:template: custom.rst
6+
:toctree: generated/
7+
8+
Sleep
9+
"""
10+
11+
from .task import Sleep
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Any, Callable, Dict, Optional
5+
6+
from flytekit.configuration import SerializationSettings
7+
from flytekit.core.python_function_task import PythonFunctionTask
8+
from flytekit.core.task import TaskPlugins
9+
10+
11+
@dataclass
12+
class Sleep:
13+
"""
14+
Route a task to the backend ``core-sleep`` plugin.
15+
16+
The sleep duration is provided as a normal task input, not plugin config.
17+
No container is launched; the backend handles the sleep directly.
18+
19+
Usage::
20+
21+
from flytekitplugins.sleep import Sleep
22+
from flytekit import task
23+
from datetime import timedelta
24+
25+
@task(task_config=Sleep())
26+
def sleep_for(duration: timedelta) -> None:
27+
pass # only runs locally; backend executes the sleep
28+
"""
29+
30+
31+
class SleepFunctionTask(PythonFunctionTask[Sleep]):
32+
_TASK_TYPE = "core-sleep"
33+
34+
def __init__(self, task_config: Optional[Sleep], task_function: Callable, **kwargs):
35+
super().__init__(
36+
task_config=task_config or Sleep(),
37+
task_function=task_function,
38+
task_type=self._TASK_TYPE,
39+
**kwargs,
40+
)
41+
42+
def get_custom(self, settings: SerializationSettings) -> Dict[str, Any]:
43+
return {}
44+
45+
46+
TaskPlugins.register_pythontask_plugin(Sleep, SleepFunctionTask)

plugins/flytekit-sleep/setup.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from setuptools import setup
2+
3+
PLUGIN_NAME = "sleep"
4+
5+
microlib_name = f"flytekitplugins-{PLUGIN_NAME}"
6+
7+
__version__ = "0.0.0+develop"
8+
9+
setup(
10+
title="Sleep",
11+
title_expanded="Flytekit Sleep Plugin",
12+
name=microlib_name,
13+
version=__version__,
14+
author="flyteorg",
15+
author_email="admin@flyte.org",
16+
description="This package holds the core-sleep plugin for flytekit",
17+
namespace_packages=["flytekitplugins"],
18+
packages=[f"flytekitplugins.{PLUGIN_NAME}"],
19+
install_requires=["flytekit>=1.15.1"],
20+
license="apache2",
21+
python_requires=">=3.9",
22+
classifiers=[
23+
"Intended Audience :: Science/Research",
24+
"Intended Audience :: Developers",
25+
"License :: OSI Approved :: Apache Software License",
26+
"Programming Language :: Python :: 3.9",
27+
"Programming Language :: Python :: 3.10",
28+
"Programming Language :: Python :: 3.11",
29+
"Topic :: Scientific/Engineering",
30+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
31+
"Topic :: Software Development",
32+
"Topic :: Software Development :: Libraries",
33+
"Topic :: Software Development :: Libraries :: Python Modules",
34+
],
35+
entry_points={"flytekit.plugins": [f"{PLUGIN_NAME}=flytekitplugins.{PLUGIN_NAME}"]},
36+
)

plugins/flytekit-sleep/tests/__init__.py

Whitespace-only changes.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from collections import OrderedDict
2+
from datetime import timedelta
3+
4+
from flytekitplugins.sleep import Sleep
5+
from flytekitplugins.sleep.task import SleepFunctionTask
6+
7+
from flytekit import task, workflow
8+
from flytekit.configuration import Image, ImageConfig, SerializationSettings
9+
from flytekit.extend import get_serializable
10+
11+
default_img = Image(name="default", fqn="test", tag="tag")
12+
serialization_settings = SerializationSettings(
13+
project="proj",
14+
domain="dom",
15+
version="123",
16+
image_config=ImageConfig(default_image=default_img, images=[default_img]),
17+
env={},
18+
)
19+
20+
21+
def test_task_type():
22+
@task(task_config=Sleep())
23+
def sleep_for(duration: timedelta) -> None:
24+
pass
25+
26+
assert isinstance(sleep_for, SleepFunctionTask)
27+
assert sleep_for.task_type == "core-sleep"
28+
29+
30+
def test_serialization():
31+
@task(task_config=Sleep())
32+
def sleep_for(duration: timedelta) -> None:
33+
pass
34+
35+
task_spec = get_serializable(OrderedDict(), serialization_settings, sleep_for)
36+
37+
assert task_spec.template.type == "core-sleep"
38+
assert task_spec.template.custom == {}
39+
40+
inputs = task_spec.template.interface.inputs
41+
assert "duration" in inputs
42+
assert len(inputs) == 1
43+
44+
outputs = task_spec.template.interface.outputs
45+
assert len(outputs) == 0
46+
47+
48+
def test_local_execution_calls_function():
49+
called = []
50+
51+
@task(task_config=Sleep())
52+
def sleep_for(duration: timedelta) -> None:
53+
called.append(duration)
54+
55+
sleep_for(duration=timedelta(seconds=1))
56+
assert called == [timedelta(seconds=1)]
57+
58+
59+
def test_workflow_integration():
60+
@task(task_config=Sleep())
61+
def sleep_for(duration: timedelta) -> None:
62+
pass
63+
64+
@workflow
65+
def wf(duration: timedelta) -> None:
66+
sleep_for(duration=duration)
67+
68+
spec = get_serializable(OrderedDict(), serialization_settings, wf)
69+
assert len(spec.template.nodes) == 1

0 commit comments

Comments
 (0)