plotly / plotly/dash

bug in pattern-matching background callbacks

Open
#3,169 1 comment 0 reactions 1 assignee View on GitHub

@T4rk1n is already working on this.

Since Feb 18, 2025.

bug community P2
Dominant language
Python
Stars
24.4k
Forks
2.3k
Avg merge
2d 7h
Merged PRs (30d)
13

Description

dash                 2.18.2

When using Pattern-Matching callbacks that use background callbacks, the current way things work. If the output is the "same", (same exact callback), it will cancel the first callback and start the new one.

Here is an example of three buttons designed to use pattern-matching to update the Divs above once the task is complete, however, the task never completes if you trigger another button push within the time of the first push:

import dash
from dash import dcc, html, Input, Output, ctx, MATCH, DiskcacheManager, no_update
import time

import diskcache
cache = diskcache.Cache("./cache")

background_callback_manager = DiskcacheManager(cache)

# Initialize the Dash app
app = dash.Dash(__name__, background_callback_manager=background_callback_manager)

# Define the layout of the app
app.layout = html.Div([
    html.Div(id='progress-container', children=[
        *[html.Div(id={'type': 'progress', 'index': i}, children=f'{i}') for i in range(3)],
        *[html.Button(id={'type': 'button', 'index': i}, children=f'trigger-{i}') for i in range(3)]
    ]),
    html.Div(id='test-out'),
    html.Button(id='download_test', children='Download Test'),
    dcc.Download(id='download_file')
])

# Pattern-matching callback to update progress
@app.callback(
    Output({'type': 'progress', 'index': MATCH}, 'children'),
    Input({'type': 'button', 'index': MATCH}, 'n_clicks'),
    progress=[Output('test-out', 'children')],
    background=True,
    prevent_initial_call=True
)
def update_progress(set_progress, n):
    if n:
        count = 0
        while count < 100:
            count += 10
            set_progress(f'{ctx.triggered_id.index} - {count}')
            time.sleep(1)
        # Read the existing data from the file
        try:
            with open('test.txt', 'r') as f:
                data = f.readlines()
        except:
            data = []

        # Append the new line to the data
        data.append(f'{ctx.triggered_id.index} - {n}\n')

        # Write the updated data back to the file
        with open('test.txt', 'w') as f:
            f.writelines(data)
        return 'Task Complete!'
    return no_update

@app.callback(
    Output('download_file', 'data'),
    Input('download_test', 'n_clicks'),
    prevent_initial_call=True
)
def downfile(n):
    if n:
        with open('test.txt', 'r') as f:
            data = f.read()  # Read the entire file content
        return dcc.send_bytes(data.encode(), filename='test.txt')
    return no_update


# Run the app
if __name__ == '__main__':
    app.run_server(debug=True)

It also doesnt just stop polling for the response from the server, it also completely drops the function call. This is dangerous, especially when dealing with processing in the background that need to complete.

Using the above example, I clicked all three buttons but only the third completed the task fully:

Image


Here is the process that cancels the background running task, maybe we could allow for opting out of this check:

https://github.com/plotly/dash/blob/974d904a66e234a6e8f49715738c9d6a0d773d9d/dash/dash-renderer/src/actions/callbacks.ts#L771-L779

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.