Repository: encode/starlette

Issue:
Support for background tasks for `GraphQLApp`
Since Response provided by starlette take a background argument, Any plans to add support for running a list of background tasks after the response has been sent in the case of `GraphQLApp`?

A naive implementation looks like this

```python
class Holder(object):
    def __init__(self):
        self._list = []

    def add(self, function, *args, **kwargs):
        func = functools.partial(function, *args, **kwargs)
        self._list.append(func)

    def clean(self):
        self._list = []

    def run_all_tasks(self):
        for i in self._list:
            i()
```

```python
class EnhancedGraphqlApp(GraphQLApp):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.holder = Holder()
        
    async def execute(self, query, variables=None, operation_name=None):  # type: ignore
        print("Started")
        print(self.holder._list)
        if self.is_async:
            result = await self.schema.execute(
                query,
                variables=variables,
                operation_name=operation_name,
                context={"holder": self.holder},
                executor=self.executor,
                return_promise=True,
            )
        else:
            func = functools.partial(
                self.schema.execute,
                context={"holder": self.holder},
                variables=variables,
                operation_name=operation_name,
            )
            loop = asyncio.get_event_loop()
            result = await loop.run_in_executor(None, func, query)
        return result

    def run_tasks(self):
        for i in self.holder._list:
            i()
        self.holder.clean()

    async def handle_graphql(self, request: Request) -> Response:
        # existing implementation
        ... 
        tasks = BackgroundTask(self.run_tasks)
        return JSONResponse(response_data, background=tasks, status_code=status_code)
```

And usage in the Schema class would be just like how context is used generally

```python
def hello(name):
    print(f"My name is {name}")

class Query(object):
    hello = graphene.String(name=graphene.String(default_value="stranger"))
    alls = graphene.List(AllModel)

    async def resolve_alls(self, info):
        rresult = await async_print_all_models()
        holder = info.context.get('holder')
        holder.add(hello,"James")
        return [AllModel(name=x.name) for x in rresult]
```


Discussion:
Yes we certainly want something like this. Using the "context" as a point for getting at the background task interface sounds right too. A few interface & implementation things I think would need ironing out here, but we could tackle those once there was a pull request to work against.

Resolving patch:
diff --git a/starlette/background.py b/starlette/background.py
--- a/starlette/background.py
+++ b/starlette/background.py
@@ -18,3 +18,18 @@ async def __call__(self) -> None:
             await self.func(*self.args, **self.kwargs)
         else:
             await run_in_threadpool(self.func, *self.args, **self.kwargs)
+
+
+class BackgroundTasks(BackgroundTask):
+    def __init__(self, tasks: typing.Sequence[BackgroundTask] = []):
+        self.tasks = list(tasks)
+
+    def add_task(
+        self, func: typing.Callable, *args: typing.Any, **kwargs: typing.Any
+    ) -> None:
+        task = BackgroundTask(func, *args, **kwargs)
+        self.tasks.append(task)
+
+    async def __call__(self) -> None:
+        for task in self.tasks:
+            await task()
diff --git a/starlette/graphql.py b/starlette/graphql.py
--- a/starlette/graphql.py
+++ b/starlette/graphql.py
@@ -3,6 +3,7 @@
 import typing
 
 from starlette import status
+from starlette.background import BackgroundTasks
 from starlette.concurrency import run_in_threadpool
 from starlette.requests import Request
 from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse, Response
@@ -73,7 +74,10 @@ async def handle_graphql(self, request: Request) -> Response:
                 status_code=status.HTTP_400_BAD_REQUEST,
             )
 
-        result = await self.execute(request, query, variables)
+        background = BackgroundTasks()
+        context = {"request": request, "background": background}
+
+        result = await self.execute(query, variables=variables, context=context)
         error_data = (
             [format_graphql_error(err) for err in result.errors]
             if result.errors
@@ -83,13 +87,14 @@ async def handle_graphql(self, request: Request) -> Response:
         status_code = (
             status.HTTP_400_BAD_REQUEST if result.errors else status.HTTP_200_OK
         )
-        return JSONResponse(response_data, status_code=status_code)
+
+        return JSONResponse(
+            response_data, status_code=status_code, background=background
+        )
 
     async def execute(  # type: ignore
-        self, request, query, variables=None, operation_name=None
+        self, query, variables=None, context=None, operation_name=None
     ):
-        context = dict(request=request)
-
         if self.is_async:
             return await self.schema.execute(
                 query,


Test patch:
diff --git a/tests/test_background.py b/tests/test_background.py
--- a/tests/test_background.py
+++ b/tests/test_background.py
@@ -1,4 +1,4 @@
-from starlette.background import BackgroundTask
+from starlette.background import BackgroundTask, BackgroundTasks
 from starlette.responses import Response
 from starlette.testclient import TestClient
 
@@ -49,3 +49,29 @@ async def asgi(receive, send):
     response = client.get("/")
     assert response.text == "task initiated"
     assert TASK_COMPLETE
+
+
+def test_multiple_tasks():
+    TASK_COUNTER = 0
+
+    def increment(amount):
+        nonlocal TASK_COUNTER
+        TASK_COUNTER += amount
+
+    def app(scope):
+        async def asgi(receive, send):
+            tasks = BackgroundTasks()
+            tasks.add_task(increment, amount=1)
+            tasks.add_task(increment, amount=2)
+            tasks.add_task(increment, amount=3)
+            response = Response(
+                "tasks initiated", media_type="text/plain", background=tasks
+            )
+            await response(receive, send)
+
+        return asgi
+
+    client = TestClient(app)
+    response = client.get("/")
+    assert response.text == "tasks initiated"
+    assert TASK_COUNTER == 1 + 2 + 3