Repository: mars-project/mars

Issue:
Add support for replacing actor classes given registration
**Is your feature request related to a problem? Please describe.**
Sometimes it is convenient for us to replace existing actor implementations when running or deploying Mars. For instance, when doing tests, we need to replace some functions of actors to report something or make some delays. We need a native mechanism in actor system to simplify implementation of these functions.


Discussion:

Resolving patch:
diff --git a/mars/actors/__init__.py b/mars/actors/__init__.py
--- a/mars/actors/__init__.py
+++ b/mars/actors/__init__.py
@@ -15,6 +15,7 @@
 # limitations under the License.
 
 
-from .core import create_actor_pool, Actor, FunctionActor, new_client
+from .core import create_actor_pool, Actor, FunctionActor, new_client, \
+    register_actor_implementation, unregister_actor_implementation
 from .errors import ActorPoolNotStarted, ActorNotExist, ActorAlreadyExist
 from .distributor import Distributor
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -61,7 +61,7 @@ def execfile(fname, globs, locs=None):
 extension_kw = dict()
 if 'CYTHON_TRACE' in os.environ:
     extension_kw['define_macros'] = [('CYTHON_TRACE_NOGIL', '1'), ('CYTHON_TRACE', '1')]
-    cythonize_kw['compiler_directives'] = {'linetrace': True, 'binding': True}
+    cythonize_kw['compiler_directives'] = {'linetrace': True}
 
 if 'MSC' in sys.version:
     extra_compile_args = ['/Ot', '/I' + os.path.join(repo_root, 'misc')]


Test patch:
diff --git a/mars/actors/pool/tests/test_gevent_pool.py b/mars/actors/pool/tests/test_gevent_pool.py
--- a/mars/actors/pool/tests/test_gevent_pool.py
+++ b/mars/actors/pool/tests/test_gevent_pool.py
@@ -25,8 +25,9 @@
 import gevent
 
 from mars.compat import six, BrokenPipeError
-from mars.actors import create_actor_pool as new_actor_pool, Actor, \
-    ActorPoolNotStarted, ActorAlreadyExist, ActorNotExist, Distributor, new_client
+from mars.actors import create_actor_pool as new_actor_pool, Actor, FunctionActor, \
+    ActorPoolNotStarted, ActorAlreadyExist, ActorNotExist, Distributor, new_client, \
+    register_actor_implementation, unregister_actor_implementation
 from mars.actors.pool.gevent_pool import Dispatcher, Connections
 from mars.utils import to_binary
 
@@ -136,6 +137,24 @@ def on_receive(self, message):  # noqa: C901
             return self.value
 
 
+class DummyFunctionActor(FunctionActor):
+    def __init__(self, value):
+        super(DummyFunctionActor, self).__init__()
+        self._val = value
+
+    def func(self, value):
+        return value + self._val
+
+
+class SurrogateFunctionActor(DummyFunctionActor):
+    def __init__(self, value):
+        super(SurrogateFunctionActor, self).__init__(value)
+        self._val = value * 2
+
+    def func(self, value):
+        return value * self._val
+
+
 class DummyDistributor(Distributor):
     def distribute(self, uid):
         if str(uid).startswith('admin-'):
@@ -203,6 +222,24 @@ def testLocalPostCreatePreDestroy(self):
             actor_ref = pool.create_actor(EventActor)
             actor_ref.destroy()
 
+    def testFunctionActor(self):
+        with create_actor_pool(n_process=1, backend='gevent') as pool:
+            actor_ref = pool.create_actor(DummyFunctionActor, 1)
+            self.assertEqual(actor_ref.func(2), 3)
+            actor_ref.destroy()
+
+            try:
+                register_actor_implementation(DummyFunctionActor, SurrogateFunctionActor)
+                actor_ref = pool.create_actor(DummyFunctionActor, 3)
+                self.assertEqual(actor_ref.func(2), 12)
+                actor_ref.destroy()
+            finally:
+                unregister_actor_implementation(DummyFunctionActor)
+
+            actor_ref = pool.create_actor(DummyFunctionActor, 2)
+            self.assertEqual(actor_ref.func(2), 4)
+            actor_ref.destroy()
+
     def testLocalCreateActor(self):
         with create_actor_pool(n_process=1, backend='gevent') as pool:
             actor_ref = pool.create_actor(DummyActor, 1)
