cocoindex-io/cocoindex · error · RuntimeError

supports a single active watch() at a time.

Error message

{self._label} supports a single active watch() at a time.

What it means

The connectorkits watch() guard enforces that only one active watch session exists per labeled resource at a time; entering the context manager twice concurrently would produce duplicated or interleaved event streams, so it raises this RuntimeError.

Solutions

  1. Exit the first watch() before entering another (restructure into sequential with-blocks)
  2. Create a separate watch handle/instance for each concurrent consumer instead of sharing one
  3. Serialize watch usage in the same task/thread, or fan out events from a single watch to multiple consumers
  4. Verify no leaked context due to an exception path that skips __exit__ (use try/finally or with-statements)

Example fix

// before
w1 = res.watch(); w2 = res.watch()
with w1, w2:  # RuntimeError
// after
with res.watch():
    handle_events()
Defensive patterns

Strategy: try-catch

Validate before calling

if watch_handle._active:
    raise RuntimeError("watch already active; exit it before re-entering")

Try / catch

try:
    with resource.watch():
        consume()
except RuntimeError as e:
    if "single active watch" in str(e):
        serialize_watch_usage()  # retry sequentially

Prevention

When it happens

Trigger: Entering the same watch() context manager a second time while a previous one is still active (not __exit__-ed) — e.g. nesting two `with resource.watch():` blocks on the same object, or re-entering from another thread/task before exit.

Common situations: Refactoring code that calls watch() in two helper functions both entered simultaneously; spawning concurrent tasks that each enter the same watch context; forgetting to exit a watch before starting another (exception in the body leaving it open is handled by __exit__, but manual misuse is not).

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of cocoindex-io/cocoindex@e84aa99b32 (2026-09-08). Data as JSON: /api/errors/538657774df7e9d5. Report an issue: GitHub.

Appendix: source

Thrown at python/cocoindex/connectorkits/__init__.py:50

    The flag resets on every exit — normal return, exception, or cancellation (a
    cancelled ``await`` inside the ``with`` unwinds through ``__exit__``) — so the
    feed can be re-watched sequentially after a prior ``watch()`` finishes.

    A plain flag (no lock) suffices when ``watch()`` runs entirely on one event
    loop, as the framework's live consumer does. A feed that already carries
    equivalent "is being watched" state can guard on that instead.
    """

    __slots__ = ("_label", "_active")

    def __init__(self, label: str) -> None:
        self._label = label
        self._active = False

    def __enter__(self) -> None:
        if self._active:
            raise RuntimeError(
                f"{self._label} supports a single active watch() at a time."
            )
        self._active = True

    def __exit__(self, *exc: object) -> None:
        self._active = False


def default_subpath_name(processor_fn: Any) -> str | None:
    """Resolve the default subpath name for a mount target.

    Honors an explicit ``__coco_subpath_name__`` attribute (set by wrappers
    like ``coco.auto_refresh`` so the wrapper class can keep an honest
    ``__name__`` while still mounting under the wrapped function's name),
    falling back to ``__name__``.
    """
    name = getattr(processor_fn, "__coco_subpath_name__", None)
    if isinstance(name, str):

View on GitHub (pinned to e84aa99b32)