Repository: PrefectHQ/prefect

Issue:
Modifying `task.name` can violate the assumption that slugs are unique
## Description
Dynamically modifying a task's name can result in overlapping slugs. This seems to be because https://github.com/PrefectHQ/prefect/blob/20cd7352705741628ff83789e779b7bdb5315969/src/prefect/core/flow.py#L473-L494 generates `slug_bases` each time that the tasks are added.

Better practice is to provide the task name when it is added to the flow, but there is no enforcement that `.name` is read only, and the use of `task_args` is less intuitive.

`_generate_task_slug` could be made more robust, or `.name` could be made read-only or raise a warning suggesting the preferred method.


## Expected Behavior
Slugs should always be unique.


## Reproduction
```python
from prefect import Flow, Parameter, task

@task
def identity(x):
    return x

with Flow('slug-overlap') as flow:
    with tags('test-tag'):
        a = Parameter('a', default=1)
        # b = identity(a, task_args=dict(name='better-practice'))
        b = identity(a)
        b.name = 'bad-practice'
        c = identity(a)
print(flow.slugs)
```
results in 
```python
{<Task: bad-practice>: 'identity-test-tag-1', <Parameter: a>: 'a', <Task: identity>: 'identity-test-tag-1'}
```



## Environment
n/a

Discussion:

Patch:
diff --git a/changes/pr4189.yaml b/changes/pr4189.yaml
new file mode 100644
index 0000000000..4281dbf640
--- /dev/null
+++ b/changes/pr4189.yaml
@@ -0,0 +1,2 @@
+fix:
+  - "Make task slug generation robust to modifying existing task names - [#4189](https://github.com/PrefectHQ/prefect/pull/4189)"
diff --git a/src/prefect/core/flow.py b/src/prefect/core/flow.py
index d790330786..0789f336ab 100644
--- a/src/prefect/core/flow.py
+++ b/src/prefect/core/flow.py
@@ -4,6 +4,7 @@ import copy
 import functools
 import hashlib
 import inspect
+import itertools
 import json
 import os
 import tempfile
@@ -176,7 +177,10 @@ class Flow:
 
         self.tasks = set()  # type: Set[Task]
         self.edges = set()  # type: Set[Edge]
-        self.slugs = dict()  # type: Dict[Task, str]
+        self._slug_counters = collections.defaultdict(
+            lambda: itertools.count(1)
+        )  # type: Dict[str, Iterator[int]]
+        self.slugs = {}  # type: Dict[Task, str]
         self.constants = collections.defaultdict(
             dict
         )  # type: Dict[Task, Dict[str, Any]]
@@ -486,12 +490,14 @@ class Flow:
         Returns:
             - str: the corresponding slug
         """
-        slug_bases = []
-        for t in self.tasks:
-            slug_bases.append(f"{t.name}-" + "-".join(sorted(t.tags)))
-        new_slug = f"{task.name}-" + "-".join(sorted(task.tags))
-        index = slug_bases.count(new_slug)
-        return f"{new_slug}{'' if new_slug.endswith('-') else '-'}{index + 1}"
+        parts = [task.name]
+        parts.extend(sorted(task.tags))
+        prefix = "-".join(parts)
+        while True:
+            ind = next(self._slug_counters[prefix])
+            slug = f"{prefix}-{ind}"
+            if slug not in self.slugs.values():
+                return slug
 
     def add_task(self, task: Task) -> Task:
         """


Test patch:
diff --git a/tests/core/test_flow.py b/tests/core/test_flow.py
index f7df8b2f84..2c2144b6e0 100644
--- a/tests/core/test_flow.py
+++ b/tests/core/test_flow.py
@@ -3196,12 +3196,16 @@ def test_run_agent_passes_flow_labels(monkeypatch, kind):
 
 class TestSlugGeneration:
     def test_slugs_are_stable(self):
-        tasks = [Task(name=str(x)) for x in range(10)]
+        tasks = [Task(name="add") for _ in range(5)]
+        tasks.extend(Task(name="mul") for _ in range(5))
         flow_one = Flow("one", tasks=tasks)
         flow_two = Flow("two", tasks=tasks)
 
-        assert set(flow_one.slugs.values()) == set([str(x) + "-1" for x in range(10)])
-        assert flow_one.slugs == flow_two.slugs
+        sol = {f"add-{i}" for i in range(1, 6)}
+        sol.update(f"mul-{i}" for i in range(1, 6))
+
+        assert set(flow_one.slugs.values()) == sol
+        assert set(flow_two.slugs.values()) == sol
 
     def test_slugs_incorporate_tags_and_order(self):
         with Flow("one") as flow_one:
@@ -3224,3 +3228,27 @@ class TestSlugGeneration:
             "a-tag1-1",
             "b-tag1-tag2-1",
         }
+
+    def test_generated_slugs_dont_collide_with_user_provided_slugs(self):
+        with Flow("test") as flow:
+            a3 = Task("a", slug="a-3")
+            flow.add_task(a3)
+            a1 = Task("a")
+            flow.add_task(a1)
+            a2 = Task("a")
+            flow.add_task(a2)
+            a4 = Task("a")
+            flow.add_task(a4)
+
+        assert flow.slugs == {a1: "a-1", a2: "a-2", a3: "a-3", a4: "a-4"}
+
+    def test_slugs_robust_to_task_name_changes(self):
+        "See https://github.com/PrefectHQ/prefect/issues/4185"
+        with Flow("test") as flow:
+            a1 = Task("a")
+            flow.add_task(a1)
+            a1.name = "changed"
+            a2 = Task("a")
+            flow.add_task(a2)
+
+        assert flow.slugs == {a1: "a-1", a2: "a-2"}