Repository: dask/distributed

Issue:
Serialization turns tuples into lists
```
>>> tup = 1,2
>>> from distributed.protocol import core
>>> core.loads(core.dumps(tup))
[1, 2]
```

It produces ignored exceptions in tests such as `distributed/tests/test_scheduler.py::test_remove_worker_from_scheduler`:

```
ERROR:tornado.application:Future <tornado.concurrent.Future object at 0x7f707583ef98> exception was never retrieved: Traceback (most recent call last):
  File "/home/antoine/tornado/tornado/gen.py", line 304, in wrapper
    yielded = next(result)
  File "/home/antoine/distributed/distributed/worker.py", line 540, in compute_one
    self.active.add(key)
TypeError: unhashable type: 'list'
```


Discussion:
@mrocklin 

Yes, this is a restriction of msgpack, for which there is only one array type.  

When using the client we map all keys to strings.  

That test should do so as well (or ideally just use the client API).  I can fix this easily sometime tomorrow.  Alternatively you could call `distributed.utils.tokey` on all of the keys of `dsk` in that test.


Resolving patch:
diff --git a/distributed/scheduler.py b/distributed/scheduler.py
--- a/distributed/scheduler.py
+++ b/distributed/scheduler.py
@@ -33,7 +33,7 @@
         Server, send_recv, coerce_to_address, error_message)
 from .utils import (All, ignoring, clear_queue, get_ip, ignore_exceptions,
         ensure_ip, get_fileno_limit, log_errors, key_split, mean,
-        divide_n_among_bins)
+        divide_n_among_bins, validate_key)
 from .utils_comm import (scatter_to_workers, gather_from_workers)
 from .versions import get_versions
 
@@ -1089,7 +1089,12 @@ def worker_stream(self, worker):
         """
         yield gen.sleep(0)
         ip, port = coerce_to_address(worker, out=tuple)
-        stream = yield connect(ip, port)
+        try:
+            stream = yield connect(ip, port)
+        except Exception as e:
+            logger.error("Failed to connect to worker '%s:%s': %s",
+                         ip, port, e)
+            return
         yield write(stream, {'op': 'compute-stream'})
         self.worker_streams[worker].start(stream)
         logger.info("Starting worker compute stream, %s", worker)
@@ -1112,6 +1117,7 @@ def worker_stream(self, worker):
                         self.correct_time_delay(worker, msg)
 
                         key = msg['key']
+                        validate_key(key)
                         if msg['status'] == 'OK':
                             r = self.stimulus_task_finished(worker=worker, **msg)
                             recommendations.update(r)
diff --git a/distributed/utils.py b/distributed/utils.py
--- a/distributed/utils.py
+++ b/distributed/utils.py
@@ -314,25 +314,35 @@ def iterator_to_queue(seq, maxsize=0):
 
 
 def tokey(o):
-    """ Convert an object to a bytestring, using str
+    """ Convert an object to a string.
 
     Examples
     --------
 
     >>> tokey(b'x')
-    b'x'
+    'x'
     >>> tokey('x')
     'x'
     >>> tokey(1)
     '1'
     """
     t = type(o)
-    if t is str or t is bytes:
+    if t is str:
         return o
+    elif t is bytes:
+        return o.decode('latin1')
     else:
         return str(o)
 
 
