chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -0,0 +1,620 @@
import os
import platform
import subprocess
import sys
import textwrap
from pathlib import Path
from typing import Set
import pytest
from click.testing import CliRunner
import ray
import ray._common.utils as utils
import ray._private.ray_constants as ray_constants
import ray.scripts.scripts as scripts
from ray._common.test_utils import wait_for_condition
from ray._private.resource_isolation_config import ResourceIsolationConfig
# These tests are intended to run in CI inside a container.
#
# If you want to run this test locally, you will need to create a cgroup that
# the ray can manage and delegate to the correct user.
#
# Run these commands locally before running the test suite:
#
# sudo mkdir -p /sys/fs/cgroup/resource_isolation_test
# sudo chown -R $(whoami):$(whoami) /sys/fs/cgroup/resource_isolation_test/
# sudo chmod -R u+rwx /sys/fs/cgroup/resource_isolation_test/
# echo $$ | sudo tee /sys/fs/cgroup/resource_isolation_test/cgroup.procs
#
# Comment the following line out.
_ROOT_CGROUP = Path("/sys/fs/cgroup")
#
# To run locally, uncomment the following line.
# _ROOT_CGROUP = Path("/sys/fs/cgroup/resource_isolation_test")
# The integration tests assume that the _ROOT_CGROUP exists and that
# the process has read and write access.
#
# This test suite will create the following cgroup hierarchy for the tests
# starting with BASE_CGROUP.
#
# ROOT_CGROUP
# |
# BASE_CGROUP
# / \
# TEST_CGROUP LEAF_CGROUP
# |
# ray-node_<node_id>
# | |
# system user
# | | |
# leaf workers non-ray
#
# NOTE: The test suite does not assume that ROOT_CGROUP is the OS's root cgroup. Therefore,
# 1. setup will migrate all processes from the ROOT_CGROUP -> LEAF_CGROUP
# 2. teardown will migrate all processes from the LEAF_CGROUP -> ROOT_CGROUP
#
# NOTE: BASE_CGROUP will have a randomly generated name to isolate tests from each other.
#
# The test suite assumes that
# 1. cpu, memory controllers are available on ROOT_CGROUP i.e. in the ROOT_CGROUP/cgroup.controllers file.
# 2. All processes inside the base_cgroup can be migrated into the leaf_cgroup to avoid not violating
# the no internal processes contstraint.
#
# All python tests should only have access to the TEST_CGROUP and nothing outside of it.
_BASE_CGROUP = _ROOT_CGROUP / ("testing_" + utils.get_random_alphanumeric_string(5))
_TEST_CGROUP = _BASE_CGROUP / "test"
_LEAF_GROUP = _BASE_CGROUP / "leaf"
_MOUNT_FILE_PATH = "/proc/mounts"
# The names are here to help debug test failures. Tests should
# only use the size of this list. These processes are expected to be moved
# into the the system cgroup.
_EXPECTED_DASHBOARD_MODULES = [
"ray.dashboard.modules.usage_stats.usage_stats_head.UsageStatsHead",
"ray.dashboard.modules.metrics.metrics_head.MetricsHead",
"ray.dashboard.modules.data.data_head.DataHead",
"ray.dashboard.modules.event.event_head.EventHead",
"ray.dashboard.modules.job.job_head.JobHead",
"ray.dashboard.modules.node.node_head.NodeHead",
"ray.dashboard.modules.reporter.reporter_head.ReportHead",
"ray.dashboard.modules.serve.serve_head.ServeHead",
"ray.dashboard.modules.state.state_head.StateHead",
"ray.dashboard.modules.train.train_head.TrainHead",
]
# The list of processes expected to be started in the system cgroup
# with default params for 'ray start' and 'ray.init(...)'
_EXPECTED_SYSTEM_PROCESSES_RAY_START = [
ray_constants.PROCESS_TYPE_DASHBOARD,
ray_constants.PROCESS_TYPE_GCS_SERVER,
ray_constants.PROCESS_TYPE_MONITOR,
ray_constants.PROCESS_TYPE_LOG_MONITOR,
ray_constants.PROCESS_TYPE_RAY_CLIENT_SERVER,
ray_constants.PROCESS_TYPE_RAYLET,
ray_constants.PROCESS_TYPE_DASHBOARD_AGENT,
ray_constants.PROCESS_TYPE_RUNTIME_ENV_AGENT,
]
_EXPECTED_SYSTEM_PROCESSES_RAY_INIT = [
ray_constants.PROCESS_TYPE_DASHBOARD,
ray_constants.PROCESS_TYPE_GCS_SERVER,
ray_constants.PROCESS_TYPE_MONITOR,
ray_constants.PROCESS_TYPE_LOG_MONITOR,
ray_constants.PROCESS_TYPE_RAYLET,
ray_constants.PROCESS_TYPE_DASHBOARD_AGENT,
ray_constants.PROCESS_TYPE_RUNTIME_ENV_AGENT,
]
@pytest.fixture(scope="session", autouse=True)
def test_suite_fixture():
"""Setups up and tears down the cgroup hierachy for the test suite."""
setup_test_suite()
yield
cleanup_test_suite()
def setup_test_suite():
"""Creates the cgroup hierarchy and moves processes out of the _ROOT_CGROUP into the _LEAF_CGROUP.
The setup involves the following steps:
1) Check if the platform is Linux.
2) Check that cgroupv2 is mounted with read, write permissions in unified mode i.e. cgroupv1 is not mounted.
3) Check that the _ROOT_CGROUP exists and has [cpu, memory] controllers available.
4) Create the _BASE_CGROUP, _TEST_CGROUP, and _LEAF_CGROUP respectively.
5) Move processes from the _ROOT_CGROUP to the _LEAF_CGROUP because of the internal processes constraint.
6) Enable [cpu, memory] controllers in the _ROOT_CGROUP, _BASE_CGROUP, and _TEST_CGROUP respectively.
If any of the steps fail, teardown will be run. Teardown will perform a subset of these steps (not the checks), in reverse order.
"""
try:
# 1) If platform is not linux.
assert (
platform.system() == "Linux"
), f"Failed because resource isolation integration tests can only run on Linux and not on {platform.system()}."
# 2) Check that cgroupv2 is mounted in read-write mode in unified mode.
with open(_MOUNT_FILE_PATH, "r") as mount_file:
lines = mount_file.readlines()
found_cgroup_v1 = False
found_cgroup_v2 = False
for line in lines:
found_cgroup_v1 = found_cgroup_v1 or ("cgroup r" in line.strip())
found_cgroup_v2 = found_cgroup_v2 or ("cgroup2 rw" in line.strip())
assert found_cgroup_v2, (
"Failed because cgroupv2 is not mounted on the system in read-write mode."
" See the following documentation for how to enable cgroupv2 properly:"
" https://kubernetes.io/docs/concepts/architecture/cgroups/#linux-distribution-cgroup-v2-support"
)
assert not found_cgroup_v1, (
"Failed because cgroupv2 and cgroupv1 is mounted on this system."
" See the following documentation for how to enable cgroupv2 in properly in unified mode:"
" https://kubernetes.io/docs/concepts/architecture/cgroups/#linux-distribution-cgroup-v2-support"
)
# 3) Check that current user has read-write access to _BASE_CGROUP_PATH by attempting
# to write the current process into it.
root_cgroup_procs_file = _ROOT_CGROUP / "cgroup.procs"
with open(root_cgroup_procs_file, "w") as procs_file:
procs_file.write(str(os.getpid()))
procs_file.flush()
# 4) Check to see that _ROOT_CGROUP has the [cpu, memory] controllers are available.
root_cgroup_controllers_path = _ROOT_CGROUP / "cgroup.controllers"
expected_controllers = {"cpu", "memory"}
with open(root_cgroup_controllers_path, "r") as available_controllers_file:
available_controllers = set(
available_controllers_file.readline().strip().split(" ")
)
assert expected_controllers.issubset(available_controllers), (
f"Failed because the cpu and memory controllers are not available in {root_cgroup_controllers_path}."
" To enable a controller, you need to add it to the cgroup.controllers file of the parent cgroup of {_ROOT_CGROUP}."
" See: https://docs.kernel.org/admin-guide/cgroup-v2.html#enabling-and-disabling."
)
# 5) Create the leaf cgroup and move all processes from _BASE_CGROUP_PATH into it.
os.mkdir(_BASE_CGROUP)
os.mkdir(_TEST_CGROUP)
os.mkdir(_LEAF_GROUP)
# 6) Move all processes into the leaf cgroup.
with open(_ROOT_CGROUP / "cgroup.procs", "r") as root_procs_file, open(
_LEAF_GROUP / "cgroup.procs", "w"
) as leaf_procs_file:
root_cgroup_lines = root_procs_file.readlines()
for line in root_cgroup_lines:
leaf_procs_file.write(line.strip())
leaf_procs_file.flush()
# 7) Enable [cpu, memory] controllers on the base and test cgroup.
with open(
_ROOT_CGROUP / "cgroup.subtree_control", "w"
) as base_subtree_control_file:
base_subtree_control_file.write("+cpu +memory")
base_subtree_control_file.flush()
with open(
_BASE_CGROUP / "cgroup.subtree_control", "w"
) as base_subtree_control_file:
base_subtree_control_file.write("+cpu +memory")
base_subtree_control_file.flush()
with open(
_TEST_CGROUP / "cgroup.subtree_control", "w"
) as test_subtree_control_file:
test_subtree_control_file.write("+cpu +memory")
test_subtree_control_file.flush()
except Exception as e:
print(
f"Failed to setup the test suite with error {str(e)}. Attempting to run teardown."
)
cleanup_test_suite()
def cleanup_test_suite():
"""Cleans up the cgroup hierarchy and moves processes out of the _LEAF_CGROUP into the _ROOT_CGROUP.
The setup involves the following steps:
1) Disable [cpu, memory] controllers in the _ROOT_CGROUP, _BASE_CGROUP, and _TEST_CGROUP respectively.
2) Move processes from the _LEAF_CGROUP to the _ROOT_CGROUP so the hierarchy can be deleted.
3) Create the _BASE_CGROUP, _TEST_CGROUP, and _LEAF_CGROUP respectively.
If any of the steps fail, teardown will fail an assertion.
"""
# 1) Disable the controllers.
try:
with open(
_TEST_CGROUP / "cgroup.subtree_control", "w"
) as test_subtree_control_file:
test_subtree_control_file.write("-cpu -memory")
test_subtree_control_file.flush()
with open(
_BASE_CGROUP / "cgroup.subtree_control", "w"
) as base_subtree_control_file:
base_subtree_control_file.write("-cpu -memory")
base_subtree_control_file.flush()
with open(
_ROOT_CGROUP / "cgroup.subtree_control", "w"
) as base_subtree_control_file:
base_subtree_control_file.write("-cpu -memory")
base_subtree_control_file.flush()
# 2) Move processes back into the root cgroup.
with open(_ROOT_CGROUP / "cgroup.procs", "w") as root_procs_file, open(
_LEAF_GROUP / "cgroup.procs", "r"
) as leaf_procs_file:
leaf_cgroup_lines = leaf_procs_file.readlines()
for line in leaf_cgroup_lines:
root_procs_file.write(line.strip())
root_procs_file.flush()
# 3) Move the current process back into the _ROOT_CGROUP
with open(_ROOT_CGROUP / "cgroup.procs", "w") as root_procs_file, open(
_TEST_CGROUP / "cgroup.procs", "r"
) as test_procs_file:
test_cgroup_lines = test_procs_file.readlines()
for line in test_cgroup_lines:
root_procs_file.write(line.strip())
root_procs_file.flush()
# 3) Delete the cgroups.
os.rmdir(_LEAF_GROUP)
os.rmdir(_TEST_CGROUP)
os.rmdir(_BASE_CGROUP)
except Exception as e:
assert False, (
f"Failed to cleanup test suite's cgroup hierarchy because of {str(e)}."
"You may have to manually clean up the hierachy under ${_ROOT_CGROUP}"
)
@pytest.fixture
def cleanup_ray():
"""Shutdown all ray instances"""
yield
runner = CliRunner()
runner.invoke(scripts.stop)
ray.shutdown()
@pytest.fixture
def ray_shutdown():
yield
ray.shutdown()
def generate_node_id():
"""Returns a random node id."""
return ray.NodeID.from_random().hex()
def assert_cgroup_hierarchy_exists_for_node(
node_id: str, resource_isolation_config: ResourceIsolationConfig
):
"""Asserts that the cgroup hierarchy was created correctly for the node.
The cgroup hierarchy looks like:
_TEST_CGROUP
|
ray-node_<node_id>
| |
system user
| | |
leaf workers non-ray
Args:
node_id: used to find the path of the cgroup subtree
resource_isolation_config: used to verify constraints enabled on the system, workers, and user cgroups
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
system_cgroup = node_cgroup / "system"
system_leaf_cgroup = system_cgroup / "leaf"
user_cgroup = node_cgroup / "user"
workers_cgroup = user_cgroup / "workers"
non_ray_cgroup = user_cgroup / "non-ray"
# 1) Check that the cgroup hierarchy is created correctly for the node.
assert node_cgroup.is_dir()
assert system_cgroup.is_dir()
assert system_leaf_cgroup.is_dir()
assert workers_cgroup.is_dir()
assert user_cgroup.is_dir()
assert non_ray_cgroup.is_dir()
# 2) Verify the constraints are applied correctly.
total_memory = ray._common.utils.get_system_memory()
with open(user_cgroup / "memory.high", "r") as memory_high_file:
contents = memory_high_file.read().strip()
assert contents == str(
total_memory - resource_isolation_config.system_reserved_memory
)
with open(system_cgroup / "memory.low", "r") as memory_low_file:
contents = memory_low_file.read().strip()
assert contents == str(resource_isolation_config.system_reserved_memory)
with open(system_cgroup / "cpu.weight", "r") as cpu_weight_file:
contents = cpu_weight_file.read().strip()
assert contents == str(resource_isolation_config.system_reserved_cpu_weight)
with open(user_cgroup / "cpu.weight", "r") as cpu_weight_file:
contents = cpu_weight_file.read().strip()
assert contents == str(
10000 - resource_isolation_config.system_reserved_cpu_weight
)
def assert_process_in_not_moved_into_ray_cgroups(
node_id: str,
resource_isolation_config: ResourceIsolationConfig,
pid: str,
):
"""Asserts that the system processes were created in the correct cgroup.
Args:
node_id: used to construct the path of the cgroup subtree
resource_isolation_config: used to construct the path of the cgroup
subtree
pid:
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
cgroup_procs_file_paths = [
node_cgroup / "system" / "leaf" / "cgroup.procs",
node_cgroup / "user" / "non-ray" / "cgroup.procs",
node_cgroup / "user" / "workers" / "cgroup.procs",
]
found_pid = False
for file_path in cgroup_procs_file_paths:
with open(file_path, "r") as cgroup_procs_file:
lines = cgroup_procs_file.readlines()
for line in lines:
found_pid = found_pid or (line.strip() == pid)
assert not found_pid
def assert_system_processes_are_in_system_cgroup(
node_id: str,
resource_isolation_config: ResourceIsolationConfig,
expected_count: int,
):
"""Asserts that the system processes were created in the correct cgroup.
Args:
node_id: used to construct the path of the cgroup subtree
resource_isolation_config: used to construct the path of the cgroup
subtree
expected_count: the number of expected system processes.
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
system_cgroup = node_cgroup / "system"
system_leaf_cgroup = system_cgroup / "leaf"
# At least the raylet process is always moved.
with open(system_leaf_cgroup / "cgroup.procs", "r") as cgroup_procs_file:
lines = cgroup_procs_file.readlines()
assert (
len(lines) == expected_count
), f"Expected only system process passed into the raylet. Found {lines}. You may have added a new dashboard module in which case you need to update _EXPECTED_DASHBOARD_MODULES"
def assert_worker_processes_are_in_workers_cgroup(
node_id: str,
resource_isolation_config: ResourceIsolationConfig,
worker_pids: Set[str],
):
"""Asserts that the worker processes were created in the correct cgroup.
Args:
node_id: used to construct the path of the cgroup subtree
resource_isolation_config: used to construct the path of the cgroup
subtree
worker_pids: a set of pids that are expected inside the workers
leaf cgroup.
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
workers_cgroup_procs = node_cgroup / "user" / "workers" / "cgroup.procs"
with open(workers_cgroup_procs, "r") as cgroup_procs_file:
pids_in_cgroup = set()
lines = cgroup_procs_file.readlines()
for line in lines:
pids_in_cgroup.add(line.strip())
assert pids_in_cgroup == worker_pids
def assert_cgroup_hierarchy_cleaned_up_for_node(
node_id: str, resource_isolation_config: ResourceIsolationConfig
):
"""Asserts that the cgroup hierarchy was deleted correctly for the node.
Args:
node_id: used to construct the path of the cgroup subtree
resource_isolation_config: used to construct the path of the cgroup
subtree
"""
base_cgroup_for_node = resource_isolation_config.cgroup_path
node_cgroup = Path(base_cgroup_for_node) / f"ray-node_{node_id}"
# If the root cgroup is deleted, there's no need to check anything else.
assert (
not node_cgroup.is_dir()
), f"Root cgroup node at {node_cgroup} was not deleted. Cgroup cleanup failed. You may have to manually delete the cgroup subtree."
def create_driver_in_internal_namespace():
"""
Returns a driver process that is a part of the '_ray_internal_' namespace.
If the driver is part of the '_ray_internal_' namespace, it will NOT
be moved into the workers cgroup by the raylet when it registers.
The Dashboard ServeHead and JobHead modules are drivers that are
technically system processes and use the '_ray_internal_' namespace and therefore
must not be moved into the workers cgroup on registration.
"""
driver_code = textwrap.dedent(
"""
import ray
import time
ray.init(namespace='_ray_internal_')
time.sleep(3600)
"""
).strip()
second_driver_proc = subprocess.Popen(["python", "-c", driver_code])
return second_driver_proc
# The following tests check for cgroup setup and cleanup with the
# ray cli.
def test_ray_cli_start_invalid_resource_isolation_config(cleanup_ray):
runner = CliRunner()
result = runner.invoke(
scripts.start,
["--cgroup-path=/doesnt/matter"],
)
assert result.exit_code != 0
assert isinstance(result.exception, ValueError)
def test_ray_cli_start_resource_isolation_creates_cgroup_hierarchy_and_cleans_up(
cleanup_ray,
):
cgroup_path = str(_TEST_CGROUP)
object_store_memory = 1024**3
system_reserved_memory = 1024**3
num_cpus = 4
system_reserved_cpu = 1
resource_isolation_config = ResourceIsolationConfig(
cgroup_path=cgroup_path,
enable_resource_isolation=True,
system_reserved_cpu=system_reserved_cpu,
system_reserved_memory=system_reserved_memory,
)
node_id = ray.NodeID.from_random().hex()
os.environ["RAY_OVERRIDE_NODE_ID_FOR_TESTING"] = node_id
runner = CliRunner()
result = runner.invoke(
scripts.start,
[
"--head",
"--num-cpus",
num_cpus,
"--enable-resource-isolation",
"--cgroup-path",
cgroup_path,
"--system-reserved-cpu",
system_reserved_cpu,
"--system-reserved-memory",
system_reserved_memory,
"--object-store-memory",
object_store_memory,
],
)
assert result.exit_code == 0
assert_cgroup_hierarchy_exists_for_node(node_id, resource_isolation_config)
@ray.remote(num_cpus=1)
class Actor:
def __init__(self):
pass
def get_pid(self):
return os.getpid()
second_driver_proc = create_driver_in_internal_namespace()
actor_refs = []
for _ in range(num_cpus):
actor_refs.append(Actor.remote())
worker_pids = set()
worker_pids.add(str(os.getpid()))
for actor in actor_refs:
worker_pids.add(str(ray.get(actor.get_pid.remote())))
assert_system_processes_are_in_system_cgroup(
node_id,
resource_isolation_config,
len(_EXPECTED_SYSTEM_PROCESSES_RAY_START) + len(_EXPECTED_DASHBOARD_MODULES),
)
assert_worker_processes_are_in_workers_cgroup(
node_id, resource_isolation_config, worker_pids
)
assert_process_in_not_moved_into_ray_cgroups(
node_id, resource_isolation_config, second_driver_proc.pid
)
second_driver_proc.kill()
wait_for_condition(lambda: second_driver_proc.wait(), timeout=5)
runner.invoke(scripts.stop)
assert_cgroup_hierarchy_cleaned_up_for_node(node_id, resource_isolation_config)
# The following tests will test integration of resource isolation
# with the ray.init() function.
def test_ray_init_resource_isolation_disabled_by_default(ray_shutdown):
ray.init(address="local")
node = ray._private.worker._global_node
assert node is not None
assert not node.resource_isolation_config.is_enabled()
def test_ray_init_resource_isolation_creates_cgroup_hierarchy_and_cleans_up(
ray_shutdown,
):
cgroup_path = str(_TEST_CGROUP)
system_reserved_cpu = 1
system_reserved_memory = 1024**3
object_store_memory = 1024**3
num_cpus = 4
resource_isolation_config = ResourceIsolationConfig(
enable_resource_isolation=True,
cgroup_path=cgroup_path,
system_reserved_cpu=system_reserved_cpu,
system_reserved_memory=system_reserved_memory,
)
node_id = generate_node_id()
os.environ["RAY_OVERRIDE_NODE_ID_FOR_TESTING"] = node_id
ray.init(
address="local",
num_cpus=num_cpus,
enable_resource_isolation=True,
cgroup_path=cgroup_path,
system_reserved_cpu=system_reserved_cpu,
system_reserved_memory=system_reserved_memory,
object_store_memory=object_store_memory,
)
assert_cgroup_hierarchy_exists_for_node(node_id, resource_isolation_config)
@ray.remote(num_cpus=1)
class Actor:
def __init__(self):
pass
def get_pid(self):
return os.getpid()
actor_refs = []
for _ in range(num_cpus):
actor_refs.append(Actor.remote())
worker_pids = set()
worker_pids.add(str(os.getpid()))
for actor in actor_refs:
worker_pids.add(str(ray.get(actor.get_pid.remote())))
assert_system_processes_are_in_system_cgroup(
node_id,
resource_isolation_config,
len(_EXPECTED_SYSTEM_PROCESSES_RAY_INIT) + len(_EXPECTED_DASHBOARD_MODULES),
)
assert_worker_processes_are_in_workers_cgroup(
node_id, resource_isolation_config, worker_pids
)
ray.shutdown()
assert_cgroup_hierarchy_cleaned_up_for_node(node_id, resource_isolation_config)
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))