Long callback function executed without being triggered

Hi,

I have a rather complex app where the layout content is generated by a callback (depending on what should be displayed). For one specific layout content, I need a long callback because one callback function takes some time to execute. The problem is that, somehow, right after the layout is generated, the long callback is triggered automatically (even when using prevent_initial_call). No problem if I use a simple callback (not long), though.

Below is a sample script to reproduce this issue (if it is intended) based on the long callback example in the documentation:

import dash
from dash import html
from dash.long_callback import DiskcacheLongCallbackManager
from dash.dependencies import Input, Output

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

app = dash.Dash(
    __name__,
    suppress_callback_exceptions=True,
    long_callback_manager=long_callback_manager,
)

layout1 = html.Div(
    [
        html.Button("Generate layout", id="button_1"),
        html.Div(id="main"),
    ],
)
layout2 = html.Div(
    [
        html.Div([html.P(id="paragraph_id", children=["Button not clicked"])]),
        html.Button(id="button_id", children="Run Job!"),
    ]
)

app.layout = layout1
app.validation_layout = html.Div([layout1, layout2])

@app.callback(
    Output("main", "children"),
    Input("button_1", "n_clicks"),
    prevent_initial_call=True,
)
def generate_layout(n_clicks):
    return layout2


@app.long_callback(
    Output("paragraph_id", "children"),
    Input("button_id", "n_clicks"),
    prevent_initial_call=True,
)
def callback(n_clicks):
    return [f"Clicked {n_clicks} times"]


if __name__ == "__main__":
    app.run_server(debug=True)

Execute the app, click on “Generate layout”, wait a few seconds, “paragraph_id” children will automatically change to “Clicked None times”.

Otherwise, if it’s actually intended, why is it happening and how to prevent it?

Edit: following the documentation, I also tried with a validation layout to reference all the callbacks, but same issue (code updated).

Hi @Kefeng! Thank you for including a MRE, it helped a lot to solve the problem :slight_smile:

Actually, your code is working fine. What prevent_initial_call=True is doing is keeping the callback from being triggered the first time the page loads. Once the user has done anything (for example, clicked the button named 'button_1', prevent_initial_call=True is no longer useful because. the initial call (loading the Dash app the first time) has already happened.

I think that what you need in this case is to include inside the long callback a raise dash.exceptions.PreventUpdate if the button 'button_id' has not. been clicked . The resulting code would look like this:

import dash
from dash import html
from dash.long_callback import DiskcacheLongCallbackManager
from dash.dependencies import Input, Output

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

app = dash.Dash(
    __name__,
    suppress_callback_exceptions=True,
    long_callback_manager=long_callback_manager,
)

layout1 = html.Div(
    [
        html.Button("Generate layout", id="button_1"),
        html.Div(id="main"),
    ],
)
layout2 = html.Div(
    [
        html.Div([html.P(id="paragraph_id", children=["Button not clicked"])]),
        html.Button(id="button_id", children="Run Job!"),
    ]
)

app.layout = layout1
app.validation_layout = html.Div([layout1, layout2])

@app.callback(
    Output("main", "children"),
    Input("button_1", "n_clicks"),
    prevent_initial_call=True,
)
def generate_layout(n_clicks):
    return layout2


@app.long_callback(
    Output("paragraph_id", "children"),
    Input("button_id", "n_clicks"),
    prevent_initial_call=True
)
def callback(n_clicks):
# here is what I added
    if n_clicks is None :
        raise dash.exceptions.PreventUpdate
    else :
        return [f"Clicked {n_clicks} times"]


if __name__ == "__main__":
    app.run_server(debug=True)

With this code, when you click 'Generate layout' you should get this:

Screenshot 2022-03-24 at 13.06.42

I hope this helps! :four_leaf_clover:

1 Like

Hi @celia,

Thanks for your reply. But I still understand why does it work (i.e., the callback is not triggered upon generation) if I use a standard callback (not a long_callback)? I am expecting callback and long_callback to behave the same way, or am I mistaken?

Besides, if I add additional arguments to the callback function, the callback is triggered multiple times (see updated function below):

@app.long_callback(
    Output("paragraph_id", "children"),
    Input("button_id", "n_clicks"),
    State("main", "children"),
    prevent_initial_call=True,
)
def callback(n_clicks, children):
    print("Callback is triggered!")
    return [f"Clicked {n_clicks} times"]

After a few seconds, looking at the console log, you will see that the callback function has been called up to 3 times.

Are any of these “issues” expected (i.e., callback and long_callback behaving differently, and callback function triggered multiple times)?

Thanks again for your time and reply!

Your report seems similar to this thread,

My guess is that it’s a bug :slight_smile:

1 Like

Yes, I think so too. I’ve seen that @Kefeng already reported it in github, thank you!

I’m copying this thread here because it is also about the same problem and it gives more information about what is happening: https://community.plotly.com/t/long-callback-interval-1-problems/60819/4

While it’s solved, I tried to find a workaround. I tried using the cache_args_to_skip argument and also cancel (with long_call_interval_1) but either it doesn’t run at all or it runs twice :smiling_face_with_tear: I’ll keep trying and I’ll post it here if I find it!