diff --git a/panel/command/serve.py b/panel/command/serve.py index 9ab987f854..ea47a088c4 100644 --- a/panel/command/serve.py +++ b/panel/command/serve.py @@ -228,6 +228,20 @@ def customize_applications(self, args, applications): applications['/'] = applications[f'/{index}'] return super().customize_applications(args, applications) + def warm_applications(self, applications, reuse_sessions): + from ..io.session import generate_session + for path, app in applications.items(): + session = generate_session(app) + with set_curdoc(session.document): + if config.session_key_func: + reuse_sessions = False + else: + state._session_key_funcs[path] = lambda r: r.path + state._sessions[path] = session + session.block_expiration() + state._on_load(None) + _cleanup_doc(session.document, destroy=not reuse_sessions) + def customize_kwargs(self, args, server_kwargs): '''Allows subclasses to customize ``server_kwargs``. @@ -304,17 +318,11 @@ def customize_kwargs(self, args, server_kwargs): applications = build_single_handler_applications(files, argvs) if args.autoreload: with record_modules(): - for app in applications.values(): - doc = app.create_document() - with set_curdoc(doc): - state._on_load(None) - _cleanup_doc(doc) + self.warm_applications( + applications, args.reuse_sessions + ) else: - for app in applications.values(): - doc = app.create_document() - with set_curdoc(doc): - state._on_load(None) - _cleanup_doc(doc) + self.warm_applications(applications, args.reuse_sessions) if args.liveness: argvs = {f: args.args for f in files} diff --git a/panel/io/document.py b/panel/io/document.py index c4a8d788a6..313b4b689c 100644 --- a/panel/io/document.py +++ b/panel/io/document.py @@ -60,7 +60,7 @@ def _dispatch_events(doc: Document, events: List[DocumentChangedEvent]) -> None: for event in events: doc.callbacks.trigger_on_change(event) -def _cleanup_doc(doc): +def _cleanup_doc(doc, destroy=True): for callback in doc.session_destroyed_callbacks: try: callback(None) @@ -89,13 +89,18 @@ def _cleanup_doc(doc): views[ref] = (pane, root, doc, comm) state._views = views + # When reusing sessions we must clean up the Panel state but we + # must **not** destroy the template or the document + if not destroy: + return + # Clean up templates if doc in state._templates: tmpl = state._templates[doc] tmpl._documents = {} del state._templates[doc] - # Destroy doc + # Destroy document doc.destroy(None) #--------------------------------------------------------------------- diff --git a/panel/io/reload.py b/panel/io/reload.py index 3b5b0b83fc..1a362101c7 100644 --- a/panel/io/reload.py +++ b/panel/io/reload.py @@ -70,6 +70,8 @@ def autoreload_watcher(): Installs a periodic callback which checks for changes in watched files and sys.modules. """ + if not state.curdoc.session_context.server_context: + return cb = partial(_reload_on_update, {}) _callbacks[state.curdoc] = pcb = PeriodicCallback(callback=cb, background=True) pcb.start() diff --git a/panel/io/server.py b/panel/io/server.py index 6b2666fe20..20615e0d8f 100644 --- a/panel/io/server.py +++ b/panel/io/server.py @@ -352,26 +352,21 @@ def _session_prefix(self): # Patch Bokeh DocHandler URL class DocHandler(BkDocHandler, SessionPrefixHandler): - _session_key_funcs = {} - @authenticated async def get_session(self): from ..config import config path = self.request.path session = None - if config.reuse_sessions and path in self._session_key_funcs: - key = self._session_key_funcs[path](self.request) + if config.reuse_sessions and path in state._session_key_funcs: + key = state._session_key_funcs[path](self.request) session = state._sessions.get(key) if session is None: session = await super().get_session() with set_curdoc(session.document): if config.reuse_sessions: - key_func = config.session_key_func or (lambda r: path) - if key_func: - self._session_key_funcs[path] = key_func - key = key_func(self.request) - else: - key = path + key_func = config.session_key_func or (lambda r: r.path) + state._session_key_funcs[path] = key_func + key = key_func(self.request) state._sessions[key] = session session.block_expiration() return session @@ -380,11 +375,8 @@ async def get_session(self): async def get(self, *args, **kwargs): app = self.application with self._session_prefix(): - key_func = self._session_key_funcs.get(self.request.path) - if key_func: - old_request = key_func(self.request) in state._sessions - else: - old_request = False + key_func = state._session_key_funcs.get(self.request.path, lambda r: r.path) + old_request = key_func(self.request) in state._sessions session = await self.get_session() if old_request and state._sessions.get(key_func(self.request)) is session: session_id = generate_session_id( @@ -392,6 +384,7 @@ async def get(self, *args, **kwargs): signed=self.application.sign_sessions ) payload = get_token_payload(session.token) + del payload['session_expiry'] token = generate_jwt_token( session_id, secret_key=app.secret_key, diff --git a/panel/io/session.py b/panel/io/session.py new file mode 100644 index 0000000000..a331aaed3a --- /dev/null +++ b/panel/io/session.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import logging + +from bokeh.document import Document +from bokeh.server.contexts import BokehSessionContext, _RequestProxy +from bokeh.server.session import ServerSession +from bokeh.settings import settings +from bokeh.util.token import generate_jwt_token, generate_session_id + +log = logging.getLogger(__name__) + +def generate_session(application): + secret_key = settings.secret_key_bytes() + sign_sessions = settings.sign_sessions() + session_id = generate_session_id( + secret_key=secret_key, + signed=sign_sessions + ) + token = generate_jwt_token( + session_id, + secret_key=secret_key, + signed=sign_sessions, + extra_payload={'headers': {}, 'cookies': {}, 'arguments': {}} + ) + doc = Document() + session_context = BokehSessionContext( + session_id, + None, + doc + ) + session_context._request = _RequestProxy( + None, arguments={}, cookies={}, headers={} + ) + doc._session_context = lambda: session_context + application.initialize_document(doc) + return ServerSession(session_id, doc, io_loop=None, token=token) diff --git a/panel/io/state.py b/panel/io/state.py index a9defe6b03..930d8ec4a9 100644 --- a/panel/io/state.py +++ b/panel/io/state.py @@ -209,6 +209,7 @@ class _state(param.Parameterized): # Sessions _sessions = {} + _session_key_funcs = {} def __repr__(self) -> str: server_info = [] @@ -735,6 +736,7 @@ def reset(self): self._thread_pool.shutdown(wait=False) self._thread_pool = None self._sessions.clear() + self._session_key_funcs.clear() def schedule_task( self, name: str, callback: Callable[[], None], at: Tat =None, diff --git a/panel/tests/conftest.py b/panel/tests/conftest.py index 4530f2d482..0041a26c06 100644 --- a/panel/tests/conftest.py +++ b/panel/tests/conftest.py @@ -368,6 +368,7 @@ def reuse_sessions(): config.reuse_sessions = False config.session_key_func = None state._sessions.clear() + state._session_key_funcs.clear() @pytest.fixture def nothreads():