import os import subprocess import sys from unittest.mock import patch import pytest import ray from ray._common.test_utils import Semaphore, wait_for_condition from ray._private.test_utils import ( client_test_enabled, get_gcs_memory_used, ) from ray.experimental.internal_kv import _internal_kv_list @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_initialized(shutdown_only_with_initialization_check): assert not ray.is_initialized() ray.init(num_cpus=0) assert ray.is_initialized() def test_ray_start_and_stop(): for i in range(10): subprocess.check_call(["ray", "start", "--head"]) subprocess.check_call(["ray", "stop"]) def test_ray_memory(shutdown_only): ray.init(num_cpus=1) subprocess.check_call(["ray", "memory"]) def test_jemalloc_env_var_propagate(): """Test `propagate_jemalloc_env_var`""" gcs_ptype = ray._private.ray_constants.PROCESS_TYPE_GCS_SERVER """ If the shared library path is not specified, it should return an empty dict. """ expected = {} actual = ray._private.services.propagate_jemalloc_env_var( jemalloc_path="", jemalloc_conf="", jemalloc_comps=[], process_type=gcs_ptype ) assert actual == expected actual = ray._private.services.propagate_jemalloc_env_var( jemalloc_path=None, jemalloc_conf="a,b,c", jemalloc_comps=[ray._private.ray_constants.PROCESS_TYPE_GCS_SERVER], process_type=gcs_ptype, ) assert actual == expected """ When the shared library is specified """ library_path = "/abc" expected = {"LD_PRELOAD": library_path, "RAY_LD_PRELOAD_ON_WORKERS": "0"} actual = ray._private.services.propagate_jemalloc_env_var( jemalloc_path=library_path, jemalloc_conf="", jemalloc_comps=[ray._private.ray_constants.PROCESS_TYPE_GCS_SERVER], process_type=gcs_ptype, ) assert actual == expected # comps should be a list type. with pytest.raises(AssertionError): ray._private.services.propagate_jemalloc_env_var( jemalloc_path=library_path, jemalloc_conf="", jemalloc_comps="ray._private.ray_constants.PROCESS_TYPE_GCS_SERVER,", process_type=gcs_ptype, ) # When comps don't match the process_type, it should not contain MALLOC_CONF. actual = ray._private.services.propagate_jemalloc_env_var( jemalloc_path=library_path, jemalloc_conf="", jemalloc_comps=[ray._private.ray_constants.PROCESS_TYPE_RAYLET], process_type=gcs_ptype, ) assert "MALLOC_CONF" not in actual """ When the malloc config is specified """ library_path = "/abc" malloc_conf = "a,b,c" expected = { "LD_PRELOAD": library_path, "MALLOC_CONF": malloc_conf, "RAY_LD_PRELOAD_ON_WORKERS": "0", } actual = ray._private.services.propagate_jemalloc_env_var( jemalloc_path=library_path, jemalloc_conf=malloc_conf, jemalloc_comps=[ray._private.ray_constants.PROCESS_TYPE_GCS_SERVER], process_type=gcs_ptype, ) assert actual == expected @patch.dict(os.environ, {"RAY_LD_PRELOAD_ON_WORKERS": "1"}) def test_enable_jemallc_for_workers(): library_path = "/abc" malloc_conf = "a,b,c" expected = { "LD_PRELOAD": library_path, "MALLOC_CONF": malloc_conf, "RAY_LD_PRELOAD_ON_WORKERS": "1", } actual = ray._private.services.propagate_jemalloc_env_var( jemalloc_path=library_path, jemalloc_conf=malloc_conf, jemalloc_comps=[ray._private.ray_constants.PROCESS_TYPE_WORKER], process_type=ray._private.ray_constants.PROCESS_TYPE_WORKER, ) assert actual == expected 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) if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))