Skip to content

pattern-matched long callbacks cancelled incorrectly #3119

Closed
@apmorton

Description

@apmorton

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2considered for next cyclebugsomething broken

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions