orchestrata_ultimate.gateway.pubsub

Reference › orchestrata_ultimate › gateway › pubsub

pubsub is the in-process topic subscription registry of the gateway (SPEC-014 US-001). PubSubManager stores Subscription records from models behind an asyncio lock. It is used by protocol for broadcasts, by rpc, by several handlers, and by the orchestrator's pipeline kernel. See gateway.

At a glance

Source src/orchestrata_ultimate/gateway/pubsub.py
Lines of code 357
Classes 1
Functions 1
Imports (internal) orchestrata_ultimate.core.base.traces, orchestrata_ultimate.gateway.metrics, orchestrata_ultimate.gateway.models
Imported by orchestrata_ultimate.gateway.handlers.config, orchestrata_ultimate.gateway.handlers.cxp, orchestrata_ultimate.gateway.handlers.gateway, orchestrata_ultimate.gateway.handlers.governance, orchestrata_ultimate.gateway.handlers.session, orchestrata_ultimate.gateway.handlers.volk, orchestrata_ultimate.gateway.protocol, orchestrata_ultimate.gateway.rpc, orchestrata_ultimate.gateway.server, orchestrata_ultimate.orchestrator.pipeline_kernel
Tests that import it tests/gateway/handlers/test_config.py, tests/gateway/handlers/test_cxp.py, tests/gateway/handlers/test_cxp_kernel_dispatch.py, tests/gateway/handlers/test_governance.py, tests/gateway/handlers/test_knowledge.py, tests/gateway/handlers/test_orchestrator_kernel_dispatch.py, tests/gateway/handlers/test_session.py, tests/gateway/handlers/test_volk.py, tests/gateway/handlers/test_workflow.py, tests/gateway/test_concurrent_read_write_regression.py, tests/gateway/test_cxp_send_receipt_check.py, tests/gateway/test_gateway_handler.py, tests/gateway/test_gateway_metrics.py, tests/gateway/test_handlers_behavioral.py, tests/gateway/test_memory_engine_wiring.py, tests/gateway/test_method_table.py, tests/gateway/test_orchestrator_evaluator_ingress.py, tests/gateway/test_protocol.py, tests/gateway/test_protocol_behavioral.py, tests/gateway/test_protocol_dispatch.py, tests/gateway/test_protocol_ws_integration.py, tests/gateway/test_publisher_wiring.py, tests/gateway/test_pubsub.py, tests/gateway/test_pubsub_behavioral.py, tests/gateway/test_reply_consumer_addressing.py … and 6 more

Class diagram

Class diagram orchestrata_ultimate.gateway.pubsub

Dependency diagram

Dependency diagram orchestrata_ultimate.gateway.pubsub

API

Classes

PubSubManager

Line 58

Tracks topic subscriptions per connection.

Method Signature Summary
__init__ __init__() -> None Initialise empty subscription store.
subscribe async subscribe(connection_id: str, topic_pattern: str, filters: dict[str, Any] | None = None) -> str Subscribe connection_id to topic_pattern.
unsubscribe async unsubscribe(connection_id: str, topic_pattern: str) -> bool Remove a single subscription by topic pattern.
unsubscribe_by_id async unsubscribe_by_id(subscription_id: str) -> bool Remove a subscription by its subscription_id.
unsubscribe_all async unsubscribe_all(connection_id: str) -> int Remove every subscription for a connection.
get_matching_connections async get_matching_connections(topic: str, payload: dict[str, Any] | None = None) -> list[str] Return connection IDs whose pattern (and filters) match topic.
get_subscriptions async get_subscriptions(connection_id: str) -> list[str] Return topic patterns for connection_id.
get_subscription_by_id get_subscription_by_id(subscription_id: str) -> Subscription | None Look up a subscription by ID.
get_total_subscription_count get_total_subscription_count() -> int Return total active subscriptions across clients.
publish async publish(topic: str, payload: dict[str, Any] | None = None) -> int Publish an event to all matching subscriptions.
_remove_sub_records _remove_sub_records(connection_id: str, topic: str) -> None Remove subscription records matching connection+topic.

Functions

Function Signature Summary
evaluate_filters traces evaluate_filters(payload: dict[str, Any], filters: dict[str, Any]) -> bool Evaluate whether payload satisfies a subscription's filters.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/gateway/pubsub.py

"""In-process pub/sub for WebSocket gateway topics."""

import asyncio
import fnmatch
import logging
from typing import Any

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.gateway.metrics import (
    subscription_added,
    subscription_removed,
)
from orchestrata_ultimate.gateway.models import Subscription

logger = logging.getLogger(__name__)


