Repository: plotly/dash

Issue:
[BUG] Duplicate callback outputs not working when deployed with `gunicorn`
**Describe your context**

Bug only present when app is deployed via Dash Enterprise. 

```
dash                          2.9.1
dash-ag-grid                  2.0.0a4
dash-core-components          2.0.0
dash-design-kit               1.6.8
dash-extensions               0.0.65
dash-html-components          2.0.0
dash-leaflet                  0.1.23
dash-mantine-components       0.12.0
dash-table                    5.0.0
```

**Describe the bug**

Callbacks with duplicate outputs don't work reliably when my app is deployed with `gunicorn` (ie. DE), giving errors like this: 

```
KeyError: "Callback function not found for output '..image-options.rowData...geojson.data...satellite-img.children@e635ba05b4e74958834c65e29d0a3207...classified-img.children@b954a7e5a88643d29e99412073ede694...image-options.selectedRows...delete-notify.children..', perhaps you forgot to prepend the '@'?"
```

There is definitely the appropriate decorator on my function. The callbacks behave as expected when I run the app locally, both with Python and with gunicorn. About 10% of the time the callback will run fine. 

**Expected behavior**

I'd expect this `KeyError` not to occur when the app is deployed. 




Discussion:

Resolving patch:
diff --git a/dash/_callback.py b/dash/_callback.py
--- a/dash/_callback.py
+++ b/dash/_callback.py
@@ -245,7 +245,7 @@ def insert_callback(
         output, prevent_initial_call, config_prevent_initial_callbacks
     )
 
-    callback_id = create_callback_id(output)
+    callback_id = create_callback_id(output, inputs)
     callback_spec = {
         "output": callback_id,
         "inputs": [c.to_dict() for c in inputs],
diff --git a/dash/_utils.py b/dash/_utils.py
--- a/dash/_utils.py
+++ b/dash/_utils.py
@@ -123,15 +123,22 @@ def first(self, *names):
             return next(iter(self), {})
 
 
-def create_callback_id(output):
+def create_callback_id(output, inputs):
     # A single dot within a dict id key or value is OK
     # but in case of multiple dots together escape each dot
     # with `\` so we don't mistake it for multi-outputs
+    hashed_inputs = None
+
     def _concat(x):
+        nonlocal hashed_inputs
         _id = x.component_id_str().replace(".", "\\.") + "." + x.component_property
         if x.allow_duplicate:
+            if not hashed_inputs:
+                hashed_inputs = hashlib.md5(
+                    ".".join(str(x) for x in inputs).encode("utf-8")
+                ).hexdigest()
             # Actually adds on the property part.
-            _id += f"@{uuid.uuid4().hex}"
+            _id += f"@{hashed_inputs}"
         return _id
 
     if isinstance(output, (list, tuple)):


Test patch:
diff --git a/dash/testing/composite.py b/dash/testing/composite.py
--- a/dash/testing/composite.py
+++ b/dash/testing/composite.py
@@ -6,14 +6,15 @@ def __init__(self, server, **kwargs):
         super().__init__(**kwargs)
         self.server = server
 
-    def start_server(self, app, **kwargs):
+    def start_server(self, app, navigate=True, **kwargs):
         """Start the local server with app."""
 
         # start server with app and pass Dash arguments
         self.server(app, **kwargs)
 
-        # set the default server_url, it implicitly call wait_for_page
-        self.server_url = self.server.url
+        if navigate:
+            # set the default server_url, it implicitly call wait_for_page
+            self.server_url = self.server.url
 
 
 class DashRComposite(Browser):
diff --git a/tests/integration/test_patch.py b/tests/integration/test_patch.py
--- a/tests/integration/test_patch.py
+++ b/tests/integration/test_patch.py
@@ -372,3 +372,51 @@ def on_click(_, value):
 
     dash_duo.wait_for_text_to_equal("#new-child", "new-child")
     dash_duo.wait_for_text_to_equal("#initial", "init")
+
+
+def test_pch004_duplicate_output_restart(dash_duo_mp):
+    # Duplicate output ids should be the same between restarts for the same ids
+    def create_app():
+        app = Dash(__name__)
+        app.layout = html.Div(
+            [
+                html.Button("Click 1", id="click1"),
+                html.Button("Click 2", id="click2"),
+                html.Div(id="output"),
+            ]
+        )
+
+        @app.callback(
+            Output("output", "children", allow_duplicate=True),
+            Input("click1", "n_clicks"),
+            prevent_initial_call=True,
+        )
+        def on_click(_):
+            return "click 1"
+
+        @app.callback(
+            Output("output", "children", allow_duplicate=True),
+            Input("click2", "n_clicks"),
+            prevent_initial_call=True,
+        )
+        def on_click(_):
+            return "click 2"
+
+        return app
+
+    dash_duo_mp.start_server(create_app())
+
+    dash_duo_mp.wait_for_element("#click1").click()
+    dash_duo_mp.wait_for_text_to_equal("#output", "click 1")
+
+    dash_duo_mp.wait_for_element("#click2").click()
+    dash_duo_mp.wait_for_text_to_equal("#output", "click 2")
+
+    dash_duo_mp.server.stop()
+
+    dash_duo_mp.start_server(create_app(), navigate=False)
+    dash_duo_mp.wait_for_element("#click1").click()
+    dash_duo_mp.wait_for_text_to_equal("#output", "click 1")
+
+    dash_duo_mp.wait_for_element("#click2").click()
+    dash_duo_mp.wait_for_text_to_equal("#output", "click 2")
diff --git a/tests/unit/library/test_grouped_callbacks.py b/tests/unit/library/test_grouped_callbacks.py
--- a/tests/unit/library/test_grouped_callbacks.py
+++ b/tests/unit/library/test_grouped_callbacks.py
@@ -41,9 +41,9 @@ def check_output_for_grouping(grouping):
     mock_fn = mock.Mock()
     mock_fn.return_value = grouping
     if multi:
-        callback_id = create_callback_id(flatten_grouping(outputs))
+        callback_id = create_callback_id(flatten_grouping(outputs), [])
     else:
-        callback_id = create_callback_id(outputs)
+        callback_id = create_callback_id(outputs, [])
 
     app.callback(
         outputs,