Panel server for multiple users

Hi everyone! I am deploying an application using a panel and encountered the problem described here Unable to to serve datas. That is, multiple users cannot independently use the application.
But the proposed solution is for Boquet’s server. I would like to know how to implement the same thing only for the Panel.
My code is very simple and looks like this:

def callback():
      return some_hv_object

dmap = hv.DynamicMap(callback)
rastered = rasterize(dmap)
pn.serve(rastered, port=5000, show=False)

you need to put your app in a function, and not like a global object. The server will call the function get_page_user each time a new user requests the app page.

import panel as pn 
import holoviews as hv

def get page_user():
    def callback():
        return some_hv_object

    dmap = hv.DynamicMap(callback)
    rastered = rasterize(dmap)

    return rastered

pn.serve(get_page_user, port=5000, show=False)
2 Likes

Thank you very much! Unfortunately, I could not understand from the documentation what exactly to do this way.

Probably it is not declared in the documentation, if you can guide us where to put this it would be great. I agree with you that good examples of multi-user applications are missing, or at least I don’t know where to find them either.

Hello again. I came back with the same problem. I did as you indicated and now, indeed, different user_page is generated for different users. The problem is that while one user_page is being processed, the server seems to freeze for the rest of the users. How can this problem be solved?

def my_layer(df_candle, df_tick):
    @pn.depends(pn.state.param.busy)
    def indicator(busy):
        return pnw.Progress(sizing_mode='scale_width') if busy else '### Done!'

    strategy = pnw.Select(name='Strategy', value='ZZ',
                          options=['ZZ', 'Shiftma'])
    start_year = pnw.Select(name='Start Year', value=2021,
                            options=[2019, 2020, 2021])
    start_day = pnw.Select(name='Start Day', value=1,
                           options=list(range(1, 32)))
    start_month = pnw.Select(name='Start Month', value=1,
                             options=list(range(1, 13)))
    end_year = pnw.Select(name='End Year', value=2022,
                          options=[2019, 2020, 2021, 2022])
    end_day = pnw.Select(name='End Day', value=1,
                         options=list(range(1, 32)))
    end_month = pnw.Select(name='End Month', value=1,
                           options=list(range(1, 13)))

    width = pnw.IntSlider(name='Plot width', value=1200, start=500, end=1500, step=50)
    height = pnw.IntSlider(name='Plot height', value=600, start=300, end=1500, step=50)
    layer = pn.bind(partial(get_layer, df_candle, df_tick), strategy=strategy, width=width, height=height,
                    start_day=start_day, start_month=start_month, start_year=start_year,
                    end_day=end_day, end_month=end_month, end_year=end_year, )
    widgets = pn.Column("<br>\n# Plot Settings", strategy, width, height, start_year, start_month, start_day,
                        end_year, end_month, end_day, indicator)
    layers = pn.Row(widgets, layer)
    return layers

def foo():
    return my_layer(df_candle, df_tick)

def run_server(func, port=8006, allow_websocket_origin=None, show=False, **kwargs):
    pn.serve(func, port=port, allow_websocket_origin=allow_websocket_origin, show=show, title='Strategy backtest',
             **kwargs)
    from tornado.ioloop import IOLoop
    loop = IOLoop.current()
    loop.start()

One option is give to the user a lighweight page and actualize the page from a thread.

import panel as pn 
import holoviews as hv
import threading, time

def thread_function(col):
    time.sleep(5) # simulate the processing for 5 seconds 
    col[0] = pn.pane.Str('This is a raw string which will not be formatted in any way except for the applied style.', style={'font-size': '12pt'})

def get_page_user():

    loading = pn.indicators.LoadingSpinner(value=True, width=100, height=100)

    col = pn.Column(loading)

    t = threading.Thread(target=thread_function, args=(col,))
    t.daemon = True
    t.start() 

    return col

pn.serve(get_page_user, port=5000, show=False)

I hope this helps, I could not watch your code due to lot of end of the year deadlines-

1 Like

Thanks a lot. Apparently it works. But could you please elaborate on the solution? Is there really no similar functionality in tornado or bokeh?
For example, why are you creating a daemon thread?

