Repository: OpenMined/PySyft

Issue:
Urgent: fail gracefully when initializing two VirtualWorker objects with the same id/name
When people are introduced to PySyft, they often fall into the mistake (particularly in Jupyter Notebbooks) of initializing a VirtualWorker with the same ID twice. This causes really strange errors because both workers end up co-existing together. We need to modify the VirtualWorker (or perhaps the BaseWorker) API to make it so that this fails gracefully (aka... that the new worker actually becomes the old worker or all intensive purposes)

Aka - we want this code to function normally

bob = sy.VirtualWorker(hook, "bob")

x = th.tensor([1,2,3,4,5]).send(bob)

bob = sy.VirtualWorker(hook, "bob")

y = th.tensor([1,2,3,4,5]).send(bob)

z = x + y

z.get() # returns [2,4,6,8,10]


We should accomplish this by having both the new "bob" and the old "bob" point to the same underlying _objects dictionary.



Discussion:
At any moment, you can check all the virtual workers present on your process by retrieving syft.hook.local_worker.(known_workers) as they get automatically added to this list on __init__. Just making a search and returning the already existing one if any should fix the pb
@LaRiffle Tried this out. Trying to naively return the known worker with the same id, causes over half of the tests to fail. I am of the impression it might be best to make this simply throw a verbose error for the moment. #2000 for reference to my changes 

Resolving patch:
diff --git a/syft/workers/base.py b/syft/workers/base.py
--- a/syft/workers/base.py
+++ b/syft/workers/base.py
@@ -73,17 +73,6 @@ def __init__(
         # objects where all objects are stored using their IDs as keys.
         self._objects = {}
 
-        # Declare workers as appropriate
-        self._known_workers = {}
-        if hook.local_worker is not None:
-            if self.id not in self.hook.local_worker._known_workers:
-                hook.local_worker.add_worker(self)
-            for worker_id, worker in hook.local_worker._known_workers.items():
-                if worker_id not in self._known_workers:
-                    self.add_worker(worker)
-                if self.id not in worker._known_workers:
-                    worker.add_worker(self)
-
         # For performance, we cache each
         self._message_router = {
             MSGTYPE.CMD: self.execute_command,
@@ -94,8 +83,29 @@ def __init__(
             MSGTYPE.GET_SHAPE: self.get_tensor_shape,
             MSGTYPE.SEARCH: self.search,
         }
+
         self.load_data(data)
 
+        # Declare workers as appropriate
+        self._known_workers = {}
+        if hook.local_worker is not None:
+            known_workers = self.hook.local_worker._known_workers
+            if self.id in known_workers:
+                if isinstance(known_workers[self.id], type(self)):
+                    # If a worker with this id already exists and it has the
+                    # same type as the one being created, we copy all the attributes
+                    # of the existing worker to this one.
+                    self.__dict__.update(known_workers[self.id].__dict__)
+                else:
+                    raise RuntimeError("Worker initialized with the same id and different types.")
+            else:
+                hook.local_worker.add_worker(self)
+                for worker_id, worker in hook.local_worker._known_workers.items():
+                    if worker_id not in self._known_workers:
+                        self.add_worker(worker)
+                    if self.id not in worker._known_workers:
+                        worker.add_worker(self)
+
     # SECTION: Methods which MUST be overridden by subclasses
     @abstractmethod
     def _send_msg(self, message: bin, location: "BaseWorker"):


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,10 +1,27 @@
 import pytest
 import torch
+from multiprocessing import Process
 
 import syft
 from syft import TorchHook
 
 
+@pytest.fixture()
+def start_proc():  # pragma: no cover
+    """ helper function for spinning up a websocket participant """
+
+    def _start_proc(participant, kwargs):
+        def target():
+            server = participant(**kwargs)
+            server.start()
+
+        p = Process(target=target)
+        p.start()
+        return p
+
+    return _start_proc
+
+
 @pytest.fixture(scope="session", autouse=True)
 def hook():
     hook = TorchHook(torch)
diff --git a/test/workers/test_base.py b/test/workers/test_base.py
new file mode 100644
--- /dev/null
+++ b/test/workers/test_base.py
@@ -0,0 +1,46 @@
+import pytest
+import time
+
+import syft as sy
+import torch as th
+
+from syft.workers import WebsocketClientWorker
+from syft.workers import WebsocketServerWorker
+
+
+def test_create_already_existing_worker(hook):
+    # Shares tensor with bob
+    bob = sy.VirtualWorker(hook, "bob")
+    x = th.tensor([1, 2, 3]).send(bob)
+
+    # Recreates bob and shares a new tensor
+    bob = sy.VirtualWorker(hook, "bob")
+    y = th.tensor([2, 2, 2]).send(bob)
+
+    # Recreates bob and shares a new tensor
+    bob = sy.VirtualWorker(hook, "bob")
+    z = th.tensor([2, 2, 10]).send(bob)
+
+    # Both workers should be the same, so the following operation should be valid
+    try:
+        _ = x + y * z
+    except KeyError:
+        assert False
+
+
+def test_create_already_existing_worker_with_different_type(hook, start_proc):
+    # Shares tensor with bob
+    bob = sy.VirtualWorker(hook, "bob")
+    _ = th.tensor([1, 2, 3]).send(bob)
+
+    kwargs = {"id": "fed1", "host": "localhost", "port": 8765, "hook": hook}
+    server = start_proc(WebsocketServerWorker, kwargs)
+
+    time.sleep(0.1)
+
+    # Recreates bob as a different type of worker
+    kwargs = {"id": "bob", "host": "localhost", "port": 8765, "hook": hook}
+    with pytest.raises(RuntimeError):
+        bob = WebsocketClientWorker(**kwargs)
+
+    server.terminate()
diff --git a/test/workers/test_websocket_worker.py b/test/workers/test_websocket_worker.py
--- a/test/workers/test_websocket_worker.py
+++ b/test/workers/test_websocket_worker.py
@@ -2,22 +2,9 @@
 import time
 from syft.workers import WebsocketClientWorker
 from syft.workers import WebsocketServerWorker
-from multiprocessing import Process
 
 
-def start_proc(participant, kwargs):  # pragma: no cover
-    """ helper function for spinning up a websocket participant """
-
-    def target():
-        server = participant(**kwargs)
-        server.start()
-
-    p = Process(target=target)
-    p.start()
-    return p
-
-
-def test_websocket_worker(hook):
+def test_websocket_worker(hook, start_proc):
     """Evaluates that you can do basic tensor operations using
     WebsocketServerWorker"""