Background_callback under sub page with sqlalchemy fails

Hello Sir,
I wanted to use background callback in my multipage app which uses sqlachemy as orm. And I find it always fails under package “dill”.

Traceback (most recent call last):
File “”, line 1, in
File “E:\Documents\Github\lrp - Copy\venv\lib\site-packages\multiprocess\spawn.py”, line 116, in spawn_main
exitcode = _main(fd, parent_sentinel)
File “E:\Documents\Github\lrp - Copy\venv\lib\site-packages\multiprocess\spawn.py”, line 126, in _main
self = reduction.pickle.load(from_parent)
File “E:\Documents\Github\lrp - Copy\venv\lib\site-packages\dill_dill.py”, line 287, in load
return Unpickler(file, ignore=ignore, **kwds).load()
File “E:\Documents\Github\lrp - Copy\venv\lib\site-packages\dill_dill.py”, line 442, in load
obj = StockUnpickler.load(self)
EOFError: Ran out of input

I made a small app with two pages that can reproduce this issue:

app.py

import dash
from dash import Dash, html, dcc, DiskcacheManager, CeleryManager
import diskcache

cache = diskcache.Cache("./cache")
background_callback_manager = DiskcacheManager(cache)

app = Dash(__name__, use_pages=True, background_callback_manager=background_callback_manager)

app.layout = html.Div([
    html.H1('Multi-page app with Dash Pages'),
    dcc.Location(id='url', refresh=True),
    html.Div([
        html.Div(
            dcc.Link(f"{page['name']} - {page['path']}", href=page["relative_path"])
        ) for page in dash.page_registry.values()
    ]),
    dash.page_container
])

if __name__ == '__main__':
    app.run(debug=True)

database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker

engine = create_engine('postgresql://postgres:12345@server/db', pool_size=200, max_overflow=100, pool_recycle=3600, pool_pre_ping=True)
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))

Base = declarative_base()

models.py

from sqlalchemy import Column, Integer, String
from database import Base



class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(String)
    email = Column(String)


    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

    def __repr__(self):
        return f'<User {self.name}>'

    def __str__(self):
        return self.name

pages/page1.py

import dash
from dash import html, callback, Input, Output, State, ctx

dash.register_page(__name__, path='/page1')

_name = __name__.split(".")[-1]

layout = html.Div([
    html.Div(id=f"page_name_{_name}"),
    html.H1(f'This is {_name}'),
    html.Div(id=_name)
])

@callback(
    Output(_name, "children"),
    Input("url", "pathname"),
    # prevent_initial_call=True,
)
def update_page_name(url):
    return url

pages/page2.py

import time

import dash
from dash import html, callback, Input, Output

from database import db_session
from models import User

dash.register_page(__name__, path='/page2')

_name = __name__.split(".")[-1]

layout = html.Div(
    [
        html.Div(
            [
                html.P(id="paragraph_id", children=["Button not clicked"]),
                html.Progress(id="progress_bar", value="0"),
            ]
        ),
        html.Button(id="button_id", children="Run Job!"),
        html.Button(id="cancel_button_id", children="Cancel Running Job!"),
    ]
)

@callback(
    output=Output("paragraph_id", "children"),
    inputs=Input("button_id", "n_clicks"),
    background=True,
    running=[
        (Output("button_id", "disabled"), True, False),
        (Output("cancel_button_id", "disabled"), False, True),
        (
            Output("paragraph_id", "style"),
            {"visibility": "hidden"},
            {"visibility": "visible"},
        ),
        (
            Output("progress_bar", "style"),
            {"visibility": "visible"},
            {"visibility": "hidden"},
        ),
    ],
    cancel=Input("cancel_button_id", "n_clicks"),
    progress=[Output("progress_bar", "value"), Output("progress_bar", "max")],
    prevent_initial_call=True
)
def update_progress(set_progress, n_clicks):
    total_records = db_session.query(User).all()
    total = len(total_records)
    for i in range(total + 1):
        set_progress((str(i), str(total)))

        time.sleep(0.1)

    return f"Clicked {n_clicks} times"

The issue will occur when you click “Run Job” in page 2.

Here is my environment:
OS Win11 64bit
Python 3.10.2 I also tried with 3.11, it still fails.
Database: PostgreSQL 15

packages:
ansi2html==1.9.1
blinker==1.7.0
certifi==2023.11.17
charset-normalizer==3.3.2
click==8.1.7
colorama==0.4.6
dash==2.14.2
dash-core-components==2.0.0
dash-html-components==2.0.0
dash-table==5.0.0
dill==0.3.7
diskcache==5.6.3
Flask==3.0.0
idna==3.6
importlib-metadata==7.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.3
multiprocess==0.70.15
nest-asyncio==1.5.8
packaging==23.2
plotly==5.18.0
psutil==5.9.7
requests==2.31.0
retrying==1.3.4
six==1.16.0
tenacity==8.2.3
typing_extensions==4.9.0
urllib3==2.1.0
Werkzeug==3.0.1
zipp==3.17.0

I found a similar issue: https://community.plotly.com/t/error-in-dash-multi-page-app-demos/70107/17 But I checked Github for dill it seems already a fix in 0.3.7. Now I don’t know if it’s a dash issue or my code issue.

Please help to advise.

Hello @kongyuan,

I believe this is due to your computers network path, if there are spaces it will fail.

You could try moving your folder to a location without spaces and see if it works properly. :grin:

Thank you @jinnyzor for this important info. But I renamed the path and it still fails.

