Panel server for multiple users

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.

@nghenzi

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.

@nghenzi , @Marc Thank you so much for your help and support
Excellent. I see that there are a lot of discussions on the topic of multiprocessing, multithreading and asynchrony in panel. I’m a bit new to this. Therefore, it is difficult for me to understand which direction to go. But I would really like to understand how it works and how to do it correctly. Plus - it will help the panel, holoviews and bokeh community as there is clearly not enough documentation. So I will formulate my task clearly and I think that data researchers who want to use panel in their work will sooner or later have this task:
I want to run a panel server to display some dynamic graphs based on holoviews and bokeh. at the same time, the user can change some parameters (sliders, for example) and the graph should be rebuilt. Rebuilding a graph can be a computationally expensive task (I think it’s important when there is a choice between multithreading and parallelism in python). Moreover, I would like the application to be multi-user. That is, when one user changes something in the schedule, it should not affect the work with the schedule of another user in any way.
The task is understandable in principle, because most often servers are deployed for this. So, let’s try to figure out how to do this, maybe update the documentation and help users.

A simple example with the sine function that was described above is enough. Instead of the sine function, there can be any holoviews object. So, how to make sure that this function does not block the server for a large number of users? Where should I start?

1 Like

HI again,

after thinking a little about the user interface, if you have a graph which has to be updated with a expensive task, you can only trigger this task once. Then the sliders, or any widget which changes often can not trigger this task. Then the example with the button it should do the task. The app should be inside a function.

import panel as pn, numpy as np
import holoviews as hv, pandas as pd
import asyncio
import time

from threading import Thread # Asyncio
from functools import partial
from holoviews.streams import Pipe, Buffer

pn.extension()
pn.config.sizing_mode = 'stretch_width'
hv.extension('bokeh')

# Example df for buffer
df = pd.DataFrame({"x":np.array([]),
                   "y":np.array([]),
                   "z":np.array([])}).set_index("x")

# Buffer that updates plot
buffer = Buffer(data=df)

# Some example scatter plot
dmap = hv.DynamicMap(hv.Scatter, 
                     streams=[buffer]).opts(bgcolor='black', 
                                            color='z', 
                                            show_legend=False, 
                                            width = 1200, height = 800,responsive=False
                                           )


@asyncio.coroutine
def update(x,y,z):
    buffer.send(pd.DataFrame({"x":x,"y":y,"z":z}).set_index("x"))
 

def blocking_task(doc):
     time.sleep(5)
     x = np.random.rand(1)
     y = np.random.rand(1)
     z = np.random.rand(1)
        # update the document from callback
     if doc:
          doc.add_next_tick_callback(partial(update,x=x,y=y,z=z))
     
    
def button_click(event):
    thread = Thread(target=partial(blocking_task, doc=pn.state.curdoc))
    thread.start()
    
    
btn = pn.widgets.Button(name='Run')    
btn.on_click(button_click)
    
    
p1 = pn.Column(btn, 
               pn.Row(dmap,  width = 1200, height = 800, sizing_mode="fixed")
              )    
p1.show('streaming hv')