Deploy Bokeh Server into production on IP address

Sorry in advance if any of what I say sounds dumb, I don’t have any experience working with servers in any capacity.

I’m having trouble deploying a bokeh server on my local IP address that allows multiple users to use it (who are on the same network) at the same time. I’m using Flask and based it off the flask example on github. I’m trying to allow multiple users to use the application off the link “IP:5001,” where I’m referring to my IP address as IP here for security purposes. It allows a user to use it who is not me but doesn’t allow multiple users to use at the same time. I would’ve thought to increase the number of processes beyond 1 but I’m on a windows machine so was looking for some help on how to do this because I’m such a newbie with this stuff. I tried to use the gunicorn example but I’m on anaconda and there were no anaconda repositories for asyncio that work in Windows 64 bit. Thanks so much in advance for any help.

The basic code is as follows:

app = Flask(__name__, static_url_path='/static')
def bkapp(doc):
    doc.add_root(tabs)

@app.route('/', methods=['GET'])
def bkapp_page():
    script = server_document('http://IP:5006/bkapp')
    return render_template("embed.html", script=script, template="Flask")


def bk_worker():

    server = Server({'/bkapp': bkapp}, io_loop=IOLoop(), allow_websocket_origin=["IP:5001"])
    server.start()
    server.io_loop.start()


Thread(target=bk_worker).start()

if __name__ == '__main__':

    app.run(port=5001, host='IP')

@alexgrossman

The bokeh server example flask_embed.py,which uses the Flask development server, from which your app is derived works well with several users on an internal network using the server’s IP address instead of localhost just as you’ve done.

As a starting point, I would confirm that works as expected for multiple concurrent users in your network environment.

If so, you should probably next inspect the differences between your application and the illustrative one in flask_embed.py.

As the bokeh server is single-threaded, is your tabs model doing anything in a blocking mode? If so, you’ll want to go through the bokeh server documentation on how to have a thread for any necessary blocking opeations, etc.

Not sure what you mean by blocking mode. I’m now trying to use the waitress package to do it, on a modification of the “gunicorn” example. Should it work to use waitress with the traditional flask example to allow for multiple users? I do think the issue is the single thread because I need multiple users to be able to use it at once. I’m really lacking some understanding with respect to what is supposed to be happening here.

@alexgrossman

Can you run the flask_embed.py example as is, with the small exception to change the localhost to your IP? And then try to have multiple concurrent users? That works fine for several users under the Flask development server with only one process.

By blocking mode, I mean that your code is running an operation or function that causes the server to wait on input from a user such that it cannot do anything else like service requests coming in from other users to update their session(s).

No functions should be blocking. And the flask example doesn’t work as is for multiple users on my company’s network. That’s what I was using originally before trying to use waitress and asyncio to address the fact that it didn’t work.

@alexgrossman

I see. It works for me; I tested with three concurrent users via laptop and two cellphones to confirm.

The server was running on Mac OSX in my case with Python 3.8.3, bokeh 2.1.1, and Flask 1.1.2 all under the most recent Anaconda distribution.

I’m using an earlier version of Bokeh because of the bug regarding the changing of dropdowns visually when switching the options of the dropdown.

I have gotten it to work for multiple users using waitress and asyncio, but cannot get render_template to work with it currently.

I’m using the same code as the Flask example, but in main i’m using:
t = Thread(target=bk_worker)
t.daemon = True
t.start()

t.join()
print('Served')
waitress.serve(app, listen='url:5006')

And adding to the top of bk_worker:

**    asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)**

@alexgrossman

What is the nature of it not working? Are there any printouts in the server terminal window when different users attempt to use the app?

Can you run from one computer with multiple browsers or browser tabs? If so that might rule out the software and point to something in your network topology.

Other users just get blocked and yes terminal outputs:

WARNING:bokeh.server.views.ws:Failed sending message as connection was closed

And it just wouldn’t open for those other users.

Works now with the edits above but render_template isn’t.

Note: with the code additions, it forced me to use computer_name.network.com:port instead of IP:port

Okay. The devs might have some input on why the websockets are being closed perhaps related to the implementation of things in Windows, which I do not use.

I do not have any experience with waitress, but the first thing I would check is that embed.html used by render_template is in the place that waitress expects. Does it look for this in the templates directory?

You can also use the JavaScript console for your client’s browser to see if there are any error logs there to see what is going on. Each browser is different, but for Chrome, right-click, select Inspect, and then activate the Console tab to see the JavaScript console.

