Repository: celery/celery

Issue:
Replaced task does not complete when replacement sig is a group (4.0.2)
## Checklist

- [x] I have included the output of ``celery -A proj report`` in the issue.

```
software -> celery:4.0.2 (latentcall) kombu:4.0.2 py:2.7.11
            billiard:3.5.0.2 py-amqp:2.1.4
platform -> system:Darwin arch:64bit imp:CPython
loader   -> celery.loaders.app.AppLoader
settings -> transport:amqp results:redis://localhost:6379/0

broker_url: u'amqp://guest:********@localhost:5672//'
result_backend: u'redis://localhost:6379/0'
```
(The above output is against `master`.)

Also:
```
(foo) $ pip freeze | grep redis
redis==2.10.5
```
- [x] I have verified that the issue exists against the `master` branch of Celery.

## Steps to reproduce
*foo.py:*
```python
from celery import Celery, group
app = Celery('foo', broker='amqp://guest:guest@localhost:5672//', backend='redis://localhost:6379/0')

@app.task
def multiply(a, b):
    return a * b

@app.task(bind=True)
def multiply_by_two(self, num):
    raise self.replace(multiply.s(num, 2))

@app.task(bind=True)
def multiply_all_by_two(self, nums):
    subtasks = [multiply_by_two.s(n) for n in nums]
    raise self.replace(group(*subtasks))
```

*bar.py:*
```python
from foo import multiply_by_two, multiply_all_by_two
if __name__ == '__main__':
    print multiply_by_two.delay(3).get()  # completes in 4.0.1 and 4.0.2
    print multiply_all_by_two.delay([1, 2, 3]).get()  # completes in 4.0.1; does not complete in 4.0.2
```

## Expected behavior
A replaced task should complete when the replacement sig is a group (and does in 4.0.1).

## Actual behavior
A replaced task does not complete when the replacement sig is a group (in 4.0.2).


Discussion:

Resolving patch:
diff --git a/celery/canvas.py b/celery/canvas.py
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -1253,7 +1253,7 @@ def apply(self, args=(), kwargs={}, propagate=True, body=None, **options):
         )
 
     def _traverse_tasks(self, tasks, value=None):
-        stack = deque(list(tasks))
+        stack = deque(tasks)
         while stack:
             task = stack.popleft()
             if isinstance(task, group):
@@ -1262,7 +1262,9 @@ def _traverse_tasks(self, tasks, value=None):
                 yield task if value is None else value
 
     def __length_hint__(self):
-        return sum(self._traverse_tasks(self.tasks, 1))
+        tasks = (self.tasks.tasks if isinstance(self.tasks, group)
+                 else self.tasks)
+        return sum(self._traverse_tasks(tasks, 1))
 
     def run(self, header, body, partial_args, app=None, interval=None,
             countdown=1, max_retries=None, eager=False,
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import, unicode_literals
 from time import sleep
-from celery import shared_task
+from celery import shared_task, group
 from celery.utils.log import get_task_logger
 
 logger = get_task_logger(__name__)
@@ -19,6 +19,13 @@ def add_replaced(self, x, y):
     raise self.replace(add.s(x, y))
 
 
+@shared_task(bind=True)
+def add_to_all(self, nums, val):
+    """Add the given value to all supplied numbers."""
+    subtasks = [add.s(num, val) for num in nums]
+    raise self.replace(group(*subtasks))
+
+
 @shared_task
 def print_unicode(log_message='håå®ƒ valmuefrø', print_message='hiöäüß'):
     """Task that both logs and print strings containing funny characters."""


Test patch:
diff --git a/t/integration/test_canvas.py b/t/integration/test_canvas.py
--- a/t/integration/test_canvas.py
+++ b/t/integration/test_canvas.py
@@ -4,7 +4,7 @@
 from celery.exceptions import TimeoutError
 from celery.result import AsyncResult, GroupResult
 from .conftest import flaky
-from .tasks import add, add_replaced, collect_ids, ids
+from .tasks import add, add_replaced, add_to_all, collect_ids, ids
 
 TIMEOUT = 120
 
@@ -88,6 +88,18 @@ def assert_ids(r, expected_value, expected_root_id, expected_parent_id):
 
 class test_chord:
 
+    @flaky
+    def test_group_chain(self, manager):
+        if not manager.app.conf.result_backend.startswith('redis'):
+            raise pytest.skip('Requires redis result backend.')
+        c = (
+            add.s(2, 2) |
+            group(add.s(i) for i in range(4)) |
+            add_to_all.s(8)
+        )
+        res = c()
+        assert res.get(timeout=TIMEOUT) == [12, 13, 14, 15]
+
     @flaky
     def test_parent_ids(self, manager):
         if not manager.app.conf.result_backend.startswith('redis'):