+def validate_key(k):
+    """Validate a key as received on a stream.
+    """
+    if type(k) is not str:
+        raise TypeError("Unexpected key type %s (value: %r)"
+                        % (type(k), k))
+
+
 def _maybe_complex(task):
     """ Possibly contains a nested task """
     return (istask(task) or
diff --git a/distributed/worker.py b/distributed/worker.py
--- a/distributed/worker.py
+++ b/distributed/worker.py
@@ -32,7 +32,8 @@
 from .protocol.pickle import dumps, loads
 from .sizeof import sizeof
 from .threadpoolexecutor import ThreadPoolExecutor
-from .utils import funcname, get_ip, _maybe_complex, log_errors, All, ignoring
+from .utils import (funcname, get_ip, _maybe_complex, log_errors, All,
+                    ignoring, validate_key)
 
 _ncores = multiprocessing.cpu_count()
 
@@ -481,6 +482,8 @@ def compute_stream(self, stream):
                 batch = []
                 for msg in msgs:
                     op = msg.pop('op', None)
+                    if 'key' in msg:
+                        validate_key(msg['key'])
                     if op == 'close':
                         closed = True
                         break


Test patch:
diff --git a/distributed/tests/test_scheduler.py b/distributed/tests/test_scheduler.py
--- a/distributed/tests/test_scheduler.py
+++ b/distributed/tests/test_scheduler.py
@@ -474,7 +474,6 @@ def test_server(s, a, b):
 
 @gen_cluster()
 def test_remove_client(s, a, b):
-    s.add_client(client='ident')
     s.update_graph(tasks={'x': dumps_task((inc, 1)),
                           'y': dumps_task((inc, 'x'))},
                    dependencies={'x': [], 'y': ['x']},
@@ -499,7 +498,7 @@ def test_server_listens_to_other_ops(s, a, b):
 
 @gen_cluster()
 def test_remove_worker_from_scheduler(s, a, b):
-    dsk = {('x', i): (inc, i) for i in range(20)}
+    dsk = {('x-%d' % i): (inc, i) for i in range(20)}
     s.update_graph(tasks=valmap(dumps_task, dsk), keys=list(dsk),
                    dependencies={k: set() for k in dsk})
     assert s.ready
@@ -520,7 +519,7 @@ def test_add_worker(s, a, b):
     w.data['y'] = 1
     yield w._start(0)
 
-    dsk = {('x-%d' % i).encode(): (inc, i) for i in range(10)}
+    dsk = {('x-%d' % i): (inc, i) for i in range(10)}
     s.update_graph(tasks=valmap(dumps_task, dsk), keys=list(dsk), client='client',
                    dependencies={k: set() for k in dsk})
 
@@ -757,7 +756,6 @@ def test_dumps_task():
 
 @gen_cluster()
 def test_ready_remove_worker(s, a, b):
-    s.add_client(client='client')
     s.update_graph(tasks={'x-%d' % i: dumps_task((inc, i)) for i in range(20)},
                    keys=['x-%d' % i for i in range(20)],
                    client='client',
@@ -781,7 +779,6 @@ def test_ready_remove_worker(s, a, b):
 
 @gen_cluster(Worker=Nanny)
 def test_restart(s, a, b):
-    s.add_client(client='client')
     s.update_graph(tasks={'x-%d' % i: dumps_task((inc, i)) for i in range(20)},
                    keys=['x-%d' % i for i in range(20)],
                    client='client',
@@ -805,7 +802,6 @@ def test_restart(s, a, b):
 
 @gen_cluster()
 def test_ready_add_worker(s, a, b):
-    s.add_client(client='client')
     s.update_graph(tasks={'x-%d' % i: dumps_task((inc, i)) for i in range(20)},
                    keys=['x-%d' % i for i in range(20)],
                    client='client',
@@ -922,7 +918,6 @@ def test_file_descriptors_dont_leak(s):
 
 @gen_cluster()
 def test_update_graph_culls(s, a, b):
-    s.add_client(client='client')
     s.update_graph(tasks={'x': dumps_task((inc, 1)),
                           'y': dumps_task((inc, 'x')),
                           'z': dumps_task((inc, 2))},
diff --git a/distributed/tests/test_utils.py b/distributed/tests/test_utils.py
--- a/distributed/tests/test_utils.py
+++ b/distributed/tests/test_utils.py
@@ -187,7 +187,7 @@ def test_iterator_to_queue():
 
 
 def test_str_graph():
-    dsk = {b'x': 1}
+    dsk = {'x': 1}
     assert str_graph(dsk) == dsk
 
     dsk = {('x', 1): (inc, 1)}
@@ -205,7 +205,7 @@ def test_str_graph():
         sdsk = str_graph(dsk)
         keys = list(dsk)
         skeys = [str(k) for k in keys]
-        assert all(isinstance(k, (str, bytes)) for k in sdsk)
+        assert all(isinstance(k, str) for k in sdsk)
         assert dask.get(dsk, keys) == dask.get(sdsk, skeys)