Problems deploying a multi-user panel dashboard app with multi-threading

Hi everyone, I’m trying to deploy a Panel dashboard application with multiuser capabilities. At the moment, as far as I have been able to investigate, I need to use asynchronous operations or threads to avoid users blocking each other, in my case these blockings occur in two moments:

  1. When the user enters the application, gets the data and serves it in graphs in a material design template.
  2. When a user needs to choose a date in a widget and the whole dashboard is updated with the data obtained from the new date.

So now I find myself trying to solve the first point. Having seen the example code in the thread linked here, I find that this code works correctly using pn.Columns:

import pandas as pd
import panel as pn
import holoviews as hv
import threading
import time

def holoviz_config(plot_engine, pandas_plot_backend, loading_type, loading_color, sizing_mode):
    hv.extension(plot_engine)
    pd.options.plotting.backend = pandas_plot_backend
    pn.extension(loading_spinner=loading_type, loading_color=loading_color, sizing_mode=sizing_mode)
    
def empty_plot_with_text(str):
    # Creates an empty plot with a text inside
    empty_plot = hv.Curve([])

    empty_plot = empty_plot * \
        hv.Text(0.6, 0.6, str)

    return empty_plot

def create_empty_plot_panel(str):
    # Creates an empty plot in a panel
    empty_plot = empty_plot_with_text(str)
    panel = pn.panel(empty_plot, sizing_mode='stretch_width', linked_axes=False)
    
    return panel

def create_dashboard(col: pn.Column): 
    
    print('Creating dashboard')
    
    panel_A = create_empty_plot_panel('A')
    
    # We are going to simulate a 5 seconds task here before plots are finally appended to template
    time.sleep(5)
    
    # Creates a grid and append the plot to it
    grid = pn.GridSpec(sizing_mode='stretch_both', ncols=3, nrows=2, mode='override')
    grid[0, :1] = panel_A
    
    # Append grid to Panel.Column object
    col[0] = grid
    
def get_user_page():
    loading = pn.indicators.LoadingSpinner(value=True, width=100, height=100, align='center')
    col = pn.Column(loading)
    
    t = threading.Thread(target=create_dashboard, args=(col, ))
    t.daemon = True
    t.start() 

    return col
     
def main():
    holoviz_config(plot_engine='bokeh', pandas_plot_backend='holoviews', loading_type='dots', loading_color='#00204e', sizing_mode="stretch_width")
    pn.serve(get_user_page, port=5006, allow_websocket_origin='localhost:5006', websocket_origin='localhost:5006', show=False, threaded=True, n_threads=8)
 
    
if __name__ == '__main__':
    main()

However, I find it difficult to adapt it to the template object I’m using in my app. Since the material design template object it is not a listable object such as pn.Col, I think the template object is returning a copy and I can’t modify the template structure from another thread in order to see the content in the website and it is always showing the loading indicator.

I have recreated my problem in the following code fragment.

import pandas as pd
import hvplot.pandas
import panel as pn
import holoviews as hv
import threading
import time

def holoviz_config(plot_engine, pandas_plot_backend, loading_type, loading_color, sizing_mode):
    hv.extension(plot_engine)
    pd.options.plotting.backend = pandas_plot_backend
    pn.extension(loading_spinner=loading_type, loading_color=loading_color, sizing_mode=sizing_mode)
    
def empty_plot_with_text(str):
    # Creates an empty plot with a text inside
    empty_plot = hv.Curve([])

    empty_plot = empty_plot * \
        hv.Text(0.6, 0.6, str)

    return empty_plot

def create_empty_plot_panel(str):
    # Creates an empty plot and appends it in a panel
    empty_plot = empty_plot_with_text(str)
    panel = pn.panel(empty_plot, sizing_mode='stretch_width', linked_axes=False)
    
    return panel

def create_dashboard(template: pn.template.MaterialTemplate): 
    
    print('Creating dashboard')
    
    panel_A = create_empty_plot_panel('A')
    
    # We are going to simulate a 5 seconds task here before plots are finally appended to template
    time.sleep(5)
    
    # Creates a grid and append plots to it
    grid = pn.GridSpec(sizing_mode='stretch_both', ncols=1, nrows=1, mode='override')
    grid[0, :1] = panel_A
    
    # Append grid to template
    template.main.append(grid)

    
def get_user_page():
    loading = pn.indicators.LoadingSpinner(value=True, width=100, height=100, align='center')
    
    material_dashboard = pn.template.MaterialTemplate(title='Threading Test APP (Template)', header_background='#00204e', favicon='/images/favicon.ico')
    material_dashboard.main.append(loading)
    
    
    t = threading.Thread(target=create_dashboard, args=(material_dashboard, ))
    t.daemon = True
    t.start() 

    return material_dashboard
    
    
