chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
import ray._private.ray_constants as ray_constants
|
||||
from ray._common.network_utils import parse_address
|
||||
from ray._common.test_utils import (
|
||||
Semaphore,
|
||||
run_string_as_driver,
|
||||
wait_for_condition,
|
||||
)
|
||||
from ray._private.test_utils import (
|
||||
client_test_enabled,
|
||||
external_redis_test_enabled,
|
||||
get_gcs_memory_used,
|
||||
run_string_as_driver_nonblocking,
|
||||
)
|
||||
from ray._raylet import GCS_PID_KEY, GcsClient
|
||||
from ray.experimental.internal_kv import _internal_kv_list
|
||||
from ray.tests.conftest import call_ray_start
|
||||
|
||||
import psutil
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def shutdown_only_with_initialization_check():
|
||||
yield None
|
||||
# The code after the yield will run as teardown code.
|
||||
ray.shutdown()
|
||||
assert not ray.is_initialized()
|
||||
|
||||
|
||||
def test_back_pressure(shutdown_only_with_initialization_check):
|
||||
ray.init()
|
||||
|
||||
signal_actor = Semaphore.options(max_pending_calls=10).remote(value=0)
|
||||
|
||||
try:
|
||||
for i in range(10):
|
||||
signal_actor.acquire.remote()
|
||||
except ray.exceptions.PendingCallsLimitExceeded:
|
||||
assert False
|
||||
|
||||
with pytest.raises(ray.exceptions.PendingCallsLimitExceeded):
|
||||
signal_actor.acquire.remote()
|
||||
|
||||
@ray.remote
|
||||
def release(signal_actor):
|
||||
ray.get(signal_actor.release.remote())
|
||||
return 1
|
||||
|
||||
# Release signal actor through common task,
|
||||
# because actor tasks will be back pressured
|
||||
for i in range(10):
|
||||
ray.get(release.remote(signal_actor))
|
||||
|
||||
# Check whether we can call remote actor normally after
|
||||
# back presssure released.
|
||||
try:
|
||||
signal_actor.acquire.remote()
|
||||
except ray.exceptions.PendingCallsLimitExceeded:
|
||||
assert False
|
||||
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
def function_entry_num(job_id):
|
||||
from ray._private.ray_constants import KV_NAMESPACE_FUNCTION_TABLE
|
||||
|
||||
return (
|
||||
len(
|
||||
_internal_kv_list(
|
||||
b"RemoteFunction:" + job_id, namespace=KV_NAMESPACE_FUNCTION_TABLE
|
||||
)
|
||||
)
|
||||
+ len(
|
||||
_internal_kv_list(
|
||||
b"ActorClass:" + job_id, namespace=KV_NAMESPACE_FUNCTION_TABLE
|
||||
)
|
||||
)
|
||||
+ len(
|
||||
_internal_kv_list(
|
||||
b"FunctionsToRun:" + job_id, namespace=KV_NAMESPACE_FUNCTION_TABLE
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
client_test_enabled(), reason="client api doesn't support namespace right now."
|
||||
)
|
||||
def test_function_table_gc(call_ray_start):
|
||||
"""This test tries to verify that function table is cleaned up
|
||||
after job exits.
|
||||
"""
|
||||
|
||||
def f():
|
||||
data = "0" * 1024 * 1024 # 1MB
|
||||
|
||||
@ray.remote
|
||||
def r():
|
||||
nonlocal data
|
||||
|
||||
@ray.remote
|
||||
class Actor:
|
||||
pass
|
||||
|
||||
return r.remote()
|
||||
|
||||
ray.init(address="auto", namespace="b")
|
||||
|
||||
# It should use > 500MB data
|
||||
ray.get([f() for _ in range(500)])
|
||||
|
||||
# It's not working on win32.
|
||||
if sys.platform != "win32":
|
||||
assert get_gcs_memory_used() > 500 * 1024 * 1024
|
||||
job_id = ray._private.worker.global_worker.current_job_id.hex().encode()
|
||||
assert function_entry_num(job_id) > 0
|
||||
ray.shutdown()
|
||||
|
||||
# now check the function table is cleaned up after job finished
|
||||
ray.init(address="auto", namespace="a")
|
||||
wait_for_condition(lambda: function_entry_num(job_id) == 0, timeout=30)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
client_test_enabled(), reason="client api doesn't support namespace right now."
|
||||
)
|
||||
def test_function_table_gc_actor(call_ray_start):
|
||||
"""If there is a detached actor, the table won't be cleaned up."""
|
||||
ray.init(address="auto", namespace="a")
|
||||
|
||||
@ray.remote
|
||||
class Actor:
|
||||
def ready(self):
|
||||
return
|
||||
|
||||
# If there is a detached actor, the function won't be deleted.
|
||||
a = Actor.options(lifetime="detached", name="a").remote()
|
||||
ray.get(a.ready.remote())
|
||||
job_id = ray._private.worker.global_worker.current_job_id.hex().encode()
|
||||
ray.shutdown()
|
||||
|
||||
ray.init(address="auto", namespace="b")
|
||||
with pytest.raises(Exception):
|
||||
wait_for_condition(lambda: function_entry_num(job_id) == 0)
|
||||
a = ray.get_actor("a", namespace="a")
|
||||
ray.kill(a)
|
||||
wait_for_condition(lambda: function_entry_num(job_id) == 0)
|
||||
|
||||
# If there is not a detached actor, it'll be deleted when the job finishes.
|
||||
a = Actor.remote()
|
||||
ray.get(a.ready.remote())
|
||||
job_id = ray._private.worker.global_worker.current_job_id.hex().encode()
|
||||
ray.shutdown()
|
||||
ray.init(address="auto", namespace="c")
|
||||
wait_for_condition(lambda: function_entry_num(job_id) == 0)
|
||||
|
||||
|
||||
def test_node_liveness_after_restart(ray_start_cluster):
|
||||
cluster = ray_start_cluster
|
||||
cluster.add_node()
|
||||
ray.init(cluster.address)
|
||||
worker = cluster.add_node(node_manager_port=9037)
|
||||
wait_for_condition(lambda: len([n for n in ray.nodes() if n["Alive"]]) == 2)
|
||||
|
||||
cluster.remove_node(worker)
|
||||
wait_for_condition(lambda: len([n for n in ray.nodes() if n["Alive"]]) == 1)
|
||||
worker = cluster.add_node(node_manager_port=9037)
|
||||
wait_for_condition(lambda: len([n for n in ray.nodes() if n["Alive"]]) == 2)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform != "linux",
|
||||
reason="This test is only run on linux machines.",
|
||||
)
|
||||
def test_worker_oom_score(shutdown_only):
|
||||
@ray.remote
|
||||
def get_oom_score():
|
||||
pid = os.getpid()
|
||||
with open(f"/proc/{pid}/oom_score", "r") as f:
|
||||
oom_score = f.read()
|
||||
return int(oom_score)
|
||||
|
||||
assert ray.get(get_oom_score.remote()) >= 1000
|
||||
|
||||
|
||||
call_ray_start_2 = call_ray_start
|
||||
|
||||
|
||||
@pytest.mark.skipif(not external_redis_test_enabled(), reason="Only valid in redis env")
|
||||
@pytest.mark.parametrize(
|
||||
"call_ray_start,call_ray_start_2",
|
||||
[
|
||||
(
|
||||
{"env": {"RAY_external_storage_namespace": "A1"}},
|
||||
{"env": {"RAY_external_storage_namespace": "A2"}},
|
||||
)
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_storage_isolation(external_redis, call_ray_start, call_ray_start_2):
|
||||
script = """
|
||||
import ray
|
||||
ray.init("{address}", namespace="a")
|
||||
@ray.remote
|
||||
class A:
|
||||
def ready(self):
|
||||
return {val}
|
||||
pass
|
||||
|
||||
a = A.options(lifetime="detached", name="A").remote()
|
||||
assert ray.get(a.ready.remote()) == {val}
|
||||
assert ray.get_runtime_context().get_job_id() == '01000000'
|
||||
"""
|
||||
run_string_as_driver(script.format(address=call_ray_start, val=1))
|
||||
run_string_as_driver(script.format(address=call_ray_start_2, val=2))
|
||||
|
||||
script = """
|
||||
import ray
|
||||
ray.init("{address}", namespace="a")
|
||||
a = ray.get_actor(name="A")
|
||||
assert ray.get(a.ready.remote()) == {val}
|
||||
assert ray.get_runtime_context().get_job_id() == '02000000'
|
||||
"""
|
||||
run_string_as_driver(script.format(address=call_ray_start, val=1))
|
||||
run_string_as_driver(script.format(address=call_ray_start_2, val=2))
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="Only works on linux.")
|
||||
def test_gcs_connection_no_leak(ray_start_cluster):
|
||||
cluster = ray_start_cluster
|
||||
head_node = cluster.add_node()
|
||||
|
||||
gcs_server_process = head_node.all_processes["gcs_server"][0].process
|
||||
gcs_server_pid = gcs_server_process.pid
|
||||
|
||||
def get_gcs_num_of_connections():
|
||||
p = psutil.Process(gcs_server_pid)
|
||||
num_connections = len(p.connections())
|
||||
print(">>", num_connections)
|
||||
return num_connections
|
||||
|
||||
@ray.remote
|
||||
class GcsKVActor:
|
||||
def __init__(self, address):
|
||||
self.gcs_client = GcsClient(address=address)
|
||||
self.gcs_client.internal_kv_get(
|
||||
GCS_PID_KEY.encode(),
|
||||
)
|
||||
|
||||
def ready(self):
|
||||
return "WORLD"
|
||||
|
||||
@ray.remote
|
||||
class A:
|
||||
def ready(self):
|
||||
print("HELLO")
|
||||
return "WORLD"
|
||||
|
||||
gcs_kv_actor = None
|
||||
|
||||
with ray.init(cluster.address):
|
||||
# Wait for workers to be ready.
|
||||
gcs_kv_actor = GcsKVActor.remote(cluster.address)
|
||||
_ = ray.get(gcs_kv_actor.ready.remote())
|
||||
# Note: `fds_with_some_workers` need to be recorded *after* `ray.init`, because
|
||||
# a prestarted worker is started on the first driver init. This worker keeps 1
|
||||
# connection to the GCS, and it stays alive even after the driver exits. If
|
||||
# we move this line before `ray.init`, we will find 1 extra connection after
|
||||
# the driver exits.
|
||||
fds_with_some_workers = get_gcs_num_of_connections()
|
||||
num_of_actors = 10
|
||||
actors = [A.remote() for _ in range(num_of_actors)]
|
||||
print(ray.get([t.ready.remote() for t in actors]))
|
||||
|
||||
# Kill the actors
|
||||
del actors
|
||||
|
||||
# Make sure the # of fds opened by the GCS dropped.
|
||||
# This assumes worker processes are not created after the actor worker
|
||||
# processes die.
|
||||
wait_for_condition(lambda: get_gcs_num_of_connections() < fds_with_some_workers)
|
||||
num_fds_after_workers_die = get_gcs_num_of_connections()
|
||||
|
||||
n = cluster.add_node(wait=True)
|
||||
|
||||
# Make sure the # of fds opened by the GCS increased.
|
||||
wait_for_condition(lambda: get_gcs_num_of_connections() > num_fds_after_workers_die)
|
||||
|
||||
cluster.remove_node(n)
|
||||
|
||||
# Make sure the # of fds opened by the GCS dropped.
|
||||
wait_for_condition(lambda: get_gcs_num_of_connections() < fds_with_some_workers)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"call_ray_start",
|
||||
["ray start --head --num-cpus=2"],
|
||||
indirect=True,
|
||||
)
|
||||
def test_demands_when_driver_exits(call_ray_start):
|
||||
script = f"""
|
||||
import ray
|
||||
ray.init(address='{call_ray_start}')
|
||||
|
||||
import os
|
||||
import time
|
||||
@ray.remote(num_cpus=3)
|
||||
def use_gpu():
|
||||
pass
|
||||
|
||||
@ray.remote(num_gpus=10)
|
||||
class A:
|
||||
pass
|
||||
|
||||
A.options(name="a", lifetime="detached").remote()
|
||||
|
||||
print(ray.get([use_gpu.remote(), use_gpu.remote()]))
|
||||
"""
|
||||
|
||||
proc = run_string_as_driver_nonblocking(script)
|
||||
gcs_cli = ray._raylet.GcsClient(address=f"{call_ray_start}")
|
||||
|
||||
def check_demands(n):
|
||||
status = gcs_cli.internal_kv_get(
|
||||
ray._private.ray_constants.DEBUG_AUTOSCALING_STATUS.encode(), namespace=None
|
||||
)
|
||||
import json
|
||||
|
||||
status = json.loads(status.decode())
|
||||
return len(status["load_metrics_report"]["resource_demand"]) == n
|
||||
|
||||
wait_for_condition(lambda: check_demands(2))
|
||||
proc.terminate()
|
||||
wait_for_condition(lambda: check_demands(1))
|
||||
|
||||
|
||||
@pytest.mark.skipif(external_redis_test_enabled(), reason="Only valid in non redis env")
|
||||
def test_redis_not_available(monkeypatch, call_ray_stop_only):
|
||||
monkeypatch.setenv("RAY_redis_db_connect_retries", "5")
|
||||
monkeypatch.setenv("RAY_REDIS_ADDRESS", "localhost:12345")
|
||||
|
||||
p = subprocess.run(
|
||||
"ray start --head",
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
)
|
||||
assert "Could not establish connection to Redis" in p.stderr.decode()
|
||||
assert "Please check " in p.stderr.decode()
|
||||
assert "redis storage is alive or not." in p.stderr.decode()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not external_redis_test_enabled(), reason="Only valid in redis env")
|
||||
def test_redis_wrong_password(monkeypatch, external_redis, call_ray_stop_only):
|
||||
monkeypatch.setenv("RAY_redis_db_connect_retries", "5")
|
||||
p = subprocess.run(
|
||||
"ray start --head --redis-password=1234",
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
assert "RedisError: ERR AUTH <password> called" in p.stderr.decode()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not external_redis_test_enabled(), reason="Only valid in redis env")
|
||||
def test_redis_full(ray_start_cluster_head):
|
||||
import redis
|
||||
|
||||
gcs_address = ray_start_cluster_head.gcs_address
|
||||
redis_addr = os.environ["RAY_REDIS_ADDRESS"]
|
||||
host, port = parse_address(redis_addr)
|
||||
if os.environ.get("TEST_EXTERNAL_REDIS_REPLICAS", "1") != "1":
|
||||
cli = redis.RedisCluster(host, int(port))
|
||||
else:
|
||||
cli = redis.Redis(host, int(port))
|
||||
# Set the max memory to 10MB
|
||||
cli.config_set("maxmemory", 5 * 1024 * 1024)
|
||||
|
||||
gcs_cli = ray._raylet.GcsClient(address=gcs_address)
|
||||
# GCS should fail
|
||||
# GcsClient assumes GCS is HA so it keeps retrying, although GCS is down. We must
|
||||
# set timeout for this.
|
||||
with pytest.raises(ray.exceptions.RpcError):
|
||||
gcs_cli.internal_kv_put(b"A", b"A" * 6 * 1024 * 1024, True, timeout=5)
|
||||
logs_dir = ray_start_cluster_head.head_node._logs_dir
|
||||
|
||||
with open(os.path.join(logs_dir, "gcs_server.err")) as err:
|
||||
assert "OOM command not allowed when used" in err.read()
|
||||
|
||||
|
||||
def test_omp_threads_set_third_party(ray_start_cluster, monkeypatch):
|
||||
###########################
|
||||
# Test the OMP_NUM_THREADS are picked up by 3rd party libraries
|
||||
# when running tasks if no OMP_NUM_THREADS is set by user.
|
||||
# e.g. numpy, numexpr
|
||||
###########################
|
||||
with monkeypatch.context() as m:
|
||||
m.delenv("OMP_NUM_THREADS", raising=False)
|
||||
|
||||
cluster = ray_start_cluster
|
||||
cluster.add_node(num_cpus=4)
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
@ray.remote(num_cpus=2)
|
||||
def f():
|
||||
# Assert numpy using 2 threads for it's parallelism backend.
|
||||
import numpy # noqa: F401
|
||||
from threadpoolctl import threadpool_info
|
||||
|
||||
for pool_info in threadpool_info():
|
||||
assert pool_info["num_threads"] == 2
|
||||
|
||||
import numexpr
|
||||
|
||||
assert numexpr.nthreads == 2
|
||||
return True
|
||||
|
||||
assert ray.get(f.remote())
|
||||
|
||||
|
||||
def test_gcs_fd_usage(shutdown_only):
|
||||
ray.init(
|
||||
_system_config={
|
||||
"prestart_worker_first_driver": False,
|
||||
"enable_worker_prestart": False,
|
||||
},
|
||||
)
|
||||
gcs_process = ray._private.worker._global_node.all_processes["gcs_server"][0]
|
||||
gcs_process = psutil.Process(gcs_process.process.pid)
|
||||
print("GCS connections", len(gcs_process.connections()))
|
||||
|
||||
@ray.remote(runtime_env={"env_vars": {"Hello": "World"}})
|
||||
class A:
|
||||
def f(self):
|
||||
return os.environ.get("Hello")
|
||||
|
||||
# In case there are still some pre-start workers, consume all of them
|
||||
aa = [A.remote() for _ in range(32)]
|
||||
for a in aa:
|
||||
assert ray.get(a.f.remote()) == "World"
|
||||
base_fd_num = len(gcs_process.connections())
|
||||
print("GCS connections", base_fd_num)
|
||||
|
||||
bb = [A.remote() for _ in range(16)]
|
||||
for b in bb:
|
||||
assert ray.get(b.f.remote()) == "World"
|
||||
new_fd_num = len(gcs_process.connections())
|
||||
print("GCS connections", new_fd_num)
|
||||
# each worker has two connections:
|
||||
# GCS -> CoreWorker
|
||||
# CoreWorker -> GCS
|
||||
# Sometimes, there is one more sockets opened. The reason
|
||||
# is still unknown.
|
||||
assert (new_fd_num - base_fd_num) <= len(bb) * 2 + 1
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform != "linux", reason="jemalloc is only prebuilt on linux"
|
||||
)
|
||||
def test_jemalloc_ray_start(monkeypatch, ray_start_cluster):
|
||||
def check_jemalloc_enabled(pid=None):
|
||||
if pid is None:
|
||||
pid = os.getpid()
|
||||
pmap = subprocess.run(
|
||||
["pmap", str(pid)], check=True, text=True, stdout=subprocess.PIPE
|
||||
)
|
||||
return "libjemalloc.so" in pmap.stdout
|
||||
|
||||
# Firstly, remove the LD_PRELOAD and make sure
|
||||
# jemalloc is loaded.
|
||||
monkeypatch.delenv("LD_PRELOAD", False)
|
||||
cluster = ray_start_cluster
|
||||
node = cluster.add_node(num_cpus=1)
|
||||
|
||||
# Make sure raylet/gcs/worker all have jemalloc
|
||||
assert check_jemalloc_enabled(
|
||||
node.all_processes[ray_constants.PROCESS_TYPE_GCS_SERVER][0].process.pid
|
||||
)
|
||||
assert check_jemalloc_enabled(
|
||||
node.all_processes[ray_constants.PROCESS_TYPE_RAYLET][0].process.pid
|
||||
)
|
||||
assert not ray.get(ray.remote(check_jemalloc_enabled).remote())
|
||||
|
||||
ray.shutdown()
|
||||
cluster.shutdown()
|
||||
|
||||
monkeypatch.setenv("LD_PRELOAD", "")
|
||||
node = cluster.add_node(num_cpus=1)
|
||||
# Make sure raylet/gcs/worker all have jemalloc
|
||||
assert not check_jemalloc_enabled(
|
||||
node.all_processes[ray_constants.PROCESS_TYPE_GCS_SERVER][0].process.pid
|
||||
)
|
||||
assert not check_jemalloc_enabled(
|
||||
node.all_processes[ray_constants.PROCESS_TYPE_RAYLET][0].process.pid
|
||||
)
|
||||
assert not ray.get(ray.remote(check_jemalloc_enabled).remote())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-sv", __file__]))
|
||||
Reference in New Issue
Block a user