Repository: celery/celery

Issue:
Forgetting a chain leaves task meta in MongoDb
## Checklist
* [x] I have included the output of ``celery -A proj report`` in the issue.

```
# celery -A celerytest report

software -> celery:4.2.1 (windowlicker) kombu:4.2.1 py:3.6.6
            billiard:3.5.0.4 py-amqp:2.3.2
platform -> system:Linux arch:64bit imp:CPython
loader   -> celery.loaders.app.AppLoader
settings -> transport:pyamqp results:mongodb://mongodb:27017/celery_results

broker_url: 'amqp://guest:********@rabbitmq:5672//'
result_backend: 'mongodb://mongodb:27017/celery_results'
```

* [x] I have verified that the issue exists against the `master` branch of Celery.
Yes, installed celery with pip install --upgrade git+https://github.com/celery/celery
Reported version was 4.2.0 (older?) but with same behavior.

## Steps to reproduce
```
from celery import Celery, chain, group

app = Celery(
    'tasks',
    broker='pyamqp://guest@rabbitmq//',
    backend='mongodb://mongodb:27017/celery_results',
)

@app.task(name='celerytest.add')  #, ignore_results=True)
def add(x, y):
    return x + y

if __name__ == '__main__':
    c = chain(
        add.s(1, 2),
        add.s(3),
    )
    res = c.delay()
    print(res)
    print(res.get())
    res.forget()
```

## Expected behavior
MongoDb should stay empty as in example code when replacing c = chain() for c = group().

## Actual behavior
One document remains in database.


```
{
    "_id" : "6ed8544d-3731-465f-bc82-bce40635d69b",
    "status" : "SUCCESS",
    "result" : "3",
    "date_done" : ISODate("2018-09-11T15:32:34.934Z"),
    "traceback" : "null",
    "children" : "[[[\"a0563d8f-1be7-437c-9f2b-e8fbb7a0b3bc\", null], null]]"
}
```




Discussion:
It seems Redis results backend has the same behavior.
If it is a bug, it is a bug not only for MongoDB.

Resolving patch:
diff --git a/celery/backends/base.py b/celery/backends/base.py
--- a/celery/backends/base.py
+++ b/celery/backends/base.py
@@ -673,6 +673,8 @@ def _store_result(self, task_id, result, state,
 
         if request and getattr(request, 'group', None):
             meta['group_id'] = request.group
+        if request and getattr(request, 'parent_id', None):
+            meta['parent_id'] = request.parent_id
 
         if self.app.conf.find_value_for_key('extended', 'result'):
             if request:
diff --git a/celery/backends/mongodb.py b/celery/backends/mongodb.py
--- a/celery/backends/mongodb.py
+++ b/celery/backends/mongodb.py
@@ -181,6 +181,8 @@ def _store_result(self, task_id, result, state,
                 self.current_task_children(request),
             ),
         }
+        if request and getattr(request, 'parent_id', None):
+            meta['parent_id'] = request.parent_id
 
         try:
             self.collection.save(meta)
diff --git a/celery/result.py b/celery/result.py
--- a/celery/result.py
+++ b/celery/result.py
@@ -128,8 +128,10 @@ def as_tuple(self):
         return (self.id, parent and parent.as_tuple()), None
 
     def forget(self):
-        """Forget about (and possibly remove the result of) this task."""
+        """Forget the result of this task and its parents."""
         self._cache = None
+        if self.parent:
+            self.parent.forget()
         self.backend.forget(self.id)
 
     def revoke(self, connection=None, terminate=False, signal=None,


Test patch:
diff --git a/t/unit/backends/test_base.py b/t/unit/backends/test_base.py
--- a/t/unit/backends/test_base.py
+++ b/t/unit/backends/test_base.py
@@ -423,6 +423,18 @@ def test_get_store_delete_result(self):
         self.b.forget(tid)
         assert self.b.get_state(tid) == states.PENDING
 
+    def test_store_result_parent_id(self):
+        tid = uuid()
+        pid = uuid()
+        state = 'SUCCESS'
+        result = 10
+        request = Context(parent_id=pid)
+        self.b.store_result(
+            tid, state=state, result=result, request=request,
+        )
+        stored_meta = self.b.decode(self.b.get(self.b.get_key_for_task(tid)))
+        assert stored_meta['parent_id'] == request.parent_id
+
     def test_store_result_group_id(self):
         tid = uuid()
         state = 'SUCCESS'
diff --git a/t/unit/backends/test_mongodb.py b/t/unit/backends/test_mongodb.py
--- a/t/unit/backends/test_mongodb.py
+++ b/t/unit/backends/test_mongodb.py
@@ -221,6 +221,33 @@ def test_store_result(self, mock_get_database):
             self.backend._store_result(
                 sentinel.task_id, sentinel.result, sentinel.status)
 
+    @patch('celery.backends.mongodb.MongoBackend._get_database')
+    def test_store_result_with_request(self, mock_get_database):
+        self.backend.taskmeta_collection = MONGODB_COLLECTION
+
+        mock_database = MagicMock(spec=['__getitem__', '__setitem__'])
+        mock_collection = Mock()
+        mock_request = MagicMock(spec=['parent_id'])
+
+        mock_get_database.return_value = mock_database
+        mock_database.__getitem__.return_value = mock_collection
+        mock_request.parent_id = sentinel.parent_id
+
+        ret_val = self.backend._store_result(
+            sentinel.task_id, sentinel.result, sentinel.status,
+            request=mock_request)
+
+        mock_get_database.assert_called_once_with()
+        mock_database.__getitem__.assert_called_once_with(MONGODB_COLLECTION)
+        parameters = mock_collection.save.call_args[0][0]
+        assert parameters['parent_id'] == sentinel.parent_id
+        assert sentinel.result == ret_val
+
+        mock_collection.save.side_effect = InvalidDocument()
+        with pytest.raises(EncodeError):
+            self.backend._store_result(
+                sentinel.task_id, sentinel.result, sentinel.status)
+
     @patch('celery.backends.mongodb.MongoBackend._get_database')
     def test_get_task_meta_for(self, mock_get_database):
         self.backend.taskmeta_collection = MONGODB_COLLECTION
@@ -322,7 +349,8 @@ def test_delete_group(self, mock_get_database):
             {'_id': sentinel.taskset_id})
 
     @patch('celery.backends.mongodb.MongoBackend._get_database')
-    def test_forget(self, mock_get_database):
+    def test__forget(self, mock_get_database):
+        # note: here tested _forget method, not forget method
         self.backend.taskmeta_collection = MONGODB_COLLECTION
 
         mock_database = MagicMock(spec=['__getitem__', '__setitem__'])
diff --git a/t/unit/tasks/test_result.py b/t/unit/tasks/test_result.py
--- a/t/unit/tasks/test_result.py
+++ b/t/unit/tasks/test_result.py
@@ -85,6 +85,16 @@ def mytask():
             pass
         self.mytask = mytask
 
+    def test_forget(self):
+        first = Mock()
+        second = self.app.AsyncResult(self.task1['id'], parent=first)
+        third = self.app.AsyncResult(self.task2['id'], parent=second)
+        last = self.app.AsyncResult(self.task3['id'], parent=third)
+        last.forget()
+        first.forget.assert_called_once()
+        assert last.result is None
+        assert second.result is None
+
     def test_ignored_getter(self):
         result = self.app.AsyncResult(uuid())
         assert result.ignored is False