BlockingCallbackTransform

Once in a while, I hit the issue that a callback is invoked (again) before the previous invocation has completed. The most typical case is pulling updates using the Interval component. If the update itself takes longer than the polling interval (e.g. due to a slow database), the callback will never complete. Here is a small example,

import time
from dash_extensions.enrich import DashProxy, dcc, html, Output, Input

app = DashProxy()
app.layout = html.Div([html.Div(id="output"), dcc.Interval(id="trigger")])  # default interval is 1s

@app.callback(Output("output", "children"), Input("trigger", "n_intervals"), blocking=True)
def update(n_intervals):
    time.sleep(5)  # emulate slow database
    return f"Hello! (n_intervals is {n_intervals})"

if __name__ == '__main__':
    app.run_server()

in which the “output.children” element will never be updated due to this issue. What I would have wanted to happen was for the callback to run until completed, even if the trigger fires again before that happens. So far, I haven’t fount any good solutions (but please let me know, if you found one), so I decided to create a BlockingCallbackTransform. It’s introduced in dash-extensions==0.0.69 and can be used like this,

import time
from dash_extensions.enrich import DashProxy, dcc, html, Output, Input, BlockingCallbackTransform

app = DashProxy(transforms=[BlockingCallbackTransform(timeout=10)])
app.layout = html.Div([html.Div(id="output"), dcc.Interval(id="trigger")])   # default interval is 1s

@app.callback(Output("output", "children"), Input("trigger", "n_intervals"), blocking=True)
def update(n_intervals):
    time.sleep(5)  # emulate slow database
    return f"Hello! (n_intervals is {n_intervals})"

if __name__ == '__main__':
    app.run_server()

The client will now wait until the callback is completed (or the timeout is exceeded) before invoking it again. Hence the app will now “work” (i.e. the hello-message will actually be displayed). I figured this approach might be of help to others :slight_smile:

8 Likes

@Emil ,
Thank you for creating and sharing about BlockingCallbackTransform(). I’ve had this mentioned to me a few times by community members, and I’ve also encountered this issue in one of my apps. This is really helpful.

Let’s say that we use blocking=True in the first callback within an app with chained callbacks. Would it be correct to assume that the second callback will trigger only after the first callback timeout is exceeded (or once callback is completed)?

Thanks! Assuming that the second callback is only triggered by the output of the first callback, then I believe the answer is yes.

1 Like



Thank You for very useful BlockingCallbackTransform(). I tried to use it for my MultiPage application, but found a problem, which cannot solve myself.

In the app.py – @app.callback is OK, as in Your example. I can see a changing “Test1 n_intervals is __” in the window of browser.

In my Page1.py - @callback does not work at all (as I thought at first). I couldn’t see a “Test2 n_intervals is __” , which should clone Test1 in the window of browser.

But occasionally I noticed, that sometimes it works, but by some unknown way. And now I understood that it works, but can’t show result the right way in the window of browser (Google Chrome, Microsoft Edge).

If I minimize a window with “Test1 n_intervals is __” for some seconds, and reopen it – I can see a both strings:

“Test1 n_intervals is __”

“Test2 n_intervals is __”

But Test1 keeps changing, and Test2 – stays freezing with some fixed number

Same result can be, if I put some application on top of my app window and then remove it – Test2 will be changed once again.

My problem is - “Test2 n_intervals is __” is not changing now in a real-time. Could you fix this problem, or maybe recommend me to change something in my code, that I could use BlockingCallbackTransform() in MultiPage mode of my application ?

app.py

import time, dash 
from dash_extensions.enrich import DashProxy, dcc, html, Output, Input, BlockingCallbackTransform
import dash_bootstrap_components as dbc  # pip install dash-bootstrap-components

FONT_AWESOME = ("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css")
app = DashProxy(
    __name__, suppress_callback_exceptions=True, 
    transforms=[BlockingCallbackTransform(timeout=10)],
    external_stylesheets=[dbc.themes.BOOTSTRAP, FONT_AWESOME], use_pages=True
) 
navbar = dbc.NavbarSimple(
    dbc.DropdownMenu(
        [
            dbc.DropdownMenuItem(page["name"], href=page["path"])
            for page in dash.page_registry.values()
            if page["module"] != "pages.not_found_404"
        ],
        nav=True,
        label="More Pages",
    ),
    brand="Multi Page App Plugin Demo",
    color="primary",
    dark=True,
    className="mb-3",
)
app.layout = dbc.Container(html.Div([
    navbar, dash.page_container,
    html.Div([html.Div(id="output1"), html.Div(id="output2"), dcc.Interval(id="trigger")]),
    dcc.Store(id="store-dropdown-value", data=None)    
]), fluid=True)

