Repository: encode/starlette

Issue:
Async executor issue / Event loop is closed
Hi!

I'm testing asynchronous GraphQL resolvers using the approach described here: https://www.starlette.io/graphql/

When I run the example as is with a simple uvicorn setup, I get the following error:

```
ERROR: Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/graphql/execution/executor.py", line 447, in resolve_or_error
    return executor.execute(resolve_fn, source, info, **args)
  File "/usr/local/lib/python3.7/site-packages/graphql/execution/executors/asyncio.py", line 71, in execute
    future = ensure_future(result, loop=self.loop)
  File "/usr/local/lib/python3.7/asyncio/tasks.py", line 581, in ensure_future
    task = loop.create_task(coro_or_future)
  File "/usr/local/lib/python3.7/asyncio/base_events.py", line 392, in create_task
    self._check_closed()
  File "/usr/local/lib/python3.7/asyncio/base_events.py", line 469, in _check_closed
    raise RuntimeError('Event loop is closed')
```

My understanding is that `uvicorn.run()` calls `uvloop_setup()`, which closes the default asyncio event loop before recreating one with the uvloop policy. Meanwhile, the `AsyncioExecutor` instance passed to the GraphQL app has already called `asyncio.get_event_loop()`, and stores the default asyncio event loop that is about to be closed.

See the problematic code below, as well as my workaround in the commented code: if I create the loop myself, pass it to the `AsyncioExecutor` instance, and to `uvicorn.run()`, my async resolver works.

Not sure if this is a bug, a documentation issue, or if I am missing something ;)

Let me know if I can help!

```python
from starlette.applications import Starlette
from uvicorn.loops.uvloop import uvloop_setup
from starlette.graphql import GraphQLApp
from graphql.execution.executors.asyncio import AsyncioExecutor
import graphene
import uvicorn

app = Starlette()


# loop = uvloop_setup()


class Query(graphene.ObjectType):
    hello = graphene.String(name=graphene.String(default_value="stranger"))

    async def resolve_hello(self, info, name):
        # We can make asynchronous network calls here.
        return "Hello " + name


# app.add_route('/', GraphQLApp(schema=graphene.Schema(query=Query), executor=AsyncioExecutor(loop)))
app.add_route('/', GraphQLApp(schema=graphene.Schema(query=Query), executor=AsyncioExecutor()))

if __name__ == '__main__':
    # uvicorn.run(app, loop=loop, host='0.0.0.0', port=8000)
    uvicorn.run(app, host='0.0.0.0', port=8000)
```


Discussion:
I have the same issue with the same code (AKA from [the documentation](https://www.starlette.io/graphql/#sync-or-async-executors)), and your workaround also works for me (even when using asyncio's event loop instead of uvloop's) -- so thank you for that.
Just wanted to note I was able to re-create this with the example code above, but running it from the command line with `uvicorn app:app` runs successfully. Might be something that needs to be addressed in Uvicorn specifically - not sure.
> but running it from the command line with uvicorn app:app runs successfully.

Okay, useful confirmation. In the command line case, the event loop will already be setup by the time the app import occurs, so the executor instance will have the uvloop instance set on it.

Does graphene provide a public or private interface for settin the loop on the executor?
Should we provide an executor class rather than an instance? (What init parameters can they take?)

The `executor` parameter is handled by the graphql-core `execute` function, Graphene only forwards it. We have to provide an executor instance - passing an executor class won't work.

Looking at the tests here, I think that the way it is implemented in starlette is correct: https://github.com/graphql-python/graphql-core/blob/master/graphql/execution/tests/test_executor_asyncio.py

For me, there are two solutions:

1. adapt the doc to make it clear that if you want to use `AsyncioExecutor` with a `if __name__ == '__main__': uvicorn.run(app, host='0.0.0.0', port=8000)` setup, some extra steps are needed
2. adapting the `GraphQLApp` class `__init__` method so that it handles the executor argument in a more flexible way (could accept a class and/or a callable, and initialize it later, the first time it is needed for example)

Resolving patch:
diff --git a/starlette/graphql.py b/starlette/graphql.py
--- a/starlette/graphql.py
+++ b/starlette/graphql.py
@@ -22,15 +22,35 @@
 
 
 class GraphQLApp:
-    def __init__(self, schema: "graphene.Schema", executor: typing.Any = None) -> None:
+    def __init__(
+        self,
+        schema: "graphene.Schema",
+        executor: typing.Any = None,
+        executor_class: type = None,
+    ) -> None:
         self.schema = schema
-        self.executor = executor
-        self.is_async = isinstance(executor, AsyncioExecutor)
+        if executor is None:
+            # New style in 0.10.0. Use 'executor_class'.
+            # See issue https://github.com/encode/starlette/issues/242
+            self.executor = executor
+            self.executor_class = executor_class
+            self.is_async = executor_class is not None and issubclass(
+                executor_class, AsyncioExecutor
+            )
+        else:
+            # Old style. Use 'executor'.
+            # We should remove this in the next median/major version bump.
+            self.executor = executor
+            self.executor_class = None
+            self.is_async = isinstance(executor, AsyncioExecutor)
 
     def __call__(self, scope: Scope) -> ASGIInstance:
         return functools.partial(self.asgi, scope=scope)
 
     async def asgi(self, receive: Receive, send: Send, scope: Scope) -> None:
+        if self.executor is None and self.executor_class is not None:
+            self.executor = self.executor_class()
+
         request = Request(scope, receive=receive)
         response = await self.handle_graphql(request)
         await response(receive, send)


Test patch:
diff --git a/tests/test_graphql.py b/tests/test_graphql.py
--- a/tests/test_graphql.py
+++ b/tests/test_graphql.py
@@ -130,7 +130,7 @@ async def resolve_hello(self, info, name):
 
 
 async_schema = graphene.Schema(query=ASyncQuery)
-async_app = GraphQLApp(schema=async_schema, executor=AsyncioExecutor())
+async_app = GraphQLApp(schema=async_schema, executor_class=AsyncioExecutor)
 
 
 def test_graphql_async():
@@ -138,3 +138,15 @@ def test_graphql_async():
     response = client.get("/?query={ hello }")
     assert response.status_code == 200
     assert response.json() == {"data": {"hello": "Hello stranger"}, "errors": None}
+
+
+async_schema = graphene.Schema(query=ASyncQuery)
+old_style_async_app = GraphQLApp(schema=async_schema, executor=AsyncioExecutor())
+
+
+def test_graphql_async_old_style_executor():
+    # See https://github.com/encode/starlette/issues/242
+    client = TestClient(old_style_async_app)
+    response = client.get("/?query={ hello }")
+    assert response.status_code == 200
+    assert response.json() == {"data": {"hello": "Hello stranger"}, "errors": None}