ALL software version info
Software Version Info
Panel: main @ c97e8098
Bokeh: 3.9.1
Python: 3.11
OS: Linux
Description of expected behavior and the observed behavior
When a hold() block exits while the Document is not connected, the finally chain in panel/io/document.py takes
elif not state._connected.get(doc):
doc.callbacks._hold = None
which clears the hold policy but leaves doc.callbacks._held_events populated. Bokeh's unhold() returns early when _hold is None, so nothing drains them there. The events sit on the Document until some later, unrelated hold()/unhold() pair flushes them — by which point the session may be connected, and they are dispatched well outside the context they were recorded in.
Expected: leaving a hold() block leaves nothing queued behind it, whether the events are dispatched or deliberately discarded.
Complete, minimal, self-contained example code that reproduces the issue
import time
import panel as pn
import requests
from panel.io.state import state
info, docs = {}, []
def app():
md = pn.pane.Markdown("initial")
docs.append(state.curdoc)
def before_ready():
doc = state.curdoc
with pn.io.hold():
md.object = "changed"
info['connected'] = state._connected.get(doc)
info['hold'] = doc.callbacks.hold_value
info['queued'] = [type(e).__name__ for e in doc.callbacks._held_events]
state.curdoc.add_next_tick_callback(before_ready)
return md
pn.serve(app, port=5099, show=False, threaded=True)
time.sleep(1)
requests.get("http://localhost:5099")
time.sleep(1)
print("on exiting the hold:", info)
print("still queued afterwards:", [type(e).__name__ for e in docs[0].callbacks._held_events])
Output:
on exiting the hold: {'connected': None, 'hold': None, 'queued': ['SessionCallbackAdded']}
still queued afterwards: ['SessionCallbackAdded']
The hold is gone but the event it collected is not, and nothing on the Document will drain it until an unrelated hold happens to.
The same applies to events queued by a hold() inside the app callable. Those currently take the threaded branch instead, because state._thread_id is still unset while the app is being built (#8691); with that fixed, a build-time hold strands TitleChangedEvent and RootAddedEvents, which are then flushed by the first hold() after the session connects. Build-time holds behaved that way before #8619 reordered this chain, so this is long-standing rather than new — but it is not obvious to me whether the not-connected behaviour is deliberate, or whether the intent was for the events to be discarded outright.
Stack traceback and/or browser JavaScript console output
None — this is silent.
Screenshots or screencasts of the bug in action
None. I have not established what a connected client actually sees when these events are replayed; the environment I reproduced this in blocks the CDN, so I could not drive a real browser against it. Reporting it on the server-side behaviour alone, since events outliving their hold looks unintended regardless of the client-visible impact.
Investigation assisted by Claude Code + Opus 5; the reproducer above was run and verified.
ALL software version info
Software Version Info
Description of expected behavior and the observed behavior
When a
hold()block exits while the Document is not connected, thefinallychain inpanel/io/document.pytakeswhich clears the hold policy but leaves
doc.callbacks._held_eventspopulated. Bokeh'sunhold()returns early when_hold is None, so nothing drains them there. The events sit on the Document until some later, unrelatedhold()/unhold()pair flushes them — by which point the session may be connected, and they are dispatched well outside the context they were recorded in.Expected: leaving a
hold()block leaves nothing queued behind it, whether the events are dispatched or deliberately discarded.Complete, minimal, self-contained example code that reproduces the issue
Output:
The hold is gone but the event it collected is not, and nothing on the Document will drain it until an unrelated hold happens to.
The same applies to events queued by a
hold()inside the app callable. Those currently take thethreadedbranch instead, becausestate._thread_idis still unset while the app is being built (#8691); with that fixed, a build-time hold strandsTitleChangedEventandRootAddedEvents, which are then flushed by the firsthold()after the session connects. Build-time holds behaved that way before #8619 reordered this chain, so this is long-standing rather than new — but it is not obvious to me whether the not-connected behaviour is deliberate, or whether the intent was for the events to be discarded outright.Stack traceback and/or browser JavaScript console output
None — this is silent.
Screenshots or screencasts of the bug in action
None. I have not established what a connected client actually sees when these events are replayed; the environment I reproduced this in blocks the CDN, so I could not drive a real browser against it. Reporting it on the server-side behaviour alone, since events outliving their hold looks unintended regardless of the client-visible impact.
Investigation assisted by Claude Code + Opus 5; the reproducer above was run and verified.