@app.callback(
    Output("output1", "children"), 
    Input(component_id= "trigger", component_property="n_intervals"),    #### 
  
    blocking=True,
    prevent_initial_call=True
)

def TEST1(n_intervals):
        time.sleep(2)  # emulate slow database
        return f"Test1 n_intervals is {n_intervals}"

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

Page1.py

import time, dash
import dash_bootstrap_components as dbc
from dash import callback
from dash_extensions.enrich import html, Output, Input

dash.register_page(__name__)

layout = dbc.Container([ 
    html.Div([
    html.H2(children= "Page1",
    style={"text-align": "center", "font-size":"100%", "color":"black"})
    ]),
    html.Hr(),
    # html.Div([html.Div(id="output")]),   # default interval is 1s

])      #### End Layouts

@callback(
    Output("output2", "children"), 
    Input(component_id= "trigger", component_property="n_intervals"),    #### 
    blocking=True,  prevent_initial_call=True
)

def TEST2(n_intervals):
        time.sleep(2)  # emulate slow database
        return f"Test2 n_intervals is {n_intervals}"

It looks like you imported the callback decorator from the wrong package (i.e. dash instead of dash-extensions).

2 Likes

Thank you very much for Your Help!! You’re right.
Now it’s working OK!


Emil! Help please with other problem!

I added to Page1.py a new component - dcc.RadioItems, and can’t use it correctly in the mode "blocking=True " . I tested several different components, adding it to “layout = dbc.Container” in Page1.py, and always got an error.
1st variant without - “blocking=True” is OK

2nd variant with “blocking=True” give me error:

If i move dcc.RadioItems from Page1.py to app.py, it’s OK again:
image

As I understand, problem is with Components from pages, other than app.py.

Could you recommend me, how can I make Components in layout of Page1.py working right with a mode “blocking=True” ?

Thank You!

app.py

import time, dash 
from dash_extensions.enrich import DashProxy, callback, dcc, html, Output, Input, BlockingCallbackTransform
import dash_bootstrap_components as dbc  # pip install dash-bootstrap-components

FONT_AWESOME = ("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css")
app = DashProxy(
    __name__, suppress_callback_exceptions=True, 
    transforms=[BlockingCallbackTransform(timeout=10)],
    external_stylesheets=[dbc.themes.BOOTSTRAP, FONT_AWESOME], use_pages=True
) 
navbar = dbc.NavbarSimple(
    dbc.DropdownMenu(
        [
            dbc.DropdownMenuItem(page["name"], href=page["path"])
            for page in dash.page_registry.values()
            if page["module"] != "pages.not_found_404"
        ],
        nav=True,
        label="More Pages",
    ),
    brand="Multi Page App Plugin Demo",
    color="primary",
    dark=True,
    className="mb-3",
)
app.layout = dbc.Container(html.Div([
    navbar, dash.page_container,
    html.Div([
        html.Div(id="output1"), html.Div(id="output2"), dcc.Interval(id="trigger"),
            # dcc.RadioItems(id="radio", options = ['On', 'Off'], value = 'Off'), 
        ]),
    dcc.Store(id="store-dropdown-value", data=None)    
]), fluid=True)

@callback(
# @app.callback(            
    Output("output1", "children"),  
    Input(component_id= "trigger", component_property="n_intervals"),    #### 
  
    blocking=True,
    prevent_initial_call=True
)

def TEST1(n_intervals):
        # time.sleep(2)  # emulate slow database
        return f"Test1 n_intervals is {n_intervals}"

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

Page1.py

import time, dash
import dash_bootstrap_components as dbc
from dash_extensions.enrich import dcc, html, Output, Input, callback

dash.register_page(__name__)

layout = dbc.Container([ 
    html.Div([
    html.H2(children= "Page1",
    style={"text-align": "center", "font-size":"100%", "color":"black"})
    ]),
    html.Hr(),
    dcc.RadioItems(id="radio", options = ['On', 'Off'], value = 'Off'), 
])      #### End Layouts


@callback(
    Output(component_id= "output2", component_property="children"), 
    Input(component_id= "radio", component_property="value"),    #### 
    Input(component_id= "trigger", component_property="n_intervals"),    #### 
    blocking=True,  
    prevent_initial_call=True
)

def TEST2(radio, n_intervals):
        # time.sleep(2)  # emulate slow database
        return f"Test2 n_intervals is {n_intervals} radio= {radio}"