Duplicated outputs error when using same event as cancel trigger in multiple background callbacks


I’ve multiple celery background callbacks running in parallel. I’d like to cancel all of them by single button
cancel=Input("cancel_processing_button_id", "n_clicks"),
however this leads to (surprisingly) duplicated Outputs error:

In the callback for output(s):
Output 0 (cancel_processing_button_id.id) is already in use.
To resolve this, set `allow_duplicate=True` on
duplicate outputs, or combine the outputs into
one callback function, distinguishing the trigger
by using `dash.callback_context` if necessary.

Any ideas what is wrong would be appreciated.

Hi. Do you use this as Output() for a callback?

No, there is no explicit reference to cancel_processing_button_id.id in the code.
I only define button:

                "Cancel processing",

and in code I use n_click property of the button:

    @dash.callback(Output({'role': 'simulation_results', 'task': 'wave_functions'}, 'data'),
                   [Input({'role':'context', 'store': 'trigger_celery'}, 'data')],
                   [State({'role':'context', 'store': 'nanostructure'}, 'data'),
                    State({'role':'context', 'store': 'material_params'}, 'data')],
                   cancel=Input("cancel_processing_button_id", "n_clicks"),
    def _simulate_wave_functions(_, nanostructure, materials_params):

(Similar code is repeated 3 times)

@AIMPED I’ve dug into dash internals and found the following line:

So, I guess it’s a “feature”.

I guess it is because Dash internally creates a callbacks, which has this component as Output. Hence, for each similar repeat, a new such callback is created, thus leading to multiple callbacks that targets the same Output. Ideally for your use case, these callbacks should be changed in the source to include allow_duplicate flags. Until that happens, you should be able to monkey patch your way out by placing code like,

import dash

class PatchOutput(dash.Output):
    def __init__(self, *args, allow_duplicate=True, **kwargs):
        super().__init__(*args, allow_duplicate=allow_duplicate, **kwargs)

dash.Output = PatchOutput
original = dash.callback
dash.callback = lambda *args, **kwargs: original(*args, prevent_initial_call=True, **kwargs)
dash._callback.callback = lambda *args, **kwargs: original(*args, prevent_initial_call=True, **kwargs)

at the top of your app file.

1 Like

Thanks @Emil for your hint. :slight_smile:
I’ve decided to go with separate cancel button per background callback for now. Though it would be nice to have that implemented properly one day.

One more observation: cancel /running actions of background callbacks seem not to work with compound ids (dicts) e.g.
id={'role': 'cancel_processing', 'task': task} however they work fine when using plain strings id=f'cancel_processing_{task}'