Repository: ray-project/ray

Issue:
Actor processes not destroyed after Python ref is released
This can be reproduced with the following script. It will create 100 python processes, but even after the list is de-referenced the processes hang around forever (check `ps -A | grep python`).

```
import ray
import time
import sys

@ray.remote
class Actor(object):
    def __init__(self):
        pass

ray.init()
refs = [Actor.remote() for _ in range(100)]
time.sleep(5)
refs = None
time.sleep(99999)
```


Discussion:
I'm looking into this and it looks like the reference counts on the actor handles are too high, so they aren't going out of scope. That is, the destructors aren't getting called.
In the meantime, as a workaround, you could call

```python
a = Actor.remote()
a.__ray_terminate__.remote(a._ray_actor_id.id())
```
Ok, looks like the bug was introduced somewhere between `0.2.0` and `0.2.1`.
Looks like the problem first appeared in #902.

Resolving patch:
diff --git a/python/ray/actor.py b/python/ray/actor.py
--- a/python/ray/actor.py
+++ b/python/ray/actor.py
@@ -251,6 +251,8 @@ def __ray_restore_from_checkpoint__(cls, pickled_checkpoint):
     # constructor.
     exported = []
 
+    # Create objects to wrap method invocations. This is done so that we can
+    # invoke methods with actor.method.remote() instead of actor.method().
     class ActorMethod(object):
         def __init__(self, actor, method_name, method_signature):
             self.actor = actor
@@ -307,14 +309,6 @@ def _manual_init(self, *args, **kwargs):
                 self._ray_method_signatures[k] = signature.extract_signature(
                     v, ignore_first=True)
 
-            # Create objects to wrap method invocations. This is done so that
-            # we can invoke methods with actor.method.remote() instead of
-            # actor.method().
-            self._actor_method_invokers = dict()
-            for k, v in self._ray_actor_methods.items():
-                self._actor_method_invokers[k] = ActorMethod(
-                    self, k, self._ray_method_signatures[k])
-
             # Do not export the actor class or the actor if run in PYTHON_MODE
             # Instead, instantiate the actor locally and add it to
             # global_worker's dictionary
@@ -390,10 +384,17 @@ def __getattribute__(self, attr):
                         "_actor_method_call"]:
                 return object.__getattribute__(self, attr)
             if attr in self._ray_actor_methods.keys():
-                return self._actor_method_invokers[attr]
-            # There is no method with this name, so raise an exception.
-            raise AttributeError("'{}' Actor object has no attribute '{}'"
-                                 .format(Class, attr))
+                # We create the ActorMethod on the fly here so that the
+                # ActorHandle doesn't need a reference to the ActorMethod. The
+                # ActorMethod has a reference to the ActorHandle and this was
+                # causing cyclic references which were prevent object
+                # deallocation from behaving in a predictable manner.
+                return ActorMethod(self, attr,
+                                   self._ray_method_signatures[attr])
+            else:
+                # There is no method with this name, so raise an exception.
+                raise AttributeError("'{}' Actor object has no attribute '{}'"
+                                     .format(Class, attr))
 
         def __repr__(self):
             return "Actor(" + self._ray_actor_id.hex() + ")"


Test patch:
diff --git a/python/ray/test/test_utils.py b/python/ray/test/test_utils.py
--- a/python/ray/test/test_utils.py
+++ b/python/ray/test/test_utils.py
@@ -118,6 +118,7 @@ def _pid_alive(pid):
     """
     try:
         os.kill(pid, 0)
+        return True
     except OSError:
         return False
 
diff --git a/test/actor_test.py b/test/actor_test.py
--- a/test/actor_test.py
+++ b/test/actor_test.py
@@ -5,10 +5,12 @@
 import collections
 import random
 import numpy as np
+import os
 import time
 import unittest
 
 import ray
+import ray.test.test_utils
 
 
 class ActorAPI(unittest.TestCase):
@@ -279,6 +281,40 @@ def f(self, y):
 
         ray.worker.cleanup()
 
+    def testActorDeletion(self):
+        ray.init(num_workers=0)
+
+        # Make sure that when an actor handles goes out of scope, the actor
+        # destructor is called.
+
+        @ray.remote
+        class Actor(object):
+            def getpid(self):
+                return os.getpid()
+
+        a = Actor.remote()
+        pid = ray.get(a.getpid.remote())
+        a = None
+        ray.test.test_utils.wait_for_pid_to_exit(pid)
+
+        actors = [Actor.remote() for _ in range(10)]
+        pids = ray.get([a.getpid.remote() for a in actors])
+        a = None
+        actors = None
+        [ray.test.test_utils.wait_for_pid_to_exit(pid) for pid in pids]
+
+        @ray.remote
+        class Actor(object):
+            def method(self):
+                return 1
+
+        # Make sure that if we create an actor and call a method on it
+        # immediately, the actor doesn't get killed before the method is
+        # called.
+        self.assertEqual(ray.get(Actor.remote().method.remote()), 1)
+
+        ray.worker.cleanup()
+
     def testActorState(self):
         ray.init()
 
diff --git a/test/jenkins_tests/multi_node_tests/many_drivers_test.py b/test/jenkins_tests/multi_node_tests/many_drivers_test.py
--- a/test/jenkins_tests/multi_node_tests/many_drivers_test.py
+++ b/test/jenkins_tests/multi_node_tests/many_drivers_test.py
@@ -30,10 +30,10 @@ def check_ids(self):
 
 
 def driver(redis_address, driver_index):
-    """The script for driver 0.
+    """The script for all drivers.
 
-    This driver should create five actors that each use one GPU and some actors
-    that use no GPUs. After a while, it should exit.
+    This driver should create five actors that each use one GPU. After a while,
+    it should exit.
     """
     ray.init(redis_address=redis_address)
 
@@ -44,7 +44,7 @@ def driver(redis_address, driver_index):
     for i in range(driver_index - max_concurrent_drivers + 1):
         _wait_for_event("DRIVER_{}_DONE".format(i), redis_address)
 
-    def try_to_create_actor(actor_class, timeout=100):
+    def try_to_create_actor(actor_class, timeout=500):
         # Try to create an actor, but allow failures while we wait for the
         # monitor to release the resources for the removed drivers.
         start_time = time.time()