Yes, embed is in templates folder. It worked fine when running on my IP. Now when using computer_name.network.com:port instead, it doesn’t seem to be recognizing it. Before, it would also load the html from my embed before loading the bokeh script. Now all of it loads when the url is put in the browser before showing any html

Using in Firefox rather than chrome because the data requirements for this script made chrome ungodly slow.

That’s fine. You can access the JavaScript console from Firefox too. CTRL+SHIFT+J or COMMAND+SHIFT+J (on Mac).

That might give some insight into why the rendering is not happening in the client’s browser assuming there are no errors related to it in the server console.

@alexgrossman

One additional thought… Given the claim that waitress is essentially a Windows equivalent to gunicorn for Unix operating systems, you might want to model your application on the bokeh flask_gunicorn_embed.py example on GitHub. The details of how this sets up/coordinates the server, IOLoop, and Tornado server might be relevant.

That’s what I’ve been trying to do. I’ve had success having multiple users use it at the same time, just not with also using an HTML template. The HTML template loads when I remove t.join() from above but in that case it seems to interpret the html file as a javascript file because I’m getting a syntax error of “<”

@alexgrossman

Here’s a version that works with waitress. I essentially modified the flask_gunicorn_embed.py example from bokeh’s GitHub by adding the APP_SERVER_ADDR and APP_SERVER_PORT variables.

I run it using the waitress-serve command line option as follows.

waitress-serve --listen=192.168.0.135:8080 --threads=4 flask_waitress_embed:app

flask_waitress_embed.py

try:
    import asyncio
except ImportError:
    raise RuntimeError("This example requries Python3 / asyncio")

from threading import Thread

from flask import Flask, render_template
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop

from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
from bokeh.embed import server_document
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Slider
from bokeh.plotting import figure
from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature
from bokeh.server.server import BaseServer
from bokeh.server.tornado import BokehTornado
from bokeh.server.util import bind_sockets
from bokeh.themes import Theme

APP_SERVER_ADDR = "192.168.0.135"
APP_SERVER_PORT = 8080

if __name__ == '__main__':
    import sys
    sys.exit()


app = Flask(__name__)

def bkapp(doc):
    df = sea_surface_temperature.copy()
    source = ColumnDataSource(data=df)

    plot = figure(x_axis_type='datetime', y_range=(0, 25), y_axis_label='Temperature (Celsius)',
                  title="Sea Surface Temperature at 43.18, -70.43")
    plot.line('time', 'temperature', source=source)

    def callback(attr, old, new):
        if new == 0:
            data = df
        else:
            data = df.rolling('{0}D'.format(new)).mean()
        source.data = ColumnDataSource.from_df(data)

    slider = Slider(start=0, end=30, value=0, step=1, title="Smoothing by N Days")
    slider.on_change('value', callback)

    doc.add_root(column(slider, plot))

    doc.theme = Theme(filename="theme.yaml")

# can't use shortcuts here, since we are passing to low level BokehTornado
bkapp = Application(FunctionHandler(bkapp))

sockets, port = bind_sockets(APP_SERVER_ADDR, 0)

@app.route('/', methods=['GET'])
def bkapp_page():
    script = server_document('http://%s:%d/bkapp' % (APP_SERVER_ADDR,port))
    return render_template("embed.html", script=script, template="Flask")

def bk_worker():
    asyncio.set_event_loop(asyncio.new_event_loop())

    bokeh_tornado = BokehTornado({'/bkapp': bkapp}, extra_websocket_origins=["%s:%d" % (APP_SERVER_ADDR,APP_SERVER_PORT)])
    bokeh_http = HTTPServer(bokeh_tornado)
    bokeh_http.add_sockets(sockets)

    server = BaseServer(IOLoop.current(), bokeh_tornado, bokeh_http)
    server.start()
    server.io_loop.start()

t = Thread(target=bk_worker)
t.daemon = True
t.start()

Every time I try that on my computer I get:

RuntimeError: There is no current event loop in thread ‘Thread-1’.

What version of bokeh, tornado, etc are you using?

This issue on GitHub notes a similar problem and mentions issues related to asyncio and tornado changes related to Tornado 5.0.

https://github.com/tornadoweb/tornado/issues/2308

I can’t figure out my tornado version, definitely using Bokeh 2.0.2