Repository: celery/celery

Issue:
AttributeError: 'str' object has no attribute 'isoformat' with ETA
## Checklist
```
$ pip freeze | grep celery
celery==4.1.0
django-celery-results==1.0.1
```

## Steps to reproduce
Create chord, add eta.

```
from datetime import datetime, timedelta
from celery import chain, group
@shared_task
def add(a, b):
    return a + b

@shared_task
def sum(inputs):
    return sum(inputs)

@shared_task
def err_handler(*args, **kwargs):
    print 'an error occurred'

def stuff():
    eta = datetime.utcnow() + timedelta(seconds=10)
    return chain(
        group(
            add.s(1, 2),
            add.s(3, 4),
        ),
        sum.s()
    ).on_error(err_handler.s()).apply_async(eta=eta)
``` 
## Expected behavior
after ETA, chain begins execution and executes to completion

## Actual behavior
Group begins execution after ETA, but callback throws exception:
```
[2018-02-26 16:29:44,350: INFO/ForkPoolWorker-5] Task tasks.add[4ecc6d45-40cc-40bc-ba79-b7efad956383] succeeded in 0.00307657103986s: 3
[2018-02-26 16:29:44,351: INFO/ForkPoolWorker-7] Task celery.chord_unlock[d17aa171-1b48-49b0-bcfa-bf961c65d0ea] retry: Retry in 1s
[2018-02-26 16:29:44,352: INFO/MainProcess] Received task: celery.chord_unlock[d17aa171-1b48-49b0-bcfa-bf961c65d0ea]  ETA:[2018-02-27 00:29:45.350604+00:00] 
[2018-02-26 16:29:44,353: INFO/ForkPoolWorker-4] Task tasks.add[62706ef5-97e3-444b-9037-33b64dac866d] succeeded in 0.00494244601578s: 7
[2018-02-26 16:29:45,795: ERROR/ForkPoolWorker-2] Chord 'a5329bed-f970-4969-9177-fd0c79af0be9' raised: AttributeError("'str' object has no attribute 'isoformat'",)
Traceback (most recent call last):
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/app/builtins.py", line 91, in unlock_chord
    callback.delay(ret)
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/canvas.py", line 182, in delay
    return self.apply_async(partial_args, partial_kwargs)
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/canvas.py", line 221, in apply_async
    return _apply(args, kwargs, **options)
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/app/task.py", line 536, in apply_async
    **options
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/app/base.py", line 729, in send_task
    root_id, parent_id, shadow, chain,
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/app/amqp.py", line 333, in as_task_v2
    eta = eta and eta.isoformat()
AttributeError: 'str' object has no attribute 'isoformat'
```
Interesting to note, I am using redis as backend, but the exception is thrown in amqp code. 
AttributeError: 'str' object has no attribute 'isoformat' with ETA
## Checklist
```
$ pip freeze | grep celery
celery==4.1.0
django-celery-results==1.0.1
```

## Steps to reproduce
Create chord, add eta.

```
from datetime import datetime, timedelta
from celery import chain, group
@shared_task
def add(a, b):
    return a + b

@shared_task
def sum(inputs):
    return sum(inputs)

@shared_task
def err_handler(*args, **kwargs):
    print 'an error occurred'

def stuff():
    eta = datetime.utcnow() + timedelta(seconds=10)
    return chain(
        group(
            add.s(1, 2),
            add.s(3, 4),
        ),
        sum.s()
    ).on_error(err_handler.s()).apply_async(eta=eta)
``` 
## Expected behavior
after ETA, chain begins execution and executes to completion

## Actual behavior
Group begins execution after ETA, but callback throws exception:
```
[2018-02-26 16:29:44,350: INFO/ForkPoolWorker-5] Task tasks.add[4ecc6d45-40cc-40bc-ba79-b7efad956383] succeeded in 0.00307657103986s: 3
[2018-02-26 16:29:44,351: INFO/ForkPoolWorker-7] Task celery.chord_unlock[d17aa171-1b48-49b0-bcfa-bf961c65d0ea] retry: Retry in 1s
[2018-02-26 16:29:44,352: INFO/MainProcess] Received task: celery.chord_unlock[d17aa171-1b48-49b0-bcfa-bf961c65d0ea]  ETA:[2018-02-27 00:29:45.350604+00:00] 
[2018-02-26 16:29:44,353: INFO/ForkPoolWorker-4] Task tasks.add[62706ef5-97e3-444b-9037-33b64dac866d] succeeded in 0.00494244601578s: 7
[2018-02-26 16:29:45,795: ERROR/ForkPoolWorker-2] Chord 'a5329bed-f970-4969-9177-fd0c79af0be9' raised: AttributeError("'str' object has no attribute 'isoformat'",)
Traceback (most recent call last):
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/app/builtins.py", line 91, in unlock_chord
    callback.delay(ret)
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/canvas.py", line 182, in delay
    return self.apply_async(partial_args, partial_kwargs)
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/canvas.py", line 221, in apply_async
    return _apply(args, kwargs, **options)
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/app/task.py", line 536, in apply_async
    **options
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/app/base.py", line 729, in send_task
    root_id, parent_id, shadow, chain,
  File "/home/steve/env/local/lib/python2.7/site-packages/celery/app/amqp.py", line 333, in as_task_v2
    eta = eta and eta.isoformat()
AttributeError: 'str' object has no attribute 'isoformat'
```
Interesting to note, I am using redis as backend, but the exception is thrown in amqp code. 


Discussion:

Resolving patch:
diff --git a/celery/app/amqp.py b/celery/app/amqp.py
--- a/celery/app/amqp.py
+++ b/celery/app/amqp.py
@@ -328,7 +328,8 @@ def as_task_v2(self, task_id, name, args=None, kwargs=None,
             expires = maybe_make_aware(
                 now + timedelta(seconds=expires), tz=timezone,
             )
-        eta = eta and eta.isoformat()
+        if not isinstance(eta, string_t):
+            eta = eta and eta.isoformat()
         # If we retry a task `expires` will already be ISO8601-formatted.
         if not isinstance(expires, string_t):
             expires = expires and expires.isoformat()


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
@@ -165,7 +165,6 @@ def test_chord_soft_timeout_recuperation(self, manager):
         result = c(delayed_sum.s(pause_time=0)).get()
         assert result == 3
 
-    @pytest.mark.xfail()
     def test_chain_error_handler_with_eta(self, manager):
         try:
             manager.app.backend.ensure_chords_allowed()
diff --git a/t/unit/app/test_amqp.py b/t/unit/app/test_amqp.py
--- a/t/unit/app/test_amqp.py
+++ b/t/unit/app/test_amqp.py
@@ -359,6 +359,13 @@ def test_expires_to_datetime(self):
         assert m.headers['expires'] == (
             now + timedelta(seconds=30)).isoformat()
 
+    def test_eta_to_datetime(self):
+        eta = datetime.utcnow()
+        m = self.app.amqp.as_task_v2(
+            uuid(), 'foo', eta=eta,
+        )
+        assert m.headers['eta'] == eta.isoformat()
+
     def test_callbacks_errbacks_chord(self):
 
         @self.app.task