@@ -290,6 +327,10 @@ def testLocalTell(self):
             pool.sleep(0.5)
             self.assertEqual(ref2.send(('get',)), 9)
 
+            # error needed when illegal uids are passed
+            with self.assertRaises(TypeError):
+                ref1.send(('tell', pool.actor_ref(set()), 'add', 3))
+
     def testLocalDestroyHasActor(self):
         with create_actor_pool(n_process=1, backend='gevent') as pool:
             ref1 = pool.create_actor(DummyActor, 1)
@@ -298,6 +339,12 @@ def testLocalDestroyHasActor(self):
             pool.destroy_actor(ref1)
             self.assertFalse(pool.has_actor(ref1))
 
+            # error needed when illegal uids are passed
+            with self.assertRaises(TypeError):
+                pool.has_actor(pool.actor_ref(set()))
+            with self.assertRaises(TypeError):
+                pool.has_actor(pool.actor_ref(set()), wait=False).result()
+
             ref1 = pool.create_actor(DummyActor, 1)
             future = pool.destroy_actor(ref1, wait=False)
             future.result()
@@ -306,7 +353,7 @@ def testLocalDestroyHasActor(self):
             ref1 = pool.create_actor(DummyActor, 1)
             ref2 = ref1.send(('create', (DummyActor, 2)))
 
-            self.assertTrue(pool.has_actor(ref2))
+            self.assertTrue(pool.has_actor(ref2, wait=False).result())
 
             ref1.send(('delete', ref2))
             self.assertFalse(ref1.send(('has', ref2)))
@@ -478,6 +525,10 @@ def testProcessTell(self):
             pool.sleep(0.5)
             self.assertEqual(ref2.send(('get',)), 9)
 
+            # error needed when illegal uids are passed
+            with self.assertRaises(TypeError):
+                ref1.send(('tell', pool.actor_ref(set()), 'add', 3))
+
     def testProcessDestroyHas(self):
         with create_actor_pool(n_process=2, distributor=DummyDistributor(2),
                                backend='gevent') as pool:
@@ -486,6 +537,12 @@ def testProcessDestroyHas(self):
             pool.destroy_actor(ref1)
             self.assertFalse(pool.has_actor(ref1))
 
+            # error needed when illegal uids are passed
+            with self.assertRaises(TypeError):
+                pool.has_actor(pool.actor_ref(set()))
+            with self.assertRaises(TypeError):
+                pool.has_actor(pool.actor_ref(set()), wait=False).result()
+
             ref1 = pool.create_actor(DummyActor, 1, uid='admin-1')
             self.assertTrue(pool.has_actor(ref1, wait=False).result())
             pool.destroy_actor(ref1, wait=False).result()
@@ -497,7 +554,7 @@ def testProcessDestroyHas(self):
             self.assertTrue(future.result())
 
             ref1.send(('delete', ref2))
-            self.assertFalse(ref1.send(('has', ref2)))
+            self.assertFalse(ref1.send(('has_async', ref2)))
 
             with self.assertRaises(ActorNotExist):
                 pool.destroy_actor(pool.actor_ref('fake_uid'))
@@ -1214,7 +1271,7 @@ def testRemoteDestroyHasLocalPool(self):
             ref1 = client.create_actor(DummyActor, 1, address=addr)
             ref2 = ref1.send(('create', (DummyActor, 2), dict(address=addr)))
 
-            self.assertTrue(client.has_actor(ref2))
+            self.assertTrue(client.has_actor(ref2, wait=False).result())
 
             ref1.send(('delete', ref2))
             self.assertFalse(ref1.send(('has', ref2)))
@@ -1269,7 +1326,7 @@ def testRemoteDestroyHasProcessPool(self):
             ref1 = client.create_actor(DummyActor, 1, address=addr)
             ref2 = ref1.send(('create', (DummyActor, 2), dict(address=addr)))
 
