chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
from typing import Optional, Type
|
||||
|
||||
import pytest
|
||||
|
||||
from ray.air.execution._internal.barrier import Barrier
|
||||
|
||||
|
||||
def _raise(exception_type: Type[Exception] = RuntimeError, msg: Optional[str] = None):
|
||||
def _raise_exception(*args, **kwargs):
|
||||
raise exception_type(msg)
|
||||
|
||||
return _raise_exception
|
||||
|
||||
|
||||
def test_barrier_max_results():
|
||||
"""Test the `max_results` attribute.
|
||||
|
||||
- Set max_results=10
|
||||
- Assert that the barrier completion callback is not invoked with num_results<10
|
||||
- Assert that callback is invoked with num_results=10
|
||||
- Assert that callback is not invoked again when more events arrive
|
||||
- Assert that more events can arrive without triggering the callback after resetting
|
||||
"""
|
||||
barrier = Barrier(max_results=10, on_completion=_raise(AssertionError))
|
||||
|
||||
for i in range(9):
|
||||
barrier.arrive(i)
|
||||
|
||||
assert not barrier.completed
|
||||
|
||||
# Will trigger the on_completion callback
|
||||
with pytest.raises(AssertionError):
|
||||
barrier.arrive(10)
|
||||
|
||||
assert barrier.completed
|
||||
|
||||
assert barrier.num_results == 10
|
||||
|
||||
# Further events will not trigger callback again
|
||||
barrier.arrive(11)
|
||||
|
||||
barrier.reset()
|
||||
|
||||
assert not barrier.completed
|
||||
|
||||
# After flushing more events can arrive
|
||||
barrier.arrive(12)
|
||||
assert barrier.num_results == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,266 @@
|
||||
import random
|
||||
from typing import Any, List, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air import ResourceRequest
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.air.execution._internal import Barrier
|
||||
from ray.air.execution._internal.actor_manager import RayActorManager
|
||||
from ray.air.execution._internal.tracked_actor import TrackedActor
|
||||
from ray.exceptions import RayActorError
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ray_start_4_cpus():
|
||||
address_info = ray.init(num_cpus=4)
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@ray.remote
|
||||
class Actor:
|
||||
"""Simple actor for testing an execution flow.
|
||||
|
||||
This actor can fail in these ways:
|
||||
|
||||
1. On init if ``actor_init_kill`` is passed as a kwarg
|
||||
2. On setup_1() if ``actor_setup_kill`` is passed as a kwarg (RayActorError)
|
||||
3. On setup_1() if ``actor_setup_fail`` is passed as a kwarg (RayTaskError)
|
||||
4. On train() if ``actor_train_kill`` is passed as a kwarg (RayTaskError)
|
||||
5. On train() if ``actor_train_fail`` is passed as a kwarg (RayTaskError)
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
if self.kwargs.get("actor_init_kill"):
|
||||
raise RuntimeError("INIT")
|
||||
|
||||
def get_kwargs(self):
|
||||
return self.kwargs
|
||||
|
||||
def setup_1(self):
|
||||
if self.kwargs.get("actor_setup_kill"):
|
||||
raise SystemExit
|
||||
|
||||
if self.kwargs.get("actor_setup_fail"):
|
||||
raise RuntimeError("Setup")
|
||||
|
||||
return True
|
||||
|
||||
def setup_2(self):
|
||||
return True
|
||||
|
||||
def train(self, value: float) -> float:
|
||||
if value == 4:
|
||||
if self.kwargs.get("actor_train_kill"):
|
||||
# SystemExit will invoke a RayActorError
|
||||
raise SystemExit
|
||||
|
||||
if self.kwargs.get("actor_train_fail"):
|
||||
# RuntimeError will invoke a RayTaskError
|
||||
raise RuntimeError("TASK")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class TrainFlow:
|
||||
"""This is a Ray Train-like execution flow.
|
||||
|
||||
- We want to run 4 actors in total ("trials")
|
||||
- Each actor runs two init functions
|
||||
- We train all actors in parallel for 10 iterations
|
||||
- Errors can come up on actor construction, in the init functions,
|
||||
or during training
|
||||
- When an actor fails, restart that actor
|
||||
- When a task fails, stop actor, and restart
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, actor_manager: RayActorManager, errors: Optional[List[str]] = None
|
||||
):
|
||||
self._actor_manager = actor_manager
|
||||
self._finished = False
|
||||
|
||||
self._actors_to_run = 4
|
||||
self._tracked_actors = []
|
||||
self._actors_stopped = 0
|
||||
|
||||
self._actors_to_replace = set()
|
||||
|
||||
self._ready_actors = set()
|
||||
self._training_barrier = Barrier(
|
||||
max_results=self._actors_to_run,
|
||||
on_completion=self.training_barrier_completed,
|
||||
)
|
||||
self._restart_training = None
|
||||
|
||||
self._training_iter = 0
|
||||
self._results = []
|
||||
|
||||
self._errors = errors
|
||||
|
||||
def setup_actors(self):
|
||||
for actor_id in range(self._actors_to_run):
|
||||
error_kwargs = {}
|
||||
if self._errors:
|
||||
error = random.choice(self._errors)
|
||||
error_kwargs[error] = True
|
||||
|
||||
print("Actor", actor_id, "will be failing with", error_kwargs)
|
||||
|
||||
tracked_actor = self._actor_manager.add_actor(
|
||||
cls=Actor,
|
||||
kwargs={"id": actor_id, **error_kwargs},
|
||||
resource_request=ResourceRequest([{"CPU": 1}]),
|
||||
on_start=self.actor_started,
|
||||
on_stop=self.actor_stopped,
|
||||
on_error=self.actor_error,
|
||||
)
|
||||
self._tracked_actors.append(tracked_actor)
|
||||
|
||||
def actor_started(self, tracked_actor: TrackedActor):
|
||||
self._actor_manager.schedule_actor_task(
|
||||
tracked_actor,
|
||||
"setup_1",
|
||||
on_error=self.setup_error,
|
||||
on_result=self.setup_1_result,
|
||||
)
|
||||
|
||||
def actor_stopped(self, tracked_actor: TrackedActor):
|
||||
self._ready_actors.discard(tracked_actor)
|
||||
|
||||
if tracked_actor in self._actors_to_replace:
|
||||
self._replace_actor(tracked_actor=tracked_actor)
|
||||
else:
|
||||
self._actors_stopped += 1
|
||||
self._finished = self._actors_stopped >= self._actors_to_run
|
||||
|
||||
def actor_error(self, tracked_actor: TrackedActor, exception: Exception):
|
||||
self._ready_actors.discard(tracked_actor)
|
||||
self._replace_actor(tracked_actor=tracked_actor)
|
||||
|
||||
def _replace_actor(self, tracked_actor: TrackedActor):
|
||||
actor_index = self._tracked_actors.index(tracked_actor)
|
||||
|
||||
replacement_actor = self._actor_manager.add_actor(
|
||||
cls=Actor,
|
||||
kwargs={"id": actor_index},
|
||||
resource_request=ResourceRequest([{"CPU": 1}]),
|
||||
on_start=self.actor_started,
|
||||
on_stop=self.actor_stopped,
|
||||
on_error=self.actor_error,
|
||||
)
|
||||
|
||||
self._tracked_actors[actor_index] = replacement_actor
|
||||
|
||||
def setup_1_result(self, tracked_actor: TrackedActor, result: Any):
|
||||
self._actor_manager.schedule_actor_task(
|
||||
tracked_actor,
|
||||
"setup_2",
|
||||
on_error=self.setup_error,
|
||||
on_result=self.setup_2_result,
|
||||
)
|
||||
|
||||
def setup_2_result(self, tracked_actor: TrackedActor, result: Any):
|
||||
self._ready_actors.add(tracked_actor)
|
||||
|
||||
if len(self._ready_actors) == self._actors_to_run:
|
||||
self.continue_training()
|
||||
|
||||
def setup_error(self, tracked_actor: TrackedActor, exception: Exception):
|
||||
if isinstance(exception, RayActorError):
|
||||
return
|
||||
|
||||
self._actors_to_replace.add(tracked_actor)
|
||||
self._actor_manager.remove_actor(tracked_actor)
|
||||
|
||||
def continue_training(self):
|
||||
if self._restart_training:
|
||||
self._training_iter = self._restart_training
|
||||
else:
|
||||
self._training_iter += 1
|
||||
|
||||
self._training_barrier.reset()
|
||||
self._actor_manager.schedule_actor_tasks(
|
||||
self._tracked_actors,
|
||||
"train",
|
||||
args=(self._training_iter,),
|
||||
on_result=self._training_barrier.arrive,
|
||||
on_error=self.training_error,
|
||||
)
|
||||
|
||||
def training_barrier_completed(self, barrier: Barrier):
|
||||
self._results.append([res for _, res in barrier.get_results()])
|
||||
self._restart_training = None
|
||||
|
||||
# If less than 10 epochs, continue training
|
||||
if self._training_iter < 10:
|
||||
return self.continue_training()
|
||||
|
||||
# Else, training finished
|
||||
for tracked_actor in self._tracked_actors:
|
||||
self._actor_manager.remove_actor(tracked_actor)
|
||||
|
||||
def training_error(self, tracked_actor: TrackedActor, exception: Exception):
|
||||
self._restart_training = self._training_iter
|
||||
|
||||
if isinstance(exception, RayActorError):
|
||||
return
|
||||
|
||||
self._actors_to_replace.add(tracked_actor)
|
||||
self._ready_actors.discard(tracked_actor)
|
||||
self._actor_manager.remove_actor(tracked_actor)
|
||||
|
||||
def run(self):
|
||||
self.setup_actors()
|
||||
|
||||
while not self._finished:
|
||||
self._actor_manager.next()
|
||||
|
||||
def get_results(self) -> List[List[float]]:
|
||||
return self._results
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"errors",
|
||||
[
|
||||
None,
|
||||
"actor_init_kill",
|
||||
"actor_setup_kill",
|
||||
"actor_setup_fail",
|
||||
"actor_train_kill",
|
||||
"actor_train_fail",
|
||||
# Chaos - every actor fails somehow, but in different ways
|
||||
[
|
||||
"actor_init_kill",
|
||||
"actor_setup_kill",
|
||||
"actor_setup_fail",
|
||||
"actor_train_kill",
|
||||
"actor_train_fail",
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_e2e(ray_start_4_cpus, resource_manager_cls, errors):
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
if errors and isinstance(errors, str):
|
||||
errors = [errors]
|
||||
|
||||
flow = TrainFlow(actor_manager=actor_manager, errors=errors)
|
||||
flow.run()
|
||||
|
||||
results = flow.get_results()
|
||||
|
||||
assert results == [[i] * 4 for i in range(1, 11)], results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,227 @@
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air import ResourceRequest
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.air.execution._internal.actor_manager import RayActorManager
|
||||
from ray.air.execution._internal.tracked_actor import TrackedActor
|
||||
from ray.exceptions import RayActorError
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ray_start_4_cpus():
|
||||
address_info = ray.init(num_cpus=4)
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@ray.remote
|
||||
class Actor:
|
||||
"""Simple actor for testing an execution flow.
|
||||
|
||||
This actor can fail in three ways:
|
||||
|
||||
1. On init if ``actor_error_init`` is passed as a kwarg
|
||||
2. On run() if ``actor_error_task`` is passed as a kwarg (RayActorError)
|
||||
3. On run() if ``task_error`` is passed as a kwarg (RayTaskError)
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
if self.kwargs.get("actor_error_init"):
|
||||
raise RuntimeError("INIT")
|
||||
|
||||
def get_kwargs(self):
|
||||
return self.kwargs
|
||||
|
||||
def run(self, value: float) -> float:
|
||||
if value == 2:
|
||||
if self.kwargs.get("actor_error_task"):
|
||||
# SystemExit will invoke a RayActorError
|
||||
raise SystemExit
|
||||
|
||||
if self.kwargs.get("task_error"):
|
||||
# RuntimeError will invoke a RayTaskError
|
||||
raise RuntimeError("TASK")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class TuneFlow:
|
||||
"""This is a Ray Tune-like execution flow.
|
||||
|
||||
- We want to run 10 actors in total ("trials")
|
||||
- Each actor collects 11 results sequentially
|
||||
- We schedule up to 6 actors at the same time
|
||||
- Every step, we see if we should add any new actors
|
||||
- Otherwise, we just yield control to the event manager and process events one
|
||||
by one
|
||||
- When an actor is started, start training flow
|
||||
- When a result comes in, schedule next future
|
||||
- If this is the 11th result, stop actor
|
||||
- When the last actor is stopped, set state to finished
|
||||
|
||||
- When an actor fails, restart
|
||||
- When a task fails, stop actor, and restart
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, actor_manager: RayActorManager, errors: Optional[List[str]] = None
|
||||
):
|
||||
self._actor_manager = actor_manager
|
||||
self._finished = False
|
||||
|
||||
self._actors_to_run = 10
|
||||
self._actors_started = 0
|
||||
self._actors_stopped = 0
|
||||
self._max_pending = 6
|
||||
|
||||
self._actor_to_id = {}
|
||||
self._results = defaultdict(list)
|
||||
|
||||
self._errors = errors
|
||||
|
||||
def maybe_add_actors(self):
|
||||
if self._actors_started >= self._actors_to_run:
|
||||
return
|
||||
|
||||
if self._actor_manager.num_pending_actors >= self._max_pending:
|
||||
return
|
||||
|
||||
error_kwargs = {}
|
||||
if self._errors:
|
||||
error = random.choice(self._errors)
|
||||
error_kwargs[error] = True
|
||||
|
||||
actor_id = self._actors_started
|
||||
|
||||
print("Actor", actor_id, "will be failing with", error_kwargs)
|
||||
|
||||
tracked_actor = self._actor_manager.add_actor(
|
||||
cls=Actor,
|
||||
kwargs={"id": actor_id, **error_kwargs},
|
||||
resource_request=ResourceRequest([{"CPU": 1}]),
|
||||
on_start=self.actor_started,
|
||||
on_stop=self.actor_stopped,
|
||||
on_error=self.actor_error,
|
||||
)
|
||||
self._actor_to_id[tracked_actor] = actor_id
|
||||
|
||||
self._actors_started += 1
|
||||
|
||||
def actor_started(self, tracked_actor: TrackedActor):
|
||||
self._actor_manager.schedule_actor_task(
|
||||
tracked_actor,
|
||||
"run",
|
||||
kwargs={"value": 0},
|
||||
on_error=self.task_error,
|
||||
on_result=self.task_result,
|
||||
)
|
||||
|
||||
def actor_stopped(self, tracked_actor: TrackedActor):
|
||||
self._actors_stopped += 1
|
||||
self._finished = self._actors_stopped >= self._actors_to_run
|
||||
|
||||
def actor_error(self, tracked_actor: TrackedActor, exception: Exception):
|
||||
actor_id = self._actor_to_id.pop(tracked_actor)
|
||||
|
||||
replacement_actor = self._actor_manager.add_actor(
|
||||
cls=Actor,
|
||||
kwargs={
|
||||
"id": actor_id,
|
||||
"actor_error_init": False,
|
||||
"actor_error_task": False,
|
||||
"task_error": False,
|
||||
},
|
||||
resource_request=ResourceRequest([{"CPU": 1}]),
|
||||
on_start=self.actor_started,
|
||||
on_stop=self.actor_stopped,
|
||||
on_error=self.actor_error,
|
||||
)
|
||||
|
||||
self._actor_to_id[replacement_actor] = actor_id
|
||||
|
||||
def task_result(self, tracked_actor: TrackedActor, result: float):
|
||||
actor_id = self._actor_to_id[tracked_actor]
|
||||
self._results[actor_id].append(result)
|
||||
|
||||
if result == 10:
|
||||
self._actor_manager.remove_actor(tracked_actor)
|
||||
else:
|
||||
self._actor_manager.schedule_actor_task(
|
||||
tracked_actor,
|
||||
"run",
|
||||
kwargs={"value": result + 1},
|
||||
on_result=self.task_result,
|
||||
on_error=self.task_error,
|
||||
)
|
||||
|
||||
def task_error(self, tracked_actor: TrackedActor, exception: Exception):
|
||||
if isinstance(exception, RayActorError):
|
||||
return
|
||||
|
||||
self._actors_stopped -= 1 # account for extra stop
|
||||
self._actor_manager.remove_actor(tracked_actor)
|
||||
actor_id = self._actor_to_id.pop(tracked_actor)
|
||||
|
||||
replacement_actor = self._actor_manager.add_actor(
|
||||
cls=Actor,
|
||||
kwargs={
|
||||
"id": actor_id,
|
||||
"actor_error_init": False,
|
||||
"actor_error_task": False,
|
||||
"task_error": False,
|
||||
},
|
||||
resource_request=ResourceRequest([{"CPU": 1}]),
|
||||
on_start=self.actor_started,
|
||||
on_stop=self.actor_stopped,
|
||||
on_error=self.actor_error,
|
||||
)
|
||||
self._actor_to_id[replacement_actor] = actor_id
|
||||
|
||||
def run(self):
|
||||
while not self._finished:
|
||||
self.maybe_add_actors()
|
||||
self._actor_manager.next(timeout=1)
|
||||
|
||||
def get_results(self) -> Dict[int, List[float]]:
|
||||
return self._results
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"errors",
|
||||
[
|
||||
None,
|
||||
"actor_error_init",
|
||||
"actor_error_task",
|
||||
"task_error",
|
||||
# Chaos - every actor fails somehow, but in different ways
|
||||
["actor_error_init", "actor_error_task", "task_error"],
|
||||
],
|
||||
)
|
||||
def test_e2e(ray_start_4_cpus, resource_manager_cls, errors):
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
if errors and isinstance(errors, str):
|
||||
errors = [errors]
|
||||
|
||||
flow = TuneFlow(actor_manager=actor_manager, errors=errors)
|
||||
flow.run()
|
||||
|
||||
results = flow.get_results()
|
||||
|
||||
assert all(res[-1] == 10 for res in results.values()), results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,224 @@
|
||||
import time
|
||||
from typing import Any, Type
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air.execution._internal import Barrier
|
||||
from ray.air.execution._internal.event_manager import RayEventManager
|
||||
from ray.exceptions import RayTaskError
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ray_start_4_cpus():
|
||||
address_info = ray.init(num_cpus=4)
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@ray.remote
|
||||
def succeeding(ret: Any = None) -> Any:
|
||||
return ret
|
||||
|
||||
|
||||
@ray.remote
|
||||
def failing(exc: Type[Exception], *args) -> None:
|
||||
raise exc(*args)
|
||||
|
||||
|
||||
@ray.remote
|
||||
def sleeping(seconds: int, result: Any) -> Any:
|
||||
time.sleep(seconds)
|
||||
return result
|
||||
|
||||
|
||||
def test_track_future_success(ray_start_4_cpus):
|
||||
"""Schedule a future that return successfully.
|
||||
|
||||
Check that the on_result callback was triggered.
|
||||
"""
|
||||
event_manager = RayEventManager()
|
||||
|
||||
seen = set()
|
||||
|
||||
def on_result(result: Any):
|
||||
seen.add(result)
|
||||
|
||||
event_manager.track_future(succeeding.remote("a"), on_result=on_result)
|
||||
|
||||
event_manager.wait()
|
||||
assert "a" in seen
|
||||
|
||||
assert not event_manager._tracked_futures
|
||||
|
||||
|
||||
def test_track_future_success_no_callback(ray_start_4_cpus):
|
||||
"""Schedule a future that return successfully.
|
||||
|
||||
Check that passing no callback still succeeds.
|
||||
"""
|
||||
event_manager = RayEventManager()
|
||||
|
||||
event_manager.track_future(succeeding.remote("a"))
|
||||
|
||||
event_manager.wait()
|
||||
|
||||
assert not event_manager._tracked_futures
|
||||
|
||||
|
||||
def test_track_future_error(ray_start_4_cpus):
|
||||
"""Schedule a future that fails.
|
||||
|
||||
Check that the on_error callback was triggered.
|
||||
"""
|
||||
event_manager = RayEventManager()
|
||||
|
||||
seen = set()
|
||||
|
||||
class CustomError(RuntimeError):
|
||||
pass
|
||||
|
||||
def on_error(exception: Exception):
|
||||
seen.add(exception)
|
||||
|
||||
event_manager.track_future(failing.remote(CustomError), on_error=on_error)
|
||||
|
||||
event_manager.wait()
|
||||
assert isinstance(seen.pop(), CustomError)
|
||||
|
||||
assert not event_manager._tracked_futures
|
||||
|
||||
|
||||
def test_track_future_error_no_callback(ray_start_4_cpus):
|
||||
"""Schedule a future that fails.
|
||||
|
||||
Check that passing no callback raises the original error.
|
||||
"""
|
||||
event_manager = RayEventManager()
|
||||
|
||||
event_manager.track_future(failing.remote(RuntimeError))
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
event_manager.wait()
|
||||
|
||||
assert not event_manager._tracked_futures
|
||||
|
||||
|
||||
@pytest.mark.parametrize("results_per_wait", [None, 1, 5, 10, 100])
|
||||
def test_many_futures(ray_start_4_cpus, results_per_wait):
|
||||
"""Schedule 500 succeeding and failing futures.
|
||||
|
||||
Check that the callbacks get triggered correctly, independent of the number
|
||||
of results we await per call to RayEventManager.wait().
|
||||
"""
|
||||
num_futures = 500
|
||||
|
||||
event_manager = RayEventManager()
|
||||
|
||||
seen_results = set()
|
||||
seen_errors = set()
|
||||
|
||||
def on_result(result: Any):
|
||||
seen_results.add(result)
|
||||
|
||||
def on_error(exception: RayTaskError):
|
||||
seen_errors.add(exception.cause.args[0])
|
||||
|
||||
for i in range(num_futures):
|
||||
event_manager.track_futures(
|
||||
[
|
||||
succeeding.remote("a" + str(i)),
|
||||
failing.remote(RuntimeError, "b" + str(i)),
|
||||
],
|
||||
on_result=on_result,
|
||||
on_error=on_error,
|
||||
)
|
||||
|
||||
while event_manager.num_futures > 0:
|
||||
event_manager.wait(num_results=results_per_wait)
|
||||
|
||||
for i in range(num_futures):
|
||||
assert "a" + str(i) in seen_results
|
||||
assert "b" + str(i) in seen_errors
|
||||
|
||||
|
||||
def test_timeout(ray_start_4_cpus):
|
||||
"""Test the timeout parameter.
|
||||
|
||||
Start 4 tasks: Two succeed immediately, two after 1 second.
|
||||
|
||||
After waiting for 0.5 seconds, the first two tasks should have returned.
|
||||
After waiting for up to 5 seconds, the other two tasks should have returned.
|
||||
But because the tasks take only 0.5 seconds to run, we should have waited
|
||||
way less than 5 seconds.
|
||||
"""
|
||||
event_manager = RayEventManager()
|
||||
|
||||
seen = set()
|
||||
|
||||
def on_result(result: Any):
|
||||
seen.add(result)
|
||||
|
||||
event_manager.track_futures(
|
||||
[
|
||||
succeeding.remote("a"),
|
||||
succeeding.remote("b"),
|
||||
sleeping.remote(1, "c"),
|
||||
sleeping.remote(1, "d"),
|
||||
],
|
||||
on_result=on_result,
|
||||
)
|
||||
|
||||
start = time.monotonic()
|
||||
event_manager.wait(num_results=None, timeout=0.5)
|
||||
assert "a" in seen
|
||||
assert "b" in seen
|
||||
assert "c" not in seen
|
||||
assert "d" not in seen
|
||||
|
||||
event_manager.wait(num_results=None, timeout=5)
|
||||
taken = time.monotonic() - start
|
||||
|
||||
assert "c" in seen
|
||||
assert "d" in seen
|
||||
|
||||
# Should have returned much earlier than after 5 seconds
|
||||
assert taken < 3
|
||||
|
||||
assert not event_manager._tracked_futures
|
||||
|
||||
|
||||
def test_task_barrier(ray_start_4_cpus):
|
||||
event_manager = RayEventManager()
|
||||
|
||||
seen = set()
|
||||
|
||||
def on_completion(barrier: Barrier):
|
||||
seen.update(barrier.get_results())
|
||||
|
||||
barrier = Barrier(max_results=4, on_completion=on_completion)
|
||||
|
||||
event_manager.track_futures(
|
||||
[
|
||||
succeeding.remote("a"),
|
||||
succeeding.remote("b"),
|
||||
succeeding.remote("c"),
|
||||
succeeding.remote("d"),
|
||||
sleeping.remote(2, "e"),
|
||||
],
|
||||
on_result=barrier.arrive,
|
||||
)
|
||||
|
||||
event_manager.wait(num_results=4)
|
||||
|
||||
assert "a" in seen
|
||||
assert "b" in seen
|
||||
assert "c" in seen
|
||||
assert "d" in seen
|
||||
assert "e" not in seen
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,178 @@
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air.execution.resources.fixed import FixedResourceManager
|
||||
from ray.air.execution.resources.request import ResourceRequest
|
||||
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
|
||||
|
||||
REQUEST_2_CPU = ResourceRequest([{"CPU": 2}])
|
||||
REQUEST_4_CPU = ResourceRequest([{"CPU": 4}])
|
||||
REQUEST_1_2_CPU = ResourceRequest([{"CPU": 1}, {"CPU": 2}])
|
||||
REQUEST_0_2_CPU = ResourceRequest([{"CPU": 0}, {"CPU": 2}])
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ray_start_4_cpus():
|
||||
address_info = ray.init(num_cpus=4)
|
||||
yield address_info
|
||||
# The code after the yield will run as teardown code.
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
def test_acquire_return_resources(ray_start_4_cpus):
|
||||
manager = FixedResourceManager(total_resources={"CPU": 4})
|
||||
|
||||
assert not manager.has_resources_ready(REQUEST_2_CPU)
|
||||
assert not manager.has_resources_ready(REQUEST_4_CPU)
|
||||
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
manager.request_resources(REQUEST_4_CPU)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_4_CPU)
|
||||
|
||||
ready_2 = manager.acquire_resources(REQUEST_2_CPU)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_2_CPU)
|
||||
assert not manager.has_resources_ready(REQUEST_4_CPU)
|
||||
|
||||
manager.free_resources(ready_2)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_4_CPU)
|
||||
|
||||
|
||||
def test_numerical_error(ray_start_4_cpus):
|
||||
"""Make sure we don't run into numerical errors when using fractional resources.
|
||||
|
||||
Legacy test: test_trial_runner::TrialRunnerTest::testResourceNumericalError
|
||||
"""
|
||||
manager = FixedResourceManager(
|
||||
total_resources={"CPU": 0.99, "GPU": 0.99, "a": 0.99}
|
||||
)
|
||||
resource_request = ResourceRequest([{"CPU": 0.33, "GPU": 0.33, "a": 0.33}])
|
||||
|
||||
for i in range(3):
|
||||
manager.request_resources(resource_request)
|
||||
assert manager.acquire_resources(
|
||||
resource_request=resource_request
|
||||
), manager._available_resources
|
||||
|
||||
assert manager._available_resources["CPU"] == 0
|
||||
assert manager._available_resources["GPU"] == 0
|
||||
assert manager._available_resources["a"] == 0
|
||||
|
||||
|
||||
def test_bind_two_bundles(ray_start_4_cpus):
|
||||
"""Test that binding two remote objects to a ready resource works.
|
||||
|
||||
- Request resources with 2 bundles (1 CPU and 2 CPUs)
|
||||
- Bind two remote tasks to these bundles, execute
|
||||
- Assert that resource allocation returns the correct resources: 1 CPU and 2 CPUs
|
||||
"""
|
||||
manager = FixedResourceManager()
|
||||
manager.request_resources(REQUEST_1_2_CPU)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_1_2_CPU)
|
||||
|
||||
@ray.remote
|
||||
def get_assigned_resources():
|
||||
return ray.get_runtime_context().get_assigned_resources()
|
||||
|
||||
acq = manager.acquire_resources(REQUEST_1_2_CPU)
|
||||
[av1] = acq.annotate_remote_entities([get_assigned_resources])
|
||||
|
||||
res1 = ray.get(av1.remote())
|
||||
|
||||
assert sum(v for k, v in res1.items() if k.startswith("CPU")) == 1
|
||||
|
||||
[av1, av2] = acq.annotate_remote_entities(
|
||||
[get_assigned_resources, get_assigned_resources]
|
||||
)
|
||||
|
||||
res1, res2 = ray.get([av1.remote(), av2.remote()])
|
||||
assert sum(v for k, v in res1.items() if k.startswith("CPU")) == 1
|
||||
assert sum(v for k, v in res2.items() if k.startswith("CPU")) == 2
|
||||
|
||||
|
||||
def test_bind_empty_head_bundle(ray_start_4_cpus):
|
||||
"""Test that binding two remote objects to a ready resource works with empty head.
|
||||
|
||||
- Request resources with 2 bundles (0 CPU and 2 CPUs)
|
||||
- Bind two remote tasks to these bundles, execute
|
||||
- Assert that resource allocation returns the correct resources: 0 CPU and 2 CPUs
|
||||
"""
|
||||
manager = FixedResourceManager()
|
||||
assert REQUEST_0_2_CPU.head_bundle_is_empty
|
||||
manager.request_resources(REQUEST_0_2_CPU)
|
||||
ray.wait(manager.get_resource_futures(), num_returns=1)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_0_2_CPU)
|
||||
|
||||
@ray.remote
|
||||
def get_assigned_resources():
|
||||
return ray.get_runtime_context().get_assigned_resources()
|
||||
|
||||
acq = manager.acquire_resources(REQUEST_0_2_CPU)
|
||||
[av1] = acq.annotate_remote_entities([get_assigned_resources])
|
||||
|
||||
res1 = ray.get(av1.remote())
|
||||
|
||||
assert sum(v for k, v in res1.items() if k.startswith("CPU")) == 0
|
||||
|
||||
[av1, av2] = acq.annotate_remote_entities(
|
||||
[get_assigned_resources, get_assigned_resources]
|
||||
)
|
||||
|
||||
res1, res2 = ray.get([av1.remote(), av2.remote()])
|
||||
assert sum(v for k, v in res1.items() if k.startswith("CPU")) == 0
|
||||
assert sum(v for k, v in res2.items() if k.startswith("CPU")) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("strategy", ["STRICT_PACK", "PACK", "SPREAD", "STRICT_SPREAD"])
|
||||
def test_strategy(ray_start_4_cpus, strategy):
|
||||
"""The fixed resoure manager does not support STRICT placement strategies."""
|
||||
manager = FixedResourceManager()
|
||||
|
||||
req = ResourceRequest([{"CPU": 2}], strategy=strategy)
|
||||
|
||||
if strategy.startswith("STRICT_"):
|
||||
with pytest.raises(RuntimeError):
|
||||
manager.request_resources(req)
|
||||
else:
|
||||
manager.request_resources(req)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("strategy", ["STRICT_PACK", "PACK", "SPREAD", "STRICT_SPREAD"])
|
||||
def test_strategy_nested(ray_start_4_cpus, strategy):
|
||||
"""The fixed resoure manager does not support STRICT_SPREAD within a PG."""
|
||||
|
||||
@ray.remote
|
||||
def nested_test():
|
||||
manager = FixedResourceManager()
|
||||
|
||||
req = ResourceRequest([{"CPU": 2}], strategy=strategy)
|
||||
|
||||
if strategy == "STRICT_SPREAD":
|
||||
with pytest.raises(RuntimeError):
|
||||
manager.request_resources(req)
|
||||
else:
|
||||
manager.request_resources(req)
|
||||
|
||||
pg = ray.util.placement_group([{"CPU": 2}])
|
||||
ray.wait([pg.ready()])
|
||||
|
||||
try:
|
||||
ray.get(
|
||||
nested_test.options(
|
||||
scheduling_strategy=PlacementGroupSchedulingStrategy(
|
||||
placement_group=pg, placement_group_capture_child_tasks=True
|
||||
)
|
||||
).remote()
|
||||
)
|
||||
finally:
|
||||
ray.util.remove_placement_group(pg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,409 @@
|
||||
import time
|
||||
from collections import Counter
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air.execution.resources.placement_group import PlacementGroupResourceManager
|
||||
from ray.air.execution.resources.request import ResourceRequest
|
||||
|
||||
REQUEST_2_CPU = ResourceRequest([{"CPU": 2}])
|
||||
REQUEST_1_2_CPU = ResourceRequest([{"CPU": 1}, {"CPU": 2}])
|
||||
REQUEST_0_2_CPU = ResourceRequest([{"CPU": 0}, {"CPU": 2}])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_start_4_cpus():
|
||||
address_info = ray.init(num_cpus=4)
|
||||
yield address_info
|
||||
# The code after the yield will run as teardown code.
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
def _count_pg_states():
|
||||
counter = Counter()
|
||||
for _, pg_info in ray.util.placement_group_table().items():
|
||||
counter[pg_info["state"]] += 1
|
||||
return counter
|
||||
|
||||
|
||||
def test_request_cancel_resources(ray_start_4_cpus):
|
||||
"""Test that canceling a resource request clears the PG futures.
|
||||
|
||||
- Create request
|
||||
- Assert actual PG is created
|
||||
- Cancel request
|
||||
- Assert staging future is removed
|
||||
- Assert actual PG is removed
|
||||
"""
|
||||
manager = PlacementGroupResourceManager(update_interval_s=0)
|
||||
assert not manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
|
||||
# Could be pending or created
|
||||
pg_states = _count_pg_states()
|
||||
assert pg_states["PENDING"] + pg_states["CREATED"] == 1
|
||||
assert pg_states["REMOVED"] == 0
|
||||
|
||||
assert manager.get_resource_futures()
|
||||
|
||||
manager.cancel_resource_request(REQUEST_2_CPU)
|
||||
|
||||
assert not manager.get_resource_futures()
|
||||
|
||||
pg_states = _count_pg_states()
|
||||
assert pg_states["PENDING"] + pg_states["CREATED"] == 0
|
||||
assert pg_states["REMOVED"] == 1
|
||||
|
||||
|
||||
def test_acquire_return_resources(ray_start_4_cpus):
|
||||
"""Tests that acquiring and returning resources works.
|
||||
|
||||
- At the start, no resources should be ready (no PG scheduled)
|
||||
- Request resources for 2 CPUs
|
||||
- (wait until they are ready)
|
||||
- Assert that these 2 CPUs are available to be acquired
|
||||
- Acquire
|
||||
- Assert that there are no 2 CPU resources available anymore
|
||||
- Free resources
|
||||
- Assert that the 2 CPU resources are still not available (no new request)
|
||||
- This is also tested in includes test_request_cancel_resources
|
||||
"""
|
||||
manager = PlacementGroupResourceManager(update_interval_s=0)
|
||||
assert not manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
# Request PG
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
|
||||
# Wait until ready
|
||||
ray.wait(manager.get_resource_futures(), num_returns=1)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
# PG exists
|
||||
pg_states = _count_pg_states()
|
||||
assert pg_states["CREATED"] == 1
|
||||
assert pg_states["REMOVED"] == 0
|
||||
|
||||
# Acquire PG
|
||||
acquired = manager.acquire_resources(REQUEST_2_CPU)
|
||||
|
||||
assert not manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
# Free resources
|
||||
manager.free_resources(acquired)
|
||||
|
||||
assert not manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
# PG still exists
|
||||
pg_states = _count_pg_states()
|
||||
assert pg_states["CREATED"] == 0
|
||||
assert pg_states["REMOVED"] == 1
|
||||
|
||||
|
||||
def test_request_pending(ray_start_4_cpus):
|
||||
"""Test that requesting too many resources leads to pending PGs.
|
||||
|
||||
- Cluster of 4 CPUs
|
||||
- Request 3 PGs a 2 CPUs
|
||||
- Acquire 2 PGs
|
||||
- Assert no resources are available anymore
|
||||
- Return both PGs
|
||||
- Assert resources are available again
|
||||
- Cancel request
|
||||
- Assert no resources are available again
|
||||
"""
|
||||
manager = PlacementGroupResourceManager(update_interval_s=0)
|
||||
assert not manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
|
||||
# Wait until some are ready
|
||||
ray.wait(manager.get_resource_futures(), num_returns=2)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_2_CPU)
|
||||
assert len(manager.get_resource_futures()) == 1
|
||||
|
||||
pg_states = _count_pg_states()
|
||||
assert pg_states["CREATED"] == 2
|
||||
assert pg_states["PENDING"] == 1
|
||||
assert pg_states["REMOVED"] == 0
|
||||
|
||||
acq1 = manager.acquire_resources(REQUEST_2_CPU)
|
||||
acq2 = manager.acquire_resources(REQUEST_2_CPU)
|
||||
|
||||
assert not manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
manager.free_resources(acq1)
|
||||
manager.free_resources(acq2)
|
||||
|
||||
# Third PG becomes ready
|
||||
ray.wait(manager.get_resource_futures(), num_returns=1)
|
||||
assert manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
pg_states = _count_pg_states()
|
||||
assert pg_states["CREATED"] == 1
|
||||
assert pg_states["PENDING"] == 0
|
||||
assert pg_states["REMOVED"] == 2
|
||||
|
||||
manager.cancel_resource_request(REQUEST_2_CPU)
|
||||
assert not manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
pg_states = _count_pg_states()
|
||||
assert pg_states["CREATED"] == 0
|
||||
assert pg_states["PENDING"] == 0
|
||||
assert pg_states["REMOVED"] == 3
|
||||
|
||||
|
||||
def test_acquire_unavailable(ray_start_4_cpus):
|
||||
"""Test that acquiring resources that are not available returns None.
|
||||
|
||||
- Try to acquire
|
||||
- Assert this does not work
|
||||
- Request resources
|
||||
- Wait until ready
|
||||
- Acquire
|
||||
- Assert this did work
|
||||
"""
|
||||
manager = PlacementGroupResourceManager(update_interval_s=0)
|
||||
assert not manager.acquire_resources(REQUEST_2_CPU)
|
||||
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
ray.wait(manager.get_resource_futures(), num_returns=1)
|
||||
assert manager.acquire_resources(REQUEST_2_CPU)
|
||||
|
||||
|
||||
def test_bind_two_bundles(ray_start_4_cpus):
|
||||
"""Test that binding two remote objects to a ready resource works.
|
||||
|
||||
- Request PG with 2 bundles (1 CPU and 2 CPUs)
|
||||
- Bind two remote tasks to these bundles, execute
|
||||
- Assert that resource allocation returns the correct resources: 1 CPU and 2 CPUs
|
||||
"""
|
||||
manager = PlacementGroupResourceManager(update_interval_s=0)
|
||||
manager.request_resources(REQUEST_1_2_CPU)
|
||||
ray.wait(manager.get_resource_futures(), num_returns=1)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_1_2_CPU)
|
||||
|
||||
@ray.remote
|
||||
def get_assigned_resources():
|
||||
return ray.get_runtime_context().get_assigned_resources()
|
||||
|
||||
acq = manager.acquire_resources(REQUEST_1_2_CPU)
|
||||
[av1] = acq.annotate_remote_entities([get_assigned_resources])
|
||||
|
||||
res1 = ray.get(av1.remote())
|
||||
|
||||
assert res1 == {"CPU": 1}
|
||||
|
||||
[av1, av2] = acq.annotate_remote_entities(
|
||||
[get_assigned_resources, get_assigned_resources]
|
||||
)
|
||||
|
||||
res1, res2 = ray.get([av1.remote(), av2.remote()])
|
||||
assert res1 == {"CPU": 1}
|
||||
assert res2 == {"CPU": 2}
|
||||
|
||||
|
||||
def test_bind_empty_head_bundle(ray_start_4_cpus):
|
||||
"""Test that binding two remote objects to a ready resource works with empty head.
|
||||
|
||||
- Request PG with 2 bundles (0 CPU and 2 CPUs)
|
||||
- Bind two remote tasks to these bundles, execute
|
||||
- Assert that resource allocation returns the correct resources: 0 CPU and 2 CPUs
|
||||
"""
|
||||
manager = PlacementGroupResourceManager(update_interval_s=0)
|
||||
assert REQUEST_0_2_CPU.head_bundle_is_empty
|
||||
manager.request_resources(REQUEST_0_2_CPU)
|
||||
ray.wait(manager.get_resource_futures(), num_returns=1)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_0_2_CPU)
|
||||
|
||||
@ray.remote
|
||||
def get_assigned_resources():
|
||||
return ray.get_runtime_context().get_assigned_resources()
|
||||
|
||||
acq = manager.acquire_resources(REQUEST_0_2_CPU)
|
||||
[av1] = acq.annotate_remote_entities([get_assigned_resources])
|
||||
|
||||
res1 = ray.get(av1.remote())
|
||||
|
||||
assert res1 == {}
|
||||
|
||||
[av1, av2] = acq.annotate_remote_entities(
|
||||
[get_assigned_resources, get_assigned_resources]
|
||||
)
|
||||
|
||||
res1, res2 = ray.get([av1.remote(), av2.remote()])
|
||||
assert res1 == {}
|
||||
assert res2 == {"CPU": 2}
|
||||
|
||||
|
||||
def test_capture_child_tasks(ray_start_4_cpus):
|
||||
"""Test that child tasks are captured when creating placement groups.
|
||||
|
||||
- Request PG with 2 bundles (1 CPU and 2 CPUs)
|
||||
- Bind a remote task that needs 2 CPUs to run
|
||||
- Assert that it can be scheduled from within the first bundle
|
||||
|
||||
This is only the case if child tasks are captured in the placement groups, as
|
||||
there is only 1 CPU available outside (on a 4 CPU cluster). The 2 CPUs
|
||||
thus have to come from the placement group.
|
||||
"""
|
||||
manager = PlacementGroupResourceManager(update_interval_s=0)
|
||||
manager.request_resources(REQUEST_1_2_CPU)
|
||||
ray.wait(manager.get_resource_futures(), num_returns=1)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_1_2_CPU)
|
||||
|
||||
@ray.remote
|
||||
def needs_cpus():
|
||||
return "Ok"
|
||||
|
||||
@ray.remote
|
||||
def spawn_child_task(num_cpus: int):
|
||||
return ray.get(needs_cpus.options(num_cpus=num_cpus).remote())
|
||||
|
||||
acq = manager.acquire_resources(REQUEST_1_2_CPU)
|
||||
[av1] = acq.annotate_remote_entities([spawn_child_task])
|
||||
|
||||
res = ray.get(av1.remote(2), timeout=2.0)
|
||||
|
||||
assert res
|
||||
|
||||
|
||||
def test_clear_state(ray_start_4_cpus):
|
||||
"""Test that clearing state will remove existing placement groups.
|
||||
|
||||
- Create resource request
|
||||
- Wait until PG is scheduled
|
||||
- Assert that Ray PG is created
|
||||
- Call `mgr.clear()`
|
||||
- Assert that resources are not ready anymore
|
||||
- Assert that Ray PG is removed
|
||||
"""
|
||||
manager = PlacementGroupResourceManager(update_interval_s=0)
|
||||
manager.request_resources(REQUEST_1_2_CPU)
|
||||
ray.wait(manager.get_resource_futures(), num_returns=1)
|
||||
|
||||
assert manager.has_resources_ready(REQUEST_1_2_CPU)
|
||||
|
||||
pg_states = _count_pg_states()
|
||||
assert pg_states["CREATED"] == 1
|
||||
assert pg_states["PENDING"] == 0
|
||||
assert pg_states["REMOVED"] == 0
|
||||
|
||||
manager.clear()
|
||||
|
||||
assert not manager.has_resources_ready(REQUEST_1_2_CPU)
|
||||
|
||||
pg_states = _count_pg_states()
|
||||
assert pg_states["CREATED"] == 0
|
||||
assert pg_states["PENDING"] == 0
|
||||
assert pg_states["REMOVED"] == 1
|
||||
|
||||
|
||||
def test_internal_state(ray_start_4_cpus):
|
||||
"""Test internal state mappings of the placement group manager.
|
||||
|
||||
This test makes assumptions and assertions around the internal state transition
|
||||
of private properties of the placement group resource manager.
|
||||
|
||||
If you change internal handling logic of the manager, you may need to change this
|
||||
test as well.
|
||||
"""
|
||||
manager = PlacementGroupResourceManager(update_interval_s=0)
|
||||
|
||||
assert manager.update_interval_s == 0
|
||||
|
||||
manager.has_resources_ready(REQUEST_2_CPU)
|
||||
|
||||
# The key may exist but the set should be empty
|
||||
assert not manager._request_to_ready_pgs[REQUEST_2_CPU]
|
||||
|
||||
####
|
||||
# 1. Request, wait until ready, cancel
|
||||
|
||||
# Request resources
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
|
||||
# PG should be staged
|
||||
assert manager._request_to_staged_pgs[REQUEST_2_CPU]
|
||||
pg = list(manager._request_to_staged_pgs[REQUEST_2_CPU])[0]
|
||||
assert manager._pg_to_request[pg] == REQUEST_2_CPU
|
||||
|
||||
# Staging future should exist
|
||||
assert manager._pg_to_staging_future[pg]
|
||||
fut = manager._pg_to_staging_future[pg]
|
||||
assert manager._staging_future_to_pg[fut] == pg
|
||||
|
||||
# Wait until PG is ready
|
||||
while not manager.has_resources_ready(resource_request=REQUEST_2_CPU):
|
||||
time.sleep(0.05)
|
||||
|
||||
# PG should now be ready
|
||||
assert manager._request_to_ready_pgs[REQUEST_2_CPU]
|
||||
# PG should not be staged anymore
|
||||
assert not manager._request_to_staged_pgs[REQUEST_2_CPU]
|
||||
# Staging future should not exist anymore
|
||||
assert not manager._pg_to_staging_future
|
||||
assert not manager._staging_future_to_pg
|
||||
|
||||
# Cancel request
|
||||
manager.cancel_resource_request(REQUEST_2_CPU)
|
||||
|
||||
# PG should not be ready anymore
|
||||
assert not manager._request_to_ready_pgs[REQUEST_2_CPU]
|
||||
# All PGs should be fully removed
|
||||
assert not manager._pg_to_request
|
||||
|
||||
####
|
||||
# 2. Request, cancel while staging
|
||||
|
||||
# Stage another PG
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
# Cancel request before it's ready
|
||||
manager.cancel_resource_request(REQUEST_2_CPU)
|
||||
# Assert no leftover
|
||||
assert not manager._pg_to_staging_future
|
||||
assert not manager._staging_future_to_pg
|
||||
assert not manager._request_to_staged_pgs[REQUEST_2_CPU]
|
||||
assert not manager._request_to_ready_pgs[REQUEST_2_CPU]
|
||||
assert not manager._pg_to_request
|
||||
|
||||
####
|
||||
# 2. Request, acquire, free
|
||||
|
||||
# Stage another PG
|
||||
manager.request_resources(REQUEST_2_CPU)
|
||||
pg = list(manager._request_to_staged_pgs[REQUEST_2_CPU])[0]
|
||||
# Wait until PG is ready
|
||||
while not manager.has_resources_ready(resource_request=REQUEST_2_CPU):
|
||||
time.sleep(0.05)
|
||||
# Acquire
|
||||
acquired_resources = manager.acquire_resources(resource_request=REQUEST_2_CPU)
|
||||
# Assert no staging/ready leftover
|
||||
assert not manager._pg_to_staging_future
|
||||
assert not manager._staging_future_to_pg
|
||||
assert not manager._request_to_staged_pgs[REQUEST_2_CPU]
|
||||
assert not manager._request_to_ready_pgs[REQUEST_2_CPU]
|
||||
# We still retain this mapping
|
||||
assert manager._pg_to_request
|
||||
# And we keep track of acquired PGs
|
||||
assert pg in manager._acquired_pgs
|
||||
|
||||
# Free PG
|
||||
manager.free_resources(acquired_resources)
|
||||
# State should be cleared now
|
||||
assert not manager._pg_to_request
|
||||
assert not manager._acquired_pgs
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,38 @@
|
||||
import pytest
|
||||
|
||||
from ray.air.execution.resources.request import ResourceRequest
|
||||
|
||||
|
||||
def test_request_same():
|
||||
"""Test that resource requests are the same if they share the same properties."""
|
||||
|
||||
assert ResourceRequest([{"CPU": 1}]) == ResourceRequest([{"CPU": 1}])
|
||||
|
||||
# multiple bundles work
|
||||
assert ResourceRequest([{"CPU": 1}, {"CPU": 2}]) == ResourceRequest(
|
||||
[{"CPU": 1}, {"CPU": 2}]
|
||||
)
|
||||
|
||||
# multiple resources work
|
||||
assert ResourceRequest([{"CPU": 1, "GPU": 1}]) == ResourceRequest(
|
||||
[{"CPU": 1, "GPU": 1}]
|
||||
)
|
||||
|
||||
# 0 resources are ignored
|
||||
assert ResourceRequest([{"CPU": 0, "GPU": 1}]) == ResourceRequest([{"GPU": 1}])
|
||||
|
||||
# PACK is implicit
|
||||
assert ResourceRequest([{"CPU": 1}], strategy="PACK") == ResourceRequest(
|
||||
[{"CPU": 1}]
|
||||
)
|
||||
|
||||
# Non match: different strategy
|
||||
assert ResourceRequest([{"CPU": 1}], strategy="PACK") != ResourceRequest(
|
||||
[{"CPU": 1}], strategy="SPREAD"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,366 @@
|
||||
import gc
|
||||
import threading
|
||||
import time
|
||||
from collections import Counter
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air import ResourceRequest
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.air.execution._internal import Barrier
|
||||
from ray.air.execution._internal.actor_manager import RayActorManager
|
||||
|
||||
|
||||
def _raise(exception_type: Type[Exception] = RuntimeError, msg: Optional[str] = None):
|
||||
def _raise_exception(*args, **kwargs):
|
||||
raise exception_type(msg)
|
||||
|
||||
return _raise_exception
|
||||
|
||||
|
||||
class Started(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class Stopped(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class Failed(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class Result(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ray_start_4_cpus():
|
||||
address_info = ray.init(num_cpus=4)
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cleanup():
|
||||
# Garbage collect at the start
|
||||
# This ensures that all resources are freed up for the upcoming test.
|
||||
gc.collect()
|
||||
yield
|
||||
|
||||
|
||||
class Actor:
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
def get_kwargs(self):
|
||||
return self.kwargs
|
||||
|
||||
def task(self, value: Any):
|
||||
return value
|
||||
|
||||
|
||||
@ray.remote(num_cpus=4)
|
||||
def fn():
|
||||
return True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
@pytest.mark.parametrize("actor_cls", [Actor, ray.remote(Actor)])
|
||||
@pytest.mark.parametrize("kill", [False, True])
|
||||
def test_start_stop_actor(ray_start_4_cpus, resource_manager_cls, actor_cls, kill):
|
||||
"""Test that starting and stopping actors work and invokes a callback.
|
||||
|
||||
- Start an actor
|
||||
- Starting should trigger start callback
|
||||
- Schedule actor task, which should resolve (meaning actor successfully started)
|
||||
- Stop actor, which should resolve and trigger stop callback
|
||||
- Schedule remote fn that takes up all cluster resources. This should resolve,
|
||||
meaning that the actor was stopped successfully.
|
||||
"""
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
# Start actor, set callbacks
|
||||
tracked_actor = actor_manager.add_actor(
|
||||
cls=actor_cls,
|
||||
kwargs={"key": "val"},
|
||||
resource_request=ResourceRequest([{"CPU": 4}]),
|
||||
on_start=_raise(Started),
|
||||
on_stop=_raise(Stopped),
|
||||
on_error=_raise(Failed),
|
||||
)
|
||||
|
||||
# Actor should be started
|
||||
with pytest.raises(Started):
|
||||
actor_manager.next()
|
||||
|
||||
# Schedule task on actor which should resolve (actor successfully started)
|
||||
actor_manager.schedule_actor_task(
|
||||
tracked_actor, "task", (1,), on_result=_raise(Result)
|
||||
)
|
||||
|
||||
with pytest.raises(Result):
|
||||
actor_manager.next()
|
||||
|
||||
# Now we can assert that there are no CPUS resources available anymore.
|
||||
# Note that actor starting is asynchronous, so we can't assert this right away
|
||||
# - that's why we wait for the actor task to resolve first.
|
||||
assert ray.available_resources().get("CPU", 0.0) == 0, ray.available_resources()
|
||||
|
||||
# Stop actor
|
||||
actor_manager.remove_actor(tracked_actor, kill=kill)
|
||||
|
||||
with pytest.raises(Stopped):
|
||||
actor_manager.next()
|
||||
|
||||
# This task takes up all the cluster resources. It should resolve now that
|
||||
# the actor was terminated.
|
||||
assert ray.get(fn.remote(), timeout=5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_start_many_actors(ray_start_4_cpus, resource_manager_cls):
|
||||
"""Test that starting more actors than fit onto the cluster works.
|
||||
|
||||
- Request 10 actors
|
||||
- 4 can be started. Assert they are started
|
||||
- Stop 2
|
||||
- Assert 2 are stopped and 2 new ones are started
|
||||
"""
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
running_actors = []
|
||||
# stats keeps track of started/stopped actors
|
||||
stats = Counter()
|
||||
|
||||
def start_callback(tracked_actor):
|
||||
running_actors.append(tracked_actor)
|
||||
stats["started"] += 1
|
||||
|
||||
def stop_callback(tracked_actor):
|
||||
running_actors.remove(tracked_actor)
|
||||
stats["stopped"] += 1
|
||||
|
||||
# start 10 actors
|
||||
expected_actors = []
|
||||
for i in range(10):
|
||||
tracked_actor = actor_manager.add_actor(
|
||||
cls=Actor,
|
||||
kwargs={"key": "val"},
|
||||
resource_request=ResourceRequest([{"CPU": 1}]),
|
||||
on_start=start_callback,
|
||||
on_stop=stop_callback,
|
||||
on_error=_raise(Failed),
|
||||
)
|
||||
expected_actors.append(tracked_actor)
|
||||
|
||||
# wait for some actor starts
|
||||
for i in range(4):
|
||||
actor_manager.next()
|
||||
|
||||
# we should now have 4 started actors
|
||||
assert stats["started"] == 4
|
||||
assert stats["stopped"] == 0
|
||||
assert len(running_actors) == 4
|
||||
assert set(running_actors) == set(expected_actors[:4])
|
||||
|
||||
# stop 2 actors
|
||||
actor_manager.remove_actor(running_actors[0])
|
||||
actor_manager.remove_actor(running_actors[1])
|
||||
|
||||
# Wait four times, twice for termination, twice for start
|
||||
for i in range(4):
|
||||
actor_manager.next()
|
||||
|
||||
# we should have 4 running actors, 6 started and 2 stopped
|
||||
assert stats["started"] == 6
|
||||
assert stats["stopped"] == 2
|
||||
assert len(running_actors) == 4
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
@pytest.mark.parametrize("where", ["init", "fn"])
|
||||
def test_actor_fail(ray_start_4_cpus, cleanup, resource_manager_cls, where):
|
||||
"""Test that actor failures are handled properly.
|
||||
|
||||
- Start actor that either fails on init or in a task (RayActorError)
|
||||
- Schedule task on actor
|
||||
- Assert that the correct callbacks are called
|
||||
"""
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
# keep track of failed tasks and actors
|
||||
stats = Counter()
|
||||
|
||||
@ray.remote
|
||||
class FailingActor:
|
||||
def __init__(self, where):
|
||||
self._where = where
|
||||
if self._where == "init":
|
||||
raise RuntimeError("INIT")
|
||||
|
||||
def fn(self):
|
||||
if self._where == "fn":
|
||||
# SystemExit will invoke a RayActorError
|
||||
raise SystemExit
|
||||
return True
|
||||
|
||||
def fail_callback_actor(tracked_actor, exception):
|
||||
stats["failed_actor"] += 1
|
||||
|
||||
def fail_callback_task(tracked_actor, exception):
|
||||
stats["failed_task"] += 1
|
||||
|
||||
# Start actor
|
||||
tracked_actor = actor_manager.add_actor(
|
||||
cls=FailingActor,
|
||||
kwargs={"where": where},
|
||||
resource_request=ResourceRequest([{"CPU": 1}]),
|
||||
on_error=fail_callback_actor,
|
||||
)
|
||||
|
||||
if where != "init":
|
||||
# Wait until it is started. This won't invoke any callback, yet
|
||||
actor_manager.next()
|
||||
|
||||
assert stats["failed_actor"] == 0
|
||||
assert stats["failed_task"] == 0
|
||||
|
||||
# Schedule task
|
||||
actor_manager.schedule_actor_task(
|
||||
tracked_actor, "fn", on_error=fail_callback_task
|
||||
)
|
||||
|
||||
# Yield control and wait for task resolution. This will invoke the callback.
|
||||
actor_manager.next()
|
||||
|
||||
assert stats["failed_actor"] == 1
|
||||
assert stats["failed_task"] == bool(where != "init")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
def test_stop_actor_before_start(
|
||||
ray_start_4_cpus, tmp_path, cleanup, resource_manager_cls
|
||||
):
|
||||
"""Test that actor failures are handled properly.
|
||||
|
||||
- Start actor that either fails on init or in a task (RayActorError)
|
||||
- Schedule task on actor
|
||||
- Assert that the correct callbacks are called
|
||||
"""
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
hang_marker = tmp_path / "hang.txt"
|
||||
|
||||
@ray.remote
|
||||
class HangingActor:
|
||||
def __init__(self):
|
||||
while not hang_marker.exists():
|
||||
time.sleep(0.05)
|
||||
|
||||
tracked_actor = actor_manager.add_actor(
|
||||
HangingActor,
|
||||
kwargs={},
|
||||
resource_request=ResourceRequest([{"CPU": 1}]),
|
||||
on_start=_raise(RuntimeError, "Should not have started"),
|
||||
on_stop=_raise(RuntimeError, "Should not have stopped"),
|
||||
)
|
||||
while not actor_manager.is_actor_started(tracked_actor):
|
||||
actor_manager.next(0.05)
|
||||
|
||||
# Actor started but hasn't triggered on_start, yet
|
||||
actor_manager.remove_actor(tracked_actor)
|
||||
hang_marker.write_text("")
|
||||
while actor_manager.is_actor_started(tracked_actor):
|
||||
actor_manager.next(0.05)
|
||||
|
||||
assert actor_manager.num_live_actors == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"resource_manager_cls", [FixedResourceManager, PlacementGroupResourceManager]
|
||||
)
|
||||
@pytest.mark.parametrize("start_thread", [False, True])
|
||||
def test_stop_actor_custom_future(
|
||||
ray_start_4_cpus, tmp_path, cleanup, resource_manager_cls, start_thread
|
||||
):
|
||||
"""If we pass a custom stop future, the actor should still be shutdown by GC.
|
||||
|
||||
This should also be the case when we start a thread in the background, as we
|
||||
do e.g. in Ray Tune's function runner.
|
||||
"""
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
hang_marker = tmp_path / "hang.txt"
|
||||
|
||||
actor_name = f"stopping_actor_{resource_manager_cls.__name__}_{start_thread}"
|
||||
|
||||
@ray.remote(name=actor_name)
|
||||
class HangingStopActor:
|
||||
def __init__(self):
|
||||
self._thread = None
|
||||
self._stop_event = threading.Event()
|
||||
if start_thread:
|
||||
|
||||
def entrypoint():
|
||||
while True:
|
||||
print("Thread!")
|
||||
time.sleep(1)
|
||||
if self._stop_event.is_set():
|
||||
sys.exit(0)
|
||||
|
||||
self._thread = threading.Thread(target=entrypoint)
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
print("Waiting")
|
||||
while not hang_marker.exists():
|
||||
time.sleep(0.05)
|
||||
self._stop_event.set()
|
||||
print("stopped")
|
||||
|
||||
start_barrier = Barrier(max_results=1)
|
||||
stop_barrier = Barrier(max_results=1)
|
||||
|
||||
tracked_actor = actor_manager.add_actor(
|
||||
HangingStopActor,
|
||||
kwargs={},
|
||||
resource_request=ResourceRequest([{"CPU": 1}]),
|
||||
on_start=start_barrier.arrive,
|
||||
on_stop=stop_barrier.arrive,
|
||||
)
|
||||
while not start_barrier.completed:
|
||||
actor_manager.next(0.05)
|
||||
|
||||
# Actor is alive
|
||||
assert ray.get_actor(actor_name)
|
||||
|
||||
stop_future = actor_manager.schedule_actor_task(tracked_actor, "stop")
|
||||
actor_manager.remove_actor(tracked_actor, kill=False, stop_future=stop_future)
|
||||
|
||||
assert not stop_barrier.completed
|
||||
|
||||
hang_marker.write_text("!")
|
||||
|
||||
while not stop_barrier.completed:
|
||||
actor_manager.next(0.05)
|
||||
|
||||
# Actor should have stopped now and should get cleaned up
|
||||
with pytest.raises(ValueError):
|
||||
ray.get_actor(actor_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
@@ -0,0 +1,122 @@
|
||||
from collections import Counter
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.air import ResourceRequest
|
||||
from ray.air.execution import FixedResourceManager, PlacementGroupResourceManager
|
||||
from ray.air.execution._internal.actor_manager import RayActorManager
|
||||
|
||||
RESOURCE_MANAGERS = [FixedResourceManager, PlacementGroupResourceManager]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ray_start_4_cpus():
|
||||
address_info = ray.init(num_cpus=4)
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@ray.remote
|
||||
class Actor:
|
||||
def foo(self, val, error: bool = False):
|
||||
if error:
|
||||
raise RuntimeError
|
||||
return val
|
||||
|
||||
|
||||
@pytest.mark.parametrize("resource_manager_cls", RESOURCE_MANAGERS)
|
||||
def test_resolve(ray_start_4_cpus, resource_manager_cls):
|
||||
"""Test that the `on_result` callback is invoked when a task completes.
|
||||
|
||||
- Instantiate global data object
|
||||
- Schedule task that returns a value
|
||||
- The callback writes the returned value to the global data object
|
||||
"""
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
seen = {"data": 0}
|
||||
|
||||
def result_callback(tracked_actor, result):
|
||||
seen["data"] = result
|
||||
|
||||
tracked_actor = actor_manager.add_actor(
|
||||
cls=Actor, kwargs={}, resource_request=ResourceRequest([{"CPU": 4}])
|
||||
)
|
||||
actor_manager.schedule_actor_task(
|
||||
tracked_actor, "foo", (4, False), on_result=result_callback
|
||||
)
|
||||
actor_manager.next()
|
||||
actor_manager.next()
|
||||
|
||||
assert seen["data"] == 4
|
||||
|
||||
|
||||
@pytest.mark.parametrize("resource_manager_cls", RESOURCE_MANAGERS)
|
||||
@pytest.mark.parametrize("num_tasks", [1, 10, 100])
|
||||
def test_resolve_many(ray_start_4_cpus, resource_manager_cls, num_tasks):
|
||||
"""Schedule ``num_tasks`` tasks and wait until ``wait_for_events`` of them resolve.
|
||||
|
||||
Every resolved task will increase a counter by its return value (1).
|
||||
"""
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
seen = {"data": 0}
|
||||
|
||||
def result_callback(tracked_actor, result):
|
||||
seen["data"] += result
|
||||
|
||||
tracked_actor = actor_manager.add_actor(
|
||||
cls=Actor, kwargs={}, resource_request=ResourceRequest([{"CPU": 4}])
|
||||
)
|
||||
actor_manager.next()
|
||||
|
||||
for i in range(num_tasks):
|
||||
actor_manager.schedule_actor_task(
|
||||
tracked_actor, "foo", (1, False), on_result=result_callback
|
||||
)
|
||||
|
||||
for i in range(num_tasks):
|
||||
actor_manager.next()
|
||||
assert seen["data"] == i + 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("resource_manager_cls", RESOURCE_MANAGERS)
|
||||
def test_error_noop(ray_start_4_cpus, resource_manager_cls):
|
||||
"""When no `on_error` callback is specified, errors should be ignored."""
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
tracked_actor = actor_manager.add_actor(
|
||||
cls=Actor, kwargs={}, resource_request=ResourceRequest([{"CPU": 4}])
|
||||
)
|
||||
actor_manager.schedule_actor_task(tracked_actor, "foo", (1, True))
|
||||
actor_manager.next()
|
||||
actor_manager.next()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("resource_manager_cls", RESOURCE_MANAGERS)
|
||||
def test_error_custom(ray_start_4_cpus, resource_manager_cls):
|
||||
"""When an `on_error` callback is specified, it is invoked."""
|
||||
actor_manager = RayActorManager(resource_manager=resource_manager_cls())
|
||||
|
||||
stats = Counter()
|
||||
|
||||
def error_callback(tracked_actor, exception):
|
||||
stats["exception"] += 1
|
||||
|
||||
tracked_actor = actor_manager.add_actor(
|
||||
cls=Actor, kwargs={}, resource_request=ResourceRequest([{"CPU": 4}])
|
||||
)
|
||||
actor_manager.schedule_actor_task(
|
||||
tracked_actor, "foo", (1, True), on_error=error_callback
|
||||
)
|
||||
|
||||
actor_manager.next()
|
||||
actor_manager.next()
|
||||
assert stats["exception"] == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main(["-v", __file__]))
|
||||
Reference in New Issue
Block a user