Repository: celery/celery

Issue:
Eager Application Synchronous Subtask Guard Blocks Canvas
## 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).
- [x] I have verified that the issue exists against the `master` branch of Celery.

## Steps to reproduce

```
celery -A minimal_eager_chain report

software -> celery:4.2.0 (latentcall) kombu:4.1.0 py:2.7.13
            billiard:3.5.0.3 py-amqp:2.2.2
platform -> system:Linux arch:64bit imp:CPython
loader   -> celery.loaders.app.AppLoader
settings -> transport:amqp results:disabled

```

Minimal script to reproduce: https://gist.github.com/npilon/9f3c8469a615081fc8454359945eebd7

## Expected behavior

- No error occurs

## Actual behavior

- We trip the synchronous subtask alarm:

```
>>> test.delay().get()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/home/npilon/Documents/celery/celery/result.py", line 949, in get
    raise self.result
RuntimeError: Never call result.get() within a task!
See http://docs.celeryq.org/en/latest/userguide/tasks.html#task-synchronous-subtasks
```


Discussion:

Resolving patch:
diff --git a/celery/canvas.py b/celery/canvas.py
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -24,7 +24,7 @@
 from celery._state import current_app
 from celery.five import python_2_unicode_compatible
 from celery.local import try_import
-from celery.result import GroupResult
+from celery.result import GroupResult, allow_join_result
 from celery.utils import abstract
 from celery.utils.functional import _regen
 from celery.utils.functional import chunks as _chunks
@@ -554,7 +554,8 @@ def apply_async(self, args=(), kwargs={}, **options):
         # python is best at unpacking kwargs, so .run is here to do that.
         app = self.app
         if app.conf.task_always_eager:
-            return self.apply(args, kwargs, **options)
+            with allow_join_result():
+                return self.apply(args, kwargs, **options)
         return self.run(args, kwargs, app=app, **(
             dict(self.options, **options) if options else self.options))
 
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -23,6 +23,13 @@ def add(x, y):
     return x + y
 
 
+@shared_task
+def chain_add(x, y):
+    (
+        add.s(x, x) | add.s(y)
+    ).apply_async()
+
+
 @shared_task
 def delayed_sum(numbers, pause_time=1):
     """Sum the iterable of numbers."""


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
@@ -59,6 +59,17 @@ def test_chain_inside_group_receives_arguments(self, manager):
         res = c()
         assert res.get(timeout=TIMEOUT) == [14, 14]
 
+    @flaky
+    def test_eager_chain_inside_task(self, manager):
+        from .tasks import chain_add
+
+        prev = chain_add.app.conf.task_always_eager
+        chain_add.app.conf.task_always_eager = True
+
+        chain_add.apply_async(args=(4, 8), throw=True).get()
+
+        chain_add.app.conf.task_always_eager = prev
+
     @flaky
     def test_group_chord_group_chain(self, manager):
         from celery.five import bytes_if_py2
diff --git a/t/unit/tasks/test_canvas.py b/t/unit/tasks/test_canvas.py
--- a/t/unit/tasks/test_canvas.py
+++ b/t/unit/tasks/test_canvas.py
@@ -411,6 +411,26 @@ def test_always_eager(self):
         self.app.conf.task_always_eager = True
         assert ~(self.add.s(4, 4) | self.add.s(8)) == 16
 
+    def test_chain_always_eager(self):
+        self.app.conf.task_always_eager = True
+        from celery import _state
+        from celery import result
+
+        fixture_task_join_will_block = _state.task_join_will_block
+        try:
+            _state.task_join_will_block = _state.orig_task_join_will_block
+            result.task_join_will_block = _state.orig_task_join_will_block
+
+            @self.app.task(shared=False)
+            def chain_add():
+                return (self.add.s(4, 4) | self.add.s(8)).apply_async()
+
+            r = chain_add.apply_async(throw=True).get()
+            assert r.get() == 16
+        finally:
+            _state.task_join_will_block = fixture_task_join_will_block
+            result.task_join_will_block = fixture_task_join_will_block
+
     def test_apply(self):
         x = chain(self.add.s(4, 4), self.add.s(8), self.add.s(10))
         res = x.apply()