Repository: celery/celery

Issue:
TypeError: get() got an unexpected keyword argument 'on_interval' when nested chain ends in group
## Checklist

- [x] I have included the output of ``celery -A proj report`` in the issue.
      (if you are not able to do this, then at least specify the Celery
       version affected).
- [ ] I have verified that the issue exists against the `master` branch of Celery.

## Steps to reproduce

Given this task…

```python
from myapp.celery import app

@app.task
def say_something(msg):
    print(msg)
```

…this moderately complex workflow…

```python
import celery

from myapp.tasks import say_something

workflow = celery.chain(
    say_something.si('outer chain start'),
    celery.group(
        say_something.si('outer group'),
        celery.chain(
            say_something.si('inner chain start'),
            celery.group(
                say_something.si('inner group a'),
                say_something.si('inner group b'),
            ),
            # say_something.si('inner chain end'),
        ),
    ),
    say_something.si('outer chain end'),
)

workflow.delay()
```

…causes this error…

    ERROR:celery.app.builtins:Chord '70cb9fbe-4843-49ba-879e-6ffd76d63226' raised: TypeError("get() got an unexpected keyword argument 'on_interval'",)
    Traceback (most recent call last):
      File "/usr/local/lib/python3.6/site-packages/celery/app/builtins.py", line 80, in unlock_chord
        ret = j(timeout=3.0, propagate=True)
      File "/usr/local/lib/python3.6/site-packages/celery/result.py", line 698, in join
        interval=interval, no_ack=no_ack, on_interval=on_interval,
    TypeError: get() got an unexpected keyword argument 'on_interval'
    INFO:celery.app.trace:Task celery.chord_unlock[cac53aaf-c193-442c-b060-73577be77d0f] succeeded in 0.04921864200150594s: None

…and the `outer chain end` task is never executed.

If I un-comment the `inner chain end` task, the whole workflow executes as expected.

Below is a sample of the output of `celery -A proj report`:

    software -> celery:4.1.0 (latentcall) kombu:4.1.0 py:3.6.1
                billiard:3.5.0.3 py-amqp:2.2.2
    platform -> system:Linux arch:64bit, ELF imp:CPython
    loader   -> celery.loaders.app.AppLoader
    settings -> transport:amqp results:django-db
    
    BROKER_URL: 'amqp://admin:********@rabbitmq:5672//'
    
    CELERY_ACCEPT_CONTENT: ['json']
    CELERY_RESULT_BACKEND: 'django-db'
    CELERY_TASK_SERIALIZER: 'json'
    CELERY_WORKER_HIJACK_ROOT_LOGGER: False

## Expected behaviour

The `outer chain end` should be executed once all other tasks have completed.

## Actual behavior

    ERROR:celery.app.builtins:Chord '70cb9fbe-4843-49ba-879e-6ffd76d63226' raised: TypeError("get() got an unexpected keyword argument 'on_interval'",)
    Traceback (most recent call last):
      File "/usr/local/lib/python3.6/site-packages/celery/app/builtins.py", line 80, in unlock_chord
        ret = j(timeout=3.0, propagate=True)
      File "/usr/local/lib/python3.6/site-packages/celery/result.py", line 698, in join
        interval=interval, no_ack=no_ack, on_interval=on_interval,
    TypeError: get() got an unexpected keyword argument 'on_interval'
    INFO:celery.app.trace:Task celery.chord_unlock[cac53aaf-c193-442c-b060-73577be77d0f] succeeded in 0.04921864200150594s: None



Discussion:
I believe there may be a similar issue when a `celery.group` is comprised solely of `celery.chain` tasks.

I can work around this issue by adding a `noop` task.

    @app.task
    def noop():
        """No-operation task, used to address https://github.com/celery/celery/issues/4274."""
        pass


