Stop embeded bokeh server when there are no active sessions

Hi,

I’m working on an application that will be be serving plots to several users. I have a flask server that will start a new (embeded) bokeh server every time a new user connects to the page. I’ve drawn inspiration form: https://github.com/bokeh/bokeh/blob/2.3.0/examples/howto/server_embed/flask_embed.py

But in this case I want to have one bokeh server per application. The document created is different for each request, so I cannot use the same server. When a new user makes a GET request, a new bokeh server is spin up with a new application for that user.

I want to stop a Bokeh server (so I can reuse the port) whenever there are no active sessions.
I’m trying to achieve this with Tornado’s PeriodicCallback, but it doesn’t seem reliable. Sometimes it works, sometimes it keeps telling me the address is in use even though the server has been closed (maybe a race condition?). The get method (not shown) just starts a new bokeh server. The code looks like this:

from tornado.ioloop import PeriodicCallback
...

    def bokeh_app(doc):                                                     
        """Bokeh application to be served by Bokeh server"""                
        doc.add_root(self.rendered_window)                                  
    def bk_worker():
        ioloop=IOLoop()                                                     
        server = Server(                                                    
            {"/": bokeh_app}, io_loop=ioloop, allow_websocket_origin=["*"], 
            port=port,  unused_session_lifetime_milliseconds=5000,  check_unused_sessions_milliseconds=5000
        )                                                                   
        def callback():                                                     
            print("Periodic callback! at ", port)                           
            print(server.get_sessions())                                    
            if not server.get_sessions():                                   
                server.stop()
                ioloop.stop()                                           
                print(f"Stopped server at {port}")                    
        pcallback = PeriodicCallback(callback, 15000)                       
        pcallback.start()                                                   
        server.start()                                                      
        server.io_loop.start()                                              
                                                                            
    print(f"\nOpening Bokeh on http://localhost:{port}/")                   
    print(                                                                  
        "If you are opening it in another machine, use the IP of your "     
        "desktop. You can use ifconfig to obtain it\n"                      
    )                                                                       
    Thread(target=bk_worker).start()

Example output:

Periodic callback! at  5006
[<bokeh.server.session.ServerSession object at 0x7fe048165a90>]
Periodic callback! at  5006
[]
Stopped server at 5006

This works sometimes. But sometimes I get the following error when calling server() after I’ve already stopped a previous server:

...
    ...pypi__tornado_py\
    thon3_deps/tornado/netutil.py", line 174, in bind_sockets
        sock.bind(sockaddr)
    OSError: [Errno 98] Address already in use

Even though the server and io_loop() have been stopped. The server architecture page recommends not using Tornado’s IOLoop, but I don’t know how else to achieve what I want.

Any thoughts on this approach, or any other way I could stop a server when there are no active connections?

This definitely strays pretty far from intended usage, so I think you are going to be fighting the library regardless. I guess my first question is: why do you want to do things this way?

I’m not sure there is a reliable way to do this with threads and re-using the same port. The server.stop and ioloop.stop just correspond to Tornado operations, and Tornado may very well flush existing tasks (e.g. the next periodic callback tick) before actually shutting down, or at least finish any in-flight ones. (I don’t actually know and it’s outside our control regardless) It’s not clear to me that calling either or both of those will result in the port being free at the OS level in any set, predictable time frame. Your best bets are probably:

  • use new random ports every time instead of re-using the same port
  • use sub-processes (which can just be killed) instead of threads

But really my suggestion is to re-think why or whether this is necessary at all.

Thanks for the input, Bryan.
To answer your question, I have a custom library built on top of bokeh that addresses some specific needs.

In this library each “Plot” object abstracts the Bokeh doc creation, creating a unique Bokeh doc for a set of inputs (that includes all the data we need to plot). Because of this I cannot just serve the same Bokeh doc again when I get a new GET request with a different set of data; I need to create a new Bokeh server to serve the new doc.

On top of this I want to be able to serve to different users from the same machine, and release the resources when they are not using it anymore (this service will be running continuously). In this case I was using a single port to illustrate the issue, but the idea is to have a set of ports assigned to this service .

So it’s almost like I’m trying to rebuild a Bokeh server server? :sweat_smile:

I was afraid this wasn’t the intended usage. I like the idea of using sub processes instead, I’m going to experiment with it.

Well, it’s hard to say much specific, but worth nothing:

Ah! Awesome, I implemented a quick prototype and this definitely is going to work for my usecase. I hadn’t read that part of the documentation that talks about passing the HTTP arguments and I was trying to work around that. A pretty obvious case of XY problem :slight_smile:

Thank you so much, Bryan.

1 Like