Repository: celery/celery

Issue:
chain().apply_async(kwargs=dict(name='demo_name', instance_id='fake_id'))  doesn't pass kwargs to first task.
## Checklist

celery version:   ``4.1.0 (latentcall)``
python versioin:   ``Python 3.6.0``
OS version:   ``Darwin iDocker 17.3.0 Darwin Kernel Version 17.3.0: Thu Nov  9 18:09:22 PST 2017; root:xnu-4570.31.3~1/RELEASE_X86_64 x86_64``

## Steps to reproduce

```python
# Two tasks

@celery.task
def get_host_info(name=None, instance_id=None):
    host_info = {'name': name, 'instance_id': instance_id,
                 'cpu_cores': 4}
    task_logger.info('Fetching host info ...')
    return host_info


@celery.task
def get_db_info_based_on_host(kwargs):
    name = kwargs.get('name')
    instance_id = kwargs.get('instance_id')
    cpu_cores = kwargs.get('cpu_cores')
    db_info = {'name': name, 'instance_id': instance_id,
               'cpu_cores': cpu_cores, 'db_version': '5.6'
    task_logger.info('Fetching db info ...')
    return db_info

# make a chain of two tasks above, and call it with  kwargs
ret = chain(get_host_info.s(), get_db_info_based_on_host.s()).apply_async(kwargs=dict(name='demo_name', instance_id='aaabb'))
ret.get()
# And I got the result as below
#{
#    "cpu_cores": 4, 
#    "instance_id": null, 
#    "db_version": "5.6", 
#    "name": null
#}


```

Here is the worker's output.
```
[2018-04-05 23:12:30,534: INFO/MainProcess] Received task: cmdb_worker.tasks.demo_tasks.get_host_info[d3b14906-b538-4670-b00e-1bcabb2e8fd4]  
[2018-04-05 23:12:30,540: INFO/ForkPoolWorker-2] cmdb_worker.tasks.demo_tasks.get_host_info[d3b14906-b538-4670-b00e-1bcabb2e8fd4]: Fetching host info ...
[2018-04-05 23:12:30,603: INFO/MainProcess] Received task: cmdb_worker.tasks.demo_tasks.get_db_info_based_on_host[f2007c3f-424f-445d-a0b9-38440bb16e67]  
[2018-04-05 23:12:30,606: INFO/ForkPoolWorker-3] cmdb_worker.tasks.demo_tasks.get_db_info[f2007c3f-424f-445d-a0b9-38440bb16e67]: Fetching db info ...
[2018-04-05 23:12:30,609: INFO/ForkPoolWorker-2] Task cmdb_worker.tasks.demo_tasks.get_host_info[d3b14906-b538-4670-b00e-1bcabb2e8fd4] succeeded in 0.06943560403306037s: {'name': None, 'instance_id': None, 'cpu_cores': 4}
[2018-04-05 23:12:30,614: INFO/ForkPoolWorker-3] Task cmdb_worker.tasks.demo_tasks.get_db_info_based_on_host[f2007c3f-424f-445d-a0b9-38440bb16e67] succeeded in 0.0084876399487257s: {'name': None, 'instance_id': None, 'cpu_cores': 4, 'db_version': '5.6'}
```

## Expected behavior

When called apply_async with kwargs, chain should pass it to the first task in the chain.

 It works when passing kwargs to task.s() directly. But it doesn't seem to be a good way. (Any suggestions ?)

```python
ret = chain(get_host_info.s(dict(name='demo_name', instance_id='aaabb')), get_db_info_based_on_host.s()).apply_async()
```

## Actual behavior

The first task got an empty kwargs even a kwargs was passed in chain.apply_async()


Is it a better way to passing kwargs to a task in a  chain ?


Discussion:
try celey 4.2rc2
@auvipy Thank you very much.  Tried with 4.2rc2 but still not working.
> It works when passing kwargs to task.s() directly. But it doesn't seem to be a good way. (Any suggestions ?)

According to [documentation](http://docs.celeryproject.org/en/latest/reference/celery.html#celery.chain) that is exactly correct way.

> When called apply_async with kwargs, chain should pass it to the first task in the chain.

May be. Now it passes only args.


Resolving patch:
diff --git a/celery/canvas.py b/celery/canvas.py
--- a/celery/canvas.py
+++ b/celery/canvas.py
@@ -569,7 +569,7 @@ def run(self, args=(), kwargs={}, group_id=None, chord=None,
                 if args and not self.immutable else self.args)
 
         tasks, results = self.prepare_steps(
-            args, self.tasks, root_id, parent_id, link_error, app,
+            args, kwargs, self.tasks, root_id, parent_id, link_error, app,
             task_id, group_id, chord,
         )
 
@@ -589,12 +589,12 @@ def freeze(self, _id=None, group_id=None, chord=None,
         # pylint: disable=redefined-outer-name
         #   XXX chord is also a class in outer scope.
         _, results = self._frozen = self.prepare_steps(
-            self.args, self.tasks, root_id, parent_id, None,
+            self.args, self.kwargs, self.tasks, root_id, parent_id, None,
             self.app, _id, group_id, chord, clone=False,
         )
         return results[0]
 
-    def prepare_steps(self, args, tasks,
+    def prepare_steps(self, args, kwargs, tasks,
                       root_id=None, parent_id=None, link_error=None, app=None,
                       last_task_id=None, group_id=None, chord_body=None,
                       clone=True, from_dict=Signature.from_dict):
@@ -632,7 +632,10 @@ def prepare_steps(self, args, tasks,
 
             # first task gets partial args from chain
             if clone:
-                task = task.clone(args) if is_first_task else task.clone()
+                if is_first_task:
+                    task = task.clone(args, kwargs)
+                else:
+                    task = task.clone()
             elif is_first_task:
                 task.args = tuple(args) + tuple(task.args)
 


Test patch:
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
@@ -333,7 +333,7 @@ def test_group_to_chord(self):
             self.add.s(30)
         )
         c._use_link = True
-        tasks, results = c.prepare_steps((), c.tasks)
+        tasks, results = c.prepare_steps((), {}, c.tasks)
 
         assert tasks[-1].args[0] == 5
         assert isinstance(tasks[-2], chord)
@@ -347,7 +347,7 @@ def test_group_to_chord(self):
 
         c2 = self.add.s(2, 2) | group(self.add.s(i, i) for i in range(10))
         c2._use_link = True
-        tasks2, _ = c2.prepare_steps((), c2.tasks)
+        tasks2, _ = c2.prepare_steps((), {}, c2.tasks)
         assert isinstance(tasks2[0], group)
 
     def test_group_to_chord__protocol_2__or(self):
@@ -372,7 +372,7 @@ def test_group_to_chord__protocol_2(self):
 
         c2 = self.add.s(2, 2) | group(self.add.s(i, i) for i in range(10))
         c2._use_link = False
-        tasks2, _ = c2.prepare_steps((), c2.tasks)
+        tasks2, _ = c2.prepare_steps((), {}, c2.tasks)
         assert isinstance(tasks2[0], group)
 
     def test_apply_options(self):