Closed
Description
Describe your context
Please provide us your environment, so we can easily reproduce the issue.
- replace the result of
pip list | grep dash
below
dash 2.18.2 /home/amorton/gh/dash
dash-core-components 2.0.0
dash_dangerously_set_inner_html 0.0.2
dash-flow-example 0.0.5
dash_generator_test_component_nested 0.0.1 /home/amorton/gh/dash/@plotly/dash-generator-test-component-nested
dash_generator_test_component_standard 0.0.1 /home/amorton/gh/dash/@plotly/dash-generator-test-component-standard
dash_generator_test_component_typescript 0.0.1 /home/amorton/gh/dash/@plotly/dash-generator-test-component-typescript
dash-html-components 2.0.0
dash-table 5.0.0
dash_test_components 0.0.1 /home/amorton/gh/dash/@plotly/dash-test-components
dash-testing-stub 0.0.2
Describe the bug
Pattern-matched long callbacks incorrectly cancelled based on wildcard output
Consider the following example app:
import time
from dash import Dash, DiskcacheManager, callback, html, MATCH, Output, Input, State
def build_output_message_id(item_id_str: str) -> dict[str, str]:
return {
'component': 'output-message',
'item_id': item_id_str,
}
def build_output_item(item_id: float) -> html.Div:
return html.Div(
[
html.Span(f'{item_id:.2} sec delay:', style={'margin-right': '1rem'}),
html.Span(0, id=build_output_message_id(str(item_id))),
html.Br(),
],
style={"margin-top": "1rem"},
)
def build_app_layout() -> html.Div:
return html.Div([
html.Button('Fire!', id='button', n_clicks=0),
html.Br(),
*[build_output_item(i * 0.2) for i in range(20)],
], style={"display": "block"})
@callback(
Output(build_output_message_id(MATCH), 'children'),
Input('button', 'n_clicks'),
State(build_output_message_id(MATCH), 'children'),
State(build_output_message_id(MATCH), 'id'),
prevent_initial_call=True,
background=True,
interval=200,
)
def update_messages(_, current_value, id_dict):
delay_secs = float(id_dict["item_id"])
time.sleep(delay_secs)
return current_value + 1
app = Dash(
background_callback_manager=DiskcacheManager(),
)
app.layout = build_app_layout()
app.run(
host='0.0.0.0',
debug=True,
)
Upon pressing the button you should see many numbers never increment, and many requests being made with a list of oldJob
values.
This is unexpected, since the outputs don't correspond to the same concrete component.
The following patch resolves the issue in this example app.
diff --git a/dash/dash-renderer/src/actions/callbacks.ts b/dash/dash-renderer/src/actions/callbacks.ts
index 23da0a3f..a73af9d0 100644
--- a/dash/dash-renderer/src/actions/callbacks.ts
+++ b/dash/dash-renderer/src/actions/callbacks.ts
@@ -561,7 +561,7 @@ function handleServerside(
cacheKey: data.cacheKey as string,
cancelInputs: data.cancel,
progressDefault: data.progressDefault,
- output
+ output: JSON.stringify(payload.outputs),
};
dispatch(addCallbackJob(jobInfo));
job = data.job;
@@ -761,9 +761,10 @@ export function executeCallback(
let lastError: any;
const additionalArgs: [string, string, boolean?][] = [];
+ const jsonOutput = JSON.stringify(payload.outputs);
values(getState().callbackJobs).forEach(
(job: CallbackJobPayload) => {
- if (cb.callback.output === job.output) {
+ if (jsonOutput === job.output) {
// Terminate the old jobs that are not completed
// set as outdated for the callback promise to
// resolve and remove after.