@traces(spec="014", story="US-001", reqs=["REQ-014-003", "REQ-014-005", "REQ-014-007"])
def evaluate_filters(payload: dict[str, Any], filters: dict[str, Any]) -> bool:
    """Evaluate whether *payload* satisfies a subscription's *filters*.

    Implements SPEC-014 FR-009's key-value predicates. An empty (or falsy)
    ``filters`` mapping matches every payload -- subscriptions created
    without filters keep receiving everything on their topic, unchanged.

    Multiple filter keys combine with AND semantics (REQ-014-007): every
    key must match for the payload to pass. Most keys are matched by plain
    equality against the top-level payload field of the same name. Two
    keys get spec-defined richer semantics:

    - ``subject``: fnmatch-style glob match against ``payload["subject"]``
      (REQ-014-005 -- "subject pattern (glob match)").
    - ``volk_id``: matches if *either* ``payload["to"]`` or
      ``payload["from"]`` equals the filter value (REQ-014-003 -- "volk_id
      (TO or FROM)"), since CXP-shaped events carry both fields.

    Args:
        payload: The event payload being considered for delivery.
        filters: The subscription's key-value filter predicates.

    Returns:
        True if payload matches every filter key (or filters is empty).
    """
    if not filters:
        return True
    for key, expected in filters.items():
        if key == "volk_id":
            if payload.get("to") != expected and payload.get("from") != expected:
                return False
        elif key == "subject":
            if not fnmatch.fnmatch(str(payload.get("subject", "")), str(expected)):
                return False
        elif payload.get(key) != expected:
            return False
    return True


