Skip to content

Commit 6773440

Browse files
authored
Merge pull request #2473 from plotly/fix-2221
Add callback id to long callback key generation.
2 parents 1bb7a74 + ba38e94 commit 6773440

File tree

7 files changed

+62
-19
lines changed

7 files changed

+62
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
88

99
- [#2479](https://github.com/plotly/dash/pull/2479) Fix `KeyError` "Callback function not found for output [...], , perhaps you forgot to prepend the '@'?" issue when using duplicate callbacks targeting the same output. This issue would occur when the app is restarted or when running with multiple `gunicorn` workers.
1010
- [#2471](https://github.com/plotly/dash/pull/2471) Fix `allow_duplicate` output with clientside callback, fix [#2467](https://github.com/plotly/dash/issues/2467)
11+
- [#2473](https://github.com/plotly/dash/pull/2473) Fix background callbacks with different outputs but same function, fix [#2221](https://github.com/plotly/dash/issues/2221)
1112

1213
## [2.9.1] - 2023-03-17
1314

dash/_callback.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,9 @@ def wrap_func(func):
316316

317317
if long is not None:
318318
long_key = BaseLongCallbackManager.register_func(
319-
func, long.get("progress") is not None
319+
func,
320+
long.get("progress") is not None,
321+
callback_id,
320322
)
321323

322324
@wraps(func)

dash/long_callback/managers/__init__.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def terminate_unhealthy_job(self, job):
3636
def job_running(self, job):
3737
raise NotImplementedError
3838

39-
def make_job_fn(self, fn, progress):
39+
def make_job_fn(self, fn, progress, key=None):
4040
raise NotImplementedError
4141

4242
def call_job_fn(self, key, job_fn, args, context):
@@ -76,11 +76,11 @@ def build_cache_key(self, fn, args, cache_args_to_ignore):
7676
return hashlib.sha1(str(hash_dict).encode("utf-8")).hexdigest()
7777

7878
def register(self, key, fn, progress):
79-
self.func_registry[key] = self.make_job_fn(fn, progress)
79+
self.func_registry[key] = self.make_job_fn(fn, progress, key)
8080

8181
@staticmethod
82-
def register_func(fn, progress):
83-
key = BaseLongCallbackManager.hash_function(fn)
82+
def register_func(fn, progress, callback_id):
83+
key = BaseLongCallbackManager.hash_function(fn, callback_id)
8484
BaseLongCallbackManager.functions.append(
8585
(
8686
key,
@@ -99,7 +99,9 @@ def _make_progress_key(key):
9999
return key + "-progress"
100100

101101
@staticmethod
102-
def hash_function(fn):
102+
def hash_function(fn, callback_id=""):
103103
fn_source = inspect.getsource(fn)
104104
fn_str = fn_source
105-
return hashlib.sha1(fn_str.encode("utf-8")).hexdigest()
105+
return hashlib.sha1(
106+
callback_id.encode("utf-8") + fn_str.encode("utf-8")
107+
).hexdigest()

dash/long_callback/managers/celery_manager.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import json
2-
import inspect
3-
import hashlib
42
import traceback
53
from contextvars import copy_context
64

@@ -78,8 +76,8 @@ def job_running(self, job):
7876
"PROGRESS",
7977
)
8078

81-
def make_job_fn(self, fn, progress):
82-
return _make_job_fn(fn, self.handle, progress)
79+
def make_job_fn(self, fn, progress, key=None):
80+
return _make_job_fn(fn, self.handle, progress, key)
8381

8482
def get_task(self, job):
8583
if job:
@@ -127,15 +125,10 @@ def get_result(self, key, job):
127125
return result
128126

129127

130-
def _make_job_fn(fn, celery_app, progress):
128+
def _make_job_fn(fn, celery_app, progress, key):
131129
cache = celery_app.backend
132130

133-
# Hash function source and module to create a unique (but stable) celery task name
134-
fn_source = inspect.getsource(fn)
135-
fn_str = fn_source
136-
fn_hash = hashlib.sha1(fn_str.encode("utf-8")).hexdigest()
137-
138-
@celery_app.task(name=f"long_callback_{fn_hash}")
131+
@celery_app.task(name=f"long_callback_{key}")
139132
def job_fn(result_key, progress_key, user_callback_args, context=None):
140133
def _set_progress(progress_value):
141134
if not isinstance(progress_value, (list, tuple)):

dash/long_callback/managers/diskcache_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def job_running(self, job):
107107
return proc.status() != psutil.STATUS_ZOMBIE
108108
return False
109109

110-
def make_job_fn(self, fn, progress):
110+
def make_job_fn(self, fn, progress, key=None):
111111
return _make_job_fn(fn, self.handle, progress)
112112

113113
def clear_cache_entry(self, key):
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from dash import Dash, Input, Output, html
2+
3+
from tests.integration.long_callback.utils import get_long_callback_manager
4+
5+
long_callback_manager = get_long_callback_manager()
6+
handle = long_callback_manager.handle
7+
8+
app = Dash(__name__, long_callback_manager=long_callback_manager)
9+
10+
app.layout = html.Div(
11+
[
12+
html.Button("click 1", id="button-1"),
13+
html.Button("click 2", id="button-2"),
14+
html.Div(id="output-1"),
15+
html.Div(id="output-2"),
16+
]
17+
)
18+
19+
20+
def gen_callback(index):
21+
@app.callback(
22+
Output(f"output-{index}", "children"),
23+
Input(f"button-{index}", "n_clicks"),
24+
background=True,
25+
prevent_initial_call=True,
26+
)
27+
def callback_name(_):
28+
return f"Clicked on {index}"
29+
30+
31+
for i in range(1, 3):
32+
gen_callback(i)
33+
34+
35+
if __name__ == "__main__":
36+
app.run_server(debug=True)

tests/integration/long_callback/test_basic_long_callback.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,3 +547,12 @@ def test_lcbc014_progress_delete(dash_duo, manager):
547547
dash_duo.wait_for_text_to_equal("#output", "done")
548548

549549
assert dash_duo.find_element("#progress-counter").text == "2"
550+
551+
552+
def test_lcbc015_diff_outputs_same_func(dash_duo, manager):
553+
with setup_long_callback_app(manager, "app_diff_outputs") as app:
554+
dash_duo.start_server(app)
555+
556+
for i in range(1, 3):
557+
dash_duo.find_element(f"#button-{i}").click()
558+
dash_duo.wait_for_text_to_equal(f"#output-{i}", f"Clicked on {i}")

0 commit comments

Comments
 (0)