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:

7 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