Repository: ipython/ipyparallel

Issue:
ipyparallel using Python 3.8 raises concurrent.futures._base.InvalidStateError: CANCELLED: <AsyncResult: f:finished>
In [Adaptive](https://github.com/python-adaptive/adaptive) simulations (related issue https://github.com/python-adaptive/adaptive/issues/289), I (and many colleagues) started to see this error when we switched to Python 3.8:
```python
During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/gscratch/home/a-banijh/miniconda3/envs/py38/lib/python3.8/concurrent/futures/_base.py", line 328, in _invoke_callbacks
    callback(self)
  File "/gscratch/home/a-banijh/miniconda3/envs/py38/lib/python3.8/site-packages/ipyparallel/client/asyncresult.py", line 233, in _resolve_result
    self.set_exception(e)
  File "/gscratch/home/a-banijh/miniconda3/envs/py38/lib/python3.8/concurrent/futures/_base.py", line 539, in set_exception
    raise InvalidStateError('{}: {!r}'.format(self._state, self))
concurrent.futures._base.InvalidStateError: CANCELLED: <AsyncResult: f:finished>
```

After *many* hours of trying to reproduce this, I've been able to minimize it to:
```python
#!/usr/bin/env python3

# ipython profile create test
# ipcluster start --n=10 --profile=test --cluster-id=''
# python fail.py

import asyncio
import time
from random import random

from ipyparallel import Client
from ipyparallel.error import NoEnginesRegistered


def f(x):
    return x


def connect_to_ipyparallel(profile, n):
    client = Client(profile=profile)
    while True:
        try:
            dview = client[:]
            if len(dview) == n:
                return client
        except NoEnginesRegistered:
            time.sleep(0.1)


async def _run(loop, executor, ncores):
    pending = set()
    done = set()

    for _ in range(10):  # do some loops that submit futures
        while len(pending) + len(done) <= ncores:
            fut = loop.run_in_executor(executor, f, random())
            pending.add(fut)
        done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)

        for fut in done:
            fut.result()  # process results that are done

    for fut in pending:  # cancel the results that are pending
        fut.cancel()

    return done


def do(loop, executor, ncores):
    coro = _run(loop, executor, ncores)
    task = loop.create_task(coro)
    loop.run_until_complete(task)


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    ncores = 10
    executor = connect_to_ipyparallel(profile="test", n=ncores).executor()

    for i in range(10):
        do(loop, executor, ncores)
```

I am using Python 3.8.5 and ipyparallel 6.3.0. The issue doesn't occur with Python 3.7.

Tagging @tlaeven @kvanhoogdalem, who encountered this issue.

Comments:

Patch:
diff --git a/docs/source/api/ipyparallel.rst b/docs/source/api/ipyparallel.rst
index cbab7042..ee7ac089 100644
--- a/docs/source/api/ipyparallel.rst
+++ b/docs/source/api/ipyparallel.rst
@@ -12,16 +12,14 @@ Classes
 -------
 
 .. autoclass:: Client
-    :members:
 
 .. autoclass:: DirectView
-    :members:
 
 .. autoclass:: LoadBalancedView
-    :members:
+
+.. autoclass:: BroadcastView
 
 .. autoclass:: ViewExecutor
-    :members:
 
 Decorators
 ----------
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 8571badb..689c240e 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -60,6 +60,15 @@ extensions = [
 # You can specify multiple suffix as a list of string:
 source_suffix = ['.rst', '.md']
 
+from traitlets.config import HasTraits
+
+# exclude members inherited from HasTraits by default
+autodoc_default_options = {
+    'members': None,
+    "inherited-members": None,
+    "exclude-members": ','.join(dir(HasTraits)),
+}
+
 # Add dev disclaimer.
 if iprelease['version_info'][-1] == 'dev':
     rst_prolog = """
diff --git a/ipyparallel/client/asyncresult.py b/ipyparallel/client/asyncresult.py
index 41ece2c5..cc19f4ff 100644
--- a/ipyparallel/client/asyncresult.py
+++ b/ipyparallel/client/asyncresult.py
@@ -269,6 +269,8 @@ class AsyncResult(Future):
         return self._ready
 
     def _resolve_result(self, f=None):
+        if self.done():
+            return
         try:
             if f:
                 results = f.result()
diff --git a/ipyparallel/serialize/canning.py b/ipyparallel/serialize/canning.py
index f2ba4140..b83ef98e 100644
--- a/ipyparallel/serialize/canning.py
+++ b/ipyparallel/serialize/canning.py
@@ -193,6 +193,16 @@ class CannedFunction(CannedObject):
         else:
             self.defaults = None
 
+        if f.__kwdefaults__:
+            self.kwdefaults = can_dict(f.__kwdefaults__)
+        else:
+            self.kwdefaults = None
+
+        if f.__annotations__:
+            self.annotations = can_dict(f.__annotations__)
+        else:
+            self.annotations = None
+
         closure = py3compat.get_closure(f)
         if closure:
             self.closure = tuple(can(cell) for cell in closure)
@@ -218,11 +228,25 @@ class CannedFunction(CannedObject):
             defaults = tuple(uncan(cfd, g) for cfd in self.defaults)
         else:
             defaults = None
+
+        if self.kwdefaults:
+            kwdefaults = uncan_dict(self.kwdefaults)
+        else:
+            kwdefaults = None
+        if self.annotations:
+            annotations = uncan_dict(self.annotations)
+        else:
+            annotations = {}
+
         if self.closure:
             closure = tuple(uncan(cell, g) for cell in self.closure)
         else:
             closure = None
         newFunc = FunctionType(self.code, g, self.__name__, defaults, closure)
+        if kwdefaults:
+            newFunc.__kwdefaults__ = kwdefaults
+        if annotations:
+            newFunc.__annotations__ = annotations
         return newFunc
 
 


Test patch:
diff --git a/ipyparallel/tests/test_canning.py b/ipyparallel/tests/test_canning.py
index f462b343..090cb837 100644
--- a/ipyparallel/tests/test_canning.py
+++ b/ipyparallel/tests/test_canning.py
@@ -21,6 +21,10 @@ def loads(obj):
     return uncan(pickle.loads(obj))
 
 
+def roundtrip(obj):
+    return loads(dumps(obj))
+
+
 def test_no_closure():
     @interactive
     def foo():
@@ -109,3 +113,20 @@ def test_can_partial_buffers():
     loaded.buffers = buffers
     pfoo2 = uncan(loaded)
     assert pfoo2() == partial_foo()
+
+
+def test_keyword_only_arguments():
+    def compute(a, *, b=3):
+        return a * b
+
+    assert compute(2) == 6
+    compute_2 = roundtrip(compute)
+    assert compute_2(2) == 6
+
+
+def test_annotations():
+    def f(a: int):
+        return a
+
+    f2 = roundtrip(f)
+    assert f2.__annotations__ == f.__annotations__