Files
2026-07-13 13:17:40 +08:00

400 lines
11 KiB
Python

import json
import os
import subprocess
import sys
import unittest
from telnetlib import Telnet
from typing import Union
import pexpect
import pytest
from pexpect.popen_spawn import PopenSpawn
import ray
from ray._common.network_utils import parse_address
from ray._common.test_utils import (
run_string_as_driver,
wait_for_condition,
)
from ray._private import ray_constants, services
from ray.cluster_utils import Cluster, cluster_not_supported
def _pexpect_spawn(cmd: str) -> Union["pexpect.spawn", PopenSpawn]:
"""Handles compatibility for pexpect on Windows."""
if sys.platform == "win32":
p = PopenSpawn(cmd, encoding="utf-8")
else:
p = pexpect.spawn(cmd)
return p
def test_ray_debugger_breakpoint(shutdown_only):
ray.init(num_cpus=1, runtime_env={"env_vars": {"RAY_DEBUG": "legacy"}})
@ray.remote
def f():
ray.util.pdb.set_trace()
return 1
result = f.remote()
wait_for_condition(
lambda: len(
ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
)
> 0
)
active_sessions = ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
assert len(active_sessions) == 1
# Now continue execution:
session = json.loads(
ray.experimental.internal_kv._internal_kv_get(
active_sessions[0], namespace=ray_constants.KV_NAMESPACE_PDB
)
)
host, port = parse_address(session["pdb_address"])
assert host == "localhost" # Should be private by default.
tn = Telnet(host, int(port))
tn.write(b"c\n")
# The message above should cause this to return now.
ray.get(result)
def test_ray_debugger_commands(shutdown_only):
ray.init(num_cpus=2, runtime_env={"env_vars": {"RAY_DEBUG": "legacy"}})
@ray.remote
def f():
"""We support unicode too: 🐛"""
ray.util.pdb.set_trace()
result1 = f.remote()
result2 = f.remote()
wait_for_condition(
lambda: len(
ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
)
> 0
)
# Make sure that calling "continue" in the debugger
# gives back control to the debugger loop:
p = _pexpect_spawn("ray debug")
p.expect("Enter breakpoint index or press enter to refresh: ")
p.sendline("0")
p.expect("-> ray.util.pdb.set_trace()")
p.sendline("ll")
# Cannot use the 🐛 symbol here because pexpect doesn't support
# unicode, but this test also does nicely:
p.expect("unicode")
p.sendline("c")
p.expect("Enter breakpoint index or press enter to refresh: ")
p.sendline("0")
p.expect("-> ray.util.pdb.set_trace()")
p.sendline("c")
ray.get([result1, result2])
def test_ray_debugger_stepping(shutdown_only):
os.environ["RAY_DEBUG"] = "legacy"
ray.init(num_cpus=1, runtime_env={"env_vars": {"RAY_DEBUG": "legacy"}})
@ray.remote
def g():
return None
@ray.remote
def f():
ray.util.pdb.set_trace()
x = g.remote()
return ray.get(x)
result = f.remote()
wait_for_condition(
lambda: len(
ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
)
> 0
)
p = _pexpect_spawn("ray debug")
p.expect("Enter breakpoint index or press enter to refresh: ")
p.sendline("0")
p.expect("-> x = g.remote()")
p.sendline("remote")
p.expect("(Pdb)")
p.sendline("get")
p.expect("(Pdb)")
p.sendline("continue")
# This should succeed now!
ray.get(result)
def test_ray_debugger_recursive(shutdown_only):
os.environ["RAY_DEBUG"] = "legacy"
ray.init(num_cpus=1, runtime_env={"env_vars": {"RAY_DEBUG": "legacy"}})
@ray.remote
def fact(n):
if n < 1:
return n
ray.util.pdb.set_trace()
n_id = fact.remote(n - 1)
return n * ray.get(n_id)
result = fact.remote(5)
wait_for_condition(
lambda: len(
ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
)
> 0
)
p = _pexpect_spawn("ray debug")
p.expect("Enter breakpoint index or press enter to refresh: ")
p.sendline("0")
p.expect("(Pdb)")
p.sendline("remote")
p.expect("(Pdb)")
p.sendline("remote")
p.expect("(Pdb)")
p.sendline("remote")
p.expect("(Pdb)")
p.sendline("remote")
p.expect("(Pdb)")
p.sendline("remote")
p.expect("(Pdb)")
p.sendline("remote")
ray.get(result)
def test_job_exit_cleanup(ray_start_regular):
address = ray_start_regular["address"]
driver_script = """
import time
import ray
ray.init(address="{}", runtime_env={{"env_vars": {{"RAY_DEBUG": "legacy"}}}})
@ray.remote
def f():
ray.util.rpdb.set_trace()
f.remote()
# Give the remote function long enough to actually run.
time.sleep(5)
""".format(
address
)
assert not len(
ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
)
run_string_as_driver(driver_script)
def one_active_session():
return len(
ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
)
wait_for_condition(one_active_session)
# Start the debugger. This should clean up any existing sessions that
# belong to dead jobs.
_ = _pexpect_spawn("ray debug")
def no_active_sessions():
return not len(
ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
)
wait_for_condition(no_active_sessions)
@pytest.mark.skipif(
sys.platform == "win32", reason="Windows does not print '--address' on init"
)
@pytest.mark.parametrize("ray_debugger_external", [False, True])
def test_ray_debugger_public(shutdown_only, call_ray_stop_only, ray_debugger_external):
redis_substring_prefix = "--address='"
cmd = ["ray", "start", "--head", "--num-cpus=1"]
if ray_debugger_external:
cmd.append("--ray-debugger-external")
out = ray._common.utils.decode(
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
)
# Get the redis address from the output.
redis_substring_prefix = "--address='"
address_location = out.find(redis_substring_prefix) + len(redis_substring_prefix)
address = out[address_location:]
address = address.split("'")[0]
ray.init(address=address, runtime_env={"env_vars": {"RAY_DEBUG": "legacy"}})
@ray.remote
def f():
ray.util.pdb.set_trace()
return 1
result = f.remote()
wait_for_condition(
lambda: len(
ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
)
> 0
)
active_sessions = ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
assert len(active_sessions) == 1
session = json.loads(
ray.experimental.internal_kv._internal_kv_get(
active_sessions[0], namespace=ray_constants.KV_NAMESPACE_PDB
)
)
host, port = parse_address(session["pdb_address"])
if ray_debugger_external:
assert host == services.get_node_ip_address(), host
else:
assert host == "localhost", host
# Check that we can successfully connect to both breakpoints.
tn = Telnet(host, int(port))
tn.write(b"c\n")
# The message above should cause this to return now.
ray.get(result)
@pytest.mark.xfail(cluster_not_supported, reason="cluster not supported")
@pytest.mark.parametrize("ray_debugger_external", [False, True])
def test_ray_debugger_public_multi_node(shutdown_only, ray_debugger_external):
c = Cluster(
initialize_head=True,
connect=False,
head_node_args={
"num_cpus": 0,
"num_gpus": 1,
"ray_debugger_external": ray_debugger_external,
},
)
c.add_node(num_cpus=1, ray_debugger_external=ray_debugger_external)
ray.init(runtime_env={"env_vars": {"RAY_DEBUG": "legacy"}})
@ray.remote
def f():
ray.util.pdb.set_trace()
return 1
# num_gpus=1 forces the task onto the head node.
head_node_result = f.options(num_cpus=0, num_gpus=1).remote()
# num_cpus=1 forces the task onto the worker node.
worker_node_result = f.options(num_cpus=1).remote()
wait_for_condition(
lambda: len(
ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
)
== 2
)
active_sessions = ray.experimental.internal_kv._internal_kv_list(
"RAY_PDB_", namespace=ray_constants.KV_NAMESPACE_PDB
)
assert len(active_sessions) == 2
session1 = json.loads(
ray.experimental.internal_kv._internal_kv_get(
active_sessions[0], namespace=ray_constants.KV_NAMESPACE_PDB
)
)
session2 = json.loads(
ray.experimental.internal_kv._internal_kv_get(
active_sessions[1], namespace=ray_constants.KV_NAMESPACE_PDB
)
)
host1, port1 = parse_address(session1["pdb_address"])
if ray_debugger_external:
assert host1 == services.get_node_ip_address(), host1
else:
assert host1 == "localhost", host1
host2, port2 = parse_address(session2["pdb_address"])
if ray_debugger_external:
assert host2 == services.get_node_ip_address(), host2
else:
assert host2 == "localhost", host2
# Check that we can successfully connect to both breakpoints.
tn1 = Telnet(host1, int(port1))
tn1.write(b"c\n")
tn2 = Telnet(host2, int(port2))
tn2.write(b"c\n")
# The messages above should cause these to return now.
ray.get([head_node_result, worker_node_result])
def test_env_var_enables_ray_debugger():
with unittest.mock.patch.dict(os.environ):
os.environ["RAY_DEBUG_POST_MORTEM"] = "1"
assert ray.util.pdb._is_ray_debugger_post_mortem_enabled(), (
"Expected post-mortem Debugger to be enabled when "
"RAY_DEBUG_POST_MORTEM env var is present."
)
with unittest.mock.patch.dict(os.environ):
if "RAY_DEBUG_POST_MORTEM" in os.environ:
del os.environ["RAY_DEBUG_POST_MORTEM"]
assert not ray.util.pdb._is_ray_debugger_post_mortem_enabled(), (
"Expected post-mortem Debugger to be disabled when "
"RAY_DEBUG_POST_MORTEM env var is absent."
)
if __name__ == "__main__":
# Make subprocess happy in bazel.
os.environ["LC_ALL"] = "en_US.UTF-8"
os.environ["LANG"] = "en_US.UTF-8"
sys.exit(pytest.main(["-sv", __file__]))