Lambdas, functools and serving multiple app instances

Fun issue today when I tried to scale up my FastAPI/Panel app combo; I have been following @Hoxbro 's advice to use the pattern … (Managing pull_session and server_session from FastAPI - #4 by sbi_vm)

pn.serve({'/app':lambda:CreateApp().gspec},)

… to ensure that separate sessions are kicked off per user (rather than a shared session).

I now have some app classes with arguments, so I thought I would be clever and do:
endpoint_dict = {f’app/{d}’:lambda:createApp(d=d,d2=d2).gspec for (d,d2) in instances_dict.items()}

Thinking that it would dynamically build out all the app endpoints, configured via the instances_dict.

The funny thing is, all the created endpoints shared the same input arguments! Seems like the lambda’s get stored with the last argument/expression values from the loop. I made a test case to see what’s happening in a simpler scenario:

import functools

def test_func(a,b):
    return a*b

terms_dict = {1:2,
                2:3,
                3:4}

func_dict = {d:lambda:test_func(d,t) for (d,t) in terms_dict.items()}

print('testing output from lambda')
for i,f in func_dict.items():
    print(f())

partials_dict = {d:functools.partial(test_func,d,t) for (d,t) in terms_dict.items()}

print('testing output from partials')
for i,p in partials_dict.items():
    print(p())
testing output from lambda
12
12
12
testing output from partials
2
6
12

So seems like functools.partials might be the way to go for scaling out a dict of panel endpoints that each spawn new instances.

To get it to work I did something clunky like:


def get_new_layout(param1,param2):
     app_inst = createApp(param1,param2)
     return app_inst.layout

endpoint_dict = {f'app/{param1}':functools.partial(get_new_layout,param1,param2)() for (param1,param2) in APP_DICT.items()}

pn.serve(endpoint_dict,
     port=5000, 
     allow_websocket_origin=["127.0.0.1:8000"],
     address="127.0.0.1", show=False)

… and this works to kick off the different app instances with the different parameters, BUT I am back to the issue where it’s a shared session (2 users at same endpoint modify same instance).

I know @Marc does a lot with multiple-app configurations; is there an easier or more canonical way to “pass” arguments to the pn.serve endpoint dictionary?

A couple of thoughts:

  1. The reason that they are sharing state is that you actually call your partial function when you define endpoint_dict with (). For them to be separate instances you would have to leave it uncalled.
  2. I tried to get pn.serve to work with uncalled partials, but I could not figure it out. Try something like the below (note: I changed your slashes to dashes because I don’t have an app folder):
import panel as pn
pn.extension()

APP_DICT = {
    'x': 'x',
    'y': 'y'
}

def get_new_layout(param1,param2):
    def createApp():
        return pn.Column(
            pn.widgets.TextInput(
                name=param1, 
                placeholder='Enter a string here...'
            ),
            pn.pane.Markdown(param2),
            width = 500, height = 200
        )
    return createApp

endpoint_dict = {
    f'app-{param1}':get_new_layout(param1,param2) 
    for param1,param2 in APP_DICT.items()
}

pn.serve(endpoint_dict)

Thank you @riziles , yes calling the partial (and therefore linking the states) seems to be the only way to get the panel.serve to recognize it; if I leave it as an “uncalled” partial, the panel.serve method throws an error (it doesn’t like a functools.partial object instead of the base function object it expects). I am probably missing something on the Python syntax side for wrapping a partial into a more “standard” function… I was hoping that there was some kind of “query parameter-like” syntax I was missing in pn.serve where we could pass arguments to the endpoint.

Will try to adapt your example this week to my larger createApp class; thank you for the feedback!

Using a closure function instead of functools should do the trick.

One question: what does the .layout refer to in your example?

Yeah, that’s the complicating issue that I will need to refactor (on my side) to try your approach; my “app” is implemented as a class, and the panel layout part sits on the app.layout attribute. I’ll probably have to re-implement as a layout function to leverage your closure idea.

Wait. Is layout an object method or static attribute?

This seems to work (thanks to @Marc for the guidance):

import panel as pn
pn.extension()

APP_DICT = {
    'x': 100,
    'y': 1000
}

import panel as pn
import holoviews as hv
import param

pn.extension()
hv.extension('bokeh')

class createApp(param.Parameterized):
    name = param.String()
    value = param.Selector(objects=[10,100,1000,10000])

    @pn.depends("value")
    def plot(self):
        return hv.Bars([('a',self.value)])

    @property
    def layout(self):
        widget = pn.widgets.RadioButtonGroup.from_param(self.param.value, button_type="primary")
        return pn.Row(pn.pane.Markdown('# ' + self.name), widget, self.plot)

def get_new_layout(param1,param2):
    def closure_func():
        app_inst = createApp(name = param1, value = param2)
        return app_inst.layout
    return closure_func
    
    
endpoint_dict = {
    f'app-{param1}':get_new_layout(param1,param2)
    for param1,param2 in APP_DICT.items()
}

pn.serve(endpoint_dict)
1 Like

Worked beautifully! Thank you.

1 Like