Skip to content

Server lifecycle hooks

Havoc Pennington edited this page Dec 11, 2015 · 4 revisions

Server applications may need to support code that runs outside of a session. The example we know of right now is a streaming IO observer, that would listen for new data from some source, and update all sessions when new data arrives.

A possible approach would be that in an application directory, we could have a file lifecycle.py (lifecycle_hooks.py? hooks.py? server_events.py?)

It would contain functions with conventional names (all of them are optional):

def on_application_load(application_context_object):
  pass
def on_application_unload(application_context_object):
  pass
def on_session_created(session_context_object):
  pass
def on_session_destroyed(session_context_object):
  pass

We would load and execute this file as a module one time, extract the callbacks by name, and then run them at appropriate times.

application_context_object would have methods for adding/removing callbacks, probably should include periodic, timeout, and immediate (IOLoop.add_callback/spawn_callback).

session_context_object would have a way to get the session document with the session lock held, maybe a context manager object and/or a method that takes a function that's run with the lock held. It would also have a way to get the session ID.

Implementation-wise, we could add methods to bokeh.application.handlers.Handler and bokeh.application.Application corresponding to each hook, and then the Server would invoke those hooks as needed.

We could either never call these outside of a server (bokeh html case), or we could make the bokeh html case look like we are creating one application and one session... adding callbacks to application_context_object in the bokeh html case would do nothing...

An issue is that an application-wide callback might want to access session documents. A way this could work is that you'd collect session context objects from on_session_created into a global, and then in your application-wide callback you could iterate over them. Or we could simply have a sessions property on the application context object that would return all live session context objects.

Some sample uses.

Implement an IO callback watching some remote database or service for changes

In on_application_load, you would add an async callback which would run forever in a loop and update all sessions when there's a change:

@gen.coroutine
def listen_for_changes(app_context):
  new_data = yield fetch_data_from_remote_source()
  for session_context in app_context.sessions:
    with (yield session_context.locked_document()) as doc:
      doc.data['foo'].append(new_data)

def on_application_load(app_context):
  app_context.add_callback(lambda: listen_for_changes(app_context))

An alternative way to handle document locking would be to add a callback to the document, since document callbacks are always invoked with the document lock:

def apply_changes(doc, new_data):
  doc.data['foo'].append(new_data)

@gen.coroutine
def listen_for_changes(app_context):
  new_data = yield fetch_data_from_remote_source()
  for session_context in app_context.sessions:
    session.document.add_callback(lambda: apply_changes(session.document, new_data))

In on_session_created tell your custom load balancer which node the session is on

# note that the hooks can be coroutines
@gen.coroutine
def on_session_created(session_context):
  yield notify_my_load_balancer(session_context.id)

In on_session_created check whether we've authorized this session ID

def on_session_created(session_context):
  if not check_signature(session_context.id):
    session_context.destroy() # we would have to have this "get rid of the session" API