class PubSubManager:
    """Tracks topic subscriptions per connection.

    Pattern matching uses fnmatch-style wildcards
    (e.g. ``agent.*`` matches ``agent.status``).

    Subscriptions are identified by a unique subscription_id
    and support optional key-value filters for event matching.
    """

    def __init__(self) -> None:
        """Initialise empty subscription store."""
        self._subs: dict[str, set[str]] = {}
        self._subscriptions: dict[str, Subscription] = {}
        self._lock = asyncio.Lock()

    async def subscribe(
        self,
        connection_id: str,
        topic_pattern: str,
        filters: dict[str, Any] | None = None,
    ) -> str:
        """Subscribe *connection_id* to *topic_pattern*.

        Args:
            connection_id: Connection to subscribe.
            topic_pattern: fnmatch-style topic pattern.
            filters: Optional key-value filters for events.

        Returns:
            The subscription_id for this subscription.

        Raises:
            TypeError: If arguments are not strings.
            ValueError: If arguments are empty.
        """
        if not isinstance(connection_id, str):
            raise TypeError("connection_id must be a str")
        if not connection_id:
            raise ValueError("connection_id must not be empty")
        if not isinstance(topic_pattern, str):
            raise TypeError("topic_pattern must be a str")
        if not topic_pattern:
            raise ValueError("topic_pattern must not be empty")
        async with self._lock:
            sub = Subscription(
                connection_id=connection_id,
                topic=topic_pattern,
                filters=filters or {},
            )
            self._subscriptions[sub.subscription_id] = sub
            patterns = self._subs.setdefault(connection_id, set())
            patterns.add(topic_pattern)
            logger.info(
                "%s subscribed to %s (sub=%s)",
                connection_id,
                topic_pattern,
                sub.subscription_id,
            )
            # REQ-014-147: live subscription count, labeled by topic.
            subscription_added(topic_pattern)
            return sub.subscription_id

    async def unsubscribe(
        self,
        connection_id: str,
        topic_pattern: str,
    ) -> bool:
        """Remove a single subscription by topic pattern.

        Args:
            connection_id: Connection to unsubscribe.
            topic_pattern: Exact pattern to remove.

        Returns:
            True if the pattern was present.

        Raises:
            TypeError: If arguments are not strings.
            ValueError: If arguments are empty.
        """
        if not isinstance(connection_id, str):
            raise TypeError("connection_id must be a str")
        if not connection_id:
            raise ValueError("connection_id must not be empty")
        if not isinstance(topic_pattern, str):
            raise TypeError("topic_pattern must be a str")
        if not topic_pattern:
            raise ValueError("topic_pattern must not be empty")
        async with self._lock:
            patterns = self._subs.get(connection_id)
            if not patterns:
                return False
            if topic_pattern not in patterns:
                return False
            patterns.discard(topic_pattern)
            if not patterns:
                del self._subs[connection_id]
            self._remove_sub_records(connection_id, topic_pattern)
            return True

    async def unsubscribe_by_id(
        self,
        subscription_id: str,
    ) -> bool:
        """Remove a subscription by its subscription_id.

        Args:
            subscription_id: ID returned from subscribe().

        Returns:
            True if the subscription existed and was removed.

        Raises:
            TypeError: If subscription_id is not a str.
            ValueError: If subscription_id is empty.
        """
        if not isinstance(subscription_id, str):
            raise TypeError("subscription_id must be a str")
        if not subscription_id:
            raise ValueError("subscription_id must not be empty")
        async with self._lock:
            sub = self._subscriptions.pop(subscription_id, None)
            if not sub:
                return False
            patterns = self._subs.get(sub.connection_id)
            if patterns:
                patterns.discard(sub.topic)
                if not patterns:
                    del self._subs[sub.connection_id]
            # REQ-014-147: one fewer live subscription on this topic.
            subscription_removed(sub.topic)
            return True

    @traces(
        spec="014",
        story="US-012",
        reqs=["REQ-014-196"],
    )
    async def unsubscribe_all(self, connection_id: str) -> int:
        """Remove every subscription for a connection.

        Args:
            connection_id: Connection to clear.

        Returns:
            Number of patterns removed.

        Raises:
            TypeError: If connection_id is not a str.
            ValueError: If connection_id is empty.
        """
        if not isinstance(connection_id, str):
            raise TypeError("connection_id must be a str")
        if not connection_id:
            raise ValueError("connection_id must not be empty")
        async with self._lock:
            patterns = self._subs.pop(connection_id, set())
            to_remove = [
                sid
                for sid, s in self._subscriptions.items()
                if s.connection_id == connection_id
            ]
            # REQ-014-147: one decrement per removed subscription record's
            # topic (not per pattern -- a connection can hold more than one
            # subscription record for the same topic pattern).
            for sid in to_remove:
                subscription_removed(self._subscriptions[sid].topic)
                del self._subscriptions[sid]
            return len(patterns)

    @traces(spec="014", story="US-001", reqs=["REQ-014-007"])
    async def get_matching_connections(
        self,
        topic: str,
        payload: dict[str, Any] | None = None,
    ) -> list[str]:
        """Return connection IDs whose pattern (and filters) match *topic*.

        Args:
            topic: Concrete topic name to match against.
            payload: Optional event payload. When given, a connection is
                included only if at least one of its subscriptions whose
                topic pattern matches *topic* also satisfies that
                subscription's ``filters`` against *payload* (SPEC-014
                FR-009). Subscriptions with no filters always pass once the
                pattern matches. When omitted, filters are not evaluated
                (topic-pattern matching only) -- preserves the historic
                behaviour for callers that only need the recipient set.

        Returns:
            List of matching connection IDs.

        Raises:
            TypeError: If topic is not a str.
            ValueError: If topic is empty.
        """
        if not isinstance(topic, str):
            raise TypeError("topic must be a str")
        if not topic:
            raise ValueError("topic must not be empty")
        # Snapshot under the lock — writers hold self._lock, so reading
        # _subscriptions without it can raise "dict changed size during
        # iteration".
        async with self._lock:
            snapshot = list(self._subscriptions.values())
        matches: list[str] = []
        seen: set[str] = set()
        for sub in snapshot:
            if sub.connection_id in seen:
                continue
            if not fnmatch.fnmatch(topic, sub.topic):
                continue
            if payload is not None and not evaluate_filters(payload, sub.filters):
                continue
            matches.append(sub.connection_id)
            seen.add(sub.connection_id)
        return matches

    async def get_subscriptions(self, connection_id: str) -> list[str]:
        """Return topic patterns for *connection_id*.

        Args:
            connection_id: Connection to query.

        Returns:
            Sorted list of subscribed topic patterns.

        Raises:
            TypeError: If connection_id is not a str.
            ValueError: If connection_id is empty.
        """
        if not isinstance(connection_id, str):
            raise TypeError("connection_id must be a str")
        if not connection_id:
            raise ValueError("connection_id must not be empty")
        # Copy under the lock — a concurrent (un)subscribe may mutate the
        # underlying set while sorted() iterates it otherwise.
        async with self._lock:
            patterns = set(self._subs.get(connection_id, set()))
        return sorted(patterns)

    def get_subscription_by_id(
        self,
        subscription_id: str,
    ) -> Subscription | None:
        """Look up a subscription by ID.

        Args:
            subscription_id: The subscription to find.

        Returns:
            The Subscription or None if not found.
        """
        return self._subscriptions.get(subscription_id)

    @traces(spec="014", story="US-012", reqs=["REQ-014-199"])
    def get_total_subscription_count(self) -> int:
        """Return total active subscriptions across clients."""
        return len(self._subscriptions)

    async def publish(
        self,
        topic: str,
        payload: dict[str, Any] | None = None,
    ) -> int:
        """Publish an event to all matching subscriptions.

        Args:
            topic: Event topic name.
            payload: Optional event payload.

        Returns:
            Number of matching connections.

        Raises:
            TypeError: If topic is not a str.
            ValueError: If topic is empty.
        """
        if not isinstance(topic, str):
            raise TypeError("topic must be a str")
        if not topic:
            raise ValueError("topic must not be empty")
        return len(await self.get_matching_connections(topic))

    def _remove_sub_records(
        self,
        connection_id: str,
        topic: str,
    ) -> None:
        """Remove subscription records matching connection+topic."""
        to_remove = [
            sid
            for sid, s in self._subscriptions.items()
            if s.connection_id == connection_id and s.topic == topic
        ]
        for sid in to_remove:
            del self._subscriptions[sid]
            # REQ-014-147: one fewer live subscription on this topic.
            subscription_removed(topic)