import asyncio import concurrent.futures import sys import time from collections import defaultdict from typing import Set import pytest import ray from ray._common.test_utils import SignalActor, wait_for_condition from ray.exceptions import TaskCancelledError from ray.util.state import list_tasks def test_input_validation(shutdown_only): # Verify force=True is not working. @ray.remote class A: async def f(self): pass a = A.remote() with pytest.raises(ValueError, match="force=True is not supported"): ray.cancel(a.f.remote(), force=True) def test_async_actor_cancel(shutdown_only): """ Test async actor task is canceled and asyncio.CancelledError is raised within a task. """ ray.init(num_cpus=1) @ray.remote class VerifyActor: def __init__(self): self.called = False self.running = False def called(self): self.called = True def set_running(self): self.running = True def is_called(self): return self.called def is_running(self): return self.running def reset(self): self.called = False self.running = False @ray.remote class Actor: async def f(self, verify_actor): try: ray.get(verify_actor.set_running.remote()) await asyncio.sleep(10) except asyncio.CancelledError: # It is False until this except block is finished. assert not asyncio.current_task().cancelled() ray.get(verify_actor.called.remote()) raise except Exception: return True return True v = VerifyActor.remote() a = Actor.remote() for i in range(50): ref = a.f.remote(v) wait_for_condition(lambda: ray.get(v.is_running.remote())) ray.cancel(ref) with pytest.raises(ray.exceptions.TaskCancelledError, match="was cancelled"): ray.get(ref) # Verify asyncio.CancelledError is raised from the actor task. assert ray.get(v.is_running.remote()) assert ray.get(v.is_called.remote()) ray.get(v.reset.remote()) def test_async_actor_client_side_cancel(ray_start_cluster): """ Test a task is cancelled while it is queued on a client side. It should raise ray.exceptions.TaskCancelledError. """ cluster = ray_start_cluster cluster.add_node(num_cpus=0) ray.init(address=cluster.address) @ray.remote(num_cpus=1) class Actor: def __init__(self): self.f_called = False async def g(self, ref): await asyncio.sleep(30) async def f(self): self.f_called = True await asyncio.sleep(5) def is_f_called(self): return self.f_called @ray.remote def f(): time.sleep(100) # Test the case where a task is queued on a client side. # Tasks are not sent until actor is created. a = Actor.remote() ref = a.f.remote() ray.cancel(ref) with pytest.raises(TaskCancelledError): ray.get(ref) cluster.add_node(num_cpus=1) assert not ray.get(a.is_f_called.remote()) # Test the case where it is canceled before dependencies # are resolved. a = Actor.remote() ref_dep_not_resolved = a.g.remote(f.remote()) ray.cancel(ref_dep_not_resolved) with pytest.raises(TaskCancelledError): ray.get(ref_dep_not_resolved) def test_async_actor_server_side_cancel(shutdown_only): """ Test Cancelation when a task is queued on a server side. """ @ray.remote class Actor: async def f(self): await asyncio.sleep(5) async def g(self): await asyncio.sleep(0) a = Actor.options(max_concurrency=1).remote() ray.get(a.__ray_ready__.remote()) ref = a.f.remote() # noqa # Queued on a server side. # Task should not be executed at all. refs = [a.g.remote() for _ in range(100)] wait_for_condition( lambda: len( list_tasks( filters=[ ("name", "=", "Actor.g"), ("STATE", "=", "PENDING_ACTOR_TASK_ORDERING_OR_CONCURRENCY"), ] ) ) == 100 ) for ref in refs: ray.cancel(ref) tasks = list_tasks(filters=[("name", "=", "Actor.g")]) for ref in refs: with pytest.raises(TaskCancelledError, match=ref.task_id().hex()): ray.get(ref) # Verify the task is submitted to the worker and never executed for task in tasks: assert task.state == "PENDING_ACTOR_TASK_ORDERING_OR_CONCURRENCY" def test_async_actor_cancel_after_task_finishes(shutdown_only): @ray.remote class Actor: async def f(self): await asyncio.sleep(5) async def empty(self): pass # Cancel after task finishes a = Actor.options(max_concurrency=1).remote() ref = a.empty.remote() ref2 = a.empty.remote() ray.get([ref, ref2]) ray.cancel(ref) ray.cancel(ref2) # Exceptions shouldn't be raised. ray.get([ref, ref2]) def test_async_actor_cancel_restart(ray_start_cluster, monkeypatch): """ Verify a cancelation works if actor is restarted. """ with monkeypatch.context() as m: # This will slow down the cancelation RPC so that # cancel won't succeed until a node is killed. m.setenv( "RAY_testing_asio_delay_us", "CoreWorkerService.grpc_server.CancelTask=3000000:3000000", ) cluster = ray_start_cluster cluster.add_node(num_cpus=0) ray.init(address=cluster.address) node = cluster.add_node(num_cpus=1) @ray.remote(num_cpus=1, max_restarts=-1, max_task_retries=-1) class Actor: async def f(self): await asyncio.sleep(10) a = Actor.remote() ref = a.f.remote() # This guarantees that a.f.remote() is executed ray.get(a.__ray_ready__.remote()) ray.cancel(ref) cluster.remove_node(node) r, ur = ray.wait([ref]) # When cancel is called, the task won't be retried anymore. # It will raise TaskCancelledError. with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ref) # This will restart actor, but task won't be retried. cluster.add_node(num_cpus=1) # Verify actor is restarted. f should be retried ray.get(a.__ray_ready__.remote()) with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ref) def test_remote_cancel(ray_start_regular): @ray.remote class Actor: async def sleep(self): await asyncio.sleep(1000) @ray.remote def f(refs): ref = refs[0] ray.cancel(ref) a = Actor.remote() sleep_ref = a.sleep.remote() wait_for_condition(lambda: list_tasks(filters=[("name", "=", "Actor.sleep")])) ref = f.remote([sleep_ref]) # noqa with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(sleep_ref) def test_cancel_recursive_tree(shutdown_only): """Verify recursive cancel works for tree-nested tasks. Task A -> Task B -> Task C """ ray.init(num_cpus=16) # Test the tree structure. @ray.remote def child(): for _ in range(5): time.sleep(1) return True @ray.remote class ChildActor: async def child(self): await asyncio.sleep(5) return True @ray.remote class Actor: def __init__(self): self.children_refs = defaultdict(list) def get_children_refs(self, task_id): return self.children_refs[task_id] async def run(self, child_actor, sig): ref1 = child.remote() ref2 = child_actor.child.remote() task_id = ray.get_runtime_context().get_task_id() self.children_refs[task_id].append(ref1) self.children_refs[task_id].append(ref2) await sig.wait.remote() await ref1 await ref2 sig = SignalActor.remote() child_actor = ChildActor.remote() a = Actor.remote() ray.get(a.__ray_ready__.remote()) """ Test the basic case. """ run_ref = a.run.remote(child_actor, sig) task_id = run_ref.task_id().hex() wait_for_condition( lambda: list_tasks(filters=[("task_id", "=", task_id)])[0].state == "RUNNING", timeout=20, ) ray.cancel(run_ref, recursive=True) ray.get(sig.send.remote()) children_refs = ray.get(a.get_children_refs.remote(task_id)) for ref in children_refs + [run_ref]: with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ref) """ Test recursive = False """ run_ref = a.run.remote(child_actor, sig) task_id = run_ref.task_id().hex() wait_for_condition( lambda: list_tasks(filters=[("task_id", "=", task_id)])[0].state == "RUNNING", timeout=20, ) ray.cancel(run_ref, recursive=False) ray.get(sig.send.remote()) children_refs = ray.get(a.get_children_refs.remote(task_id)) for ref in children_refs: assert ray.get(ref) with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(run_ref) """ Test concurrent cases. """ run_refs = [a.run.remote(ChildActor.remote(), sig) for _ in range(10)] task_ids = [] for i, run_ref in enumerate(run_refs): task_id = run_ref.task_id().hex() task_ids.append(task_id) wait_for_condition( lambda task_id=task_id: list_tasks(filters=[("task_id", "=", task_id)])[ 0 ].state == "RUNNING", timeout=20, ) children_refs = ray.get(a.get_children_refs.remote(task_id)) for child_ref in children_refs: task_id = child_ref.task_id().hex() wait_for_condition( lambda task_id=task_id: list_tasks(filters=[("task_id", "=", task_id)])[ 0 ].state == "RUNNING", timeout=20, ) recursive = i % 2 == 0 ray.cancel(run_ref, recursive=recursive) ray.get(sig.send.remote()) for i, task_id in enumerate(task_ids): children_refs = ray.get(a.get_children_refs.remote(task_id)) if i % 2 == 0: for ref in children_refs: with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ref) else: for ref in children_refs: assert ray.get(ref) with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(run_refs[i]) @pytest.mark.parametrize("recursive", [True, False]) def test_cancel_recursive_chain(shutdown_only, recursive): @ray.remote class RecursiveActor: def __init__(self, child=None): self.child = child self.chlid_ref = None async def run(self, sig): if self.child is None: await sig.wait.remote() return True ref = self.child.run.remote(sig) self.child_ref = ref return await ref def get_child_ref(self): return self.child_ref sig = SignalActor.remote() r1 = RecursiveActor.remote() r2 = RecursiveActor.remote(r1) r3 = RecursiveActor.remote(r2) r4 = RecursiveActor.remote(r3) ref = r4.run.remote(sig) ray.get(r4.__ray_ready__.remote()) wait_for_condition( lambda: len(list_tasks(filters=[("name", "=", "RecursiveActor.run")])) == 4 ) ray.cancel(ref, recursive=recursive) ray.get(sig.send.remote()) if recursive: with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ref) with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ray.get(r4.get_child_ref.remote())) with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ray.get(r3.get_child_ref.remote())) with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ray.get(r2.get_child_ref.remote())) else: assert ray.get(ray.get(r2.get_child_ref.remote())) assert ray.get(ray.get(r3.get_child_ref.remote())) assert ray.get(ray.get(r4.get_child_ref.remote())) with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ref) def test_concurrent_submission_and_cancellation(shutdown_only): """Test submitting and then cancelling many tasks concurrently. This is a regression test for race conditions such as: https://github.com/ray-project/ray/issues/52628. """ NUM_TASKS = 2500 @ray.remote(num_cpus=0) class Worker: async def sleep(self, i: int): # NOTE: all tasks should be cancelled, so this won't actually sleep for the # full duration if the test is passing. await asyncio.sleep(30) worker = Worker.remote() # Submit many tasks in parallel to cause queueing on the caller and receiver. with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_TASKS) as executor: futures = [executor.submit(worker.sleep.remote, i) for i in range(NUM_TASKS)] refs = [f.result() for f in concurrent.futures.as_completed(futures)] # Cancel the tasks in reverse order of submission. for ref in reversed(refs): ray.cancel(ref) # Check that all tasks were successfully cancelled (none ran to completion). for ref in refs: with pytest.raises(ray.exceptions.TaskCancelledError): ray.get(ref) def test_is_canceled_sync_actor_task(shutdown_only): """Test that is_canceled() works correctly for sync actor tasks.""" signal_actor = SignalActor.remote() @ray.remote class Actor: def __init__(self): self._was_canceled = False def wait_until_canceled(self): ray.get(signal_actor.wait.remote()) wait_for_condition(lambda: ray.get_runtime_context().is_canceled()) self._was_canceled = True def was_canceled(self) -> bool: return self._was_canceled a = Actor.remote() ref = a.wait_until_canceled.remote() # Wait for the task to be actively waiting on the signal. wait_for_condition(lambda: ray.get(signal_actor.cur_num_waiters.remote()) == 1) # Cancel the task while it's blocked on the signal. ray.cancel(ref, recursive=False) # Now signal the task to unblock. The task result should be `TaskCancelledError`. ray.get(signal_actor.send.remote()) with pytest.raises(TaskCancelledError): ray.get(ref) # Check that `is_canceled` was set correctly. assert ray.get(a.was_canceled.remote()) def test_is_canceled_concurrent_actor_task(shutdown_only): """Test that is_canceled() works correctly for concurrent actor tasks.""" signal_actor = SignalActor.remote() @ray.remote class ConcurrentActor: def __init__(self): self._canceled_task_indices = set() def task_with_cancel_check(self, task_index: int, expect_canceled: bool): ray.get(signal_actor.wait.remote()) if expect_canceled: wait_for_condition(lambda: ray.get_runtime_context().is_canceled()) self._canceled_task_indices.add(task_index) return task_index def get_canceled_task_indices(self) -> Set[int]: return self._canceled_task_indices actor = ConcurrentActor.options(max_concurrency=3).remote() # Submit multiple tasks concurrently. Only task_index=1 will be canceled. refs = [actor.task_with_cancel_check.remote(i, i == 1) for i in range(3)] # Wait for all tasks to be running (waiting on the signal). wait_for_condition(lambda: ray.get(signal_actor.cur_num_waiters.remote()) == 3) # Cancel task_index=1. ray.cancel(refs[1], recursive=False) # Send signal to unblock all tasks. ray.get(signal_actor.send.remote()) # The canceled task should raise TaskCancelledError. with pytest.raises(TaskCancelledError): ray.get(refs[1]) # The other tasks should complete normally. assert ray.get([refs[0], refs[2]]) == [0, 2] # Verify that `is_canceled` was propagated for task_index=1. assert ray.get(actor.get_canceled_task_indices.remote()) == {1} def test_is_canceled_not_supported_in_async_actor(shutdown_only): """Test is_canceled() for async actors.""" @ray.remote class AsyncActor: def __init__(self): self.is_canceled = False async def async_task(self): # is_canceled() doesn't work for async actors if ray.get_runtime_context().is_canceled(): self.is_canceled = True return "canceled" return "completed" def is_canceled(self): return self.is_canceled actor = AsyncActor.remote() ref = actor.async_task.remote() # is_canceled() is not supported for async actors with pytest.raises( RuntimeError, match="This method is not supported in an async actor." ): ray.get(ref) # Verify the state for async actor does NOT change as there's no graceful # termination for async actor task assert not ray.get(actor.is_canceled.remote()) if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))