Repository: dask/distributed

Issue:
Future Type
It would be nice to be able to inspect the type a future may contain.  @mrocklin suggested this is already supported in  concurrent.futures



Discussion:

Resolving patch:
diff --git a/distributed/executor.py b/distributed/executor.py
--- a/distributed/executor.py
+++ b/distributed/executor.py
@@ -158,11 +158,23 @@ def traceback(self):
         """
         return sync(self.executor.loop, self._traceback)
 
+    @property
+    def type(self):
+        return self.executor.futures[self.key].get('type', None)
+
     def __del__(self):
         self.executor._dec_ref(self.key)
 
     def __str__(self):
-        return '<Future: status: %s, key: %s>' % (self.status, self.key)
+        if self.type:
+            try:
+                typ = self.type.__name__
+            except AttributeError:
+                typ = str(self.type)
+            return '<Future: status: %s, type: %s, key: %s>' % (self.status,
+                    typ, self.key)
+        else:
+            return '<Future: status: %s, key: %s>' % (self.status, self.key)
 
     __repr__ = __str__
 
@@ -357,6 +369,9 @@ def _handle_report(self, start_event):
                 if msg['key'] in self.futures:
                     self.futures[msg['key']]['status'] = 'finished'
                     self.futures[msg['key']]['event'].set()
+                    if (msg.get('type') and
+                        not self.futures[msg['key']].get('type')):
+                        self.futures[msg['key']]['type'] = msg['type']
             if msg['op'] == 'lost-data':
                 if msg['key'] in self.futures:
                     self.futures[msg['key']]['status'] = 'lost'
diff --git a/distributed/scheduler.py b/distributed/scheduler.py
--- a/distributed/scheduler.py
+++ b/distributed/scheduler.py
@@ -348,7 +348,7 @@ def mark_ready_to_run(self, key):
         self.stacks[new_worker].append(key)
         self.ensure_occupied(new_worker)
 
-    def mark_key_in_memory(self, key, workers=None):
+    def mark_key_in_memory(self, key, workers=None, type=None):
         """ Mark that a key now lives in distributed memory """
         logger.debug("Mark %s in memory", key)
         if workers is None:
@@ -376,9 +376,12 @@ def mark_key_in_memory(self, key, workers=None):
                 if not s and dep and dep not in self.who_wants:
                     self.delete_data(keys=[dep])
 
-        self.report({'op': 'key-in-memory',
-                     'key': key,
-                     'workers': workers})
+        msg = {'op': 'key-in-memory',
+               'key': key,
+               'workers': workers}
+        if type:
+            msg['type'] = type
+        self.report(msg)
 
     def ensure_occupied(self, worker):
         """ Send tasks to worker while it has tasks and free cores """
@@ -475,12 +478,12 @@ def mark_failed(self, key, failing_key=None):
         for dep in self.dependents[key]:
             self.mark_failed(dep, failing_key)
 
-    def mark_task_finished(self, key, worker, nbytes):
+    def mark_task_finished(self, key, worker, nbytes, type=None):
         """ Mark that a task has finished execution on a particular worker """
         logger.debug("Mark task as finished %s, %s", key, worker)
         if key in self.processing[worker]:
             self.nbytes[key] = nbytes
-            self.mark_key_in_memory(key, [worker])
+            self.mark_key_in_memory(key, [worker], type=type)
             self.ensure_occupied(worker)
             for plugin in self.plugins[:]:
                 try:
@@ -905,13 +908,15 @@ def worker_core(self, ident, i):
                              ident, key, response, content)
                 if response == b'error':
                     error, traceback = content
-                    self.mark_task_erred(key, ident, error, traceback)
+                    self.mark_task_erred(key, ident, content['exception'],
+                                                     content['traceback'])
 
                 elif response == b'missing-data':
                     self.mark_missing_data(content.args, key=key, worker=ident)
 
                 else:
-                    self.mark_task_finished(key, ident, nbytes)
+                    self.mark_task_finished(key, ident, nbytes,
+                                            type=content.get('type'))
 
         yield worker.close(close=True)
         worker.close_streams()
diff --git a/distributed/worker.py b/distributed/worker.py
--- a/distributed/worker.py
+++ b/distributed/worker.py
@@ -254,6 +254,8 @@ def compute(self, stream, function=None, key=None, args=(), kwargs={},
                     logger.warn('Could not report results to center: %s',
                                 response.decode())
             out = (b'OK', {'nbytes': sizeof(result)})
+            if result is not None:
+                out[1]['type'] = type(result)
         except Exception as e:
             tb = get_traceback()
             e2 = truncate_exception(e, 1000)
@@ -265,7 +267,7 @@ def compute(self, stream, function=None, key=None, args=(), kwargs={},
                 str(funcname(function))[:1000], str(args2)[:1000],
                 str(kwargs2)[:1000])
 
-            out = (b'error', (e2, tb))
+            out = (b'error', {'exception': e2, 'traceback': tb})
 
         logger.debug("Send compute response to client: %s, %s", key, out)
         with ignoring(KeyError):


Test patch:
diff --git a/distributed/tests/test_executor.py b/distributed/tests/test_executor.py
--- a/distributed/tests/test_executor.py
+++ b/distributed/tests/test_executor.py
@@ -2071,3 +2071,16 @@ def test_cancel(loop):
 
             z.cancel()
             assert z.cancelled()
+
+
+@gen_cluster()
+def test_future_type(s, a, b):
+    e = Executor((s.ip, s.port), start=False)
+    yield e._start()
+
+    x = e.submit(inc, 1)
+    yield _wait([x])
+    assert x.type == int
+    assert 'int' in str(x)
+
+    yield e._shutdown()
diff --git a/distributed/tests/test_worker.py b/distributed/tests/test_worker.py
--- a/distributed/tests/test_worker.py
+++ b/distributed/tests/test_worker.py
@@ -54,13 +54,13 @@ def f(c, a, b):
         def bad_func():
             1 / 0
 
-        response, (error, traceback) = yield bb.compute(key='z',
+        response, content = yield bb.compute(key='z',
                 function=bad_func, args=(), needed=(), close=True)
         assert not b.active
         assert response == b'error'
-        assert isinstance(error, ZeroDivisionError)
+        assert isinstance(content['exception'], ZeroDivisionError)
         if sys.version_info[0] >= 3:
-            assert any('1 / 0' in line for line in traceback)
+            assert any('1 / 0' in line for line in content['traceback'])
 
         aa.close_streams()
         yield a._close()