1337 lines
39 KiB
Python
1337 lines
39 KiB
Python
import asyncio
|
|
import os
|
|
import sys
|
|
import time
|
|
from collections import defaultdict
|
|
from functools import reduce
|
|
from typing import Dict
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
from ray._common.test_utils import (
|
|
async_wait_for_condition,
|
|
run_string_as_driver,
|
|
wait_for_condition,
|
|
)
|
|
from ray._private import ray_constants
|
|
from ray._private.state_api_test_utils import (
|
|
_is_actor_task_running,
|
|
get_state_api_manager,
|
|
verify_failed_task,
|
|
wait_for_task_states,
|
|
)
|
|
from ray._private.test_utils import (
|
|
run_string_as_driver_nonblocking,
|
|
wait_for_aggregator_agent_if_enabled,
|
|
)
|
|
from ray.util.state import (
|
|
StateApiClient,
|
|
list_actors,
|
|
list_jobs,
|
|
list_tasks,
|
|
)
|
|
from ray.util.state.common import ListApiOptions, StateResource
|
|
|
|
import psutil
|
|
|
|
# Run every test in this module twice: default and aggregator-enabled
|
|
pytestmark = [
|
|
pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
),
|
|
pytest.mark.usefixtures("event_routing_config"),
|
|
]
|
|
|
|
_SYSTEM_CONFIG = {
|
|
"task_events_report_interval_ms": 100,
|
|
"metrics_report_interval_ms": 200,
|
|
"enable_timeline": False,
|
|
"gcs_mark_task_failed_on_job_done_delay_ms": 1000,
|
|
"gcs_mark_task_failed_on_worker_dead_delay_ms": 1000,
|
|
}
|
|
|
|
|
|
@ray.remote
|
|
class ActorOk:
|
|
def ready(self):
|
|
pass
|
|
|
|
|
|
@ray.remote
|
|
class ActorInitFailed:
|
|
def __init__(self):
|
|
raise ValueError("Actor init is expected to fail")
|
|
|
|
def ready(self):
|
|
pass
|
|
|
|
|
|
def test_actor_creation_task_ok(shutdown_only):
|
|
ray.init(_system_config=_SYSTEM_CONFIG)
|
|
a = ActorOk.remote()
|
|
ray.get(a.ready.remote())
|
|
|
|
def verify():
|
|
tasks = list_tasks(filters=[("name", "=", "ActorOk.__init__")])
|
|
actors = list_actors(filters=[("class_name", "=", "ActorOk")])
|
|
|
|
assert len(tasks) == 1
|
|
assert len(actors) == 1
|
|
actor = actors[0]
|
|
task = tasks[0]
|
|
assert task["state"] == "FINISHED"
|
|
assert task["actor_id"] == actor["actor_id"]
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
def test_actor_creation_task_failed(shutdown_only):
|
|
ray.init(_system_config=_SYSTEM_CONFIG)
|
|
a = ActorInitFailed.remote()
|
|
|
|
with pytest.raises(ray.exceptions.RayActorError):
|
|
ray.get(a.ready.remote())
|
|
|
|
def verify():
|
|
tasks = list_tasks(filters=[("name", "=", "ActorInitFailed.__init__")])
|
|
actors = list_actors(filters=[("class_name", "=", "ActorInitFailed")])
|
|
|
|
assert len(tasks) == 1
|
|
assert len(actors) == 1
|
|
actor = actors[0]
|
|
task = tasks[0]
|
|
assert task["state"] == "FAILED"
|
|
assert task["actor_id"] == actor["actor_id"]
|
|
assert actor["state"] == "DEAD"
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
def test_actor_creation_nested_failure_from_actor(shutdown_only):
|
|
ray.init(_system_config=_SYSTEM_CONFIG)
|
|
|
|
@ray.remote
|
|
class NestedActor:
|
|
def ready(self):
|
|
a = ActorInitFailed.remote()
|
|
ray.get(a.ready.remote())
|
|
|
|
a = NestedActor.remote()
|
|
|
|
with pytest.raises(ray.exceptions.RayTaskError):
|
|
ray.get(a.ready.remote())
|
|
|
|
def verify():
|
|
creation_tasks = list_tasks(filters=[("type", "=", "ACTOR_CREATION_TASK")])
|
|
actors = list_actors()
|
|
|
|
assert len(creation_tasks) == 2
|
|
assert len(actors) == 2
|
|
for actor in actors:
|
|
if "NestedActor" in actor["class_name"]:
|
|
assert actor["state"] == "ALIVE"
|
|
else:
|
|
assert "ActorInitFailed" in actor["class_name"]
|
|
assert actor["state"] == "DEAD"
|
|
|
|
for task in creation_tasks:
|
|
if "ActorInitFailed" in task["name"]:
|
|
assert task["state"] == "FAILED"
|
|
else:
|
|
assert task["name"] == "NestedActor.__init__"
|
|
assert task["state"] == "FINISHED"
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
def test_actor_creation_canceled(shutdown_only):
|
|
ray.init(num_cpus=2, _system_config=_SYSTEM_CONFIG)
|
|
|
|
# An actor not gonna be scheduled
|
|
a = ActorOk.options(num_cpus=10).remote()
|
|
|
|
# Kill it before it could be scheduled.
|
|
ray.kill(a)
|
|
|
|
def verify():
|
|
tasks = list_tasks(filters=[("name", "=", "ActorOk.__init__")])
|
|
actors = list_actors(filters=[("class_name", "=", "ActorOk")])
|
|
|
|
assert len(tasks) == 1
|
|
assert len(actors) == 1
|
|
actor = actors[0]
|
|
task = tasks[0]
|
|
assert task["state"] == "FAILED"
|
|
assert task["actor_id"] == actor["actor_id"]
|
|
assert actor["state"] == "DEAD"
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
def test_handle_driver_tasks(shutdown_only):
|
|
ray.init(_system_config=_SYSTEM_CONFIG)
|
|
|
|
job_id = ray.get_runtime_context().get_job_id()
|
|
script = """
|
|
import ray
|
|
import time
|
|
ray.init("auto")
|
|
|
|
@ray.remote
|
|
def f():
|
|
time.sleep(3)
|
|
|
|
|
|
ray.get(f.remote())
|
|
"""
|
|
run_string_as_driver_nonblocking(script)
|
|
|
|
client = StateApiClient()
|
|
|
|
def list_tasks(exclude_driver):
|
|
return client.list(
|
|
StateResource.TASKS,
|
|
# Filter out this driver
|
|
options=ListApiOptions(
|
|
exclude_driver=exclude_driver, filters=[("job_id", "!=", job_id)]
|
|
),
|
|
raise_on_missing_output=True,
|
|
)
|
|
|
|
# Check driver running
|
|
def verify():
|
|
tasks_with_driver = list_tasks(exclude_driver=False)
|
|
assert len(tasks_with_driver) == 2, tasks_with_driver
|
|
task_types = {task["type"] for task in tasks_with_driver}
|
|
assert task_types == {"NORMAL_TASK", "DRIVER_TASK"}
|
|
|
|
for task in tasks_with_driver:
|
|
if task["type"] == "DRIVER_TASK":
|
|
assert task["state"] == "RUNNING", task
|
|
|
|
return True
|
|
|
|
wait_for_condition(verify, timeout=15, retry_interval_ms=1000)
|
|
|
|
# Check driver finishes
|
|
def verify():
|
|
tasks_with_driver = list_tasks(exclude_driver=False)
|
|
assert len(tasks_with_driver) == 2, tasks_with_driver
|
|
for task in tasks_with_driver:
|
|
if task["type"] == "DRIVER_TASK":
|
|
assert task["state"] == "FINISHED", task
|
|
|
|
tasks_no_driver = list_tasks(exclude_driver=True)
|
|
assert len(tasks_no_driver) == 1, tasks_no_driver
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
def test_fault_tolerance_detached_actor(shutdown_only):
|
|
"""
|
|
Tests that tasks from a detached actor **shouldn't** be marked as failed
|
|
"""
|
|
ray.init(_system_config=_SYSTEM_CONFIG)
|
|
|
|
# Check a detached actor's parent task's failure do not
|
|
# affect the actor's task subtree.
|
|
@ray.remote(max_retries=0)
|
|
def parent_starts_detached_actor():
|
|
@ray.remote
|
|
class DetachedActor:
|
|
def __init__(self):
|
|
pass
|
|
|
|
async def running(self):
|
|
while not self.running:
|
|
await asyncio.sleep(0.1)
|
|
pass
|
|
|
|
async def run(self):
|
|
self.running = True
|
|
await asyncio.sleep(999)
|
|
|
|
# start a detached actor
|
|
a = DetachedActor.options(
|
|
name="detached-actor", lifetime="detached", namespace="test"
|
|
).remote()
|
|
a.run.options(name="detached-actor-run").remote()
|
|
ray.get(a.running.remote())
|
|
|
|
# Enough time for events to be reported to GCS.
|
|
time.sleep(1)
|
|
# fail this parent task
|
|
os._exit(1)
|
|
|
|
with pytest.raises(ray.exceptions.WorkerCrashedError):
|
|
ray.get(parent_starts_detached_actor.remote())
|
|
|
|
wait_for_task_states({"detached-actor-run": "RUNNING"})
|
|
|
|
a = ray.get_actor("detached-actor", namespace="test")
|
|
ray.kill(a)
|
|
|
|
wait_for_task_states({"detached-actor-run": "FAILED"})
|
|
|
|
# Verify failed task marked with expected info.
|
|
wait_for_condition(
|
|
verify_failed_task,
|
|
name="detached-actor-run",
|
|
error_type="WORKER_DIED",
|
|
error_message="The actor is dead because it was killed by `ray.kill`",
|
|
)
|
|
|
|
|
|
def test_fault_tolerance_job_failed(shutdown_only):
|
|
sys_config = _SYSTEM_CONFIG.copy()
|
|
config = {
|
|
"gcs_mark_task_failed_on_job_done_delay_ms": 1000,
|
|
# make worker failure not trigger task failure
|
|
"gcs_mark_task_failed_on_worker_dead_delay_ms": 30000,
|
|
}
|
|
sys_config.update(config)
|
|
ray.init(num_cpus=8, _system_config=sys_config)
|
|
script = """
|
|
import ray
|
|
import time
|
|
|
|
ray.init("auto")
|
|
NUM_CHILD = 2
|
|
|
|
@ray.remote
|
|
def grandchild():
|
|
time.sleep(999)
|
|
|
|
@ray.remote
|
|
def child():
|
|
ray.get(grandchild.remote())
|
|
|
|
@ray.remote
|
|
def finished_child():
|
|
ray.put(1)
|
|
return
|
|
|
|
@ray.remote
|
|
def parent():
|
|
children = [child.remote() for _ in range(NUM_CHILD)]
|
|
finished_children = ray.get([finished_child.remote() for _ in range(NUM_CHILD)])
|
|
ray.get(children)
|
|
|
|
ray.get(parent.remote())
|
|
|
|
"""
|
|
proc = run_string_as_driver_nonblocking(script)
|
|
|
|
def all_tasks_running():
|
|
tasks = list_tasks()
|
|
assert len(tasks) == 7, (
|
|
"Incorrect number of tasks are reported. "
|
|
"Expected length: 1 parent + 2 finished child + 2 failed child + "
|
|
"2 failed grandchild tasks"
|
|
)
|
|
return True
|
|
|
|
wait_for_condition(
|
|
all_tasks_running,
|
|
timeout=10,
|
|
retry_interval_ms=500,
|
|
)
|
|
time_sleep_s = 3
|
|
# Sleep for a while to allow driver job runs async.
|
|
time.sleep(time_sleep_s)
|
|
|
|
proc.kill()
|
|
|
|
def verify():
|
|
tasks = list_tasks(detail=True)
|
|
assert len(tasks) == 7, (
|
|
"Incorrect number of tasks are reported. "
|
|
"Expected length: 1 parent + 2 finished child + 2 failed child + "
|
|
"2 failed grandchild tasks"
|
|
)
|
|
for task in tasks:
|
|
if "finished" in task["func_or_class_name"]:
|
|
assert (
|
|
task["state"] == "FINISHED"
|
|
), f"task {task['func_or_class_name']} has wrong state"
|
|
else:
|
|
assert (
|
|
task["state"] == "FAILED"
|
|
), f"task {task['func_or_class_name']} has wrong state"
|
|
|
|
assert task["error_type"] == "WORKER_DIED"
|
|
assert "Job finishes" in task["error_message"]
|
|
|
|
duration_ms = task["end_time_ms"] - task["start_time_ms"]
|
|
assert (
|
|
# It takes time for the job to run
|
|
duration_ms > time_sleep_s / 2 * 1000
|
|
and duration_ms < 2 * time_sleep_s * 1000
|
|
)
|
|
|
|
return True
|
|
|
|
wait_for_condition(
|
|
verify,
|
|
timeout=10,
|
|
retry_interval_ms=500,
|
|
)
|
|
|
|
|
|
@ray.remote
|
|
def task_finish_child():
|
|
pass
|
|
|
|
|
|
@ray.remote
|
|
def task_sleep_child():
|
|
time.sleep(999)
|
|
|
|
|
|
@ray.remote
|
|
class ChildActor:
|
|
def children(self):
|
|
ray.get(task_finish_child.options(name="task_finish_child").remote())
|
|
ray.get(task_sleep_child.options(name="task_sleep_child").remote())
|
|
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def fail_parent(self):
|
|
ray.get(task_finish_child.options(name="task_finish_child").remote())
|
|
task_sleep_child.options(name="task_sleep_child").remote()
|
|
wait_for_task_states(
|
|
{"task_sleep_child": "RUNNING", "task_finish_child": "FINISHED"}
|
|
)
|
|
raise ValueError("expected to fail.")
|
|
|
|
def child_actor(self):
|
|
a = ChildActor.remote()
|
|
a.children.options(name="children").remote()
|
|
wait_for_task_states({"task_sleep_child": "RUNNING"})
|
|
raise ValueError("expected to fail.")
|
|
|
|
|
|
def test_fault_tolerance_actor_tasks_failed(shutdown_only):
|
|
ray.init(_system_config=_SYSTEM_CONFIG)
|
|
# Test actor tasks
|
|
with pytest.raises(ray.exceptions.RayTaskError):
|
|
a = Actor.remote()
|
|
ray.get(a.fail_parent.options(name="fail_parent").remote())
|
|
|
|
wait_for_task_states(
|
|
{
|
|
"fail_parent": "FAILED",
|
|
"task_finish_child": "FINISHED",
|
|
"task_sleep_child": "RUNNING",
|
|
}
|
|
)
|
|
|
|
|
|
def test_fault_tolerance_nested_actors_failed(shutdown_only):
|
|
ray.init(_system_config=_SYSTEM_CONFIG)
|
|
# Test nested actor tasks
|
|
with pytest.raises(ray.exceptions.RayTaskError):
|
|
a = Actor.remote()
|
|
ray.get(a.child_actor.options(name="child_actor").remote())
|
|
|
|
wait_for_task_states(
|
|
{
|
|
"child_actor": "FAILED",
|
|
"children": "RUNNING",
|
|
"task_finish_child": "FINISHED",
|
|
"task_sleep_child": "RUNNING",
|
|
}
|
|
)
|
|
|
|
|
|
def test_ray_intentional_errors(shutdown_only):
|
|
"""
|
|
Test in the below cases, ray task should not be marked as failure:
|
|
1. ray.actor_exit_actor()
|
|
2. __ray_terminate__.remote()
|
|
3. max calls reached.
|
|
4. task that exit with exit(0)
|
|
"""
|
|
|
|
# Test `exit_actor`
|
|
@ray.remote
|
|
class Actor:
|
|
def ready(self):
|
|
pass
|
|
|
|
def exit(self):
|
|
ray.actor.exit_actor()
|
|
|
|
def exit_normal(self):
|
|
exit(0)
|
|
|
|
# Avoid worker-dead marking racing with max_calls task completion.
|
|
sys_config = _SYSTEM_CONFIG.copy()
|
|
sys_config["gcs_mark_task_failed_on_worker_dead_delay_ms"] = 30000
|
|
ray.init(num_cpus=1, _system_config=sys_config)
|
|
|
|
a = Actor.remote()
|
|
ray.get(a.ready.remote())
|
|
|
|
a.exit.remote()
|
|
|
|
def verify():
|
|
ts = list_tasks(filters=[("name", "=", "Actor.exit")])
|
|
assert len(ts) == 1
|
|
t = ts[0]
|
|
|
|
assert t["state"] == "FINISHED"
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
# Test `__ray_terminate__`
|
|
b = Actor.remote()
|
|
|
|
ray.get(b.ready.remote())
|
|
|
|
b.__ray_terminate__.remote()
|
|
|
|
def verify():
|
|
ts = list_tasks(filters=[("name", "=", "Actor.__ray_terminate__")])
|
|
assert len(ts) == 1
|
|
t = ts[0]
|
|
|
|
assert t["state"] == "FINISHED"
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
# Test max calls reached exiting workers should not fail the task.
|
|
@ray.remote(max_calls=1)
|
|
def f():
|
|
pass
|
|
|
|
for _ in range(3):
|
|
ray.get(f.remote())
|
|
|
|
def verify():
|
|
ts = list_tasks(filters=[("name", "=", "f")])
|
|
assert len(ts) == 3
|
|
workers = set()
|
|
for t in ts:
|
|
assert t["state"] == "FINISHED"
|
|
workers.add(t["worker_id"])
|
|
|
|
assert len(workers) == 3
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
# Test tasks that fail with exit(0)
|
|
@ray.remote
|
|
def g():
|
|
exit(0)
|
|
|
|
def verify():
|
|
ts = list_tasks(filters=[("name", "=", "g")])
|
|
assert len(ts) == 1
|
|
t = ts[0]
|
|
|
|
assert t["state"] == "FINISHED"
|
|
return True
|
|
|
|
c = Actor.remote()
|
|
ray.get(c.ready.remote())
|
|
|
|
c.exit_normal.remote()
|
|
|
|
def verify():
|
|
ts = list_tasks(filters=[("name", "=", "Actor.exit_normal")])
|
|
assert len(ts) == 1
|
|
t = ts[0]
|
|
|
|
assert t["state"] == "FINISHED"
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"exit_type",
|
|
["exit_kill", "exit_exception"],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"actor_or_normal_tasks",
|
|
["normal_task", "actor_task"],
|
|
)
|
|
def test_fault_tolerance_chained_task_fail(
|
|
shutdown_only, exit_type, actor_or_normal_tasks
|
|
):
|
|
ray_context = ray.init(_system_config=_SYSTEM_CONFIG)
|
|
address = ray_context.address_info["address"]
|
|
node_id = ray_context.address_info["node_id"]
|
|
# TODO(#57203): remove this once task event buffer handles this internally.
|
|
wait_for_aggregator_agent_if_enabled(address, node_id)
|
|
|
|
def sleep_or_fail(exit_type=None, wait_for_chain=False):
|
|
if exit_type is None:
|
|
time.sleep(999)
|
|
if wait_for_chain:
|
|
wait_for_task_states({"A": "RUNNING", "B": "RUNNING", "C": "RUNNING"})
|
|
|
|
if exit_type == "exit_kill":
|
|
os._exit(1)
|
|
else:
|
|
raise ValueError("Expected to fail")
|
|
|
|
@ray.remote(max_retries=0)
|
|
def A(exit_type):
|
|
x = B.remote()
|
|
sleep_or_fail(exit_type, wait_for_chain=True)
|
|
ray.get(x)
|
|
|
|
@ray.remote(max_retries=0)
|
|
def B():
|
|
x = C.remote()
|
|
sleep_or_fail()
|
|
ray.get(x)
|
|
|
|
@ray.remote(max_retries=0)
|
|
def C():
|
|
sleep_or_fail()
|
|
|
|
@ray.remote(max_restarts=0, max_task_retries=0)
|
|
class Actor:
|
|
def run(self):
|
|
with pytest.raises(
|
|
(ray.exceptions.RayTaskError, ray.exceptions.WorkerCrashedError)
|
|
):
|
|
ray.get(A.remote(exit_type=exit_type))
|
|
|
|
if actor_or_normal_tasks == "normal_task":
|
|
with pytest.raises(
|
|
(ray.exceptions.RayTaskError, ray.exceptions.WorkerCrashedError)
|
|
):
|
|
ray.get(A.remote(exit_type=exit_type))
|
|
else:
|
|
a = Actor.remote()
|
|
ray.get(a.run.remote())
|
|
|
|
if exit_type == "exit_kill":
|
|
expected_states = {"A": "FAILED", "B": "FAILED", "C": "FAILED"}
|
|
else:
|
|
expected_states = {"A": "FAILED", "B": "RUNNING", "C": "RUNNING"}
|
|
wait_for_task_states(expected_states)
|
|
|
|
|
|
NORMAL_TASK = "normal_task"
|
|
ACTOR_TASK = "actor_task"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"death_list",
|
|
[
|
|
[("A", "exit_kill")],
|
|
[("Abb", "exit_kill"), ("C", "exit_exception")],
|
|
[("D", "exit_kill"), ("Ca", "exit_kill"), ("A", "exit_exception")],
|
|
],
|
|
)
|
|
def test_fault_tolerance_advanced_tree(shutdown_only, death_list):
|
|
"""
|
|
Test fault tolerance for a more complicated execution graph.
|
|
"""
|
|
# Root should always be finish
|
|
execution_graph = {
|
|
"root": [
|
|
(NORMAL_TASK, "A"),
|
|
(ACTOR_TASK, "B"),
|
|
(NORMAL_TASK, "C"),
|
|
(ACTOR_TASK, "D"),
|
|
],
|
|
"A": [(ACTOR_TASK, "Aa"), (NORMAL_TASK, "Ab")],
|
|
"C": [(ACTOR_TASK, "Ca"), (NORMAL_TASK, "Cb")],
|
|
"D": [
|
|
(NORMAL_TASK, "Da"),
|
|
(NORMAL_TASK, "Db"),
|
|
(ACTOR_TASK, "Dc"),
|
|
(ACTOR_TASK, "Dd"),
|
|
],
|
|
"Aa": [],
|
|
"Ab": [(ACTOR_TASK, "Aba"), (NORMAL_TASK, "Abb"), (NORMAL_TASK, "Abc")],
|
|
"Ca": [(ACTOR_TASK, "Caa"), (NORMAL_TASK, "Cab")],
|
|
"Abb": [(NORMAL_TASK, "Abba")],
|
|
"Abc": [],
|
|
"Abba": [(NORMAL_TASK, "Abbaa"), (ACTOR_TASK, "Abbab")],
|
|
"Abbaa": [(NORMAL_TASK, "Abbaaa"), (ACTOR_TASK, "Abbaab")],
|
|
}
|
|
|
|
ray.init(_system_config=_SYSTEM_CONFIG)
|
|
|
|
killed_names = {name for name, _ in death_list}
|
|
exit_killed_names = {name for name, kind in death_list if kind == "exit_kill"}
|
|
|
|
parent_of = {"root": None}
|
|
for p, cs in execution_graph.items():
|
|
for _, c in cs:
|
|
parent_of[c] = p
|
|
|
|
def has_exit_killed_ancestor(name):
|
|
cur = parent_of.get(name)
|
|
while cur is not None:
|
|
if cur in exit_killed_names:
|
|
return True
|
|
cur = parent_of.get(cur)
|
|
return False
|
|
|
|
@ray.remote
|
|
class Killer:
|
|
def __init__(self, death_list):
|
|
self.idx_ = 0
|
|
self.death_list_ = death_list
|
|
self.kill_started = False
|
|
|
|
async def start_killing(self):
|
|
self.kill_started = True
|
|
|
|
async def next_to_kill(self):
|
|
while not self.kill_started:
|
|
await asyncio.sleep(0.5)
|
|
|
|
# if no more tasks to kill - simply sleep to keep all running tasks blocked.
|
|
while self.idx_ >= len(self.death_list_):
|
|
await asyncio.sleep(999)
|
|
|
|
to_kill = self.death_list_[self.idx_]
|
|
return to_kill
|
|
|
|
async def all_killed(self):
|
|
while self.idx_ < len(self.death_list_):
|
|
await asyncio.sleep(0.5)
|
|
|
|
async def advance_next(self):
|
|
self.idx_ += 1
|
|
|
|
def expected_state_for(name):
|
|
if name in killed_names or has_exit_killed_ancestor(name):
|
|
return "FAILED"
|
|
return "RUNNING"
|
|
|
|
def run_children(my_name, killer, execution_graph):
|
|
children = execution_graph.get(my_name, [])
|
|
for task_type, child_name in children:
|
|
if task_type == NORMAL_TASK:
|
|
task.options(name=child_name).remote(
|
|
child_name, killer, execution_graph
|
|
)
|
|
else:
|
|
a = Actor.remote()
|
|
a.actor_task.options(name=child_name).remote(
|
|
child_name, killer, execution_graph
|
|
)
|
|
|
|
# Block until killed
|
|
while True:
|
|
to_fail, fail_kind = ray.get(killer.next_to_kill.remote())
|
|
if to_fail == my_name:
|
|
ray.get(killer.advance_next.remote())
|
|
if fail_kind == "exit_kill":
|
|
os._exit(1)
|
|
elif fail_kind == "exit_exception":
|
|
raise ValueError("Killed by test")
|
|
else:
|
|
assert False, f"Test invalid kill options: {fail_kind}"
|
|
else:
|
|
# Sleep a bit to wait for death.
|
|
time.sleep(0.1)
|
|
|
|
@ray.remote(max_task_retries=0, max_restarts=0)
|
|
class Actor:
|
|
def actor_task(self, my_name, killer, execution_graph):
|
|
run_children(my_name, killer, execution_graph)
|
|
|
|
@ray.remote(max_retries=0)
|
|
def task(my_name, killer, execution_graph):
|
|
run_children(my_name, killer, execution_graph)
|
|
|
|
killer = Killer.remote(death_list)
|
|
# Kick off the workload
|
|
task.options(name="root").remote("root", killer, execution_graph)
|
|
|
|
# Calculate all expected tasks
|
|
tasks = []
|
|
|
|
def add_task_recur(task):
|
|
# Not all tasks are keyed in the execution graph.
|
|
tasks.append(task)
|
|
children = execution_graph.get(task, [])
|
|
for _, child in children:
|
|
add_task_recur(child)
|
|
|
|
add_task_recur("root")
|
|
|
|
def tasks_in_execution_graph_all_running():
|
|
running_tasks = [
|
|
task["name"]
|
|
for task in list_tasks(filters=[("state", "=", "RUNNING")], limit=10000)
|
|
]
|
|
for task in tasks:
|
|
assert task in running_tasks, f"Task {task} not running"
|
|
print("All tasks in execution graph are running")
|
|
return True
|
|
|
|
wait_for_condition(
|
|
tasks_in_execution_graph_all_running, timeout=20, retry_interval_ms=2000
|
|
)
|
|
|
|
# Starts killing :O
|
|
print("start killing")
|
|
ray.get(killer.start_killing.remote())
|
|
print("waiting for all killed")
|
|
ray.get(killer.all_killed.remote())
|
|
print("all killed")
|
|
|
|
expected_states = {name: expected_state_for(name) for name in tasks}
|
|
wait_for_task_states(expected_states)
|
|
|
|
|
|
def check_file(type, task_name, expected_log, expect_no_end=False):
|
|
"""Check file of type = 'out'/'err'"""
|
|
|
|
def _read_file(filepath, start, end):
|
|
with open(filepath, "r") as f:
|
|
f.seek(start, 0)
|
|
if end is None:
|
|
return f.read()
|
|
return f.read(end - start)
|
|
|
|
tasks = list_tasks(filters=[("name", "=", f"{task_name}")], detail=True)
|
|
assert len(tasks) == 1
|
|
task = tasks[0]
|
|
assert task["task_log_info"] is not None
|
|
log_info = task["task_log_info"]
|
|
|
|
file = log_info.get(f"std{type}_file", None)
|
|
start_offset = log_info.get(f"std{type}_start", None)
|
|
end_offset = log_info.get(f"std{type}_end", None)
|
|
if not expect_no_end:
|
|
assert end_offset >= start_offset
|
|
else:
|
|
assert end_offset is None
|
|
assert start_offset > 0, "offsets should be > 0 with magical log prefix"
|
|
actual_log = _read_file(file, start_offset, end_offset)
|
|
assert actual_log == expected_log
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not ray_constants.RAY_ENABLE_RECORD_ACTOR_TASK_LOGGING or sys.platform == "win32",
|
|
reason=(
|
|
"Skipping if not recording task logs offsets, "
|
|
"and windows has logging race issues."
|
|
),
|
|
)
|
|
def test_task_logs_info_basic(shutdown_only):
|
|
"""Test tasks (normal tasks/actor tasks) execution logging
|
|
to files have the correct task log info
|
|
"""
|
|
ray.init(num_cpus=1)
|
|
|
|
def do_print(x):
|
|
out_msg = ""
|
|
err_msg = ""
|
|
for j in range(3):
|
|
out_msg += f"this is log line {j} to stdout from {x}\n"
|
|
print(out_msg, end="", file=sys.stdout)
|
|
|
|
for j in range(3):
|
|
err_msg += f"this is log line {j} to stderr from {x}\n"
|
|
print(err_msg, end="", file=sys.stderr)
|
|
return out_msg, err_msg
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def print(self, x):
|
|
return do_print(x)
|
|
|
|
@ray.remote
|
|
def task_print(x):
|
|
return do_print(x)
|
|
|
|
a = Actor.remote()
|
|
expected_logs = {}
|
|
for j in range(3):
|
|
exp_actor_out, exp_actor_err = ray.get(
|
|
a.print.options(name=f"actor-task-{j}").remote(f"actor-task-{j}")
|
|
)
|
|
expected_logs[f"actor-task-{j}-out"] = exp_actor_out
|
|
expected_logs[f"actor-task-{j}-err"] = exp_actor_err
|
|
|
|
for j in range(3):
|
|
exp_task_out, exp_task_err = ray.get(
|
|
task_print.options(name=f"normal-task-{j}").remote(f"normal-task-{j}")
|
|
)
|
|
expected_logs[f"normal-task-{j}-out"] = exp_task_out
|
|
expected_logs[f"normal-task-{j}-err"] = exp_task_err
|
|
|
|
def verify():
|
|
# verify logs
|
|
for j in range(3):
|
|
check_file("out", f"normal-task-{j}", expected_logs[f"normal-task-{j}-out"])
|
|
check_file("err", f"normal-task-{j}", expected_logs[f"normal-task-{j}-err"])
|
|
check_file("out", f"actor-task-{j}", expected_logs[f"actor-task-{j}-out"])
|
|
check_file("err", f"actor-task-{j}", expected_logs[f"actor-task-{j}-err"])
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not ray_constants.RAY_ENABLE_RECORD_ACTOR_TASK_LOGGING,
|
|
reason="Skipping if not recording task logs offsets.",
|
|
)
|
|
def test_task_logs_info_disabled(shutdown_only, monkeypatch):
|
|
"""Test when redirect disabled, no task log info is available
|
|
due to missing log file
|
|
"""
|
|
with monkeypatch.context() as m:
|
|
m.setenv(ray_constants.LOGGING_REDIRECT_STDERR_ENVIRONMENT_VARIABLE, "1")
|
|
|
|
ray.init(num_cpus=1)
|
|
|
|
@ray.remote
|
|
def f():
|
|
print("hi")
|
|
|
|
ray.get(f.remote())
|
|
|
|
def verify():
|
|
tasks = list_tasks()
|
|
|
|
assert len(tasks) == 1
|
|
assert tasks[0].get("task_log_info") is None
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not ray_constants.RAY_ENABLE_RECORD_ACTOR_TASK_LOGGING,
|
|
reason="Skipping if not recording task logs offsets.",
|
|
)
|
|
def test_task_logs_info_running_task(shutdown_only):
|
|
ray.init(num_cpus=1)
|
|
|
|
@ray.remote
|
|
def do_print_sleep(out_msg, err_msg):
|
|
print(out_msg, end="", file=sys.stdout)
|
|
print(err_msg, end="", file=sys.stderr)
|
|
time.sleep(999)
|
|
|
|
err_msg = "this is log line to stderr before sleeping\n"
|
|
out_msg = "this is log line to stdout before sleeping\n"
|
|
task_name = "log-running-task"
|
|
do_print_sleep.options(name=task_name).remote(out_msg, err_msg)
|
|
|
|
def verify():
|
|
check_file("err", task_name, err_msg, expect_no_end=True)
|
|
check_file("out", task_name, out_msg, expect_no_end=True)
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_task_events_gc_jobs(shutdown_only):
|
|
"""
|
|
Test that later jobs should override previous jobs' task events.
|
|
"""
|
|
ctx = ray.init(
|
|
num_cpus=8,
|
|
_system_config={
|
|
"task_events_max_num_task_in_gcs": 3,
|
|
"task_events_skip_driver_for_test": True,
|
|
"task_events_report_interval_ms": 100,
|
|
},
|
|
)
|
|
|
|
script = """
|
|
import ray
|
|
|
|
ray.init("auto")
|
|
@ray.remote
|
|
def f():
|
|
pass
|
|
|
|
ray.get([f.options(name="f.{task_name}").remote() for _ in range(10)])
|
|
"""
|
|
|
|
gcs_address = ctx.address_info["gcs_address"]
|
|
manager = get_state_api_manager(gcs_address)
|
|
|
|
def get_last_job() -> str:
|
|
jobs = list_jobs()
|
|
return max(jobs, key=lambda x: x.job_id).job_id
|
|
|
|
async def verify_tasks(task_name: str):
|
|
# Query with job directly.
|
|
resp = await manager.list_tasks(
|
|
option=ListApiOptions(
|
|
filters=[("name", "=", task_name), ("job_id", "=", get_last_job())]
|
|
)
|
|
)
|
|
assert len(resp.result) == 3
|
|
assert resp.total == 10
|
|
assert resp.num_after_truncation == 3
|
|
|
|
return True
|
|
|
|
for i in range(10):
|
|
# Run the script
|
|
run_string_as_driver(script.format(task_name=i))
|
|
|
|
await async_wait_for_condition(
|
|
verify_tasks, task_name=f"f.{i}", retry_interval_ms=500
|
|
)
|
|
|
|
|
|
def test_task_events_gc_default_policy(shutdown_only):
|
|
@ray.remote
|
|
def finish_task():
|
|
pass
|
|
|
|
@ray.remote
|
|
def running_task():
|
|
time.sleep(999)
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def actor_finish_task(self):
|
|
pass
|
|
|
|
def actor_running_task(self):
|
|
time.sleep(999)
|
|
|
|
def ready(self):
|
|
pass
|
|
|
|
@ray.remote(max_retries=0)
|
|
def error_task():
|
|
raise ValueError("Expected to fail")
|
|
|
|
ray_context = ray.init(
|
|
num_cpus=8,
|
|
_system_config={
|
|
"task_events_max_num_task_in_gcs": 5,
|
|
"task_events_skip_driver_for_test": True,
|
|
"task_events_report_interval_ms": 100,
|
|
},
|
|
)
|
|
address = ray_context.address_info["address"]
|
|
node_id = ray_context.address_info["node_id"]
|
|
# TODO(#57203): remove this once task event buffer handles this internally.
|
|
wait_for_aggregator_agent_if_enabled(address, node_id)
|
|
|
|
a = Actor.remote()
|
|
ray.get(a.ready.remote())
|
|
# Run 10 and 5 should be evicted
|
|
ray.get([finish_task.remote() for _ in range(10)])
|
|
|
|
def verify_tasks(expected_tasks_cnt: Dict[str, int]):
|
|
tasks = list_tasks(raise_on_missing_output=False)
|
|
total_cnt = reduce(lambda x, y: x + y, expected_tasks_cnt.values())
|
|
assert len(tasks) == total_cnt
|
|
actual_cnt = defaultdict(int)
|
|
for task in tasks:
|
|
actual_cnt[task.name] += 1
|
|
assert actual_cnt == expected_tasks_cnt
|
|
return True
|
|
|
|
wait_for_condition(verify_tasks, expected_tasks_cnt={"finish_task": 5})
|
|
|
|
# Run a few other tasks to occupy the buffer
|
|
running_task.remote()
|
|
error_task.remote()
|
|
|
|
wait_for_condition(
|
|
verify_tasks,
|
|
expected_tasks_cnt={"finish_task": 3, "running_task": 1, "error_task": 1},
|
|
)
|
|
|
|
# Run more finished tasks should not evict those running/error tasks
|
|
for _ in range(3):
|
|
ray.get(a.actor_finish_task.remote())
|
|
|
|
wait_for_condition(
|
|
verify_tasks,
|
|
expected_tasks_cnt={
|
|
"Actor.actor_finish_task": 3,
|
|
"running_task": 1,
|
|
"error_task": 1,
|
|
},
|
|
)
|
|
|
|
# Run actor non-finished tasks should not evict those running/error tasks
|
|
[a.actor_running_task.remote() for _ in range(3)]
|
|
|
|
wait_for_condition(
|
|
verify_tasks,
|
|
expected_tasks_cnt={
|
|
"Actor.actor_running_task": 3,
|
|
"running_task": 1,
|
|
"error_task": 1,
|
|
},
|
|
)
|
|
|
|
# Run more error tasks now should evict the older "running_task"
|
|
[error_task.remote() for _ in range(5)]
|
|
|
|
wait_for_condition(verify_tasks, expected_tasks_cnt={"error_task": 5})
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform != "linux",
|
|
reason="setproctitle has different definitions of `title` on different OSes",
|
|
)
|
|
class TestIsActorTaskRunning:
|
|
def test_main_thread_short_comm(self, ray_start_regular):
|
|
"""
|
|
Test that the main thread's comm is not truncated.
|
|
"""
|
|
|
|
@ray.remote
|
|
class A:
|
|
def check(self):
|
|
pid = os.getpid()
|
|
|
|
def _check():
|
|
assert _is_actor_task_running(pid, "check")
|
|
assert psutil.Process(pid).name() == "ray::A.check"
|
|
assert psutil.Process(pid).cmdline()[0] == "ray::A.check"
|
|
return True
|
|
|
|
wait_for_condition(_check)
|
|
|
|
return pid
|
|
|
|
a = A.remote()
|
|
pid = ray.get(a.check.remote())
|
|
wait_for_condition(lambda: not _is_actor_task_running(pid, "check"))
|
|
|
|
def test_main_thread_long_comm(self, ray_start_regular):
|
|
"""
|
|
In this case, the process comm should be truncated because of the
|
|
name is more than 15 characters ("ray::Actor.check_long_comm"). Hence,
|
|
`psutil.Process.name()` will return `cmdline()[0]` instead.
|
|
"""
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def check_long_comm(self):
|
|
pid = os.getpid()
|
|
|
|
def _check():
|
|
assert _is_actor_task_running(pid, "check_long_comm")
|
|
assert psutil.Process(pid).name() == "ray::Actor.check_long_comm"
|
|
assert (
|
|
psutil.Process(pid).cmdline()[0] == "ray::Actor.check_long_comm"
|
|
)
|
|
return True
|
|
|
|
wait_for_condition(_check)
|
|
|
|
return pid
|
|
|
|
a = Actor.remote()
|
|
pid = ray.get(a.check_long_comm.remote())
|
|
wait_for_condition(lambda: not _is_actor_task_running(pid, "check_long_comm"))
|
|
|
|
def test_main_thread_options_name_short_comm(self, ray_start_regular):
|
|
"""
|
|
The task name is passed in as `options.name`.
|
|
"""
|
|
task_name = "hello"
|
|
|
|
@ray.remote
|
|
class A:
|
|
def check(self):
|
|
pid = os.getpid()
|
|
|
|
def _check():
|
|
assert _is_actor_task_running(pid, task_name)
|
|
assert psutil.Process(pid).name() == f"ray::{task_name}"
|
|
assert psutil.Process(pid).cmdline()[0] == f"ray::{task_name}"
|
|
return True
|
|
|
|
wait_for_condition(_check)
|
|
|
|
return pid
|
|
|
|
a = A.remote()
|
|
pid = ray.get(a.check.options(name=task_name).remote())
|
|
wait_for_condition(lambda: not _is_actor_task_running(pid, task_name))
|
|
|
|
def test_main_thread_options_name_long_comm(self, ray_start_regular):
|
|
"""
|
|
The task name is passed in as `options.name`, and it's longer than 15
|
|
characters. `psutil.Process.name()` will return `cmdline()[0]` instead.
|
|
"""
|
|
task_name = "very_long_task_name_1234567890"
|
|
|
|
@ray.remote
|
|
class A:
|
|
def check(self):
|
|
pid = os.getpid()
|
|
|
|
def _check():
|
|
assert _is_actor_task_running(pid, task_name)
|
|
assert psutil.Process(pid).name() == f"ray::{task_name}"
|
|
assert psutil.Process(pid).cmdline()[0] == f"ray::{task_name}"
|
|
return True
|
|
|
|
wait_for_condition(_check)
|
|
|
|
return pid
|
|
|
|
a = A.remote()
|
|
pid = ray.get(a.check.options(name=task_name).remote())
|
|
wait_for_condition(lambda: not _is_actor_task_running(pid, task_name))
|
|
|
|
def test_default_thread_short_comm(self, ray_start_regular):
|
|
"""
|
|
`check` is not running in the main thread, so `/proc/pid/comm` will
|
|
not be updated but `/proc/pid/cmdline` will still be updated.
|
|
"""
|
|
|
|
@ray.remote(concurrency_groups={"io": 1})
|
|
class A:
|
|
def check(self):
|
|
pid = os.getpid()
|
|
|
|
def _check():
|
|
assert _is_actor_task_running(pid, "check")
|
|
assert psutil.Process(pid).name() == "ray::A"
|
|
assert psutil.Process(pid).cmdline()[0] == "ray::A.check"
|
|
return True
|
|
|
|
wait_for_condition(_check)
|
|
|
|
return pid
|
|
|
|
a = A.remote()
|
|
pid = ray.get(a.check.remote())
|
|
wait_for_condition(lambda: not _is_actor_task_running(pid, "check"))
|
|
|
|
def test_default_thread_long_comm(self, ray_start_regular):
|
|
"""
|
|
`check` is not running in the main thread, so `/proc/pid/comm` will
|
|
not be updated but `/proc/pid/cmdline` will still be updated.
|
|
|
|
In this example, because `ray::VeryLongCommActor` is longer than 15
|
|
characters, `psutil.Process.name()` will return `cmdline()[0]` instead.
|
|
"""
|
|
|
|
@ray.remote(concurrency_groups={"io": 1})
|
|
class VeryLongCommActor:
|
|
def check_long_comm(self):
|
|
pid = os.getpid()
|
|
|
|
def _check():
|
|
assert _is_actor_task_running(pid, "check_long_comm")
|
|
assert (
|
|
psutil.Process(pid).name()
|
|
== "ray::VeryLongCommActor.check_long_comm"
|
|
)
|
|
assert (
|
|
psutil.Process(pid).cmdline()[0]
|
|
== "ray::VeryLongCommActor.check_long_comm"
|
|
)
|
|
return True
|
|
|
|
wait_for_condition(_check)
|
|
|
|
return pid
|
|
|
|
a = VeryLongCommActor.remote()
|
|
pid = ray.get(a.check_long_comm.remote())
|
|
wait_for_condition(lambda: not _is_actor_task_running(pid, "check_long_comm"))
|
|
|
|
def test_default_thread_options_name_short_comm(self, ray_start_regular):
|
|
"""
|
|
`check` is not running in the main thread, so `/proc/pid/comm` will
|
|
not be updated but `/proc/pid/cmdline` will still be updated.
|
|
"""
|
|
task_name = "hello"
|
|
|
|
@ray.remote(concurrency_groups={"io": 1})
|
|
class A:
|
|
def check(self):
|
|
pid = os.getpid()
|
|
|
|
def _check():
|
|
assert _is_actor_task_running(pid, task_name)
|
|
assert psutil.Process(pid).name() == "ray::A"
|
|
assert psutil.Process(pid).cmdline()[0] == f"ray::{task_name}"
|
|
return True
|
|
|
|
wait_for_condition(_check)
|
|
return pid
|
|
|
|
a = A.remote()
|
|
pid = ray.get(a.check.options(name=task_name).remote())
|
|
wait_for_condition(lambda: not _is_actor_task_running(pid, task_name))
|
|
|
|
def test_default_thread_options_name_long_comm(self, ray_start_regular):
|
|
task_name = "hello"
|
|
|
|
@ray.remote(concurrency_groups={"io": 1})
|
|
class Actor:
|
|
def check_long_comm(self):
|
|
pid = os.getpid()
|
|
|
|
def _check():
|
|
assert _is_actor_task_running(pid, task_name)
|
|
assert psutil.Process(pid).name() == "ray::Actor"
|
|
assert psutil.Process(pid).cmdline()[0] == f"ray::{task_name}"
|
|
return True
|
|
|
|
wait_for_condition(_check)
|
|
return pid
|
|
|
|
a = Actor.remote()
|
|
pid = ray.get(a.check_long_comm.options(name=task_name).remote())
|
|
wait_for_condition(lambda: not _is_actor_task_running(pid, task_name))
|
|
|
|
def test_default_thread_options_name_long_comm_2(self, ray_start_regular):
|
|
"""
|
|
`/proc/PID/comm` is truncated to 15 characters, so the process title
|
|
is "ray::VeryLongCo". `psutil.Process.name()` doesn't return `cmdline()[0]`
|
|
because `cmdline()[0]` doesn't start with "ray::VeryLongCo". This is the
|
|
implementation detail of `psutil`.
|
|
"""
|
|
task_name = "hello"
|
|
|
|
@ray.remote(concurrency_groups={"io": 1})
|
|
class VeryLongCommActor:
|
|
def check_long_comm(self):
|
|
pid = os.getpid()
|
|
|
|
def _check():
|
|
assert _is_actor_task_running(pid, task_name)
|
|
# The first 15 characters of "ray::VeryLongCommActor"
|
|
assert psutil.Process(pid).name() == "ray::VeryLongCo"
|
|
assert psutil.Process(pid).cmdline()[0] == f"ray::{task_name}"
|
|
return True
|
|
|
|
wait_for_condition(_check)
|
|
return pid
|
|
|
|
a = VeryLongCommActor.remote()
|
|
pid = ray.get(a.check_long_comm.options(name=task_name).remote())
|
|
wait_for_condition(lambda: not _is_actor_task_running(pid, task_name))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|