def main():
    holoviz_config(plot_engine='bokeh', pandas_plot_backend='holoviews', loading_type='dots', loading_color='#00204e', sizing_mode="stretch_width")
    pn.serve(get_user_page, port=5006, allow_websocket_origin='localhost:5006', websocket_origin='localhost:5006', show=False, threaded=True, n_threads=8)
    
if __name__ == '__main__':
    main()

You can run it from the command line just using:

python app.py

Any suggestion or strategy on how to modify material design template structure from another thread and see the contents updated in the object returned in pn.serve?

Thank you for your time, any idea or suggestion will be welcome.

I think the same trick attaching a column to the main section of the template does the work. I used a servable object to iterate fast with the changes “panel serve --show app.py --autoreload”

import pandas as pd
import hvplot.pandas
import panel as pn
import holoviews as hv
import threading
import time

def holoviz_config(plot_engine, pandas_plot_backend, loading_type, loading_color, sizing_mode):
    hv.extension(plot_engine)
    pd.options.plotting.backend = pandas_plot_backend
    pn.extension(loading_spinner=loading_type, loading_color=loading_color, sizing_mode=sizing_mode)
    
def empty_plot_with_text(str):
    # Creates an empty plot with a text inside
    empty_plot = hv.Curve([])

    empty_plot = empty_plot * \
        hv.Text(0.6, 0.6, str)

    return empty_plot

def create_empty_plot_panel(str):
    # Creates an empty plot and appends it in a panel
    empty_plot = empty_plot_with_text(str)
    panel = pn.panel(empty_plot, sizing_mode='stretch_width', linked_axes=False)
    
    return panel

def create_dashboard(template: pn.template.MaterialTemplate): 
    
    print('Creating dashboard')
    
    panel_A = create_empty_plot_panel('A')
    
    # We are going to simulate a 5 seconds task here before plots are finally appended to template
    time.sleep(0.2)
    

    # Creates a grid and append plots to it
    grid = pn.GridSpec(sizing_mode='stretch_both', ncols=1, nrows=1, mode='override')
    grid[0, :1] = panel_A

    print ('updating',  template.main.objects)
    # Append grid to template
    template.main.objects[0].sizing_mode = 'stretch_both' 
    template.main.objects[0][0] = grid
    template.main.objects[0][0]
    # template.main.objects[0].param.param_values
    print ('updating',  template.main.objects,    template.main.objects[0].param.__dict__) 

    
def get_user_page():
    loading = pn.indicators.LoadingSpinner(value=True, width=100, height=100, align='center')
    
    material_dashboard = pn.template.MaterialTemplate(title='Threading Test APP (Template)', header_background='#00204e', favicon='/images/favicon.ico')
    material_dashboard.main.append(pn.Column(loading))
    
    
    t = threading.Thread(target=create_dashboard, args=(material_dashboard, ))
    t.daemon = True
    t.start() 

    return material_dashboard
    
    


get_user_page().servable()

def main():
    holoviz_config(plot_engine='bokeh', pandas_plot_backend='holoviews', loading_type='dots', loading_color='#00204e', sizing_mode="stretch_width")
    pn.serve(get_user_page, port=5006, allow_websocket_origin='localhost:5006', websocket_origin='localhost:5006', show=False, threaded=True, n_threads=8)
    
if __name__ == '__main__':
    main()

update template

1 Like

Amazing! Thank you very much @nghenzi it is working very nice. I managed to use the same strategy when updating the plots from a date picker widget. I have modified the example code to add the date selection feature, in case anyone is interested in having a similar example.

import pandas as pd
import hvplot.pandas
import panel as pn
import holoviews as hv
import threading
import time
import datetime as dt
import random

def holoviz_config(plot_engine, pandas_plot_backend, loading_type, loading_color, sizing_mode):
    hv.extension(plot_engine)
    pd.options.plotting.backend = pandas_plot_backend
    pn.extension(loading_spinner=loading_type, loading_color=loading_color, sizing_mode=sizing_mode)
    
def empty_plot_with_text(panel_name_id):
    # Creates an empty plot with a text inside
    empty_plot = hv.Curve([])

    empty_plot = empty_plot * \
        hv.Text(0.6, 0.6, panel_name_id)

    return empty_plot
    
    
