Files

148 lines
6.5 KiB
Python

"""Poll trigger debug inputs using injected event and schedule runtime stores."""
from __future__ import annotations
import hashlib
from collections.abc import Callable, Iterable, Mapping
from datetime import datetime
from typing import Any, Protocol
from pydantic import BaseModel
from core.plugin.entities.request import TriggerInvokeEventResponse
from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE, TRIGGER_SCHEDULE_NODE_TYPE, TRIGGER_WEBHOOK_NODE_TYPE
from core.workflow.nodes.trigger_plugin.entities import TriggerEventNodeData
from graphon.entities.graph_config import NodeConfigDict
from libs.datetime_utils import ensure_naive_utc
from libs.schedule_utils import calculate_next_run_at
from models.provider_ids import TriggerProviderID
from services.trigger.debug_events import (
PluginTriggerDebugEvent,
WebhookDebugEvent,
build_plugin_pool_key,
build_webhook_pool_key,
)
from services.trigger.debug_store import ScheduleDebugRuntime, ScheduleDebugRuntimeStore, TriggerDebugEventStore
from services.trigger.schedule_service import ScheduleService
from services.trigger.webhook_service import build_webhook_workflow_inputs
class TriggerDebugEvent(BaseModel):
workflow_args: Mapping[str, Any]
node_id: str
class PluginTriggerEventInvoker(Protocol):
def __call__(
self, *, tenant_id: str, user_id: str, node_config: NodeConfigDict, event: PluginTriggerDebugEvent
) -> TriggerInvokeEventResponse: ...
class TriggerDebugService:
"""Route debug events and schedule ticks through injected runtime stores."""
def __init__(
self,
*,
events: TriggerDebugEventStore,
schedules: ScheduleDebugRuntimeStore,
invoke_plugin_event: PluginTriggerEventInvoker,
now: Callable[[], datetime],
) -> None:
self._events = events
self._schedules = schedules
self._invoke_plugin_event = invoke_plugin_event
self._now = now
def dispatch_webhook_debug(self, *, tenant_id: str, app_id: str, event: WebhookDebugEvent) -> int:
"""Deliver an event to waiting listeners for its tenant, app, and node."""
pool_key = build_webhook_pool_key(tenant_id, app_id, event.node_id)
return self._events.dispatch(tenant_id=tenant_id, event=event, pool_key=pool_key)
def poll(
self, *, node_config: NodeConfigDict, tenant_id: str, user_id: str, app_id: str
) -> TriggerDebugEvent | None:
"""Poll a supported trigger node for this user; return None when no event is ready.
Unsupported node types raise ValueError; store and plugin errors propagate.
"""
node_type = node_config["data"].type
node_id = node_config["id"]
if node_type == TRIGGER_PLUGIN_NODE_TYPE:
return self._poll_plugin(node_config=node_config, tenant_id=tenant_id, user_id=user_id, app_id=app_id)
if node_type == TRIGGER_WEBHOOK_NODE_TYPE:
event = self._events.poll(
event_type=WebhookDebugEvent,
pool_key=build_webhook_pool_key(tenant_id, app_id, node_id),
tenant_id=tenant_id,
user_id=user_id,
app_id=app_id,
node_id=node_id,
)
if event is None:
return None
inputs = event.payload.get("inputs")
if inputs is None:
inputs = build_webhook_workflow_inputs(event.payload.get("webhook_data", {}))
return TriggerDebugEvent(workflow_args={"inputs": inputs or {}, "files": []}, node_id=node_id)
if node_type == TRIGGER_SCHEDULE_NODE_TYPE:
return self._poll_schedule(node_config=node_config, tenant_id=tenant_id, user_id=user_id, app_id=app_id)
raise ValueError(f"unable to create event poller for node type {node_type}")
def select_events(
self, *, node_configs: Iterable[NodeConfigDict], tenant_id: str, user_id: str, app_id: str
) -> TriggerDebugEvent | None:
"""Poll in graph-request order and stop after the first available event."""
for node_config in node_configs:
event = self.poll(node_config=node_config, tenant_id=tenant_id, user_id=user_id, app_id=app_id)
if event is not None:
return event
return None
def _poll_plugin(
self, *, node_config: NodeConfigDict, tenant_id: str, user_id: str, app_id: str
) -> TriggerDebugEvent | None:
"""Consume and invoke a plugin event; cancelled or absent events return None."""
node = TriggerEventNodeData.model_validate(node_config["data"], from_attributes=True)
pool_key = build_plugin_pool_key(
tenant_id, str(TriggerProviderID(node.provider_id)), node.subscription_id, node.event_name
)
event = self._events.poll(
event_type=PluginTriggerDebugEvent,
pool_key=pool_key,
tenant_id=tenant_id,
user_id=user_id,
app_id=app_id,
node_id=node_config["id"],
)
if event is None:
return None
response = self._invoke_plugin_event(
tenant_id=tenant_id, user_id=event.user_id, node_config=node_config, event=event
)
if response.cancelled:
return None
return TriggerDebugEvent(workflow_args={"inputs": response.variables, "files": []}, node_id=node_config["id"])
def _poll_schedule(
self, *, node_config: NodeConfigDict, tenant_id: str, user_id: str, app_id: str
) -> TriggerDebugEvent | None:
"""Initialize or consume a user's schedule tick according to its stored next run time."""
config = ScheduleService.to_schedule_config(node_config)
cron_hash = hashlib.sha256(config.cron_expression.encode()).hexdigest()
cache_key = f"schedule_debug_runtime:{tenant_id}:{user_id}:{app_id}:{node_config['id']}:{cron_hash}"
runtime = self._schedules.get_runtime(cache_key)
now = self._now()
if runtime is None:
runtime = ScheduleDebugRuntime(
cache_key=cache_key,
timezone=config.timezone,
cron_expression=config.cron_expression,
next_run_at=ensure_naive_utc(calculate_next_run_at(config.cron_expression, config.timezone, now)),
)
self._schedules.save_runtime(runtime)
if ensure_naive_utc(runtime.next_run_at) > ensure_naive_utc(now):
return None
self._schedules.delete_runtime(cache_key)
return TriggerDebugEvent(workflow_args={"inputs": {}, "files": []}, node_id=node_config["id"])