-            self.assertTrue(client.has_actor(ref2))
+            self.assertTrue(client.has_actor(ref2, wait=False).result())
 
             ref1.send(('delete', ref2))
             self.assertFalse(ref1.send(('has', ref2)))
@@ -1326,7 +1383,7 @@ def testRemoteDestroyHasLocalPoolToLocalPool(self):
                 ref1 = client.create_actor(DummyActor, 1, address=addr1)
                 ref2 = ref1.send(('create', (DummyActor, 2), dict(address=addr2)))
 
-                self.assertTrue(client.has_actor(ref2))
+                self.assertTrue(client.has_actor(ref2, wait=False).result())
 
                 ref1.send(('delete', ref2))
                 self.assertFalse(ref1.send(('has', ref2)))
@@ -1383,7 +1440,7 @@ def testRemoteDestroyHasProcessPoolToProcessPool(self):
                 ref1 = client.create_actor(DummyActor, 1, address=addr1)
                 ref2 = ref1.send(('create', (DummyActor, 2), dict(address=addr2)))
 
-                self.assertTrue(client.has_actor(ref2))
+                self.assertTrue(client.has_actor(ref2, wait=False).result())
 
                 ref1.send(('delete', ref2))
                 self.assertFalse(ref1.send(('has', ref2)))
@@ -1434,7 +1491,7 @@ def testRemoteDestroyHasLocalPoolToProcessPool(self):
                 ref1 = client.create_actor(DummyActor, 1, address=addr1)
                 ref2 = ref1.send(('create', (DummyActor, 2), dict(address=addr2)))
 
-                self.assertTrue(client.has_actor(ref2))
+                self.assertTrue(client.has_actor(ref2, wait=False).result())
 
                 ref1.send(('delete', ref2))
                 self.assertFalse(ref1.send(('has', ref2)))
@@ -1486,7 +1543,7 @@ def testRemoteDestroyHasProcessPoolToLocalPool(self):
                 ref1 = client.create_actor(DummyActor, 1, address=addr1)
                 ref2 = ref1.send(('create', (DummyActor, 2), dict(address=addr2)))
 
-                self.assertTrue(client.has_actor(ref2))
+                self.assertTrue(client.has_actor(ref2, wait=False).result())
 
                 ref1.send(('delete', ref2))
                 self.assertFalse(ref1.send(('has', ref2)))
diff --git a/mars/scheduler/tests/op_delayer.py b/mars/scheduler/tests/op_delayer.py
--- a/mars/scheduler/tests/op_delayer.py
+++ b/mars/scheduler/tests/op_delayer.py
@@ -15,25 +15,22 @@
 import os
 
 from mars.scheduler.operands import OperandActor, OperandPosition
+from mars.actors.core import register_actor_implementation
 
-_old_on_running = OperandActor._on_running
-_old_on_finished = OperandActor._on_finished
 
+class DelayedOperandActor(OperandActor):
+    def _on_running(self):
+        super(DelayedOperandActor, self)._on_running()
+        if 'DELAY_STATE_FILE' in os.environ and os.path.exists(os.environ['DELAY_STATE_FILE']):
+            self.ctx.sleep(1)
 
-def _on_running(self):
-    _old_on_running(self)
-    if 'DELAY_STATE_FILE' in os.environ and os.path.exists(os.environ['DELAY_STATE_FILE']):
-        self.ctx.sleep(1)
+    def _on_finished(self):
+        super(DelayedOperandActor, self)._on_finished()
+        if self._position == OperandPosition.TERMINAL and 'TERMINATE_STATE_FILE' in os.environ:
+            try:
+                open(os.environ['TERMINATE_STATE_FILE'], 'w').close()
+            except OSError:
+                pass
 
 
-def _on_finished(self):
-    _old_on_finished(self)
-    if self._position == OperandPosition.TERMINAL and 'TERMINATE_STATE_FILE' in os.environ:
-        try:
-            open(os.environ['TERMINATE_STATE_FILE'], 'w').close()
-        except OSError:
-            pass
-
-
-OperandActor._on_running = _on_running
-OperandActor._on_finished = _on_finished
+register_actor_implementation(OperandActor, DelayedOperandActor)