def create_empty_plot_panel(panel_name_id, date_picker, template):
    
    @pn.depends(date_picker.param.value, watch=True)
    def init_threaded_update_plot_task(date_picker):
        
        t = threading.Thread(target=update_plot, args=(date_picker, ))
        t.daemon = True
        t.start() 

    def get_panel_by_name(panel_name_id):
        result_panel = None
        match panel_name_id:
            case 'A':
                result_panel = template.main.objects[0][0][0, 0]
            
            case 'B':
                result_panel = template.main.objects[0][0][0, 1]
                
            case 'C':
                result_panel = template.main.objects[0][0][0, 2]
                
            case 'D':
               result_panel = template.main.objects[0][0][1, 0]
            
            case 'E':
               result_panel = template.main.objects[0][0][1, 2]
               
        return result_panel
        
    def update_plot(date_picker):
        with pn.param.set_values(get_panel_by_name(panel_name_id), loading=True): 
            
            print('Updating plot ' + panel_name_id)
            
            # sleep a random value between 0.2 and 3 seconds
            time.sleep(random.uniform(0.2, 3))
            
            date_str = date_picker.strftime('%Y-%m-%d')
            updated_plot = empty_plot_with_text('Updated ' + panel_name_id + ' ' + date_str)
            updated_plot_panel = pn.panel(updated_plot, sizing_mode='stretch_width', linked_axes=False)
        
            update_grid(updated_plot_panel)
        
    def update_grid(panel):  
        print('Appending new panel to grid', panel)
        
        match panel_name_id:
            case 'A':
                template.main.objects[0][0][0, 0] = panel
            
            case 'B':
                template.main.objects[0][0][0, 1] = panel
                
            case 'C':
                template.main.objects[0][0][0, 2] = panel
                
            case 'D':
                template.main.objects[0][0][1, 0:2] = panel
            
            case 'E':
                template.main.objects[0][0][1, 2:3] = panel
        
        
        
        
    # Creates an empty plot and appends it in a panel
    empty_plot = empty_plot_with_text(panel_name_id)
    panel = pn.panel(empty_plot, sizing_mode='stretch_width', linked_axes=False)
    
    return panel
    


def create_dashboard(template: pn.template.MaterialTemplate): 
    
    print('Creating dashboard')
    
    
    # We are going to simulate a 5 seconds task here before plots are finally appended to template
    time.sleep(0.5)
    
    # Creates a date picker widget
    date_picker = pn.widgets.DatePicker(
        name='Date Selection', value=dt.date.today(), end=dt.date.today())
    
    panel_A = create_empty_plot_panel('A', date_picker, template)
    panel_B = create_empty_plot_panel('B', date_picker, template)
    panel_C = create_empty_plot_panel('C', date_picker, template)
    panel_D = create_empty_plot_panel('D', date_picker, template)
    panel_E = create_empty_plot_panel('E', date_picker, template)
    
    # Creates a grid and append plots to it
    grid = pn.GridSpec(sizing_mode='stretch_both', ncols=3, nrows=2, mode='override')
    grid[0, 0] = panel_A
    grid[0, 1] = panel_B
    grid[0, 2] = panel_C
    grid[1, 0:2] = panel_D
    grid[1, 2:3] = panel_E
    
    sidebar_col = pn.Column(pn.layout.HSpacer(), date_picker)
    
    # Append grid to template main
    template.main.objects[0].sizing_mode = 'stretch_both'
    template.main.objects[0][0] = grid
    
    # Append other thing to template sidebar
    template.sidebar.objects[0][0] = sidebar_col

    
def get_user_page():
    loading = pn.indicators.LoadingSpinner(value=True, width=100, height=100, align='center')
    
    material_dashboard = pn.template.MaterialTemplate(title='Threading Test APP (Template)', header_background='#00204e', favicon='/images/favicon.ico')
    material_dashboard.main.append(pn.Column(loading))
    material_dashboard.sidebar.append(pn.Column(pn.Column()))
    
    
    t = threading.Thread(target=create_dashboard, args=(material_dashboard, ))
    t.daemon = True
    t.start() 

    return material_dashboard
    
    
def main():
    holoviz_config(plot_engine='bokeh', pandas_plot_backend='holoviews', loading_type='dots', loading_color='#00204e', sizing_mode="stretch_width")
    pn.serve(get_user_page, port=5006, allow_websocket_origin='localhost:5006', websocket_origin='localhost:5006', show=False, threaded=True, n_threads=0)
    
if __name__ == '__main__':
    main()

It is more or less the pattern I’m using into the app I’m working on.

Some thoughs about using threads

At the moment this solution works fine, it is the first time I work with threads and I am still wondering if this is the right thing to do. Let me explain, right now the design of the application is such that every time a user enter the url in the browser and enters the web service, the create_dashboard method is executed in a separate thread.

  • What happens if many users enter at the same time, exceeding the capacity or certain limit of the threads that would be appropriate to create?

The same happens with the graphics update strategy.

  • Does this solution scale well with multiple concurrent users? What is your opinion?

Once again, thank you for your time, below there is a preview of how looks the application I’m working on.

gif of app working Holoviz APP Demo