Skip to content
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

Sneaky Race condition using asyncio create_task (reliably reproduced) #6603

Closed
tomascsantos opened this issue Mar 28, 2024 · 5 comments · Fixed by #6667 or #7128
Closed

Sneaky Race condition using asyncio create_task (reliably reproduced) #6603

tomascsantos opened this issue Mar 28, 2024 · 5 comments · Fixed by #6667 or #7128
Assignees
Milestone

Comments

@tomascsantos
Copy link

Context

Hey team, I'm back with I think perhaps the same bug as: #6019
I've managed to significantly simplify the minimum reproducible code, and I suspect that the last issue was really caused by this bug as my code was not fixed in the patch that was released to address the previous ticket. (Thank you for trying!)

I've been stuck on panel==1.3.4 because I think this race condition becomes worse starting from 1.3.5 onwards. I saw the awesome changes in 1.4.0 and would love to jump to the latest version.

ALL software version info

panel==1.4.0 (error is much less likely but still possible to reproduce with 1.3.4)
bokeh==3.4.0
Python==3.11.5
param==2.1.0

Description of expected behavior and the observed behavior

The final visible "state" should be "SUCCESS", instead it gets stuck in "LOADING". In the code below, I add some comments indicating how to increase or decrease the likelihood of the race condition appearing. It's tuned right now to fail every time.

Complete, minimal, self-contained example code that reproduces the issue

"""A minimum reproducible example of a timing bug"""
import asyncio
import panel as pn
import param
from structlog import get_logger
log = get_logger(__name__)

TERMINAL_STATES = ['ERROR', 'SUCCESS', 'WARNING']

class StateMachine(param.Parameterized):
    """A simple state machine for fetching data"""
    state = param.String(default='SUCCESS')

    async def fetch_data_simple(self):
        """Would normally return a dataframe, not included for MVP"""
        self.state = 'STARTING'
        await asyncio.sleep(.01)  # the smaller this number the more likely, increasing to 1 fixes.
        self.state = 'SUCCESS'


class QueryViewer(pn.viewable.Viewer):
    """A viewer which transitions views based on the state machine"""

    box: StateMachine = param.ClassSelector(class_=StateMachine)

    def __init__(self, box, **params) -> None:
        super().__init__(box=box, **params)

        self.container = pn.Column()
        self.loading = pn.Column('LOADING', styles={'background-color': 'aqua'})
        self.green = pn.Column('SUCCESS', styles={'background-color': 'green'})

        self._update_state()
        self.box.param.watch(self._update_state, 'state')

    def _update_state(self, event=None):
        """Update the viewer state based on the state machine"""
        log.info('_update_state', state=self.box.state)
        if self.box.state == 'SUCCESS':
            self.container.objects = [self.green]
        elif self.box.state not in TERMINAL_STATES:
            self.container.objects = [self.loading]

    def __panel__(self):
        return self.container

class App():
    """Tie the components together"""

    def __init__(self, **params):
        super().__init__(**params)
        self.tasks = set()
        self.queries = [StateMachine() for _ in range(3)]  # the bigger this number the more likely
        self.viewers = [QueryViewer(m) for m in self.queries]
        self.template = pn.Column(*self.viewers)

        pn.state.onload(self.onload)

    async def onload(self, *args):
        self.fetch_queries()

    def fetch_queries(self):
        for q in self.queries:
            task = asyncio.create_task(q.fetch_data_simple())
            self.tasks.add(task)
            task.add_done_callback(self.tasks.discard)


App().template.servable()

Screenshots or screencasts of the bug in action

(Not captured in the gif, it does flash to "SUCCESS" briefly
found_bug

@philippjfr philippjfr added this to the v1.4.1 milestone Mar 29, 2024
@philippjfr
Copy link
Member

Super helpful, thanks @tomascsantos!

@philippjfr
Copy link
Member

Sadly my fix caused a bunch of other issues. Will have to tackle this again after the 1.4.1 release.

@tomascsantos
Copy link
Author

I'm so excited to see this fixed! Thanks a ton @philippjfr!

I just came across something that feels super similar and I'm wondering if your fix applies to this as well:

# my_notebook.ipynb
from time import sleep
c = pn.Column('hi')
display(c)
c.objects = ['hello']

# Show loading message
p1 = figure()
p1.text(x=[0], y=[0], text=['loading'])
c.objects = [pn.pane.Bokeh(p1)]
sleep(1)

# show final figure
p2 = figure()
p2.text(x=[0], y=[0], text=['final figure'])
c.objects = [pn.pane.Bokeh(p2)]

This shows loading but I would expect it to show "final figure"

image

@philippjfr
Copy link
Member

I'm so excited to see this fixed!

Me too, I tried so many times and somehow I hit on the right approach this time.

I think that's probably a different issue. Can you open a new issue if you get a chance?

@tomascsantos
Copy link
Author

Sure--#7145

Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment