Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pattern-matched long callbacks cancelled incorrectly #3119

Open
apmorton opened this issue Jan 8, 2025 · 0 comments
Open

pattern-matched long callbacks cancelled incorrectly #3119

apmorton opened this issue Jan 8, 2025 · 0 comments
Labels
bug something broken P2 considered for next cycle

Comments

@apmorton
Copy link

apmorton commented Jan 8, 2025

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.
@gvwilson gvwilson changed the title [BUG] Pattern-matched long callbacks incorrectly cancelled pattern-matched long callbacks cancelled incorrectly Jan 9, 2025
@gvwilson gvwilson added bug something broken P2 considered for next cycle labels Jan 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken P2 considered for next cycle
Projects
None yet
Development

No branches or pull requests

2 participants