E:\Documents\Github\dash_test\venv\Scripts\python.exe E:\Documents\Github\dash_test\app.py 
Dash is running on http://127.0.0.1:8050/

 * Serving Flask app 'app'
 * Debug mode: on
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "E:\Documents\Github\dash_test\venv\lib\site-packages\multiprocess\spawn.py", line 116, in spawn_main
    exitcode = _main(fd, parent_sentinel)
  File "E:\Documents\Github\dash_test\venv\lib\site-packages\multiprocess\spawn.py", line 126, in _main
    self = reduction.pickle.load(from_parent)
  File "E:\Documents\Github\dash_test\venv\lib\site-packages\dill\_dill.py", line 287, in load
    return Unpickler(file, ignore=ignore, **kwds).load()
  File "E:\Documents\Github\dash_test\venv\lib\site-packages\dill\_dill.py", line 442, in load
    obj = StockUnpickler.load(self)
EOFError: Ran out of input

Ok, now move the callback into the main app file, instead of the pages file.

Hi @jinnyzor ,it works after I move the callback into app.py. Is there any workaround to keep the callback in page2.py instead of app.py?

app.py

import dash
from dash import Dash, html, dcc, DiskcacheManager, CeleryManager, Input, Output, callback
import diskcache
from database import db_session
from models import User
import time

cache = diskcache.Cache("./cache")
background_callback_manager = DiskcacheManager(cache)

app = Dash(__name__, use_pages=True, background_callback_manager=background_callback_manager)

app.layout = html.Div([
    html.H1('Multi-page app with Dash Pages'),
    dcc.Location(id='url', refresh=True),
    html.Div([
        html.Div(
            dcc.Link(f"{page['name']} - {page['path']}", href=page["relative_path"])
        ) for page in dash.page_registry.values()
    ]),
    dash.page_container
])

@callback(
    output=Output("paragraph_id", "children"),
    inputs=Input("button_id", "n_clicks"),
    background=True,
    running=[
        (Output("button_id", "disabled"), True, False),
        (Output("cancel_button_id", "disabled"), False, True),
        (
            Output("paragraph_id", "style"),
            {"visibility": "hidden"},
            {"visibility": "visible"},
        ),
        (
            Output("progress_bar", "style"),
            {"visibility": "visible"},
            {"visibility": "hidden"},
        ),
    ],
    cancel=Input("cancel_button_id", "n_clicks"),
    progress=[Output("progress_bar", "value"), Output("progress_bar", "max")],
    prevent_initial_call=True
)
def update_progress(set_progress, n_clicks):
    total_records = db_session.query(User).all()
    total = len(total_records)
    for i in range(total + 1):
        set_progress((str(i), str(total)))

        time.sleep(0.1)

    return f"Clicked {n_clicks} times"

if __name__ == '__main__':
    app.run(debug=True)

page2.py

import dash
from dash import html, callback, Input, Output

dash.register_page(__name__, path='/page2')

_name = __name__.split(".")[-1]

layout = html.Div(
    [
        html.Div(
            [
                html.P(id="paragraph_id", children=["Button not clicked"]),
                html.Progress(id="progress_bar", value="0"),
            ]
        ),
        html.Button(id="button_id", children="Run Job!"),
        html.Button(id="cancel_button_id", children="Cancel Running Job!"),
    ]
)

I think it is an issue with the underlying pickling of the data that is a known issue but as of yet doesn’t have a workaround as it is not a plotly library.

I’m not sure if you can put the callback into a file and then import the callback from it?

Oh it works!

app.py

import dash
from dash import Dash, html, dcc, DiskcacheManager
import diskcache


cache = diskcache.Cache("./cache")
background_callback_manager = DiskcacheManager(cache)

app = Dash(__name__, use_pages=True, background_callback_manager=background_callback_manager)

app.layout = html.Div([
    html.H1('Multi-page app with Dash Pages'),
    dcc.Location(id='url', refresh=True),
    html.Div([
        html.Div(
            dcc.Link(f"{page['name']} - {page['path']}", href=page["relative_path"])
        ) for page in dash.page_registry.values()
    ]),
    dash.page_container
])

if __name__ == '__main__':
    app.run(debug=True)

additinal_callbacks.py

import time
from dash import html, callback, Input, Output

from database import db_session
from models import User


@callback(
    output=Output("paragraph_id", "children"),
    inputs=Input("button_id", "n_clicks"),
    background=True,
    running=[
        (Output("button_id", "disabled"), True, False),
        (Output("cancel_button_id", "disabled"), False, True),
        (
            Output("paragraph_id", "style"),
            {"visibility": "hidden"},
            {"visibility": "visible"},
        ),
        (
            Output("progress_bar", "style"),
            {"visibility": "visible"},
            {"visibility": "hidden"},
        ),
    ],
    cancel=Input("cancel_button_id", "n_clicks"),
    progress=[Output("progress_bar", "value"), Output("progress_bar", "max")],
    prevent_initial_call=True
)
def update_progress(set_progress, n_clicks):
    total_records = db_session.query(User).all()
    total = len(total_records)
    for i in range(total + 1):
        set_progress((str(i), str(total)))

        time.sleep(0.1)

    return f"Clicked {n_clicks} times"

pages/page2.py

import dash
from dash import html

dash.register_page(__name__, path='/page2')

_name = __name__.split(".")[-1]

layout = html.Div(
    [
        html.Div(
            [
                html.P(id="paragraph_id", children=["Button not clicked"]),
                html.Progress(id="progress_bar", value="0"),
            ]
        ),
        html.Button(id="button_id", children="Run Job!"),
        html.Button(id="cancel_button_id", children="Cancel Running Job!"),
    ]
)
from additional_callbacks import update_progress

Thanks a lot @jinnyzor

3 Likes