"""Reference counting tests that require their own custom fixture. The other reference counting tests use a shared Ray instance across the test module to reduce overheads & overall test runtime. """ # coding: utf-8 import logging import platform import random import sys import time import numpy as np import pytest import ray import ray.cluster_utils from ray._common.test_utils import ( SignalActor, fetch_prometheus_metrics, wait_for_condition, ) from ray._private.internal_api import memory_summary logger = logging.getLogger(__name__) def _fill_object_store_and_get(obj, succeed=True, object_MiB=20, num_objects=5): for _ in range(num_objects): ray.put(np.zeros(object_MiB * 1024 * 1024, dtype=np.uint8)) if type(obj) is bytes: obj = ray.ObjectRef(obj) if succeed: wait_for_condition( lambda: ray._private.worker.global_worker.core_worker.object_exists(obj) ) else: wait_for_condition( lambda: not ray._private.worker.global_worker.core_worker.object_exists(obj) ) @pytest.mark.skipif(platform.system() in ["Windows"], reason="Failing on Windows.") def test_object_unpin(ray_start_cluster): nodes = [] cluster = ray_start_cluster head_node = cluster.add_node( num_cpus=0, object_store_memory=100 * 1024 * 1024, _system_config={ "subscriber_timeout_ms": 100, "health_check_initial_delay_ms": 0, "health_check_period_ms": 1000, "health_check_failure_threshold": 5, }, ) ray.init(address=cluster.address) # Add worker nodes. for i in range(2): nodes.append( cluster.add_node( num_cpus=1, resources={f"node_{i}": 1}, object_store_memory=100 * 1024 * 1024, ) ) cluster.wait_for_nodes() one_mb_array = np.ones(1 * 1024 * 1024, dtype=np.uint8) ten_mb_array = np.ones(10 * 1024 * 1024, dtype=np.uint8) @ray.remote class ObjectsHolder: def __init__(self): self.ten_mb_objs = [] self.one_mb_objs = [] def put_10_mb(self): self.ten_mb_objs.append(ray.put(ten_mb_array)) def put_1_mb(self): self.one_mb_objs.append(ray.put(one_mb_array)) def pop_10_mb(self): if len(self.ten_mb_objs) == 0: return False self.ten_mb_objs.pop() return True def pop_1_mb(self): if len(self.one_mb_objs) == 0: return False self.one_mb_objs.pop() return True # Head node contains 11MB of data. one_mb_arrays = [] ten_mb_arrays = [] one_mb_arrays.append(ray.put(one_mb_array)) ten_mb_arrays.append(ray.put(ten_mb_array)) def check_memory(mb): return f"Plasma memory usage {mb} MiB" in memory_summary( address=head_node.address, stats_only=True ) def wait_until_node_dead(node): for n in ray.nodes(): if n["ObjectStoreSocketName"] == node.address_info["object_store_address"]: return not n["Alive"] return False wait_for_condition(lambda: check_memory(11)) # Pop one mb array and see if it works. one_mb_arrays.pop() wait_for_condition(lambda: check_memory(10)) # Pop 10 MB. ten_mb_arrays.pop() wait_for_condition(lambda: check_memory(0)) # Put 11 MB for each actor. # actor 1: 1MB + 10MB # actor 2: 1MB + 10MB actor_on_node_1 = ObjectsHolder.options(resources={"node_0": 1}).remote() actor_on_node_2 = ObjectsHolder.options(resources={"node_1": 1}).remote() ray.get(actor_on_node_1.put_1_mb.remote()) ray.get(actor_on_node_1.put_10_mb.remote()) ray.get(actor_on_node_2.put_1_mb.remote()) ray.get(actor_on_node_2.put_10_mb.remote()) wait_for_condition(lambda: check_memory(22)) # actor 1: 10MB # actor 2: 1MB ray.get(actor_on_node_1.pop_1_mb.remote()) ray.get(actor_on_node_2.pop_10_mb.remote()) wait_for_condition(lambda: check_memory(11)) # The second node is dead, and actor 2 is dead. cluster.remove_node(nodes[1], allow_graceful=False) wait_for_condition(lambda: wait_until_node_dead(nodes[1])) wait_for_condition(lambda: check_memory(10)) # The first actor is dead, so object should be GC'ed. ray.kill(actor_on_node_1) wait_for_condition(lambda: check_memory(0)) @pytest.mark.skipif(platform.system() in ["Windows"], reason="Failing on Windows.") def test_object_unpin_stress(ray_start_cluster): nodes = [] cluster = ray_start_cluster cluster.add_node( num_cpus=1, resources={"head": 1}, object_store_memory=1000 * 1024 * 1024 ) ray.init(address=cluster.address) # Add worker nodes. for i in range(2): nodes.append( cluster.add_node( num_cpus=1, resources={f"node_{i}": 1}, object_store_memory=1000 * 1024 * 1024, ) ) cluster.wait_for_nodes() one_mb_array = np.ones(1 * 1024 * 1024, dtype=np.uint8) ten_mb_array = np.ones(10 * 1024 * 1024, dtype=np.uint8) @ray.remote class ObjectsHolder: def __init__(self): self.ten_mb_objs = [] self.one_mb_objs = [] def put_10_mb(self): self.ten_mb_objs.append(ray.put(ten_mb_array)) def put_1_mb(self): self.one_mb_objs.append(ray.put(one_mb_array)) def pop_10_mb(self): if len(self.ten_mb_objs) == 0: return False self.ten_mb_objs.pop() return True def pop_1_mb(self): if len(self.one_mb_objs) == 0: return False self.one_mb_objs.pop() return True def get_obj_size(self): return len(self.ten_mb_objs) * 10 + len(self.one_mb_objs) actor_on_node_1 = ObjectsHolder.options(resources={"node_0": 1}).remote() actor_on_node_2 = ObjectsHolder.options(resources={"node_1": 1}).remote() actor_on_head_node = ObjectsHolder.options(resources={"head": 1}).remote() ray.get(actor_on_node_1.get_obj_size.remote()) ray.get(actor_on_node_2.get_obj_size.remote()) ray.get(actor_on_head_node.get_obj_size.remote()) def random_ops(actors): r = random.random() for actor in actors: if r <= 0.25: actor.put_10_mb.remote() elif r <= 0.5: actor.put_1_mb.remote() elif r <= 0.75: actor.pop_10_mb.remote() else: actor.pop_1_mb.remote() total_iter = 15 for _ in range(total_iter): random_ops([actor_on_node_1, actor_on_node_2, actor_on_head_node]) # Simulate node dead. cluster.remove_node(nodes[1]) for _ in range(total_iter): random_ops([actor_on_node_1, actor_on_head_node]) total_size = sum( [ ray.get(actor_on_node_1.get_obj_size.remote()), ray.get(actor_on_head_node.get_obj_size.remote()), ] ) wait_for_condition( lambda: ( (f"Plasma memory usage {total_size} MiB") in memory_summary(stats_only=True) ) ) @pytest.mark.parametrize("inline_args", [True, False]) def test_inlined_nested_refs(ray_start_cluster, inline_args): cluster = ray_start_cluster config = {} if not inline_args: config["max_direct_call_object_size"] = 0 cluster.add_node( num_cpus=2, object_store_memory=100 * 1024 * 1024, _system_config=config ) ray.init(address=cluster.address) @ray.remote class Actor: def __init__(self): return def nested(self): return ray.put("x") @ray.remote def nested_nested(a): return a.nested.remote() @ray.remote def foo(ref): time.sleep(1) return ray.get(ref) a = Actor.remote() nested_nested_ref = nested_nested.remote(a) # We get nested_ref's value directly from its owner. nested_ref = ray.get(nested_nested_ref) del nested_nested_ref x = foo.remote(nested_ref) del nested_ref ray.get(x) # https://github.com/ray-project/ray/issues/17553 @pytest.mark.parametrize("inline_args", [True, False]) def test_return_nested_ids(shutdown_only, inline_args): config = dict() if inline_args: config["max_direct_call_object_size"] = 100 * 1024 * 1024 else: config["max_direct_call_object_size"] = 0 ray.init(object_store_memory=100 * 1024 * 1024, _system_config=config) class Nested: def __init__(self, blocks): self._blocks = blocks @ray.remote def echo(fn): return fn() @ray.remote def create_nested(): refs = [ray.put(np.random.random(1024 * 1024)) for _ in range(10)] return Nested(refs) @ray.remote def test(): ref = create_nested.remote() result1 = ray.get(ref) del ref result = echo.remote(lambda: result1) # noqa del result1 time.sleep(5) block = ray.get(result)._blocks[0] print(ray.get(block)) ray.get(test.remote()) def _check_refcounts(expected): actual = ray._private.worker.global_worker.core_worker.get_all_reference_counts() assert len(expected) == len(actual) for object_ref, (local, submitted) in expected.items(): hex_id = object_ref.hex().encode("ascii") assert hex_id in actual assert local == actual[hex_id]["local"] assert submitted == actual[hex_id]["submitted"] def test_out_of_band_serialized_object_ref(ray_start_regular): assert ( len(ray._private.worker.global_worker.core_worker.get_all_reference_counts()) == 0 ) obj_ref = ray.put("hello") _check_refcounts({obj_ref: (1, 0)}) obj_ref_str = ray.cloudpickle.dumps(obj_ref) _check_refcounts({obj_ref: (2, 0)}) del obj_ref assert ( len(ray._private.worker.global_worker.core_worker.get_all_reference_counts()) == 1 ) assert ray.get(ray.cloudpickle.loads(obj_ref_str)) == "hello" def test_captured_object_ref(ray_start_regular): captured_id = ray.put(np.zeros(1024, dtype=np.uint8)) @ray.remote def f(signal): ray.get(signal.wait.remote()) ray.get(captured_id) # noqa: F821 signal = SignalActor.remote() obj_ref = f.remote(signal) # Delete local references. del f del captured_id # Test that the captured object ref is pinned despite having no local # references. ray.get(signal.send.remote()) _fill_object_store_and_get(obj_ref) captured_id = ray.put(np.zeros(1024, dtype=np.uint8)) @ray.remote class Actor: def get(self, signal): ray.get(signal.wait.remote()) ray.get(captured_id) # noqa: F821 signal = SignalActor.remote() actor = Actor.remote() obj_ref = actor.get.remote(signal) # Delete local references. del Actor del captured_id # Test that the captured object ref is pinned despite having no local # references. ray.get(signal.send.remote()) _fill_object_store_and_get(obj_ref) def test_borrowed_id_failure_while_pulling(ray_start_cluster): """The driver creates an object and passes the ref to actor A via an actor task. That task passes the ref on to B, then A kills itself before finishing the task, so the task never reports the borrower B to the driver. The driver can therefore erase the ref while B is still pulling the object, and B's get must then fail promptly instead of hanging. """ cluster = ray_start_cluster cluster.add_node( num_cpus=1, resources={"head_node": 1}, object_store_memory=100 * 1024 * 1024, _system_config={ "testing_asio_delay_us": ( "ObjectManagerService.grpc_server.Pull=5000000000:5000000000" ), "metrics_report_interval_ms": 200, }, ) ray.init(address=cluster.address) cluster.add_node( num_cpus=1, resources={"worker_node": 1}, object_store_memory=100 * 1024 * 1024, ) cluster.wait_for_nodes() @ray.remote(resources={"head_node": 1}) class A: def pass_ref(self, ref, b): ray.get(b.receive_ref.remote(ref)) sys.exit(-1) @ray.remote(resources={"worker_node": 1}) class B: def __init__(self): self.ref = None def receive_ref(self, ref): self.ref = ref[0] def resolve_ref(self): with pytest.raises(ray.exceptions.ObjectLostError): ray.get(self.ref) return True def ping(self): return a = A.remote() b = B.remote() ray.get(b.ping.remote()) obj = ray.put(np.zeros(1024 * 1024, dtype=np.uint8)) with pytest.raises(ray.exceptions.RayActorError): ray.get(a.pass_ref.remote([obj], b)) resolved = b.resolve_ref.remote() def pull_in_flight(): (worker_node,) = [n for n in ray.nodes() if "worker_node" in n["Resources"]] address = ( f"{worker_node['NodeManagerAddress']}:{worker_node['MetricsExportPort']}" ) samples = fetch_prometheus_metrics([address]).get( "ray_pull_manager_usage_bytes", [] ) return any( s.labels.get("Type") == "BeingPulled" and s.value > 0 for s in samples ) # make sure to only del the last ref to let ref count go to 0 after actor B's raylet starts pulling that object. wait_for_condition(pull_in_flight, timeout=30) del obj assert ray.get(resolved, timeout=30) if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))