Repository: ray-project/ray

Issue:
Hanging in test/runtest.py::ResourcesTest::testZeroCPUsActor on Travis.
I've seen the following test failure a fair amount in the `RAY_USE_XRAY=1` job.

```
test/runtest.py::ResourcesTest::testResourceConstraints [32mPASSED[0m[36m           [ 78%][0m
test/runtest.py::ResourcesTest::testTwoCustomResources [32mPASSED[0m[36m            [ 80%][0m
test/runtest.py::ResourcesTest::testZeroCPUs [32mPASSED[0m[36m                      [ 81%][0m
test/runtest.py::ResourcesTest::testZeroCPUsActor 

No output has been received in the last 10m0s, this potentially indicates a stalled build or something wrong with the build itself.
Check the details on how to adjust your build configuration on: https://docs.travis-ci.com/user/common-build-problems/#Build-times-out-because-no-output-was-received

The build has been terminated
```



Discussion:
I can reproduce this with

```
for i in {1..100}; do python -m pytest -s -v test/runtest.py::ResourcesTest::testZeroCPUsActor; done
```

in docker.

Resolving patch:
diff --git a/python/ray/actor.py b/python/ray/actor.py
--- a/python/ray/actor.py
+++ b/python/ray/actor.py
@@ -373,6 +373,15 @@ def _submit(self,
                 self._num_cpus, self._num_gpus, self._resources, num_cpus,
                 num_gpus, resources)
 
+            # If the actor methods require CPU resources, then set the required
+            # placement resources. If actor_placement_resources is empty, then
+            # the required placement resources will be the same as resources.
+            actor_placement_resources = {}
+            assert self._actor_method_cpus in [0, 1]
+            if self._actor_method_cpus == 1:
+                actor_placement_resources = resources.copy()
+                actor_placement_resources["CPU"] += 1
+
             creation_args = [self._class_id]
             function_id = compute_actor_creation_function_id(self._class_id)
             [actor_cursor] = worker.submit_task(
@@ -380,7 +389,8 @@ def _submit(self,
                 creation_args,
                 actor_creation_id=actor_id,
                 num_return_vals=1,
-                resources=resources)
+                resources=resources,
+                placement_resources=actor_placement_resources)
 
         # We initialize the actor counter at 1 to account for the actor
         # creation task.
@@ -566,6 +576,7 @@ def _actor_method_call(self,
             # We add one for the dummy return ID.
             num_return_vals=num_return_vals + 1,
             resources={"CPU": self._ray_actor_method_cpus},
+            placement_resources={},
             driver_id=self._ray_actor_driver_id)
         # Update the actor counter and cursor to reflect the most recent
         # invocation.
diff --git a/python/ray/worker.py b/python/ray/worker.py
--- a/python/ray/worker.py
+++ b/python/ray/worker.py
@@ -556,6 +556,7 @@ def submit_task(self,
                     execution_dependencies=None,
                     num_return_vals=None,
                     resources=None,
+                    placement_resources=None,
                     driver_id=None):
         """Submit a remote task to the scheduler.
 
@@ -581,6 +582,9 @@ def submit_task(self,
             num_return_vals: The number of return values this function should
                 have.
             resources: The resource requirements for this task.
+            placement_resources: The resources required for placing the task.
+                If this is not provided or if it is an empty dictionary, then
+                the placement resources will be equal to resources.
             driver_id: The ID of the relevant driver. This is almost always the
                 driver ID of the driver that is currently running. However, in
                 the exceptional case that an actor task is being dispatched to
@@ -634,6 +638,9 @@ def submit_task(self,
                     raise ValueError(
                         "Resource quantities must all be whole numbers.")
 
+            if placement_resources is None:
+                placement_resources = {}
+
             with self.state_lock:
                 # Increment the worker's task index to track how many tasks
                 # have been submitted by the current task so far.
@@ -646,7 +653,8 @@ def submit_task(self,
                 num_return_vals, self.current_task_id, task_index,
                 actor_creation_id, actor_creation_dummy_object_id, actor_id,
                 actor_handle_id, actor_counter, is_actor_checkpoint_method,
-                execution_dependencies, resources, self.use_raylet)
+                execution_dependencies, resources, placement_resources,
+                self.use_raylet)
             self.local_scheduler_client.submit(task)
 
             return task.returns()
@@ -2144,7 +2152,7 @@ def connect(info,
             worker.current_task_id, worker.task_index,
             ray.ObjectID(NIL_ACTOR_ID), ray.ObjectID(NIL_ACTOR_ID),
             ray.ObjectID(NIL_ACTOR_ID), ray.ObjectID(NIL_ACTOR_ID),
-            nil_actor_counter, False, [], {"CPU": 0}, worker.use_raylet)
+            nil_actor_counter, False, [], {"CPU": 0}, {}, worker.use_raylet)
 
         # Add the driver task to the task table.
         if not worker.use_raylet:


Test patch:
diff --git a/src/ray/raylet/worker_pool_test.cc b/src/ray/raylet/worker_pool_test.cc
--- a/src/ray/raylet/worker_pool_test.cc
+++ b/src/ray/raylet/worker_pool_test.cc
@@ -65,7 +65,7 @@ static inline TaskSpecification ExampleTaskSpec(
     const Language &language = Language::PYTHON) {
   return TaskSpecification(UniqueID::nil(), UniqueID::nil(), 0, ActorID::nil(),
                            ObjectID::nil(), actor_id, ActorHandleID::nil(), 0,
-                           FunctionID::nil(), {}, 0, {{}}, language);
+                           FunctionID::nil(), {}, 0, {{}}, {{}}, language);
 }
 
 TEST_F(WorkerPoolTest, HandleWorkerRegistration) {
diff --git a/test/failure_test.py b/test/failure_test.py
--- a/test/failure_test.py
+++ b/test/failure_test.py
@@ -548,6 +548,25 @@ class Foo(object):
     wait_for_errors(ray_constants.INFEASIBLE_TASK_ERROR, 2)
 
 
+@pytest.mark.skipif(
+    os.environ.get("RAY_USE_XRAY") != "1",
+    reason="This test only works with xray.")
+def test_warning_for_infeasible_zero_cpu_actor(shutdown_only):
+    # Check that we cannot place an actor on a 0 CPU machine and that we get an
+    # infeasibility warning (even though the actor creation task itself
+    # requires no CPUs).
+
+    ray.init(num_cpus=0)
+
+    @ray.remote
+    class Foo(object):
+        pass
+
+    # The actor creation should be infeasible.
+    Foo.remote()
+    wait_for_errors(ray_constants.INFEASIBLE_TASK_ERROR, 1)
+
+
 @pytest.fixture
 def ray_start_two_nodes():
     # Start the Ray processes.