-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add long_callback decorator #1702
Conversation
@jonmmease encountering some issues here getting this up and running on my machine. Tested with examples 1 and 5 (using diskcache) and get this:
Currently trying the Celery option. |
super().__init__(cache_by) | ||
|
||
# Handle process class import | ||
if platform.system() == "Windows": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@LiamConnors Can you try change this to if True:
and install the multiprocess
library and see if that makes a difference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @jonmmease. Yep, that worked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! I went ahead and made this change, so all platforms will use this logic path.
|
||
@app.long_callback( | ||
diskcache_manager, | ||
[Output("result", "children"), Output("run-button", "n_clicks")], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: n_clicks
needs to be set back to 0 in the output so that it's value is always zero when used as part of the cache key. Otherwise, every incremented value of n_clicks
invalidates the cache.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not too cumbersome but does feel a bit hacky, as discussed this morning you're going to try and find a way to exclude certain args from the cache key?
This is ready for review, with a few issues still to consider.
|
random API idea... another decorator that stacks onto @app.long(...)
@app.callback(...)
def thing():
return thing |
One limitation I think this has is you can't use a long callback with Relatedly: what happens if the inputs and outputs you're attaching this to don't exist on the page in the initial layout? Then you have some partially-defined inputs & outputs, which will be an error, right? This seems like a common requirement, do we need a way to pull the extra components out and add them to the layout manually? |
Test with celery long callback manager as well as diskcache
Set explicit celery task name to avoid task conflict
Set explicit celery task name to avoid task conflict
Thanks @alexcjohnson, those are both good questions. I'm working on getting celery tests up and running, but then I'll think about these more carefully. |
This can vary between how celery and dash are launched.
@jonmmease I understand Dash 2.0 is going to have a simplified import statement (https://github.com/plotly/ddk-dash-docs/issues/41) Are Long Callbacks part of that? Does that mean, example 1 import statement above should look like this in the docs for diskcache examples?
|
We haven't talked about whether to elevate |
Summary of updates since Friday
Loose ends:
@alexcjohnson I'm not sure I understand this one. The extra components that long callback adds to the layout will always be there from the start. If some of the other input/output components aren't on the page yet, wouldn't the situation be the same as for a regular callback, where you'd need to suppress the callback errors? |
# Conflicts: # dash/dash.py
try it out, but I think suppressing only works when all the inputs and outputs are missing. When only some are present it’s still an error, right? |
Yup, you're right. I didn't realize that |
Add test where the dependencies of long_callback are added dynamically, and validation is handled using validation_layout and prevent_initial_call=True
# Conflicts: # CHANGELOG.md
Ok, I got one approach working.
So now a long_callback can reference components not in the initial layout if:
How does that sound? I also added a test that does this, and makes sure there are no errors logged ( |
|
||
:param long_callback_manager: Long callback manager instance to support the | ||
``@app.long_callback`` decorator. Currently one of | ||
``DiskcacheLongCallbackManager`` or ``CeleryLongCallbackManager`` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though the first sentence should be clear, the second sentence makes it sound like you can just give the class, like long_callback_manager=DiskcacheLongCallbackManager
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. updated in 3892ecb (apologies for the bogus commit message)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So now a long_callback can reference components not in the initial layout if:
- The components are part of validation_layout
- prevent_initial_call=True
How does that sound?
Works for me. Longer term I think we'll want to find a nice way to relax some of these constraints, but that's going to take some thought to do right.
💃
This PR adds the
@long_callback
decorator the was developed in Dash Labs.Draft documentation below
TODO:
long_callback
docstringOverview
This decorator is designed to make it easier to create callback functions that take a long time to run, without locking up the Dash app or timing out.
@long_callback
is designed to support multiple backend executors. Two backends are currently implemented:The
@long_callback
decorator requires a long callback manager instance. This manager instance may be provided to thedash.Dash
app constructor as thelong_callback_manager
keyword argument. Or, it may be provided as themanager
argument to the@app.long_callback
decorator itself.The
@app.long_callback
decorator supports the same arguments as the normal@app.callback
decorator, but also includes support for several additional optional arguments that will be discussed below:manager
,running
,cancel
,progress
, andprogress_default
.Dependencies
The examples below use the
discache
manager, which requires thediskcache
,multiprocess
, andpsutil
librariesExample 1: Simple background callback
Here is a simple example of using the
@long_callback
decorator to register a callback function that updates anhtml.P
element with the number of times that a button has been clicked. The callback usestime.sleep
to simulate a long-running operation.Example 2: Disable button while callback is running
In the previous example, there is no visual indication that the long callback was running. It is also possible to click the "Run Job!" button multiple times before the original job has the chance to complete. This example addresses these shortcomings by disabling the button while the callback is running, and re-enabling it when the callback completes.
This is accomplished using the
running
argument to@long_callback
. This argument accepts a list of 3-element tuples. The first element of each tuple should be anOutput
dependency object referencing a property of a component in the app layout. The second elements is the value that the property should be set to while the callback is running, and the third element is the value the property should be set to when the callback completes.This example uses
running
to set thedisabled
property of the button toTrue
while the callback is running, andFalse
when it completes. In this example, the long callback manager is provided to thedash.Dash
app constructor instead of the@app.long_callback
decorator.Example 3: Cancelable callback
This example builds on the previous example, adding support for canceling a long-running callback using the
cancel
argument to the@long_callback
decorator. Thecancel
argument should be set to a list ofInput
dependency objects that reference a property of a component in the app's layout. When the value of this property changes while a callback is running, the callback is canceled. Note that the value of the property is not significant, any change in value will result in the cancellation of the running job (if any).Example 4: Progress bar
This example uses the
progress
argument to the@long_callback
decorator to update a progress bar while the callback is running. Theprogress
argument should be set to anOutput
dependency grouping that references properties of components in the app's layout.When a dependency grouping is assigned to the
progress
argument of@long_callback
, the decorated function will be called with a new special argument as the first argument to the function. This special argument, namedset_progress
in the example below, is a function handle that the decorated function should call in order to provide updates to the app on its current progress. Theset_progress
function accepts a single argument, which correspond to the grouping of properties specified in theOutput
dependency grouping passed to theprogress
argument of@long_callback
.Example 5: Progress bar chart graph
The
progress
argument to the@long_callback
decorator can be used to update arbitrary component properties. This example creates and updates a plotly bar graph to display the current calculation status. This example also uses theprogress_default
argument tolong_callback
to specify a grouping of values that should be assigned to the components specified by theprogress
argument when the callback is not in progress. Ifprogress_default
is not provided, all the dependency properties specified inprogress
will be set toNone
when the callback is not running. In this case,progress_default
is set to a figure with a zero width bar.Caching results with long_callback
The
long_callback
decorator can optionally memoize callback function results through caching, and it provides a flexible API for configuring when cached results may be reused.How it works
Here is a high-level description of how caching works in
long_callback
. Conceptually, you can imagine a dictionary is associated with each decorated callback function. Each time the decorated function is called, the input arguments to the function (and potentially other information about the environment) are hashed to generate a key. Thelong_callback
decorator then checks the dictionary to see if there is already a value stored using this key. If so, the decorated function is not called, and the cached result is returned. If not, the function is called and the result is stored in the dictionary using the associated key.The built-in
functools.lru_cache
decorator uses a Pythondict
just like this. The situation is slightly more complicated with Dash for two reasons:For these reasons, a simple Python
dict
is not a suitable storage container for caching Dash callbacks. Instead,long_callback
uses the current diskcache or Celery callback manager to store cached results.Caching flexibility requirements
To support caching in a variety of development and production use cases,
long_callback
may be configured by one or more zero-argument functions, where the return values of these functions are combined with the function input arguments when generating the cache key. Several common use-cases will be described below.Enabling caching
Caching is enabled by providing one or more zero-argument functions to the
cache_by
argument oflong_callback
. These functions are called each time the status of along_callback
function is checked, and their return values are hashed as part of the cache key.Here is an example using the diskcache callback manager. In this example, the
cache_by
argument is set to alambda
function that returns a fixed UUID that is randomly generated during app initialization. The implication of thiscache_by
function is that the cache is shared across all invocations of the callback across all user sessions that are handled by a single server instance. Each time a server process is restarted, the cache is cleared an a new UUID is generated.Here you can see that it takes a few seconds to run the callback function, but the cached results are used after
n_clicks
cycles back around to 0. By interacting with the app in a separate tab, you can see that the cache results are shared across user sessions.cache_by function workflows
Various
cache_by
functions can be used to accomplish a variety of caching policies. Here are a few examples:cache_by
function could return the file modification time of a dataset to automatically invalidate the cache when an input dataset changes.cache_by
function could return the git hash of the app, making it possible to persist the cache across redeploys, but invalidate it when the app's source changes.cache_by
function could return user meta-data to prevent cached values from being shared across authenticated users.Celery configuration
Here is an example of configuring the
CeleryLongCallbackManager
for use in@long_callback
. This callback manager uses Celery as the execution backend rather than a background process with diskcache.See the Celery documentation for more information on configuring a
Celery
app instance.