Repository: celery/celery

Issue:
Make AMQP message properties available in task
<!--
Please fill this template entirely and do not erase parts of it.
We reserve the right to close without a response
feature requests which are incomplete.
-->
# Checklist
<!--
To check an item on the list replace [ ] with [x].
-->

- [x] I have checked the [issues list](https://github.com/celery/celery/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22Issue+Type%3A+Feature+Request%22+)
  for similar or identical feature requests.
- [x] I have checked the [pull requests list](https://github.com/celery/celery/pulls?utf8=%E2%9C%93&q=is%3Apr+label%3A%22PR+Type%3A+Feature%22+)
  for existing proposed implementations of this feature.
- [x] I have checked the [commit log](https://github.com/celery/celery/commits/master)
  to find out if the if the same feature was already implemented in the
  master branch.
- [x] I have included all related issues and possible duplicate issues
  in this issue (If there are none, check this box anyway).

## Related Issues and Possible Duplicates
<!--
Please make sure to search and mention any related issues
or possible duplicates to this issue as requested by the checklist above.

This may or may not include issues in other repositories that the Celery project
maintains or other repositories that are dependencies of Celery.

If you don't know how to mention issues, please refer to Github's documentation
on the subject: https://help.github.com/en/articles/autolinked-references-and-urls#issues-and-pull-requests
-->

#### Related Issues

- None

#### Possible Duplicates

- None

# Brief Summary
I would like to be able to access arbitrary AMQP message properties (e.g. `user-id`) within my tasks.
# Design

## Architectural Considerations
<!--
If more components other than Celery are involved,
describe them here and the effect it would have on Celery.
-->
None

## Proposed Behavior
If the message received from the transport has a `properties` attribute, a reference to the value should be available in the `app.task.Context` instance available as `self.request` in a bound task. If there is no `properties` attribute, the new `Context` attribute may be set to `None`.

## Proposed UI/UX
The new attribute in `app.task.Context` might be named `properties`.

## Diagrams
<!--
Please include any diagrams that might be relevant
to the implementation of this feature such as:
* Class Diagrams
* Sequence Diagrams
* Activity Diagrams
You can drag and drop images into the text box to attach them to this issue.
-->
N/A

## Alternatives
<!--
If you have considered any alternative implementations
describe them in detail below.
-->
Application-defined headers are already available to the task, but some functionality may be provided through broker interactions which cannot be replicated using application-defined headers (e.g. RabbitMQ's validated User-ID).
Another option would be to expose each of the remaining AMQP 0-9-1 message properties on the `app.task.Context` object similar to `reply_to`. This would have the disadvantage of introducing broker-specific logic.



Discussion:
Hey @InvalidInterrupt :wave:,
Thank you for opening an issue. We will get back to you as soon as we can.
Also, check out our [Open Collective](https://opencollective.com/celery) and consider backing us - every little helps!

We also offer priority support for our sponsors.
If you require immediate assistance please consider sponsoring us.


Resolving patch:
diff --git a/celery/app/task.py b/celery/app/task.py
--- a/celery/app/task.py
+++ b/celery/app/task.py
@@ -85,6 +85,7 @@ class Context:
     loglevel = None
     origin = None
     parent_id = None
+    properties = None
     retries = 0
     reply_to = None
     root_id = None
diff --git a/celery/worker/request.py b/celery/worker/request.py
--- a/celery/worker/request.py
+++ b/celery/worker/request.py
@@ -93,7 +93,8 @@ def __init__(self, message, on_ack=noop,
                  maybe_make_aware=maybe_make_aware,
                  maybe_iso8601=maybe_iso8601, **opts):
         self._message = message
-        self._request_dict = message.headers if headers is None else headers
+        self._request_dict = (message.headers.copy() if headers is None
+                              else headers.copy())
         self._body = message.body if body is None else body
         self._app = app
         self._utc = utc
@@ -157,6 +158,7 @@ def __init__(self, message, on_ack=noop,
             'redelivered': delivery_info.get('redelivered'),
         }
         self._request_dict.update({
+            'properties': properties,
             'reply_to': properties.get('reply_to'),
             'correlation_id': properties.get('correlation_id'),
             'hostname': self._hostname,
diff --git a/t/integration/tasks.py b/t/integration/tasks.py
--- a/t/integration/tasks.py
+++ b/t/integration/tasks.py
@@ -305,6 +305,11 @@ def return_priority(self, *_args):
     return "Priority: %s" % self.request.delivery_info['priority']
 
 
+@shared_task(bind=True)
+def return_properties(self):
+    return self.request.properties
+
+
 class ClassBasedAutoRetryTask(Task):
     name = 'auto_retry_class_task'
     autoretry_for = (ValueError,)


Test patch:
diff --git a/t/integration/test_tasks.py b/t/integration/test_tasks.py
--- a/t/integration/test_tasks.py
+++ b/t/integration/test_tasks.py
@@ -9,7 +9,8 @@
 from .conftest import get_active_redis_channels
 from .tasks import (ClassBasedAutoRetryTask, ExpectedException, add,
                     add_ignore_result, add_not_typed, fail, print_unicode,
-                    retry, retry_once, retry_once_priority, sleeping)
+                    retry, retry_once, retry_once_priority, return_properties,
+                    sleeping)
 
 TIMEOUT = 10
 
@@ -270,6 +271,11 @@ def test_unicode_task(self, manager):
             timeout=TIMEOUT, propagate=True,
         )
 
+    @flaky
+    def test_properties(self, celery_session_worker):
+        res = return_properties.apply_async(app_id="1234")
+        assert res.get(timeout=TIMEOUT)["app_id"] == "1234"
+
 
 class tests_task_redis_result_backend:
     def setup(self, manager):