plotly / dash

Data Apps & Dashboards for Python. No JavaScript Required.
https://plotly.com/dash
MIT License
21.15k stars 2.04k forks source link

Flask contexts not available inside background callback #2235

Open ArtsiomAntropau opened 1 year ago

ArtsiomAntropau commented 1 year ago

Describe your context

dash                              2.6.1
dash-core-components              2.0.0
dash-html-components              2.0.0
dash-table                        5.0.0

Describe the bug

Background callbacks don't have flask's app and request contexts inside.

Expected behavior

Background callbacks have flask's app and request contexts inside.

So, after creating callback function and before providing them to the Celery, we should provide flask contexts inside this function to imitate the default callback behaviour

with flask_app.app_context():
    return (copy_current_request_context(job_fn) if has_request_context() else job_fn)(*args, **kwargs)

Any recommendations for now?

T4rk1n commented 1 year ago

For celery the copy_current_request_context method doesn't work. An alternative we could do is provide the values like headers, path, remote, etc. inside the callback_context instead.

ArtsiomAntropau commented 1 year ago

@T4rk1n can you highlight why it doesn't work? In common, this function simply copies the current request context and provided it inside the function, like dash already doing with dash callback context.

ArtsiomAntropau commented 1 year ago

The main problem and thing that we need to remember – we should not try to push it when we're packing the function for celery, it should be passed to the prepared function as arguments (I mean wrapper that will call user's callback function).

T4rk1n commented 1 year ago

The issue is that wrapping the function in the callback where it should be, doesn't return a celery task object to call (Missing func.delay). The callback_context I refactored to use a context_var instead of the flask.g object for that matter.

ArtsiomAntropau commented 1 year ago

@T4rk1n so what do you think about it? (except of temp inability to serialize contextvars)

def flask_patcher():
    with flask.current_app.app_context():
        @flask.copy_current_request_context
        def _(fn, *args, **kwargs):
            return fn(*args, **kwargs)

    return _
    def call_job_fn(self, key, job_fn, args, context):
        task = job_fn.delay(key, self._make_progress_key(key), args, flask_patcher())
        return task.task_id
    @celery_app.task(name=f"long_callback_{fn_hash}")
    def job_fn(result_key, progress_key, user_callback_args, context=None, patcher=None):
                if isinstance(user_callback_args, dict):
                    user_callback_output = patcher(fn, *maybe_progress, **user_callback_args)
                elif isinstance(user_callback_args, (list, tuple)):
                    user_callback_output = patcher(fn, *maybe_progress, *user_callback_args)
                else:
                    user_callback_output = patcher(fn, *maybe_progress, user_callback_args)
T4rk1n commented 1 year ago

@ArtsiomAntropau If you can prove that works with a test, (can add path=flask.request.path to tests/integration/long_callback/app_callback_ctx.py line 34 then test in test_lcbc012_long_callback_ctx) I'll gladly take a look in a PR.

alexcjohnson commented 10 months ago

From @JamesKunstle in #2636:

Update: For anyone looking for a solution, you can pass the 'request=flask.request' object into background callbacks and that seems to work.

https://community.plotly.com/t/how-to-read-cookies-inside-a-background-callback/70224/11

kovalev-vladimir-da commented 8 months ago

Hi @alexcjohnson ! It appears for me (and not only) that this method is not working cus i gor "Working outside of request context"

So, this solution request=flask.request is working when DiskCache is on, but not for Celery.