Resolving patch:
diff --git a/celery/result.py b/celery/result.py
--- a/celery/result.py
+++ b/celery/result.py
@@ -628,7 +628,7 @@ def iterate(self, timeout=None, propagate=True, interval=0.5):
 
     def get(self, timeout=None, propagate=True, interval=0.5,
             callback=None, no_ack=True, on_message=None,
-            disable_sync_subtasks=True):
+            disable_sync_subtasks=True, on_interval=None):
         """See :meth:`join`.
 
         This is here for API compatibility with :class:`AsyncResult`,
@@ -640,7 +640,8 @@ def get(self, timeout=None, propagate=True, interval=0.5,
         return (self.join_native if self.supports_native_join else self.join)(
             timeout=timeout, propagate=propagate,
             interval=interval, callback=callback, no_ack=no_ack,
-            on_message=on_message, disable_sync_subtasks=disable_sync_subtasks
+            on_message=on_message, disable_sync_subtasks=disable_sync_subtasks,
+            on_interval=on_interval,
         )
 
     def join(self, timeout=None, propagate=True, interval=0.5,


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
@@ -130,6 +130,24 @@ def test_parent_ids(self, manager):
             assert parent_id == expected_parent_id
             assert value == i + 2
 
+    @flaky
+    def test_nested_group(self, manager):
+        assert manager.inspect().ping()
+
+        c = group(
+            add.si(1, 10),
+            group(
+                add.si(1, 100),
+                group(
+                    add.si(1, 1000),
+                    add.si(1, 2000),
+                ),
+            ),
+        )
+        res = c()
+
+        assert res.get(timeout=TIMEOUT) == [11, 101, 1001, 2001]
+
 
 def assert_ids(r, expected_value, expected_root_id, expected_parent_id):
     root_id, parent_id, value = r.get(timeout=TIMEOUT)
@@ -152,6 +170,32 @@ def test_group_chain(self, manager):
         res = c()
         assert res.get(timeout=TIMEOUT) == [12, 13, 14, 15]
 
+    @flaky
+    def test_nested_group_chain(self, manager):
+        try:
+            manager.app.backend.ensure_chords_allowed()
+        except NotImplementedError as e:
+            raise pytest.skip(e.args[0])
+
+        if not manager.app.backend.supports_native_join:
+            raise pytest.skip('Requires native join support.')
+        c = chain(
+            add.si(1, 0),
+            group(
+                add.si(1, 100),
+                chain(
+                    add.si(1, 200),
+                    group(
+                        add.si(1, 1000),
+                        add.si(1, 2000),
+                    ),
+                ),
+            ),
+            add.si(1, 10),
+        )
+        res = c()
+        assert res.get(timeout=TIMEOUT) == 11
+
     @flaky
     def test_parent_ids(self, manager):
         if not manager.app.conf.result_backend.startswith('redis'):
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
@@ -519,12 +519,16 @@ def get(self, propagate=True, **kwargs):
 class MockAsyncResultSuccess(AsyncResult):
     forgotten = False
 
+    def __init__(self, *args, **kwargs):
+        self._result = kwargs.pop('result', 42)
+        super(MockAsyncResultSuccess, self).__init__(*args, **kwargs)
+
     def forget(self):
         self.forgotten = True
 
     @property
     def result(self):
-        return 42
+        return self._result
 
     @property
     def state(self):
@@ -622,6 +626,37 @@ def test_forget(self):
         for sub in subs:
             assert sub.forgotten
 
+    def test_get_nested_without_native_join(self):
+        backend = SimpleBackend()
+        backend.supports_native_join = False
+        ts = self.app.GroupResult(uuid(), [
+            MockAsyncResultSuccess(uuid(), result='1.1',
+                                   app=self.app, backend=backend),
+            self.app.GroupResult(uuid(), [
+                MockAsyncResultSuccess(uuid(), result='2.1',
+                                       app=self.app, backend=backend),
+                self.app.GroupResult(uuid(), [
+                    MockAsyncResultSuccess(uuid(), result='3.1',
+                                           app=self.app, backend=backend),
+                    MockAsyncResultSuccess(uuid(), result='3.2',
+                                           app=self.app, backend=backend),
+                ]),
+            ]),
+        ])
+        ts.app.backend = backend
+
+        vals = ts.get()
+        assert vals == [
+            '1.1',
+            [
+                '2.1',
+                [
+                    '3.1',
+                    '3.2',
+                ]
+            ],
+        ]
+
     def test_getitem(self):
         subs = [MockAsyncResultSuccess(uuid(), app=self.app),
                 MockAsyncResultSuccess(uuid(), app=self.app)]