Skip to content

Stream.trigger has no per-subscriber error isolation — one failing subscriber silently cancels every later subscriber's refresh #6894

@johngu

Description

@johngu

ALL software version info

Python              :  3.13.13
holoviews           :  1.22.1
bokeh               :  3.9.0
numpy               :  2.4.4
panel               :  1.9.0
param               :  2.4.0

Description of expected behavior and the observed behavior

holoviews.streams.Stream.trigger (in holoviews/streams.py, around line 213) iterates over subscribers in a bare for loop with no try/except:

with triggering_streams(streams):
    for subscriber in subscribers:
        subscriber(**dict(union))

If any single subscriber(...) call raises, the loop terminates and every subsequent subscriber in the list is silently skipped. There is no logging — the exception bubbles up through the asyncio task and is consumed by IOLoop._discard_future_result, leaving the visible state of the application a strict subset of what trigger() should have updated.

In practice this means a single buggy operation on one plot can silently freeze every other plot whose RangeX (or any shared stream) was supposed to refresh in the same trigger pass. The frozen plots show no visible error: their CDSes simply don't update, leaving them stuck on whatever state they were rendered in last.

I hit this in a multi-panel time-series setup driven by downsample1d. The triggering bug was an empty-Curve crash in interpolate_curve.pts_to_poststep (filed separately: ). But the visible symptom — sibling plots stuck on a zoomed-in sample even after wheel-out/Reset — took hours to diagnose because the actual exception fires once in the Python kernel for an unrelated operation, with no surfacing of "and by the way, 6 other plots' refreshes never ran."

Expected: a failing subscriber in Stream.trigger should not prevent other subscribers from running. At minimum the exception should be logged (or surfaced via the document-error machinery, the way Callback.on_msg does for CallbackError) so downstream debugging has a chance.

Observed: the first failing subscriber kills the loop. Subsequent subscribers are silently skipped. The exception escapes to the asyncio handler and is logged as a generic "Exception in callback" with no holoviews-side context.

Complete, minimal, self-contained example code that reproduces the issue

import holoviews as hv
import panel as pn
from holoviews.streams import Stream

hv.extension("bokeh")

# Set up two subscribers on a shared stream. The first raises; the
# second should still run, but doesn't.
stream = Stream.define("S", value=0)()

calls = []

def bad_subscriber(**kw):
    calls.append("bad")
    raise RuntimeError("simulated upstream bug")

def good_subscriber(**kw):
    calls.append("good")

stream.add_subscriber(bad_subscriber)
stream.add_subscriber(good_subscriber)

try:
    Stream.trigger([stream])
except Exception:
    pass

print(f"calls: {calls}")
# Observed: ['bad'] — good_subscriber never ran.
# Expected: ['bad', 'good'] — good_subscriber should run despite bad's failure.

Suggested fix

Wrap each subscriber call in try/except inside Stream.trigger. Log (and optionally surface via state._handles-style document error display) failed subscribers, but always continue iterating. Something like:

import logging
log = logging.getLogger(__name__)

with triggering_streams(streams):
    for subscriber in subscribers:
        try:
            subscriber(**dict(union))
        except Exception:
            log.exception(
                "Stream subscriber %r raised; continuing remaining subscribers",
                subscriber,
            )

This matches the resilience already present in Callback.on_msg (which has dedicated CallbackError handling) and prevents one bad operation from silently affecting unrelated plots in the same trigger pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions