Sfoglia il codice sorgente

fix: optimize execution history and restore scheduler on restart

- Replace 5-second HTTP polling with WebSocket for execution list updates
- Remove input data duplication in nodeExecutions storage (reduces response size)
- Enable gzip compression for HTTP responses (CPPHTTPLIB_ZLIB_SUPPORT)
- Load active scheduled workflows from database on webserver startup

Fixes slow execution detail loading (~188kB -> ~50kB) and missing
scheduled workflow executions after service restart.
fszontagh 6 mesi fa
parent
commit
b62c1e1f4c

+ 5 - 0
CMakeLists.txt

@@ -198,6 +198,10 @@ target_include_directories(smartbotic-webserver PRIVATE
     ${CMAKE_CURRENT_SOURCE_DIR}/src
     ${CMAKE_CURRENT_BINARY_DIR}
 )
+# Enable gzip compression for HTTP responses
+target_compile_definitions(smartbotic-webserver PRIVATE
+    CPPHTTPLIB_ZLIB_SUPPORT
+)
 target_link_libraries(smartbotic-webserver PRIVATE
     smartbotic_common
     smartbotic_logging
@@ -211,6 +215,7 @@ target_link_libraries(smartbotic-webserver PRIVATE
     gRPC::grpc++
     OpenSSL::SSL
     OpenSSL::Crypto
+    ZLIB::ZLIB
 )
 
 # Runner service

+ 1 - 0
cmake/FindPackages.cmake

@@ -5,6 +5,7 @@ find_package(PkgConfig REQUIRED)
 find_package(Threads REQUIRED)
 find_package(OpenSSL REQUIRED)
 find_package(CURL REQUIRED)
+find_package(ZLIB REQUIRED)
 
 # nlohmann-json
 find_package(nlohmann_json 3.11 QUIET)

+ 2 - 1
src/runner/workflow_engine.cpp

@@ -87,7 +87,8 @@ nlohmann::json ExecutionResult::toJson() const {
         nr["status"] = nodeStatusToString(result.status);
         nr["startedAt"] = result.started_at;
         nr["finishedAt"] = result.finished_at;
-        nr["input"] = result.input;
+        // Note: input is intentionally not stored to avoid data duplication
+        // Each node's input can be reconstructed from upstream node outputs + connections
         nr["output"] = result.output;
         nr["error"] = result.error;
         nr["retryCount"] = result.retry_count;

+ 61 - 0
src/webserver/webserver_service.cpp

@@ -147,6 +147,9 @@ void WebServerService::start() {
     // Start workflow scheduler
     scheduler_->start();
 
+    // Load active workflows from database and register with scheduler
+    loadScheduledWorkflows();
+
     // Start NodeSync gRPC server for runners
     node_sync_server_->start();
 
@@ -227,6 +230,64 @@ void WebServerService::setupRoutes() {
     LOG_INFO("API routes registered");
 }
 
+void WebServerService::loadScheduledWorkflows() {
+    LOG_INFO("Loading scheduled workflows from database...");
+
+    // Query all active workflows
+    storage::QueryOptions options;
+    options.filters.push_back({"active", true});
+    options.page_size = 1000;  // Load up to 1000 workflows
+
+    auto result = storage_->query("workflows", options);
+    if (result.failed()) {
+        LOG_ERROR("Failed to load workflows: {}", result.error().message());
+        return;
+    }
+
+    int registered_count = 0;
+    for (const auto& workflow : result.value().documents) {
+        std::string workflow_id = workflow.value("_id", "");
+        std::string workflow_name = workflow.value("name", "");
+        auto nodes = workflow.value("nodes", nlohmann::json::array());
+
+        // Find scheduled trigger nodes
+        for (const auto& node : nodes) {
+            std::string node_id = node.value("id", "");
+            std::string node_type = node.value("type", "");
+
+            // Get node definition to check if it's a scheduled trigger
+            auto node_result = node_store_->get(node_type);
+            if (node_result.failed()) {
+                continue;
+            }
+
+            const auto& node_def = node_result.value();
+            if (!node_def.is_trigger || !node_def.is_scheduled) {
+                continue;
+            }
+
+            // Get interval from node config
+            auto config = node.value("config", nlohmann::json::object());
+            int interval = config.value("pollInterval", 0);
+
+            if (interval > 0) {
+                scheduler_->registerWorkflow(
+                    workflow_id,
+                    workflow_name,
+                    node_id,
+                    node_type,
+                    interval
+                );
+                registered_count++;
+                LOG_DEBUG("Registered workflow '{}' ({}) for scheduled execution every {} minutes",
+                         workflow_name, workflow_id, interval);
+            }
+        }
+    }
+
+    LOG_INFO("Loaded {} scheduled workflows", registered_count);
+}
+
 void WebServerService::executeScheduledWorkflow(const std::string& workflow_id,
                                                  const std::string& trigger_node_id,
                                                  const std::string& trigger_type) {

+ 1 - 0
src/webserver/webserver_service.hpp

@@ -75,6 +75,7 @@ public:
 
 private:
     void setupRoutes();
+    void loadScheduledWorkflows();
     void executeScheduledWorkflow(const std::string& workflow_id,
                                   const std::string& trigger_node_id,
                                   const std::string& trigger_type);

+ 2 - 1
webui/src/api/workflows.ts

@@ -255,7 +255,8 @@ export interface NodeExecution {
   status: string
   startedAt: number
   finishedAt: number
-  input: any
+  // Note: input is not stored to avoid data duplication (each node's input = upstream outputs)
+  input?: any
   output: any
   error?: string
   retryCount?: number

+ 4 - 1
webui/src/components/workflow/ExecutionListPanel.tsx

@@ -14,6 +14,7 @@ import {
   ChevronRight,
 } from 'lucide-react'
 import { executionsApi, type ExecutionListItem, type ExecutionDetail } from '../../api/workflows'
+import { useExecutionListUpdates } from '../../hooks/useExecutionListUpdates'
 
 interface ExecutionListPanelProps {
   workflowId: string
@@ -80,9 +81,11 @@ export function ExecutionListPanel({
     queryKey: ['executions', workflowId, page],
     queryFn: () => executionsApi.list(workflowId, undefined, page, PAGE_SIZE),
     enabled: isOpen && !!workflowId,
-    refetchInterval: 5000,
   })
 
+  // Subscribe to WebSocket for real-time updates instead of polling
+  useExecutionListUpdates({ workflowId, enabled: isOpen && !!workflowId })
+
   const handleAction = async (
     executionId: string,
     action: 'view' | 'pin'

+ 88 - 0
webui/src/hooks/useExecutionListUpdates.ts

@@ -0,0 +1,88 @@
+import { useEffect, useCallback, useRef } from 'react'
+import { useQueryClient } from '@tanstack/react-query'
+import { wsClient } from '../api/client'
+
+interface UseExecutionListUpdatesOptions {
+  /** Workflow ID to filter updates (optional - if not provided, listens to all executions) */
+  workflowId?: string
+  /** Whether the hook is active */
+  enabled?: boolean
+}
+
+/**
+ * Hook that subscribes to WebSocket execution events and invalidates
+ * the execution list query when relevant events occur.
+ *
+ * Replaces polling with real-time WebSocket updates.
+ */
+export function useExecutionListUpdates(options: UseExecutionListUpdatesOptions = {}) {
+  const { workflowId, enabled = true } = options
+  const queryClient = useQueryClient()
+  const handlerRef = useRef<((data: any) => void) | null>(null)
+
+  const handleExecutionEvent = useCallback((data: any) => {
+    const channel = data._channel || ''
+
+    // Only process lifecycle events that affect the list
+    const isLifecycleEvent =
+      channel.includes('.started') ||
+      channel.includes('.completed') ||
+      channel.includes('.failed') ||
+      channel.includes('.cancelled')
+
+    if (!isLifecycleEvent) return
+
+    // If filtering by workflowId, check if this event is for our workflow
+    if (workflowId && data.workflowId && data.workflowId !== workflowId) {
+      return
+    }
+
+    // Invalidate execution list queries to trigger a refetch
+    // This is more efficient than polling every 5 seconds
+    if (workflowId) {
+      // Invalidate specific workflow's execution list
+      queryClient.invalidateQueries({ queryKey: ['executions', workflowId] })
+    } else {
+      // Invalidate all execution lists
+      queryClient.invalidateQueries({ queryKey: ['executions'] })
+    }
+  }, [workflowId, queryClient])
+
+  useEffect(() => {
+    if (!enabled) return
+
+    // Store handler ref for cleanup
+    handlerRef.current = handleExecutionEvent
+
+    // Subscribe to all execution events
+    wsClient.subscribe(['executions.*'])
+    wsClient.on('executions.*', handleExecutionEvent)
+
+    return () => {
+      if (handlerRef.current) {
+        wsClient.off('executions.*', handlerRef.current)
+      }
+      wsClient.unsubscribe(['executions.*'])
+    }
+  }, [enabled, handleExecutionEvent])
+
+  // Handle reconnection - refetch to sync state
+  useEffect(() => {
+    if (!enabled) return
+
+    const handleReconnect = () => {
+      // On reconnect, invalidate to get fresh data
+      if (workflowId) {
+        queryClient.invalidateQueries({ queryKey: ['executions', workflowId] })
+      } else {
+        queryClient.invalidateQueries({ queryKey: ['executions'] })
+      }
+    }
+
+    wsClient.onReconnect(handleReconnect)
+
+    return () => {
+      wsClient.offReconnect(handleReconnect)
+    }
+  }, [enabled, workflowId, queryClient])
+}

+ 4 - 1
webui/src/pages/ExecutionsPage.tsx

@@ -1,6 +1,7 @@
 import { useState } from 'react'
 import { useQuery } from '@tanstack/react-query'
 import { executionsApi } from '../api/workflows'
+import { useExecutionListUpdates } from '../hooks/useExecutionListUpdates'
 import { formatDistanceToNow } from 'date-fns'
 import { CheckCircle, XCircle, Clock, Ban, RefreshCw, ChevronLeft, ChevronRight } from 'lucide-react'
 import clsx from 'clsx'
@@ -25,9 +26,11 @@ export default function ExecutionsPage() {
   const { data, isLoading, refetch } = useQuery({
     queryKey: ['executions', page],
     queryFn: () => executionsApi.list(undefined, undefined, page, PAGE_SIZE),
-    refetchInterval: 5000,
   })
 
+  // Subscribe to WebSocket for real-time updates instead of polling
+  useExecutionListUpdates()
+
   const executions = data?.executions || []
   const total = data?.total || 0
   const totalPages = Math.ceil(total / PAGE_SIZE)