Files
dify/api/services/trigger/endpoint_service.py

113 lines
4.2 KiB
Python

"""Dispatch persisted trigger endpoints without holding a subscription transaction."""
import logging
import secrets
import time
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Any, Protocol
from flask import Request, Response
from core.plugin.entities.request import TriggerDispatchResponse
from services.trigger.subscription_service import TriggerSubscriptionSnapshot
from services.workflow.entities import PluginTriggerDispatchData
logger = logging.getLogger(__name__)
class TriggerEndpointLoadError(Exception):
"""A persisted subscription or its provider configuration could not be loaded."""
class TriggerEndpointProviderNotFoundError(Exception):
"""The subscription's plugin provider is no longer available."""
class TriggerEndpointEventNotFoundError(Exception):
"""The plugin dispatched an event absent from its provider declaration."""
class TriggerEndpointQuery(Protocol):
def get_by_endpoint(self, *, endpoint_id: str) -> TriggerSubscriptionSnapshot | None:
"""Return the capability's detached owner/configuration, or None when absent."""
...
@dataclass(frozen=True, slots=True)
class TriggerEndpointDispatch:
result: TriggerDispatchResponse
event_names: frozenset[str]
class TriggerEndpointGateway(Protocol):
def dispatch(self, *, subscription: TriggerSubscriptionSnapshot, request: Request) -> TriggerEndpointDispatch: ...
class TriggerEndpointRequestStore(Protocol):
def persist_request(self, request_id: str, request: Request) -> None: ...
def persist_payload(self, request_id: str, payload: Mapping[str, Any]) -> None: ...
class TriggerEndpointService:
def __init__(
self,
*,
subscriptions: TriggerEndpointQuery,
provider: TriggerEndpointGateway,
requests: TriggerEndpointRequestStore,
publish: Callable[[PluginTriggerDispatchData], None],
) -> None:
self._subscriptions = subscriptions
self._provider = provider
self._requests = requests
self._publish = publish
def process(self, endpoint_id: str, request: Request) -> Response | None:
"""Return the plugin response, or None so the caller can try a temporary builder."""
timestamp = int(time.time())
try:
subscription = self._subscriptions.get_by_endpoint(endpoint_id=endpoint_id)
except Exception as error:
raise TriggerEndpointLoadError(f"Failed to load trigger endpoint {endpoint_id}.") from error
if subscription is None:
return None
dispatched = self._provider.dispatch(subscription=subscription, request=request)
result = dispatched.result
if result.events:
request_id = f"trigger_request_{timestamp}_{secrets.token_hex(6)}"
self._requests.persist_request(request_id, request)
self._requests.persist_payload(request_id, result.payload)
for event_name in result.events:
if event_name not in dispatched.event_names:
logger.error(
"Event name %s not found in provider %s for endpoint %s",
event_name,
subscription.provider_id,
endpoint_id,
)
raise TriggerEndpointEventNotFoundError(
f"Event name {event_name} not found in provider {subscription.provider_id}"
)
self._publish(
PluginTriggerDispatchData(
user_id=result.user_id,
tenant_id=subscription.tenant_id,
endpoint_id=subscription.endpoint_id,
provider_id=subscription.provider_id,
subscription_id=subscription.id,
timestamp=timestamp,
events=list(result.events),
request_id=request_id,
)
)
logger.info(
"Queued async dispatching for %d triggers on endpoint %s with request_id %s",
len(result.events),
endpoint_id,
request_id,
)
return result.response