GibbsConsulting / django-plotly-dash

Expose plotly dash apps as django tags
MIT License
538 stars 121 forks source link

allow_duplicate on outputs doesn't work #475

Closed zyassine closed 8 months ago

zyassine commented 9 months ago

I have a dash app where I update the Output in many Dash Callbacks. In order to do it, I have to use allow_duplicate=True in the Output argument, but it doesn't work on a django dash app.

This is a basic Dash app to showcase the use of allow_duplicate:

from dash import Dash, Input, Output, html, dcc

app = Dash(__name__)

app.layout = html.Div([
    dcc.Input(id='input_text_1', type="text", value=''),
    dcc.Input(id='input_text_2', type="text", value=''),
    html.Div(id="output")
])

@app.callback(
    Output(component_id='output', component_property='children', allow_duplicate=True),
    Input(component_id='input_text_1', component_property='value'),
    prevent_initial_call=True,
)
def update_1(value):
    return f"Input: {value}"

@app.callback(
    Output(component_id='output', component_property='children', allow_duplicate=True),
    Input(component_id='input_text_2', component_property='value'),
    prevent_initial_call=True,
)
def update_2(value):
    return f"Input: {value}"

app.run(debug=False)

If I use app = DjangoDash(__name__), then the output doesn't get updated

delsim commented 9 months ago

This is essentially the statement that django-plotly-dash does not support duplicate callbacks.

Are you in a position to make your callback structure less ill-defined and consolidate the calculations so that only one callback is used for an output value? If the multiple callbacks are non-trivial to combine you could insert a dummy hidden output for each callback (add in also a timestamp or similar if you need to work out which one to use) and then add a third callback to determine which is to be used.

Given that multiple callbacks implicitly say 'use whatever arrived the most recently, dont care what that is' then at worst this introduces the overhead of a third callback (which one could make run locally in the browser if an issue) but also permits the question of which value to be used to be directly addressed.

zachsiegel-capsida commented 8 months ago

I would also like to use allow_duplicate in a django-plotly-dash app!

For what it's worth, the race condition described by @delsim does not necessarily arise if prevent_initial_call=True is used in all (or even all but one) of the callbacks involved and some pathological patterns are avoided. The Plotly team created this argument/feature in Dash 2.9 because it can absolutely work.

@delsim any chance this argument will be supported in django-plotly-dash? Any comment on why it doesn't work already (to the extent to which DPD is a wrapper to Dash, I don't see exactly where it should fail).

Thank you so much for any response on this!

zachsiegel-capsida commented 8 months ago

I would also like to use allow_duplicate in a django-plotly-dash app!

For what it's worth, the race condition described by @delsim does not necessarily arise if prevent_initial_call=True is used in all (or even all but one) of the callbacks involved and some pathological patterns are avoided. The Plotly team created this argument/feature in Dash 2.9 because it can absolutely work.

@delsim any chance this argument will be supported in django-plotly-dash? Any comment on why it doesn't work already (to the extent to which DPD is a wrapper to Dash, I don't see exactly where it should fail).

Thank you so much for any response on this!

Ah @delsim sorry for my confusion, I found that allow_duplicate does already work. prevent_initial_call has to be used on all callbacks that use this type of Output, as documented by Plotly.

@zyassine perhaps that answers your question?

zachsiegel-capsida commented 8 months ago

Whoops - @delsim I notice allow_duplicate works only in the sense that the Dash app starts, but the Output functionality doesn't actually send anything to the component properties with the allow_duplicate argument (even if they in fact only have one Output)!

@zyassine perhaps this is what you referred to originally.

ryan-bloom commented 7 months ago

@delsim I am noticing the same thing that @zyassine and @zachsiegel-capsida are seeing and would love the allow_duplicate functionality to work for my django-plotly-dash app. As @zachsiegel-capsida said, you can add the allow_duplicate argument to a callback and the application starts up as expected but the expected update is never seen when the callback runs. (e.g. if the callback fires on a button click and changes the children of another element, the button click doesn't raise any error, but the output element's children component is not changed at all)

Any more updates on this functionality? Personally would be using it because I have a bunch of input elements that correspond to local session storage items and would want a change to any of the inputs to update the session storage that it corresponds to. Therefore, I want multiple callbacks pointing to update the same session storage object.

delsim commented 6 months ago

@zachsiegel-capsida I'm struggling to see how prevent_initial_callbacks can stop a race condition. If I have two callbacks that both update some target value, and some interaction in the UI causes first one and then shortly after the second to be called, there is no way of controlling which one will return first.

Whilst this issue might not manifest on a single-threaded synchronous server such as the test ones in Django, Flask etc., as these will only process the callbacks in order, it will nevertheless become quite apparent as soon as one moves to a production environment and uses multiple threads or processes or an async server; there is no guarantee of the order in which the callbacks will return their value to the server. There is a note to this effect in the dash documentation.

@ryan-bloom what stops you having each callback update a separate value and then combine all of these values into the session object with an additional callback? Or, if the purpose of the session object is to maintain some server-side composite record, make updating it a side-effect of a callback rather than its return value.

zachsiegel-capsida commented 6 months ago

@zachsiegel-capsida I'm struggling to see how prevent_initial_callbacks can stop a race condition. If I have two callbacks that both update some target value, and some interaction in the UI causes first one and then shortly after the second to be called, there is no way of controlling which one will return first.

Whilst this issue might not manifest on a single-threaded synchronous server such as the test ones in Django, Flask etc., as these will only process the callbacks in order, it will nevertheless become quite apparent as soon as one moves to a production environment and uses multiple threads or processes or an async server; there is no guarantee of the order in which the callbacks will return their value to the server. There is a note to this effect in the dash documentation.

@delsim I totally see your point! Thanks so much for this response. I do stand by my comment below, though:

For what it's worth, the race condition described by @delsim does not necessarily arise if prevent_initial_call=True is used in all (or even all but one) of the callbacks involved and some pathological patterns are avoided. The Plotly team created this argument/feature in Dash 2.9 because it can absolutely work.

You're taking issue with my claim that only "pathological patterns" cause race conditions and I see your point, but it is possible to avoid them to an acceptable level in practice and that's why Dash supports allow_duplicate! One way is disabling interactive UI elements and showing spinners via clientside callbacks to discourage or prevent multiple triggers. Furthermore, the previously-required Dash pattern of setting up dummy hidden output elements that all write to the real output element via a single callback ALSO suffers from race conditions for the same reason.

Enforcing this pattern in django-plotly-dash when Dash no longer enforces it is probably not what most people expect from this (excellent!) package. I think most people want/expect a wrapper that lets them use Dash from Django, and the more the django-plotly-dash interface deviates from Dash the more difficult that will be (and the more documentation dpd requires to clarify those differences).

As always thank you for maintaining this package and for responding thoughtfully to GitHub issues!