Repository: celery/celery

Issue:
Unnamed add_periodic_task unexpectedly override another periodic task
<!--
Please fill this template entirely and do not erase parts of it.
We reserve the right to close without a response
enhancement 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?q=is%3Aissue+label%3A%22Issue+Type%3A+Enhancement%22+-label%3A%22Category%3A+Documentation%22)
  for similar or identical enhancement to an existing feature.
- [x] I have checked the [pull requests list](https://github.com/celery/celery/pulls?q=is%3Apr+label%3A%22Issue+Type%3A+Enhancement%22+-label%3A%22Category%3A+Documentation%22)
  for existing proposed enhancements.
- [x] I have checked the [commit log](https://github.com/celery/celery/commits/main)
  to find out if the if the same enhancement was already implemented in the
  main 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

- Somewhat related #2834

#### Possible Duplicates

- None

# Brief Summary
<!--
Please include a brief summary of what the enhancement is
and why it is needed.
-->

Adding the same periodic task twice with different schedules unexpectedly override the first one.

```python
sig = add.s(2, 2)
app.add_periodic_task(10, sig)
app.add_periodic_task(20, sig)
assert len(app.conf.beat_schedule) == 1
```

# 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
<!--
Please describe in detail how this enhancement is going to change the behavior
of an existing feature.
Describe what happens in case of failures as well if applicable.
-->

Now I understand that I can just set a name explicitly to avoid the behavior, but it is not clear in the docs and looks like it could be warned/failed in runtime to improve developer experience. I tried it in #8143.

## Proposed UI/UX
<!--
Please provide your ideas for the API, CLI options,
configuration key names etc. that will be adjusted for this enhancement.
-->

## Diagrams
<!--
Please include any diagrams that might be relevant
to the implementation of this enhancement 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.
-->
None



Discussion:

Resolving patch:
diff --git a/celery/app/base.py b/celery/app/base.py
--- a/celery/app/base.py
+++ b/celery/app/base.py
@@ -1003,7 +1003,8 @@ def _load_config(self):
         # load lazy periodic tasks
         pending_beat = self._pending_periodic_tasks
         while pending_beat:
-            self._add_periodic_task(*pending_beat.popleft())
+            periodic_task_args, periodic_task_kwargs = pending_beat.popleft()
+            self._add_periodic_task(*periodic_task_args, **periodic_task_kwargs)
 
         self.on_after_configure.send(sender=self, source=self._conf)
         return self._conf
@@ -1023,12 +1024,19 @@ def signature(self, *args, **kwargs):
 
     def add_periodic_task(self, schedule, sig,
                           args=(), kwargs=(), name=None, **opts):
+        """
+        Add a periodic task to beat schedule.
+
+        Celery beat store tasks based on `sig` or `name` if provided. Adding the
+        same signature twice make the second task override the first one. To
+        avoid the override, use distinct `name` for them.
+        """
         key, entry = self._sig_to_periodic_task_entry(
             schedule, sig, args, kwargs, name, **opts)
         if self.configured:
-            self._add_periodic_task(key, entry)
+            self._add_periodic_task(key, entry, name=name)
         else:
-            self._pending_periodic_tasks.append((key, entry))
+            self._pending_periodic_tasks.append([(key, entry), {"name": name}])
         return key
 
     def _sig_to_periodic_task_entry(self, schedule, sig,
@@ -1045,7 +1053,13 @@ def _sig_to_periodic_task_entry(self, schedule, sig,
             'options': dict(sig.options, **opts),
         }
 
-    def _add_periodic_task(self, key, entry):
+    def _add_periodic_task(self, key, entry, name=None):
+        if name is None and key in self._conf.beat_schedule:
+            logger.warning(
+                f"Periodic task key='{key}' shadowed a previous unnamed periodic task."
+                " Pass a name kwarg to add_periodic_task to silence this warning."
+            )
+
         self._conf.beat_schedule[key] = entry
 
     def create_task_cls(self):


Test patch:
diff --git a/t/unit/app/test_app.py b/t/unit/app/test_app.py
--- a/t/unit/app/test_app.py
+++ b/t/unit/app/test_app.py
@@ -916,6 +916,33 @@ def add(x, y):
         assert 'add1' in self.app.conf.beat_schedule
         assert 'add2' in self.app.conf.beat_schedule
 
+    def test_add_periodic_task_expected_override(self):
+
+        @self.app.task
+        def add(x, y):
+            pass
+        sig = add.s(2, 2)
+        self.app.add_periodic_task(10, sig, name='add1', expires=3)
+        self.app.add_periodic_task(20, sig, name='add1', expires=3)
+        assert 'add1' in self.app.conf.beat_schedule
+        assert len(self.app.conf.beat_schedule) == 1
+
+    def test_add_periodic_task_unexpected_override(self, caplog):
+
+        @self.app.task
+        def add(x, y):
+            pass
+        sig = add.s(2, 2)
+        self.app.add_periodic_task(10, sig, expires=3)
+        self.app.add_periodic_task(20, sig, expires=3)
+
+        assert len(self.app.conf.beat_schedule) == 1
+        assert caplog.records[0].message == (
+            "Periodic task key='t.unit.app.test_app.add(2, 2)' shadowed a"
+            " previous unnamed periodic task. Pass a name kwarg to"
+            " add_periodic_task to silence this warning."
+        )
+
     @pytest.mark.masked_modules('multiprocessing.util')
     def test_pool_no_multiprocessing(self, mask_modules):
         pool = self.app.pool