I am not sure there is similar functionality in bokeh. I am not an expert, but bokeh and tornado are single thread with async capacilities, so you can not have sync blocking functions. The only way to have blocking functions is using threads. You can check this page

or this PR

Thanks a lot. I was reading the bokeh server documentation. It would be nice if the panel developers would comment on something. Do you know how the num_procs parameter in panel.serve () works? As far as I understand, this allows you to specify the number of processes. And it kind of solves my problem too. But how does this differ from your method with threads?

When you start the server you can also specify a number of processes to start.

For example like pn.serve(APP_ROUTES, port=port, dev=False, address=address, num_procs=4).

My understanding is that it will start 4 independent servers and users will be distributed to these. This will minimize the risk of users “blocking” the server at the same time.

I have not tried but my understanding would be that pn.state.cache for example is not shared across these 4 processes.

I also understand that. But how does this globally differ from the method proposed above using threads? Which is preferable?

1 Like

Hi @padu

My advice would be to start out with --num-procs. Its the preferable solution because it will keep your code simpler.

You would only move into threads or async if you need the session of one user to process things in parallel or if you cannot start more processes but still need to support more users.

The next version of Panel will enable you to specify a number of threads similar to a number of processes I believe.

Hi @Marc and thanks a lot for the answer.

@nghenzi2019

Here’s a simple vanilla interactive graph example. How to implement the threading approach as described above?

def user_page():
    def sine_curve(phase, freq):
        if phase==0.5:
            time.sleep(.1)
        else:
            time.sleep(5)
        xvals = [0.1 * i for i in range(100)]

        return hv.Curve((xvals, [np.sin(phase + freq * x) for x in xvals]))

    dmap = hv.DynamicMap(sine_curve, kdims=['phase', 'frequency']).redim.range(phase=(0.5, 1), frequency=(0.5, 1.25))
    return dmap



if __name__=='__main__':
    pn.serve(user_page, port=5000)

I do not use holoviews much, only panel. I would do something like this, it works, but the response time to the interaction with the sliders is really bad. I think there was some problem when the dynamic maps were embedded in columns or rows.

import panel as pn, numpy as np  
import holoviews as hv
import threading, time


def sine_curve(phase, freq):
    if phase==0.5:
        time.sleep(.1)
    else:
        time.sleep(5)
    xvals = [0.1 * i for i in range(100)]

    return hv.Curve((xvals, [np.sin(phase + freq * x) for x in xvals]))



def thread_function(col):
    time.sleep(15) # simulate the processing for 5 seconds 
    dmap = hv.DynamicMap(sine_curve, 
            kdims=['phase', 'frequency']).redim.range(phase=(0.5, 1), 
                            frequency=(0.5, 1.25))
    col[0] = dmap

def get_page_user():

    loading = pn.indicators.LoadingSpinner(value=True, width=100, height=100)

    col = pn.Column(loading)

    t = threading.Thread(target=thread_function, args=(col,))
    t.daemon = True
    t.start() 
    return col

pn.serve(get_page_user, port=5000, show=False)

If someone else confirms the slow response time, maybe it would be good to fill an issue.

ps> in windows num-procs does not work, you only can use one process, then the only was is using threads-

1 Like

Unfortunately, this does not solve the problem of server freezing that I mentioned above. I added a time.sleep() to the sine_curve () function if the phase value is different from .5 to simulate the time delay that can occur in complex computations. The video clearly shows that for the first user, the server hangs when the second user blocks the sine_curve function

Hi @padu

The video is broken for me. Stops after 8 seconds. Could you upload a new version?

Hi @Marc. Try again pls.

The problem is still there. If I download the video it looks like the below after 8 secs.

@Marc , Ok. i made a gif file, should work anyway
panel_server

1 Like

ok, it is clear the dynamic map is scheduled in the main thread. Sorry for that, and not having a better answer.
Maybe you can check this thread, there the update is done with a thread and a button, but for sure it can be done with a slider too. I do not know other answer, let’s wait someone provide with a better answer. Initiate a thread to each change of the sliders does not seem good idea.