Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions panel/io/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import logging
import os
import threading
import typing as t

from functools import partial
Expand Down Expand Up @@ -165,6 +166,10 @@ def _set_session_prefix(self, doc):

def initialize_document(self, doc):
logger.info(LOG_SESSION_LAUNCHING, id(doc))
# Claim the Document for the current thread before user code runs,
# so APIs that behave differently when invoked off the Document's
# thread (e.g. hold) can tell the two cases apart during the build.
state._thread_id_[doc] = threading.get_ident()
self._set_session_prefix(doc)
super().initialize_document(doc)
if doc in state._templates and doc not in state._templates[doc]._documents:
Expand Down
21 changes: 21 additions & 0 deletions panel/tests/io/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ def update_in_hold(i):
wait_until(lambda: not doc.callbacks.hold_value, timeout=5000)


@pytest.mark.xdist_group(name="server")
def test_hold_in_app_callable_does_not_leak_hold():
build = {}

def app():
with hold():
pass
# A hold leaked here defers the unhold past ServerSession
# construction, which registers this callback a second time
# when the queued SessionCallbackAdded event is replayed.
pn.state.add_periodic_callback(lambda: None, period=10000)
build['thread_id'] = state._thread_id
build['hold'] = state.curdoc.callbacks.hold_value
return IntSlider()

serve_and_request(app)

assert build['thread_id'] is not None
assert build['hold'] is None


class _FakeProtocol:
def create(self, msgtype, events):
return object()
Expand Down