项目文件夹

文件
2026-07-13 13:17:40 +08:00

1337 行
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__]))