tasker-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +443 -0
- data/Rakefile +10 -0
- data/app/controllers/tasker/analytics_controller.rb +179 -0
- data/app/controllers/tasker/application_controller.rb +45 -0
- data/app/controllers/tasker/graphql_controller.rb +193 -0
- data/app/controllers/tasker/handlers_controller.rb +217 -0
- data/app/controllers/tasker/health_controller.rb +229 -0
- data/app/controllers/tasker/metrics_controller.rb +111 -0
- data/app/controllers/tasker/page_sort.rb +97 -0
- data/app/controllers/tasker/task_diagrams_controller.rb +30 -0
- data/app/controllers/tasker/tasks_controller.rb +123 -0
- data/app/controllers/tasker/workflow_steps_controller.rb +69 -0
- data/app/graphql/examples/all_tasks.graphql +22 -0
- data/app/graphql/examples/pending_tasks.graphql +23 -0
- data/app/graphql/tasker/graph_ql_types/annotation_type.rb +14 -0
- data/app/graphql/tasker/graph_ql_types/base_argument.rb +9 -0
- data/app/graphql/tasker/graph_ql_types/base_connection.rb +11 -0
- data/app/graphql/tasker/graph_ql_types/base_edge.rb +10 -0
- data/app/graphql/tasker/graph_ql_types/base_enum.rb +9 -0
- data/app/graphql/tasker/graph_ql_types/base_field.rb +10 -0
- data/app/graphql/tasker/graph_ql_types/base_input_object.rb +10 -0
- data/app/graphql/tasker/graph_ql_types/base_interface.rb +14 -0
- data/app/graphql/tasker/graph_ql_types/base_object.rb +10 -0
- data/app/graphql/tasker/graph_ql_types/base_scalar.rb +9 -0
- data/app/graphql/tasker/graph_ql_types/base_union.rb +11 -0
- data/app/graphql/tasker/graph_ql_types/dependent_system_object_map_type.rb +18 -0
- data/app/graphql/tasker/graph_ql_types/dependent_system_type.rb +13 -0
- data/app/graphql/tasker/graph_ql_types/mutation_type.rb +16 -0
- data/app/graphql/tasker/graph_ql_types/named_step_type.rb +16 -0
- data/app/graphql/tasker/graph_ql_types/named_task_type.rb +14 -0
- data/app/graphql/tasker/graph_ql_types/named_tasks_named_step_type.rb +19 -0
- data/app/graphql/tasker/graph_ql_types/node_type.rb +12 -0
- data/app/graphql/tasker/graph_ql_types/query_type.rb +20 -0
- data/app/graphql/tasker/graph_ql_types/task_annotation_type.rb +17 -0
- data/app/graphql/tasker/graph_ql_types/task_interface.rb +17 -0
- data/app/graphql/tasker/graph_ql_types/task_type.rb +26 -0
- data/app/graphql/tasker/graph_ql_types/workflow_step_type.rb +154 -0
- data/app/graphql/tasker/graph_ql_types.rb +42 -0
- data/app/graphql/tasker/mutations/base_mutation.rb +13 -0
- data/app/graphql/tasker/mutations/cancel_step.rb +29 -0
- data/app/graphql/tasker/mutations/cancel_task.rb +29 -0
- data/app/graphql/tasker/mutations/create_task.rb +52 -0
- data/app/graphql/tasker/mutations/update_step.rb +36 -0
- data/app/graphql/tasker/mutations/update_task.rb +41 -0
- data/app/graphql/tasker/queries/all_annotation_types.rb +17 -0
- data/app/graphql/tasker/queries/all_tasks.rb +23 -0
- data/app/graphql/tasker/queries/base_query.rb +9 -0
- data/app/graphql/tasker/queries/helpers.rb +16 -0
- data/app/graphql/tasker/queries/one_step.rb +24 -0
- data/app/graphql/tasker/queries/one_task.rb +18 -0
- data/app/graphql/tasker/queries/tasks_by_annotation.rb +31 -0
- data/app/graphql/tasker/queries/tasks_by_status.rb +30 -0
- data/app/graphql/tasker/tasker_rails_schema.rb +52 -0
- data/app/jobs/tasker/application_job.rb +8 -0
- data/app/jobs/tasker/metrics_export_job.rb +252 -0
- data/app/jobs/tasker/task_runner_job.rb +224 -0
- data/app/models/tasker/annotation_type.rb +26 -0
- data/app/models/tasker/application_record.rb +70 -0
- data/app/models/tasker/dependent_system.rb +26 -0
- data/app/models/tasker/dependent_system_object_map.rb +64 -0
- data/app/models/tasker/diagram/edge.rb +106 -0
- data/app/models/tasker/diagram/flowchart.rb +137 -0
- data/app/models/tasker/diagram/node.rb +99 -0
- data/app/models/tasker/named_step.rb +41 -0
- data/app/models/tasker/named_task.rb +121 -0
- data/app/models/tasker/named_tasks_named_step.rb +82 -0
- data/app/models/tasker/step_dag_relationship.rb +65 -0
- data/app/models/tasker/step_readiness_status.rb +59 -0
- data/app/models/tasker/task.rb +424 -0
- data/app/models/tasker/task_annotation.rb +36 -0
- data/app/models/tasker/task_diagram.rb +332 -0
- data/app/models/tasker/task_execution_context.rb +29 -0
- data/app/models/tasker/task_namespace.rb +41 -0
- data/app/models/tasker/task_transition.rb +235 -0
- data/app/models/tasker/workflow_step.rb +461 -0
- data/app/models/tasker/workflow_step_edge.rb +94 -0
- data/app/models/tasker/workflow_step_transition.rb +434 -0
- data/app/serializers/tasker/annotation_type_serializer.rb +8 -0
- data/app/serializers/tasker/handler_serializer.rb +109 -0
- data/app/serializers/tasker/task_annotation_serializer.rb +32 -0
- data/app/serializers/tasker/task_serializer.rb +168 -0
- data/app/serializers/tasker/workflow_step_serializer.rb +27 -0
- data/app/services/tasker/analytics_service.rb +409 -0
- data/app/views/tasker/task/_diagram.html.erb +32 -0
- data/config/initializers/dry_struct.rb +11 -0
- data/config/initializers/statesman.rb +6 -0
- data/config/initializers/tasker_orchestration.rb +17 -0
- data/config/initializers/time_formats.rb +4 -0
- data/config/routes.rb +34 -0
- data/config/tasker/subscriptions/example_integrations.yml +67 -0
- data/config/tasker/system_events.yml +305 -0
- data/db/functions/calculate_dependency_levels_v01.sql +45 -0
- data/db/functions/get_analytics_metrics_v01.sql +137 -0
- data/db/functions/get_slowest_steps_v01.sql +82 -0
- data/db/functions/get_slowest_tasks_v01.sql +96 -0
- data/db/functions/get_step_readiness_status_batch_v01.sql +140 -0
- data/db/functions/get_step_readiness_status_v01.sql +139 -0
- data/db/functions/get_system_health_counts_v01.sql +108 -0
- data/db/functions/get_task_execution_context_v01.sql +108 -0
- data/db/functions/get_task_execution_contexts_batch_v01.sql +104 -0
- data/db/init/schema.sql +2277 -0
- data/db/migrate/20250701165431_initial_tasker_schema.rb +116 -0
- data/db/views/tasker_step_dag_relationships_v01.sql +69 -0
- data/docs/APPLICATION_GENERATOR.md +384 -0
- data/docs/AUTH.md +1780 -0
- data/docs/CIRCUIT_BREAKER.md +224 -0
- data/docs/DEVELOPER_GUIDE.md +2665 -0
- data/docs/EVENT_SYSTEM.md +637 -0
- data/docs/EXECUTION_CONFIGURATION.md +341 -0
- data/docs/FLOW_CHART.md +149 -0
- data/docs/HEALTH.md +542 -0
- data/docs/METRICS.md +731 -0
- data/docs/OPTIMIZATION_PLAN.md +1479 -0
- data/docs/OVERVIEW.md +552 -0
- data/docs/QUICK_START.md +270 -0
- data/docs/REGISTRY_SYSTEMS.md +373 -0
- data/docs/REST_API.md +632 -0
- data/docs/ROADMAP.md +221 -0
- data/docs/SQL_FUNCTIONS.md +1408 -0
- data/docs/TASK_DIAGRAM.md +252 -0
- data/docs/TASK_EXECUTION_CONTROL_FLOW.md +237 -0
- data/docs/TELEMETRY.md +795 -0
- data/docs/TROUBLESHOOTING.md +756 -0
- data/docs/TaskHandlerGenerator.html +255 -0
- data/docs/Tasker/Analysis/RuntimeGraphAnalyzer.html +907 -0
- data/docs/Tasker/Analysis/TemplateGraphAnalyzer.html +1236 -0
- data/docs/Tasker/Analysis.html +117 -0
- data/docs/Tasker/AnalyticsController.html +450 -0
- data/docs/Tasker/AnalyticsService/BottleneckAnalytics.html +816 -0
- data/docs/Tasker/AnalyticsService/PerformanceAnalytics.html +586 -0
- data/docs/Tasker/AnalyticsService.html +2221 -0
- data/docs/Tasker/AnnotationType.html +137 -0
- data/docs/Tasker/AnnotationTypeSerializer.html +124 -0
- data/docs/Tasker/ApplicationController.html +147 -0
- data/docs/Tasker/ApplicationJob.html +128 -0
- data/docs/Tasker/ApplicationRecord.html +378 -0
- data/docs/Tasker/Authentication/AuthenticationError.html +124 -0
- data/docs/Tasker/Authentication/ConfigurationError.html +124 -0
- data/docs/Tasker/Authentication/Coordinator.html +242 -0
- data/docs/Tasker/Authentication/Interface.html +560 -0
- data/docs/Tasker/Authentication/InterfaceError.html +124 -0
- data/docs/Tasker/Authentication/NoneAuthenticator.html +338 -0
- data/docs/Tasker/Authentication.html +119 -0
- data/docs/Tasker/Authorization/AuthorizationError.html +139 -0
- data/docs/Tasker/Authorization/BaseCoordinator.html +927 -0
- data/docs/Tasker/Authorization/ConfigurationError.html +153 -0
- data/docs/Tasker/Authorization/ResourceConstants/ACTIONS.html +428 -0
- data/docs/Tasker/Authorization/ResourceConstants/RESOURCES.html +365 -0
- data/docs/Tasker/Authorization/ResourceConstants.html +146 -0
- data/docs/Tasker/Authorization/ResourceRegistry.html +882 -0
- data/docs/Tasker/Authorization/UnauthorizedError.html +153 -0
- data/docs/Tasker/Authorization.html +582 -0
- data/docs/Tasker/CacheCapabilities.html +167 -0
- data/docs/Tasker/CacheStrategy.html +1297 -0
- data/docs/Tasker/Concerns/Authenticatable.html +116 -0
- data/docs/Tasker/Concerns/Authorizable/AdminStatusChecker.html +256 -0
- data/docs/Tasker/Concerns/Authorizable.html +816 -0
- data/docs/Tasker/Concerns/ControllerAuthorizable.html +157 -0
- data/docs/Tasker/Concerns/EventPublisher.html +4023 -0
- data/docs/Tasker/Concerns/IdempotentStateTransitions.html +806 -0
- data/docs/Tasker/Concerns/LifecycleEventHelpers.html +129 -0
- data/docs/Tasker/Concerns/OrchestrationPublisher.html +129 -0
- data/docs/Tasker/Concerns/StateMachineBase/ClassMethods.html +1075 -0
- data/docs/Tasker/Concerns/StateMachineBase/StateMachineBase/ClassMethods.html +191 -0
- data/docs/Tasker/Concerns/StateMachineBase/StateMachineBase.html +126 -0
- data/docs/Tasker/Concerns/StateMachineBase.html +153 -0
- data/docs/Tasker/Concerns/StructuredLogging.html +1413 -0
- data/docs/Tasker/Concerns.html +117 -0
- data/docs/Tasker/Configuration/AuthConfiguration.html +1023 -0
- data/docs/Tasker/Configuration/ConfigurationProxy.html +581 -0
- data/docs/Tasker/Configuration/DatabaseConfiguration.html +475 -0
- data/docs/Tasker/Configuration/EngineConfiguration.html +1265 -0
- data/docs/Tasker/Configuration/HealthConfiguration.html +791 -0
- data/docs/Tasker/Configuration/TelemetryConfiguration.html +1308 -0
- data/docs/Tasker/Configuration/TelemetryConfigurationProxy.html +388 -0
- data/docs/Tasker/Configuration.html +1669 -0
- data/docs/Tasker/ConfigurationError.html +143 -0
- data/docs/Tasker/ConfiguredTask.html +514 -0
- data/docs/Tasker/Constants/EventDefinitions.html +590 -0
- data/docs/Tasker/Constants/LifecycleEvents.html +137 -0
- data/docs/Tasker/Constants/ObservabilityEvents/Step.html +152 -0
- data/docs/Tasker/Constants/ObservabilityEvents/Task.html +142 -0
- data/docs/Tasker/Constants/ObservabilityEvents.html +126 -0
- data/docs/Tasker/Constants/RegistryEvents.html +285 -0
- data/docs/Tasker/Constants/StepEvents.html +177 -0
- data/docs/Tasker/Constants/TaskEvents.html +167 -0
- data/docs/Tasker/Constants/TaskExecution/ExecutionStatus.html +207 -0
- data/docs/Tasker/Constants/TaskExecution/HealthStatus.html +191 -0
- data/docs/Tasker/Constants/TaskExecution/RecommendedAction.html +207 -0
- data/docs/Tasker/Constants/TaskExecution.html +126 -0
- data/docs/Tasker/Constants/TaskFinalization/ErrorMessages.html +132 -0
- data/docs/Tasker/Constants/TaskFinalization/PendingReasons.html +207 -0
- data/docs/Tasker/Constants/TaskFinalization/ReenqueueReasons.html +239 -0
- data/docs/Tasker/Constants/TaskFinalization.html +126 -0
- data/docs/Tasker/Constants/TaskStatuses.html +223 -0
- data/docs/Tasker/Constants/TestEvents.html +163 -0
- data/docs/Tasker/Constants/WorkflowEvents.html +222 -0
- data/docs/Tasker/Constants/WorkflowStepStatuses.html +223 -0
- data/docs/Tasker/Constants.html +561 -0
- data/docs/Tasker/DependentSystem.html +137 -0
- data/docs/Tasker/DependentSystemObjectMap.html +250 -0
- data/docs/Tasker/DetectorRegistry.html +598 -0
- data/docs/Tasker/Diagram/Edge.html +1191 -0
- data/docs/Tasker/Diagram/Flowchart.html +1539 -0
- data/docs/Tasker/Diagram/Node.html +1165 -0
- data/docs/Tasker/Diagram.html +117 -0
- data/docs/Tasker/Engine.html +215 -0
- data/docs/Tasker/Error.html +139 -0
- data/docs/Tasker/Events/Bus.html +1226 -0
- data/docs/Tasker/Events/Catalog/CatalogPrinter.html +258 -0
- data/docs/Tasker/Events/Catalog/CustomEventRegistrar.html +276 -0
- data/docs/Tasker/Events/Catalog/ExamplePayloadGenerator.html +294 -0
- data/docs/Tasker/Events/Catalog.html +1291 -0
- data/docs/Tasker/Events/CustomRegistry.html +943 -0
- data/docs/Tasker/Events/DefinitionLoader.html +575 -0
- data/docs/Tasker/Events/EventPayloadBuilder/ErrorInfoExtractor.html +286 -0
- data/docs/Tasker/Events/EventPayloadBuilder/StepPayloadBuilder.html +312 -0
- data/docs/Tasker/Events/EventPayloadBuilder.html +664 -0
- data/docs/Tasker/Events/Publisher.html +365 -0
- data/docs/Tasker/Events/Subscribers/BaseSubscriber/ErrorCategorizer/ErrorTypeClassifier.html +1128 -0
- data/docs/Tasker/Events/Subscribers/BaseSubscriber/ErrorCategorizer.html +270 -0
- data/docs/Tasker/Events/Subscribers/BaseSubscriber/MetricTagsExtractor.html +266 -0
- data/docs/Tasker/Events/Subscribers/BaseSubscriber.html +2556 -0
- data/docs/Tasker/Events/Subscribers/MetricsSubscriber.html +723 -0
- data/docs/Tasker/Events/Subscribers/TelemetrySubscriber.html +2251 -0
- data/docs/Tasker/Events/Subscribers.html +117 -0
- data/docs/Tasker/Events/SubscriptionLoader.html +493 -0
- data/docs/Tasker/Events.html +294 -0
- data/docs/Tasker/EventsGenerator.html +459 -0
- data/docs/Tasker/Functions/FunctionBasedAnalyticsMetrics/AnalyticsMetrics.html +135 -0
- data/docs/Tasker/Functions/FunctionBasedAnalyticsMetrics.html +412 -0
- data/docs/Tasker/Functions/FunctionBasedDependencyLevels.html +598 -0
- data/docs/Tasker/Functions/FunctionBasedSlowestSteps/SlowestStep.html +135 -0
- data/docs/Tasker/Functions/FunctionBasedSlowestSteps.html +453 -0
- data/docs/Tasker/Functions/FunctionBasedSlowestTasks/SlowestTask.html +135 -0
- data/docs/Tasker/Functions/FunctionBasedSlowestTasks.html +453 -0
- data/docs/Tasker/Functions/FunctionBasedStepReadinessStatus.html +1457 -0
- data/docs/Tasker/Functions/FunctionBasedSystemHealthCounts/HealthMetrics.html +135 -0
- data/docs/Tasker/Functions/FunctionBasedSystemHealthCounts.html +370 -0
- data/docs/Tasker/Functions/FunctionBasedTaskExecutionContext.html +1250 -0
- data/docs/Tasker/Functions/FunctionWrapper.html +479 -0
- data/docs/Tasker/Functions.html +117 -0
- data/docs/Tasker/Generators/AuthenticatorGenerator/UsageInstructionsFormatter.html +244 -0
- data/docs/Tasker/Generators/AuthenticatorGenerator.html +373 -0
- data/docs/Tasker/Generators/AuthorizationCoordinatorGenerator.html +430 -0
- data/docs/Tasker/Generators/SubscriberGenerator.html +377 -0
- data/docs/Tasker/Generators/TaskHandlerGenerator.html +263 -0
- data/docs/Tasker/Generators.html +117 -0
- data/docs/Tasker/GraphQLTypes/AnnotationType.html +132 -0
- data/docs/Tasker/GraphQLTypes/BaseArgument.html +124 -0
- data/docs/Tasker/GraphQLTypes/BaseConnection.html +124 -0
- data/docs/Tasker/GraphQLTypes/BaseEdge.html +130 -0
- data/docs/Tasker/GraphQLTypes/BaseEnum.html +124 -0
- data/docs/Tasker/GraphQLTypes/BaseField.html +124 -0
- data/docs/Tasker/GraphQLTypes/BaseInputObject.html +124 -0
- data/docs/Tasker/GraphQLTypes/BaseInterface.html +116 -0
- data/docs/Tasker/GraphQLTypes/BaseObject.html +128 -0
- data/docs/Tasker/GraphQLTypes/BaseScalar.html +124 -0
- data/docs/Tasker/GraphQLTypes/BaseUnion.html +124 -0
- data/docs/Tasker/GraphQLTypes/DependentSystemObjectMapType.html +132 -0
- data/docs/Tasker/GraphQLTypes/DependentSystemType.html +132 -0
- data/docs/Tasker/GraphQLTypes/MutationType.html +132 -0
- data/docs/Tasker/GraphQLTypes/NamedStepType.html +132 -0
- data/docs/Tasker/GraphQLTypes/NamedTaskType.html +132 -0
- data/docs/Tasker/GraphQLTypes/NamedTasksNamedStepType.html +132 -0
- data/docs/Tasker/GraphQLTypes/NodeType.html +118 -0
- data/docs/Tasker/GraphQLTypes/QueryType.html +139 -0
- data/docs/Tasker/GraphQLTypes/TaskAnnotationType.html +132 -0
- data/docs/Tasker/GraphQLTypes/TaskInterface.html +111 -0
- data/docs/Tasker/GraphQLTypes/TaskType.html +201 -0
- data/docs/Tasker/GraphQLTypes/WorkflowStepType.html +694 -0
- data/docs/Tasker/GraphQLTypes.html +130 -0
- data/docs/Tasker/GraphqlController.html +251 -0
- data/docs/Tasker/HandlerFactory.html +1518 -0
- data/docs/Tasker/HandlerSerializer.html +682 -0
- data/docs/Tasker/HandlersController.html +574 -0
- data/docs/Tasker/HashIdentityStrategy.html +278 -0
- data/docs/Tasker/Health/ReadinessChecker.html +712 -0
- data/docs/Tasker/Health/StatusChecker.html +653 -0
- data/docs/Tasker/Health.html +117 -0
- data/docs/Tasker/HealthController.html +523 -0
- data/docs/Tasker/IdentityStrategy.html +276 -0
- data/docs/Tasker/InvalidTaskHandlerConfig.html +135 -0
- data/docs/Tasker/LifecycleEvents/Events/Step.html +162 -0
- data/docs/Tasker/LifecycleEvents/Events/Task.html +162 -0
- data/docs/Tasker/LifecycleEvents/Events.html +204 -0
- data/docs/Tasker/LifecycleEvents/Publisher.html +132 -0
- data/docs/Tasker/LifecycleEvents.html +799 -0
- data/docs/Tasker/Logging/CorrelationIdGenerator.html +688 -0
- data/docs/Tasker/Logging.html +115 -0
- data/docs/Tasker/MetricsController.html +293 -0
- data/docs/Tasker/MetricsExportJob.html +414 -0
- data/docs/Tasker/Mutations/BaseMutation.html +128 -0
- data/docs/Tasker/Mutations/CancelStep.html +219 -0
- data/docs/Tasker/Mutations/CancelTask.html +221 -0
- data/docs/Tasker/Mutations/CreateTask.html +243 -0
- data/docs/Tasker/Mutations/UpdateStep.html +243 -0
- data/docs/Tasker/Mutations/UpdateTask.html +243 -0
- data/docs/Tasker/Mutations.html +117 -0
- data/docs/Tasker/NamedStep.html +216 -0
- data/docs/Tasker/NamedTask.html +910 -0
- data/docs/Tasker/NamedTasksNamedStep.html +435 -0
- data/docs/Tasker/Orchestration/BackoffCalculator.html +404 -0
- data/docs/Tasker/Orchestration/ConnectionBuilder/ConfigValidator.html +258 -0
- data/docs/Tasker/Orchestration/ConnectionBuilder.html +435 -0
- data/docs/Tasker/Orchestration/ConnectionPoolIntelligence.html +513 -0
- data/docs/Tasker/Orchestration/Coordinator.html +641 -0
- data/docs/Tasker/Orchestration/FutureStateAnalyzer.html +1045 -0
- data/docs/Tasker/Orchestration/Orchestrator.html +679 -0
- data/docs/Tasker/Orchestration/PluginIntegration.html +1127 -0
- data/docs/Tasker/Orchestration/ResponseProcessor.html +504 -0
- data/docs/Tasker/Orchestration/RetryHeaderParser.html +304 -0
- data/docs/Tasker/Orchestration/StepExecutor.html +995 -0
- data/docs/Tasker/Orchestration/StepSequenceFactory.html +644 -0
- data/docs/Tasker/Orchestration/TaskFinalizer/BlockageChecker.html +264 -0
- data/docs/Tasker/Orchestration/TaskFinalizer/ContextManager.html +254 -0
- data/docs/Tasker/Orchestration/TaskFinalizer/DelayCalculator.html +556 -0
- data/docs/Tasker/Orchestration/TaskFinalizer/FinalizationDecisionMaker.html +348 -0
- data/docs/Tasker/Orchestration/TaskFinalizer/FinalizationProcessor.html +286 -0
- data/docs/Tasker/Orchestration/TaskFinalizer/ReasonDeterminer.html +432 -0
- data/docs/Tasker/Orchestration/TaskFinalizer/ReenqueueManager.html +296 -0
- data/docs/Tasker/Orchestration/TaskFinalizer/UnclearStateHandler.html +314 -0
- data/docs/Tasker/Orchestration/TaskFinalizer.html +1212 -0
- data/docs/Tasker/Orchestration/TaskInitializer.html +766 -0
- data/docs/Tasker/Orchestration/TaskReenqueuer.html +506 -0
- data/docs/Tasker/Orchestration/ViableStepDiscovery.html +442 -0
- data/docs/Tasker/Orchestration/WorkflowCoordinator.html +510 -0
- data/docs/Tasker/Orchestration.html +130 -0
- data/docs/Tasker/PageSort/PageSortParamsBuilder.html +296 -0
- data/docs/Tasker/PageSort.html +247 -0
- data/docs/Tasker/PermanentError.html +518 -0
- data/docs/Tasker/ProceduralError.html +147 -0
- data/docs/Tasker/Queries/AllAnnotationTypes.html +217 -0
- data/docs/Tasker/Queries/AllTasks.html +221 -0
- data/docs/Tasker/Queries/BaseQuery.html +128 -0
- data/docs/Tasker/Queries/Helpers.html +187 -0
- data/docs/Tasker/Queries/OneStep.html +225 -0
- data/docs/Tasker/Queries/OneTask.html +217 -0
- data/docs/Tasker/Queries/TasksByAnnotation.html +231 -0
- data/docs/Tasker/Queries/TasksByStatus.html +233 -0
- data/docs/Tasker/Queries.html +119 -0
- data/docs/Tasker/Railtie.html +124 -0
- data/docs/Tasker/Registry/BaseRegistry.html +1690 -0
- data/docs/Tasker/Registry/EventPublisher.html +667 -0
- data/docs/Tasker/Registry/InterfaceValidator.html +569 -0
- data/docs/Tasker/Registry/RegistrationError.html +132 -0
- data/docs/Tasker/Registry/RegistryError.html +139 -0
- data/docs/Tasker/Registry/StatisticsCollector.html +841 -0
- data/docs/Tasker/Registry/SubscriberRegistry.html +1504 -0
- data/docs/Tasker/Registry/ValidationError.html +132 -0
- data/docs/Tasker/Registry.html +119 -0
- data/docs/Tasker/RetryableError.html +515 -0
- data/docs/Tasker/StateMachine/Compatibility.html +282 -0
- data/docs/Tasker/StateMachine/InvalidStateTransition.html +135 -0
- data/docs/Tasker/StateMachine/StepStateMachine/StandardizedPayloadBuilder.html +260 -0
- data/docs/Tasker/StateMachine/StepStateMachine.html +2215 -0
- data/docs/Tasker/StateMachine/TaskStateMachine.html +734 -0
- data/docs/Tasker/StateMachine.html +602 -0
- data/docs/Tasker/StepDagRelationship.html +657 -0
- data/docs/Tasker/StepHandler/Api/Config.html +1091 -0
- data/docs/Tasker/StepHandler/Api.html +884 -0
- data/docs/Tasker/StepHandler/AutomaticEventPublishing.html +321 -0
- data/docs/Tasker/StepHandler/Base.html +970 -0
- data/docs/Tasker/StepHandler.html +119 -0
- data/docs/Tasker/StepReadinessStatus.html +836 -0
- data/docs/Tasker/Task.html +2575 -0
- data/docs/Tasker/TaskAnnotation.html +137 -0
- data/docs/Tasker/TaskAnnotationSerializer.html +124 -0
- data/docs/Tasker/TaskBuilder/StepNameValidator.html +264 -0
- data/docs/Tasker/TaskBuilder/StepTemplateDefiner.html +264 -0
- data/docs/Tasker/TaskBuilder.html +764 -0
- data/docs/Tasker/TaskDiagram/StepToStepEdgeBuilder.html +260 -0
- data/docs/Tasker/TaskDiagram/TaskToRootStepEdgeBuilder.html +290 -0
- data/docs/Tasker/TaskDiagram.html +548 -0
- data/docs/Tasker/TaskDiagramsController.html +240 -0
- data/docs/Tasker/TaskExecutionContext.html +469 -0
- data/docs/Tasker/TaskHandler/ClassMethods/StepTemplateDefiner/ClassBasedEventRegistrar.html +238 -0
- data/docs/Tasker/TaskHandler/ClassMethods/StepTemplateDefiner/YamlEventRegistrar.html +254 -0
- data/docs/Tasker/TaskHandler/ClassMethods/StepTemplateDefiner.html +988 -0
- data/docs/Tasker/TaskHandler/ClassMethods.html +357 -0
- data/docs/Tasker/TaskHandler/InstanceMethods.html +1396 -0
- data/docs/Tasker/TaskHandler/StepGroup.html +1748 -0
- data/docs/Tasker/TaskHandler.html +271 -0
- data/docs/Tasker/TaskNamespace.html +312 -0
- data/docs/Tasker/TaskRunnerJob.html +406 -0
- data/docs/Tasker/TaskSerializer.html +474 -0
- data/docs/Tasker/TaskTransition.html +1517 -0
- data/docs/Tasker/TaskWorkflowSummary.html +988 -0
- data/docs/Tasker/TaskerRailsSchema/InvalidObjectTypeError.html +132 -0
- data/docs/Tasker/TaskerRailsSchema/TypeResolutionError.html +139 -0
- data/docs/Tasker/TaskerRailsSchema/UnknownInterfaceError.html +132 -0
- data/docs/Tasker/TaskerRailsSchema.html +384 -0
- data/docs/Tasker/TasksController.html +595 -0
- data/docs/Tasker/Telemetry/EventMapping.html +1307 -0
- data/docs/Tasker/Telemetry/EventRouter.html +2178 -0
- data/docs/Tasker/Telemetry/Events/ExportEvents.html +246 -0
- data/docs/Tasker/Telemetry/Events.html +115 -0
- data/docs/Tasker/Telemetry/ExportCoordinator/DistributedLockTimeoutError.html +135 -0
- data/docs/Tasker/Telemetry/ExportCoordinator.html +2137 -0
- data/docs/Tasker/Telemetry/IntelligentCacheManager.html +1083 -0
- data/docs/Tasker/Telemetry/LogBackend.html +1088 -0
- data/docs/Tasker/Telemetry/MetricTypes/Counter.html +1054 -0
- data/docs/Tasker/Telemetry/MetricTypes/Gauge.html +1270 -0
- data/docs/Tasker/Telemetry/MetricTypes/Histogram.html +1492 -0
- data/docs/Tasker/Telemetry/MetricTypes.html +153 -0
- data/docs/Tasker/Telemetry/MetricsBackend.html +2510 -0
- data/docs/Tasker/Telemetry/MetricsExportService.html +578 -0
- data/docs/Tasker/Telemetry/PluginRegistry.html +1774 -0
- data/docs/Tasker/Telemetry/Plugins/BaseExporter.html +1835 -0
- data/docs/Tasker/Telemetry/Plugins/CsvExporter.html +768 -0
- data/docs/Tasker/Telemetry/Plugins/JsonExporter.html +747 -0
- data/docs/Tasker/Telemetry/Plugins.html +117 -0
- data/docs/Tasker/Telemetry/PrometheusExporter.html +481 -0
- data/docs/Tasker/Telemetry/TraceBackend.html +891 -0
- data/docs/Tasker/Telemetry.html +130 -0
- data/docs/Tasker/Types/AuthConfig.html +886 -0
- data/docs/Tasker/Types/BackoffConfig.html +1063 -0
- data/docs/Tasker/Types/BaseConfig.html +227 -0
- data/docs/Tasker/Types/CacheConfig.html +1731 -0
- data/docs/Tasker/Types/DatabaseConfig.html +388 -0
- data/docs/Tasker/Types/DependencyGraph.html +526 -0
- data/docs/Tasker/Types/DependencyGraphConfig.html +753 -0
- data/docs/Tasker/Types/EngineConfig.html +1181 -0
- data/docs/Tasker/Types/ExecutionConfig.html +1963 -0
- data/docs/Tasker/Types/GraphEdge.html +517 -0
- data/docs/Tasker/Types/GraphMetadata.html +781 -0
- data/docs/Tasker/Types/GraphNode.html +694 -0
- data/docs/Tasker/Types/HealthConfig.html +784 -0
- data/docs/Tasker/Types/StepSequence.html +353 -0
- data/docs/Tasker/Types/StepTemplate.html +1193 -0
- data/docs/Tasker/Types/TaskRequest.html +1179 -0
- data/docs/Tasker/Types/TelemetryConfig.html +2746 -0
- data/docs/Tasker/Types.html +154 -0
- data/docs/Tasker/WorkflowStep/StepFinder.html +282 -0
- data/docs/Tasker/WorkflowStep.html +2724 -0
- data/docs/Tasker/WorkflowStepEdge.html +304 -0
- data/docs/Tasker/WorkflowStepSerializer.html +305 -0
- data/docs/Tasker/WorkflowStepTransition/TransitionDescriptionFormatter.html +282 -0
- data/docs/Tasker/WorkflowStepTransition.html +2201 -0
- data/docs/Tasker/WorkflowStepsController.html +462 -0
- data/docs/Tasker.html +452 -0
- data/docs/VISION.md +584 -0
- data/docs/WHY.md +21 -0
- data/docs/_index.html +2375 -0
- data/docs/class_list.html +54 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +503 -0
- data/docs/events/migration_plan_outcomes.md +80 -0
- data/docs/file.README.html +541 -0
- data/docs/file_list.html +59 -0
- data/docs/frames.html +22 -0
- data/docs/index.html +541 -0
- data/docs/js/app.js +344 -0
- data/docs/js/full_list.js +242 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +9182 -0
- data/docs/top-level-namespace.html +110 -0
- data/lib/generators/tasker/authenticator_generator.rb +301 -0
- data/lib/generators/tasker/authorization_coordinator_generator.rb +139 -0
- data/lib/generators/tasker/events_generator.rb +91 -0
- data/lib/generators/tasker/subscriber_generator.rb +107 -0
- data/lib/generators/tasker/task_handler_generator.rb +138 -0
- data/lib/generators/tasker/templates/api_token_authenticator.rb.erb +113 -0
- data/lib/generators/tasker/templates/api_token_authenticator_spec.rb.erb +144 -0
- data/lib/generators/tasker/templates/authorization_coordinator.rb.erb +95 -0
- data/lib/generators/tasker/templates/authorization_coordinator_spec.rb.erb +142 -0
- data/lib/generators/tasker/templates/custom_authenticator.rb.erb +108 -0
- data/lib/generators/tasker/templates/custom_authenticator_spec.rb.erb +162 -0
- data/lib/generators/tasker/templates/custom_events.yml.erb +62 -0
- data/lib/generators/tasker/templates/custom_subscriber.rb.erb +72 -0
- data/lib/generators/tasker/templates/devise_authenticator.rb.erb +101 -0
- data/lib/generators/tasker/templates/devise_authenticator_spec.rb.erb +126 -0
- data/lib/generators/tasker/templates/initialize.rb.erb +202 -0
- data/lib/generators/tasker/templates/jwt_authenticator.rb.erb +144 -0
- data/lib/generators/tasker/templates/jwt_authenticator_spec.rb.erb +298 -0
- data/lib/generators/tasker/templates/metrics_subscriber.rb.erb +258 -0
- data/lib/generators/tasker/templates/metrics_subscriber_spec.rb.erb +308 -0
- data/lib/generators/tasker/templates/omniauth_authenticator.rb.erb +135 -0
- data/lib/generators/tasker/templates/omniauth_authenticator_spec.rb.erb +196 -0
- data/lib/generators/tasker/templates/opentelemetry_initializer.rb +52 -0
- data/lib/generators/tasker/templates/subscriber.rb.erb +64 -0
- data/lib/generators/tasker/templates/subscriber_spec.rb.erb +80 -0
- data/lib/generators/tasker/templates/task_config.yaml.erb +117 -0
- data/lib/generators/tasker/templates/task_handler.rb.erb +59 -0
- data/lib/generators/tasker/templates/task_handler_spec.rb.erb +159 -0
- data/lib/tasker/analysis/runtime_graph_analyzer.rb +1168 -0
- data/lib/tasker/analysis/template_graph_analyzer.rb +328 -0
- data/lib/tasker/authentication/coordinator.rb +78 -0
- data/lib/tasker/authentication/errors.rb +9 -0
- data/lib/tasker/authentication/interface.rb +36 -0
- data/lib/tasker/authentication/none_authenticator.rb +26 -0
- data/lib/tasker/authorization/base_coordinator.rb +112 -0
- data/lib/tasker/authorization/errors.rb +26 -0
- data/lib/tasker/authorization/resource_constants.rb +74 -0
- data/lib/tasker/authorization/resource_registry.rb +143 -0
- data/lib/tasker/authorization.rb +75 -0
- data/lib/tasker/cache_capabilities.rb +131 -0
- data/lib/tasker/cache_strategy.rb +469 -0
- data/lib/tasker/concerns/authenticatable.rb +41 -0
- data/lib/tasker/concerns/authorizable.rb +204 -0
- data/lib/tasker/concerns/controller_authorizable.rb +124 -0
- data/lib/tasker/concerns/event_publisher.rb +716 -0
- data/lib/tasker/concerns/idempotent_state_transitions.rb +128 -0
- data/lib/tasker/concerns/state_machine_base.rb +218 -0
- data/lib/tasker/concerns/structured_logging.rb +387 -0
- data/lib/tasker/configuration.rb +325 -0
- data/lib/tasker/constants/event_definitions.rb +147 -0
- data/lib/tasker/constants/registry_events.rb +54 -0
- data/lib/tasker/constants.rb +417 -0
- data/lib/tasker/engine.rb +90 -0
- data/lib/tasker/errors.rb +90 -0
- data/lib/tasker/events/catalog.rb +432 -0
- data/lib/tasker/events/custom_registry.rb +175 -0
- data/lib/tasker/events/definition_loader.rb +199 -0
- data/lib/tasker/events/event_payload_builder.rb +461 -0
- data/lib/tasker/events/publisher.rb +149 -0
- data/lib/tasker/events/subscribers/base_subscriber.rb +601 -0
- data/lib/tasker/events/subscribers/metrics_subscriber.rb +120 -0
- data/lib/tasker/events/subscribers/telemetry_subscriber.rb +462 -0
- data/lib/tasker/events/subscription_loader.rb +161 -0
- data/lib/tasker/events.rb +37 -0
- data/lib/tasker/functions/function_based_analytics_metrics.rb +103 -0
- data/lib/tasker/functions/function_based_dependency_levels.rb +54 -0
- data/lib/tasker/functions/function_based_slowest_steps.rb +84 -0
- data/lib/tasker/functions/function_based_slowest_tasks.rb +84 -0
- data/lib/tasker/functions/function_based_step_readiness_status.rb +183 -0
- data/lib/tasker/functions/function_based_system_health_counts.rb +94 -0
- data/lib/tasker/functions/function_based_task_execution_context.rb +148 -0
- data/lib/tasker/functions/function_wrapper.rb +42 -0
- data/lib/tasker/functions.rb +12 -0
- data/lib/tasker/handler_factory.rb +322 -0
- data/lib/tasker/health/readiness_checker.rb +186 -0
- data/lib/tasker/health/status_checker.rb +203 -0
- data/lib/tasker/identity_strategy.rb +38 -0
- data/lib/tasker/logging/correlation_id_generator.rb +120 -0
- data/lib/tasker/orchestration/backoff_calculator.rb +184 -0
- data/lib/tasker/orchestration/connection_builder.rb +122 -0
- data/lib/tasker/orchestration/connection_pool_intelligence.rb +177 -0
- data/lib/tasker/orchestration/coordinator.rb +119 -0
- data/lib/tasker/orchestration/future_state_analyzer.rb +137 -0
- data/lib/tasker/orchestration/plugin_integration.rb +124 -0
- data/lib/tasker/orchestration/response_processor.rb +168 -0
- data/lib/tasker/orchestration/retry_header_parser.rb +78 -0
- data/lib/tasker/orchestration/step_executor.rb +941 -0
- data/lib/tasker/orchestration/step_sequence_factory.rb +67 -0
- data/lib/tasker/orchestration/task_finalizer.rb +564 -0
- data/lib/tasker/orchestration/task_initializer.rb +140 -0
- data/lib/tasker/orchestration/task_reenqueuer.rb +71 -0
- data/lib/tasker/orchestration/viable_step_discovery.rb +65 -0
- data/lib/tasker/orchestration/workflow_coordinator.rb +294 -0
- data/lib/tasker/orchestration.rb +45 -0
- data/lib/tasker/railtie.rb +9 -0
- data/lib/tasker/registry/base_registry.rb +177 -0
- data/lib/tasker/registry/event_publisher.rb +91 -0
- data/lib/tasker/registry/interface_validator.rb +140 -0
- data/lib/tasker/registry/statistics_collector.rb +381 -0
- data/lib/tasker/registry/subscriber_registry.rb +285 -0
- data/lib/tasker/registry.rb +22 -0
- data/lib/tasker/state_machine/step_state_machine.rb +508 -0
- data/lib/tasker/state_machine/task_state_machine.rb +192 -0
- data/lib/tasker/state_machine.rb +83 -0
- data/lib/tasker/step_handler/api.rb +410 -0
- data/lib/tasker/step_handler/base.rb +206 -0
- data/lib/tasker/task_builder.rb +432 -0
- data/lib/tasker/task_handler/class_methods.rb +324 -0
- data/lib/tasker/task_handler/instance_methods.rb +293 -0
- data/lib/tasker/task_handler/step_group.rb +182 -0
- data/lib/tasker/task_handler.rb +43 -0
- data/lib/tasker/telemetry/event_mapping.rb +126 -0
- data/lib/tasker/telemetry/event_router.rb +318 -0
- data/lib/tasker/telemetry/events/export_events.rb +38 -0
- data/lib/tasker/telemetry/export_coordinator.rb +497 -0
- data/lib/tasker/telemetry/intelligent_cache_manager.rb +508 -0
- data/lib/tasker/telemetry/log_backend.rb +224 -0
- data/lib/tasker/telemetry/metric_types.rb +368 -0
- data/lib/tasker/telemetry/metrics_backend.rb +1227 -0
- data/lib/tasker/telemetry/metrics_export_service.rb +392 -0
- data/lib/tasker/telemetry/plugin_registry.rb +333 -0
- data/lib/tasker/telemetry/plugins/base_exporter.rb +246 -0
- data/lib/tasker/telemetry/plugins/csv_exporter.rb +198 -0
- data/lib/tasker/telemetry/plugins/json_exporter.rb +141 -0
- data/lib/tasker/telemetry/prometheus_exporter.rb +249 -0
- data/lib/tasker/telemetry/trace_backend.rb +186 -0
- data/lib/tasker/telemetry.rb +59 -0
- data/lib/tasker/types/auth_config.rb +81 -0
- data/lib/tasker/types/backoff_config.rb +142 -0
- data/lib/tasker/types/cache_config.rb +257 -0
- data/lib/tasker/types/database_config.rb +39 -0
- data/lib/tasker/types/dependency_graph.rb +225 -0
- data/lib/tasker/types/dependency_graph_config.rb +149 -0
- data/lib/tasker/types/engine_config.rb +131 -0
- data/lib/tasker/types/execution_config.rb +289 -0
- data/lib/tasker/types/health_config.rb +84 -0
- data/lib/tasker/types/step_sequence.rb +24 -0
- data/lib/tasker/types/step_template.rb +63 -0
- data/lib/tasker/types/task_request.rb +60 -0
- data/lib/tasker/types/telemetry_config.rb +273 -0
- data/lib/tasker/types.rb +64 -0
- data/lib/tasker/version.rb +7 -0
- data/lib/tasker.rb +82 -0
- data/lib/tasks/tasker_tasks.rake +302 -0
- metadata +958 -0
@@ -0,0 +1,1168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../functions/function_based_dependency_levels'
|
4
|
+
require_relative '../telemetry/intelligent_cache_manager'
|
5
|
+
|
6
|
+
module Tasker
|
7
|
+
module Analysis
|
8
|
+
# Runtime Graph Analyzer for Workflow Dependencies
|
9
|
+
#
|
10
|
+
# Provides comprehensive analysis of workflow step dependencies, execution flow,
|
11
|
+
# and performance bottlenecks using database-backed graph analysis. Leverages
|
12
|
+
# existing SQL functions for optimal performance and consistency.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# analyzer = RuntimeGraphAnalyzer.new(task: my_task)
|
16
|
+
# analysis = analyzer.analyze
|
17
|
+
# puts analysis[:critical_paths][:longest_path_length]
|
18
|
+
#
|
19
|
+
# @example Analyzing bottlenecks
|
20
|
+
# bottlenecks = analyzer.analyze[:bottlenecks]
|
21
|
+
# critical_bottlenecks = bottlenecks[:bottlenecks].select { |b| b[:severity] == 'Critical' }
|
22
|
+
#
|
23
|
+
# @since 2.2.1
|
24
|
+
class RuntimeGraphAnalyzer
|
25
|
+
# @return [Tasker::Task] The task being analyzed
|
26
|
+
attr_reader :task
|
27
|
+
|
28
|
+
# @return [Integer] The task ID for database queries
|
29
|
+
attr_reader :task_id
|
30
|
+
|
31
|
+
# Initialize the analyzer for a specific task
|
32
|
+
#
|
33
|
+
# @param task [Tasker::Task] The task to analyze
|
34
|
+
# @raise [ArgumentError] if task is nil or invalid
|
35
|
+
def initialize(task:)
|
36
|
+
@task = task
|
37
|
+
@task_id = task.task_id
|
38
|
+
@cache = {}
|
39
|
+
@intelligent_cache = Tasker::Telemetry::IntelligentCacheManager.new
|
40
|
+
end
|
41
|
+
|
42
|
+
# Perform comprehensive workflow analysis
|
43
|
+
#
|
44
|
+
# Returns a complete analysis of the workflow including dependency graphs,
|
45
|
+
# critical paths, parallelism opportunities, error chains, and bottlenecks.
|
46
|
+
# Results are cached using IntelligentCacheManager with adaptive TTL.
|
47
|
+
#
|
48
|
+
# @return [Hash] Complete analysis with the following keys:
|
49
|
+
# - :dependency_graph [Hash] Graph structure with nodes, edges, and adjacency lists
|
50
|
+
# - :critical_paths [Hash] Critical path analysis with longest paths and bottlenecks
|
51
|
+
# - :parallelism_opportunities [Hash] Analysis of parallel execution opportunities
|
52
|
+
# - :error_chains [Hash] Error propagation analysis and recovery strategies
|
53
|
+
# - :bottlenecks [Hash] Bottleneck identification with impact scoring
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# analysis = analyzer.analyze
|
57
|
+
# puts "Longest path: #{analysis[:critical_paths][:longest_path_length]} steps"
|
58
|
+
# puts "Critical bottlenecks: #{analysis[:bottlenecks][:critical_bottlenecks]}"
|
59
|
+
def analyze
|
60
|
+
# Use intelligent caching for expensive workflow analysis
|
61
|
+
cache_key = "tasker:analysis:runtime_graph:#{task_id}:#{task_analysis_cache_version}"
|
62
|
+
|
63
|
+
@intelligent_cache.intelligent_fetch(cache_key, base_ttl: 90.seconds) do
|
64
|
+
{
|
65
|
+
dependency_graph: build_dependency_graph,
|
66
|
+
critical_paths: analyze_critical_paths,
|
67
|
+
parallelism_opportunities: analyze_parallelism,
|
68
|
+
error_chains: analyze_error_chains,
|
69
|
+
bottlenecks: identify_bottlenecks,
|
70
|
+
generated_at: Time.current,
|
71
|
+
task_id: task_id
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Clear all cached analysis results
|
77
|
+
#
|
78
|
+
# Forces fresh analysis on next call to {#analyze}. Useful when task state
|
79
|
+
# has changed and you need updated analysis.
|
80
|
+
#
|
81
|
+
# @return [void]
|
82
|
+
def clear_cache!
|
83
|
+
@cache.clear
|
84
|
+
|
85
|
+
# Clear intelligent cache for this task
|
86
|
+
cache_key = "tasker:analysis:runtime_graph:#{task_id}:#{task_analysis_cache_version}"
|
87
|
+
@intelligent_cache.clear_performance_data(cache_key)
|
88
|
+
Rails.cache.delete(cache_key)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Build complete dependency graph structure
|
92
|
+
#
|
93
|
+
# Constructs a comprehensive graph representation of workflow step dependencies
|
94
|
+
# using database edges and SQL-based topological sorting for optimal performance.
|
95
|
+
#
|
96
|
+
# @return [Hash] Graph structure containing:
|
97
|
+
# - :nodes [Array<Hash>] Graph nodes with id, name, and dependency level
|
98
|
+
# - :edges [Array<Hash>] Graph edges with from/to IDs and step names
|
99
|
+
# - :adjacency_list [Hash] Forward adjacency list for graph traversal
|
100
|
+
# - :reverse_adjacency_list [Hash] Reverse adjacency list for dependency analysis
|
101
|
+
# - :dependency_levels [Hash] Step ID to dependency level mapping
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# graph = analyzer.build_dependency_graph
|
105
|
+
# root_steps = graph[:nodes].select { |n| n[:level] == 0 }
|
106
|
+
# puts "Root steps: #{root_steps.map { |s| s[:name] }}"
|
107
|
+
def build_dependency_graph
|
108
|
+
steps = load_workflow_steps
|
109
|
+
step_map = steps.index_by(&:workflow_step_id)
|
110
|
+
edges = load_workflow_edges
|
111
|
+
adjacency_lists = build_adjacency_lists(steps, edges)
|
112
|
+
dependency_levels = calculate_dependency_levels_sql
|
113
|
+
|
114
|
+
{
|
115
|
+
nodes: build_graph_nodes(steps, dependency_levels),
|
116
|
+
edges: build_graph_edges(edges, step_map),
|
117
|
+
adjacency_list: adjacency_lists[:forward],
|
118
|
+
reverse_adjacency_list: adjacency_lists[:reverse],
|
119
|
+
dependency_levels: dependency_levels
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# Get configuration for dependency graph calculations
|
126
|
+
#
|
127
|
+
# @return [Tasker::Types::DependencyGraphConfig] Configuration for calculations
|
128
|
+
# @api private
|
129
|
+
def dependency_graph_config
|
130
|
+
@dependency_graph_config ||= Tasker.configuration.dependency_graph
|
131
|
+
end
|
132
|
+
|
133
|
+
# Generate cache version based on task state for intelligent cache invalidation
|
134
|
+
#
|
135
|
+
# @return [String] Cache version string that changes when task state changes
|
136
|
+
# @api private
|
137
|
+
def task_analysis_cache_version
|
138
|
+
# Include task updated_at and step count to invalidate when task changes
|
139
|
+
step_count = task.workflow_steps.count
|
140
|
+
task_updated = task.updated_at.to_i
|
141
|
+
"v1:#{task_updated}:#{step_count}"
|
142
|
+
end
|
143
|
+
|
144
|
+
# Load workflow steps with named step associations
|
145
|
+
#
|
146
|
+
# @return [ActiveRecord::Relation] Workflow steps with eager-loaded named steps
|
147
|
+
# @api private
|
148
|
+
def load_workflow_steps
|
149
|
+
task.workflow_steps.includes(:named_step)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Load workflow edges for dependency analysis
|
153
|
+
#
|
154
|
+
# @return [ActiveRecord::Relation] Workflow step edges for this task
|
155
|
+
# @api private
|
156
|
+
def load_workflow_edges
|
157
|
+
WorkflowStepEdge.joins(:from_step, :to_step)
|
158
|
+
.where(from_step: { task_id: task_id })
|
159
|
+
.select(:from_step_id, :to_step_id)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Build forward and reverse adjacency lists for graph traversal
|
163
|
+
#
|
164
|
+
# Creates both forward (parent → children) and reverse (child → parents)
|
165
|
+
# adjacency lists for efficient graph analysis in both directions.
|
166
|
+
#
|
167
|
+
# @param steps [Array] Workflow steps to initialize
|
168
|
+
# @param edges [Array] Workflow step edges to populate
|
169
|
+
# @return [Hash] Hash with :forward and :reverse adjacency lists
|
170
|
+
# @api private
|
171
|
+
def build_adjacency_lists(steps, edges)
|
172
|
+
adjacency_list = {}
|
173
|
+
reverse_adjacency_list = {}
|
174
|
+
|
175
|
+
# Initialize empty lists for all steps
|
176
|
+
steps.each do |step|
|
177
|
+
adjacency_list[step.workflow_step_id] = []
|
178
|
+
reverse_adjacency_list[step.workflow_step_id] = []
|
179
|
+
end
|
180
|
+
|
181
|
+
# Populate adjacency lists from edges
|
182
|
+
edges.each do |edge|
|
183
|
+
adjacency_list[edge.from_step_id] << edge.to_step_id
|
184
|
+
reverse_adjacency_list[edge.to_step_id] << edge.from_step_id
|
185
|
+
end
|
186
|
+
|
187
|
+
{ forward: adjacency_list, reverse: reverse_adjacency_list }
|
188
|
+
end
|
189
|
+
|
190
|
+
# Build graph nodes with dependency level information
|
191
|
+
#
|
192
|
+
# @param steps [Array] Workflow steps to convert to nodes
|
193
|
+
# @param dependency_levels [Hash] Step ID to dependency level mapping
|
194
|
+
# @return [Array<Hash>] Graph nodes with id, name, and level
|
195
|
+
# @api private
|
196
|
+
def build_graph_nodes(steps, dependency_levels)
|
197
|
+
steps.map do |step|
|
198
|
+
{
|
199
|
+
id: step.workflow_step_id,
|
200
|
+
name: step.named_step.name,
|
201
|
+
level: dependency_levels[step.workflow_step_id] || 0
|
202
|
+
}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Build graph edges with human-readable step names
|
207
|
+
#
|
208
|
+
# @param edges [Array] Raw workflow step edges
|
209
|
+
# @param step_map [Hash] Step ID to step object mapping
|
210
|
+
# @return [Array<Hash>] Graph edges with from/to IDs and names
|
211
|
+
# @api private
|
212
|
+
def build_graph_edges(edges, step_map)
|
213
|
+
edges.map do |edge|
|
214
|
+
{
|
215
|
+
from: edge.from_step_id,
|
216
|
+
to: edge.to_step_id,
|
217
|
+
from_name: step_map[edge.from_step_id]&.named_step&.name,
|
218
|
+
to_name: step_map[edge.to_step_id]&.named_step&.name
|
219
|
+
}
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Calculate dependency levels using optimized SQL function
|
224
|
+
#
|
225
|
+
# @return [Hash] Step ID to dependency level mapping
|
226
|
+
# @api private
|
227
|
+
def calculate_dependency_levels_sql
|
228
|
+
Tasker::Functions::FunctionBasedDependencyLevels.levels_hash_for_task(task_id)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Analyze critical paths through the dependency graph
|
232
|
+
def analyze_critical_paths
|
233
|
+
graph = build_dependency_graph
|
234
|
+
root_nodes = find_root_nodes(graph)
|
235
|
+
leaf_nodes = find_leaf_nodes(graph)
|
236
|
+
all_paths = find_all_root_to_leaf_paths(root_nodes, leaf_nodes, graph[:adjacency_list])
|
237
|
+
critical_paths = analyze_all_paths_for_criticality(all_paths, graph)
|
238
|
+
|
239
|
+
{
|
240
|
+
total_paths: all_paths.length,
|
241
|
+
longest_path_length: critical_paths.first&.dig(:length) || 0,
|
242
|
+
paths: critical_paths.first(5), # Top 5 critical paths
|
243
|
+
root_nodes: root_nodes,
|
244
|
+
leaf_nodes: leaf_nodes
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
# Find root nodes (nodes with no incoming edges)
|
249
|
+
def find_root_nodes(graph)
|
250
|
+
graph[:adjacency_list].select do |node, _children|
|
251
|
+
graph[:reverse_adjacency_list][node].empty?
|
252
|
+
end.keys
|
253
|
+
end
|
254
|
+
|
255
|
+
# Find leaf nodes (nodes with no outgoing edges)
|
256
|
+
def find_leaf_nodes(graph)
|
257
|
+
graph[:adjacency_list].select do |_node, children|
|
258
|
+
children.empty?
|
259
|
+
end.keys
|
260
|
+
end
|
261
|
+
|
262
|
+
# Find all paths from root nodes to leaf nodes
|
263
|
+
def find_all_root_to_leaf_paths(root_nodes, leaf_nodes, adjacency_list)
|
264
|
+
all_paths = []
|
265
|
+
root_nodes.each do |root|
|
266
|
+
leaf_nodes.each do |leaf|
|
267
|
+
paths = find_all_paths(root, leaf, adjacency_list)
|
268
|
+
all_paths.concat(paths)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
all_paths
|
272
|
+
end
|
273
|
+
|
274
|
+
# Analyze all paths for criticality and sort by importance
|
275
|
+
def analyze_all_paths_for_criticality(all_paths, graph)
|
276
|
+
all_paths.map do |path|
|
277
|
+
analyze_path_criticality(path, graph)
|
278
|
+
end.sort_by { |analysis| -analysis[:criticality_score] }
|
279
|
+
end
|
280
|
+
|
281
|
+
# Find all paths between two nodes using DFS
|
282
|
+
def find_all_paths(start_node, end_node, adjacency_list, current_path = [], visited = Set.new)
|
283
|
+
return [] if visited.include?(start_node)
|
284
|
+
|
285
|
+
current_path += [start_node]
|
286
|
+
visited += [start_node]
|
287
|
+
|
288
|
+
return [current_path] if start_node == end_node
|
289
|
+
|
290
|
+
paths = []
|
291
|
+
adjacency_list[start_node].each do |neighbor|
|
292
|
+
neighbor_paths = find_all_paths(neighbor, end_node, adjacency_list, current_path, visited)
|
293
|
+
paths.concat(neighbor_paths)
|
294
|
+
end
|
295
|
+
|
296
|
+
paths
|
297
|
+
end
|
298
|
+
|
299
|
+
# Analyze a specific path for criticality factors
|
300
|
+
def analyze_path_criticality(path, _graph)
|
301
|
+
# Get step readiness data for analysis
|
302
|
+
step_readiness_data = get_step_readiness_data
|
303
|
+
task_context = get_task_execution_context
|
304
|
+
path_steps = path.filter_map { |step_id| step_readiness_data[step_id] }
|
305
|
+
|
306
|
+
# Use efficient counting methods for path-specific metrics
|
307
|
+
path_metrics = calculate_path_metrics(path_steps)
|
308
|
+
|
309
|
+
# Calculate estimated duration and bottlenecks
|
310
|
+
estimated_duration = calculate_path_duration(path_steps)
|
311
|
+
bottlenecks = identify_path_bottlenecks(path_steps)
|
312
|
+
|
313
|
+
# Calculate overall criticality score using task context for efficiency
|
314
|
+
criticality_score = calculate_path_criticality_score(
|
315
|
+
path.length, path_metrics, estimated_duration, task_context
|
316
|
+
)
|
317
|
+
|
318
|
+
{
|
319
|
+
path: path,
|
320
|
+
length: path.length,
|
321
|
+
step_names: path_steps.map(&:name),
|
322
|
+
**path_metrics,
|
323
|
+
estimated_duration: estimated_duration,
|
324
|
+
bottlenecks: bottlenecks,
|
325
|
+
criticality_score: criticality_score,
|
326
|
+
completion_percentage: (path_metrics[:completed_steps].to_f / path.length * 100).round(1)
|
327
|
+
}
|
328
|
+
end
|
329
|
+
|
330
|
+
# Calculate metrics for a specific path efficiently
|
331
|
+
def calculate_path_metrics(path_steps)
|
332
|
+
# Use single pass through path_steps for all counts
|
333
|
+
metrics = {
|
334
|
+
completed_steps: 0,
|
335
|
+
blocked_steps: 0,
|
336
|
+
error_steps: 0,
|
337
|
+
retry_steps: 0,
|
338
|
+
ready_steps: 0
|
339
|
+
}
|
340
|
+
|
341
|
+
path_steps.each do |step|
|
342
|
+
metrics[:completed_steps] += 1 if step.current_state.in?(%w[complete resolved_manually])
|
343
|
+
metrics[:blocked_steps] += 1 unless step.dependencies_satisfied
|
344
|
+
metrics[:error_steps] += 1 if step.current_state == 'error'
|
345
|
+
metrics[:retry_steps] += 1 if step.attempts.positive? && step.current_state == 'error'
|
346
|
+
metrics[:ready_steps] += 1 if step.ready_for_execution
|
347
|
+
end
|
348
|
+
|
349
|
+
metrics
|
350
|
+
end
|
351
|
+
|
352
|
+
# Calculate criticality score using task context for efficiency
|
353
|
+
# Calculate path criticality score using configurable multipliers
|
354
|
+
#
|
355
|
+
# Uses configurable impact multipliers for flexible path scoring.
|
356
|
+
#
|
357
|
+
# @param path_length [Integer] Length of the path
|
358
|
+
# @param path_metrics [Hash] Metrics about the path steps
|
359
|
+
# @param duration [Integer] Estimated path duration
|
360
|
+
# @param task_context [Object] Task execution context
|
361
|
+
# @return [Float] Path criticality score using configurable weights
|
362
|
+
def calculate_path_criticality_score(path_length, path_metrics, duration, task_context)
|
363
|
+
config = dependency_graph_config
|
364
|
+
|
365
|
+
# Base score from path length relative to total task size
|
366
|
+
score = path_length * config.impact_multipliers[:path_length_weight]
|
367
|
+
|
368
|
+
# Factor in task-wide context for better scoring
|
369
|
+
task_completion_factor = task_context.completion_percentage / 100.0
|
370
|
+
task_health_factor = task_context.health_status == 'healthy' ? 1.0 : 1.5
|
371
|
+
|
372
|
+
# Reduce score for completed work
|
373
|
+
score -= path_metrics[:completed_steps] * config.impact_multipliers[:completed_penalty]
|
374
|
+
|
375
|
+
# Increase score for problems (weighted by task health)
|
376
|
+
score += path_metrics[:blocked_steps] * config.impact_multipliers[:blocked_penalty] * task_health_factor
|
377
|
+
score += path_metrics[:error_steps] * config.impact_multipliers[:error_penalty] * task_health_factor
|
378
|
+
score += path_metrics[:retry_steps] * config.impact_multipliers[:retry_penalty]
|
379
|
+
|
380
|
+
# Factor in estimated duration and task completion
|
381
|
+
score += (duration / 60.0) * 2 # 2 points per minute
|
382
|
+
score *= (1.0 - (task_completion_factor * 0.3)) # Reduce score for mostly complete tasks
|
383
|
+
|
384
|
+
[score, 0].max # Ensure non-negative score
|
385
|
+
end
|
386
|
+
|
387
|
+
# Get step readiness data with memoization
|
388
|
+
#
|
389
|
+
# Retrieves and caches step readiness status for all steps in the task.
|
390
|
+
# Uses SQL-based functions for optimal performance and consistency.
|
391
|
+
#
|
392
|
+
# @return [Hash] Step ID to step readiness status object mapping
|
393
|
+
# @api private
|
394
|
+
def get_step_readiness_data
|
395
|
+
@get_step_readiness_data ||= begin
|
396
|
+
data = Tasker::Functions::FunctionBasedStepReadinessStatus.for_task(task_id)
|
397
|
+
data.index_by(&:workflow_step_id)
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# Get task execution context with memoization
|
402
|
+
#
|
403
|
+
# Retrieves and caches task-wide execution metrics including completion
|
404
|
+
# percentages, step counts, and health status.
|
405
|
+
#
|
406
|
+
# @return [Object] Task execution context with metrics and status
|
407
|
+
# @api private
|
408
|
+
def get_task_execution_context
|
409
|
+
@get_task_execution_context ||= Tasker::Functions::FunctionBasedTaskExecutionContext.find(task_id)
|
410
|
+
end
|
411
|
+
|
412
|
+
# Calculate estimated duration for a path using configurable estimates
|
413
|
+
#
|
414
|
+
# Uses configurable duration constants for flexible time estimation.
|
415
|
+
#
|
416
|
+
# @param path_steps [Array] Steps in the path
|
417
|
+
# @return [Integer] Estimated duration in seconds using configurable estimates
|
418
|
+
def calculate_path_duration(path_steps)
|
419
|
+
config = dependency_graph_config
|
420
|
+
|
421
|
+
# Base estimation: configurable seconds per step
|
422
|
+
base_duration = path_steps.length * config.duration_estimates[:base_step_seconds]
|
423
|
+
|
424
|
+
# Calculate penalties in a single pass
|
425
|
+
error_penalty = 0
|
426
|
+
retry_penalty = 0
|
427
|
+
|
428
|
+
path_steps.each do |step|
|
429
|
+
error_penalty += config.duration_estimates[:error_penalty_seconds] if step.current_state == 'error'
|
430
|
+
retry_penalty += step.attempts * config.duration_estimates[:retry_penalty_seconds]
|
431
|
+
end
|
432
|
+
|
433
|
+
base_duration + error_penalty + retry_penalty
|
434
|
+
end
|
435
|
+
|
436
|
+
# Identify bottlenecks within a specific path
|
437
|
+
def identify_path_bottlenecks(path_steps)
|
438
|
+
bottlenecks = []
|
439
|
+
|
440
|
+
path_steps.each do |step|
|
441
|
+
# A step is a bottleneck if it's blocking and has high retry count or long backoff
|
442
|
+
next unless !step.dependencies_satisfied || step.current_state == 'error'
|
443
|
+
|
444
|
+
severity = calculate_bottleneck_severity(step)
|
445
|
+
bottlenecks << {
|
446
|
+
step_id: step.workflow_step_id,
|
447
|
+
step_name: step.name,
|
448
|
+
reason: determine_bottleneck_reason(step),
|
449
|
+
severity: severity,
|
450
|
+
impact: "Blocks #{path_steps.length - path_steps.index(step) - 1} downstream steps"
|
451
|
+
}
|
452
|
+
end
|
453
|
+
|
454
|
+
bottlenecks.sort_by { |b| -b[:severity] }
|
455
|
+
end
|
456
|
+
|
457
|
+
# Calculate bottleneck severity score
|
458
|
+
def calculate_bottleneck_severity(step)
|
459
|
+
severity = 0
|
460
|
+
severity += 10 if step.current_state == 'error'
|
461
|
+
severity += step.attempts * 5 # More attempts = higher severity
|
462
|
+
severity += 20 if step.attempts >= (step.retry_limit || 3) # Exhausted retries
|
463
|
+
severity += 5 unless step.dependencies_satisfied # Dependency issues
|
464
|
+
severity
|
465
|
+
end
|
466
|
+
|
467
|
+
# Determine the reason for a bottleneck using step readiness status
|
468
|
+
#
|
469
|
+
# Leverages the existing step readiness infrastructure to provide consistent
|
470
|
+
# and accurate bottleneck reason identification. Maps technical blocking
|
471
|
+
# reasons to user-friendly descriptions.
|
472
|
+
#
|
473
|
+
# @param step [Object] Step readiness status object
|
474
|
+
# @return [String] User-friendly description of why the step is a bottleneck
|
475
|
+
#
|
476
|
+
# @example
|
477
|
+
# reason = determine_bottleneck_reason(step)
|
478
|
+
# puts "Step blocked: #{reason}" # "Dependencies not satisfied"
|
479
|
+
#
|
480
|
+
# @api private
|
481
|
+
def determine_bottleneck_reason(step)
|
482
|
+
# Use the existing blocking_reason from step readiness status
|
483
|
+
blocking_reason = step.blocking_reason
|
484
|
+
|
485
|
+
# Map the technical blocking reasons to user-friendly descriptions
|
486
|
+
case blocking_reason
|
487
|
+
when 'dependencies_not_satisfied'
|
488
|
+
'Dependencies not satisfied'
|
489
|
+
when 'retry_not_eligible'
|
490
|
+
if step.attempts >= (step.retry_limit || 3)
|
491
|
+
'Exhausted retries'
|
492
|
+
else
|
493
|
+
'In backoff period'
|
494
|
+
end
|
495
|
+
when 'invalid_state'
|
496
|
+
case step.current_state
|
497
|
+
when 'error'
|
498
|
+
'In error state'
|
499
|
+
else
|
500
|
+
"Invalid state: #{step.current_state}"
|
501
|
+
end
|
502
|
+
when 'unknown'
|
503
|
+
'Unknown blocking condition'
|
504
|
+
when nil
|
505
|
+
# Step is ready for execution, shouldn't be a bottleneck
|
506
|
+
'Not blocked (ready for execution)'
|
507
|
+
else
|
508
|
+
"Unrecognized blocking reason: #{blocking_reason}"
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# Analyze parallelism opportunities in the workflow
|
513
|
+
def analyze_parallelism
|
514
|
+
graph = build_dependency_graph
|
515
|
+
dependency_levels = graph[:dependency_levels]
|
516
|
+
step_readiness_data = get_step_readiness_data
|
517
|
+
task_context = get_task_execution_context
|
518
|
+
|
519
|
+
# Group steps by dependency level
|
520
|
+
levels_groups = dependency_levels.group_by { |_step_id, level| level }
|
521
|
+
|
522
|
+
parallelism_analysis = levels_groups.map do |level, step_level_pairs|
|
523
|
+
step_ids = step_level_pairs.map(&:first)
|
524
|
+
step_data = step_ids.filter_map { |id| step_readiness_data[id] }
|
525
|
+
|
526
|
+
analyze_level_parallelism(level, step_data)
|
527
|
+
end.sort_by { |analysis| analysis[:level] }
|
528
|
+
|
529
|
+
{
|
530
|
+
# Use task context for overall counts (more efficient)
|
531
|
+
total_steps: task_context.total_steps,
|
532
|
+
total_levels: levels_groups.keys.max + 1,
|
533
|
+
max_parallel_steps: parallelism_analysis.pluck(:total_steps).max || 0,
|
534
|
+
current_parallel_opportunities: task_context.ready_steps,
|
535
|
+
blocked_parallel_opportunities: parallelism_analysis.sum { |a| a[:blocked_steps] },
|
536
|
+
overall_completion: task_context.completion_percentage,
|
537
|
+
levels: parallelism_analysis,
|
538
|
+
parallelism_efficiency: calculate_parallelism_efficiency(parallelism_analysis)
|
539
|
+
}
|
540
|
+
end
|
541
|
+
|
542
|
+
# Analyze parallelism opportunities at a specific dependency level
|
543
|
+
def analyze_level_parallelism(level, step_data)
|
544
|
+
# For level-specific analysis, we still need to count within this level
|
545
|
+
# But we can optimize by using the step data directly
|
546
|
+
ready_steps = step_data.count(&:ready_for_execution)
|
547
|
+
blocked_steps = step_data.count { |s| !s.dependencies_satisfied }
|
548
|
+
error_steps = step_data.count { |s| s.current_state == 'error' }
|
549
|
+
completed_steps = step_data.count { |s| s.current_state.in?(%w[complete resolved_manually]) }
|
550
|
+
|
551
|
+
{
|
552
|
+
level: level,
|
553
|
+
total_steps: step_data.length,
|
554
|
+
ready_steps: ready_steps,
|
555
|
+
blocked_steps: blocked_steps,
|
556
|
+
error_steps: error_steps,
|
557
|
+
completed_steps: completed_steps,
|
558
|
+
step_names: step_data.map(&:name),
|
559
|
+
parallelism_potential: if ready_steps > 1
|
560
|
+
'High'
|
561
|
+
else
|
562
|
+
ready_steps == 1 ? 'Medium' : 'Low'
|
563
|
+
end,
|
564
|
+
bottleneck_risk: blocked_steps > (step_data.length / 2) ? 'High' : 'Low'
|
565
|
+
}
|
566
|
+
end
|
567
|
+
|
568
|
+
# Calculate overall parallelism efficiency
|
569
|
+
def calculate_parallelism_efficiency(parallelism_analysis)
|
570
|
+
total_steps = parallelism_analysis.sum { |a| a[:total_steps] }
|
571
|
+
return 0 if total_steps.zero?
|
572
|
+
|
573
|
+
# Efficiency is based on how well steps are distributed across levels
|
574
|
+
# and how many can run in parallel
|
575
|
+
parallel_opportunities = parallelism_analysis.count { |a| a[:ready_steps] > 1 }
|
576
|
+
total_levels = parallelism_analysis.length
|
577
|
+
|
578
|
+
return 0 if total_levels.zero?
|
579
|
+
|
580
|
+
(parallel_opportunities.to_f / total_levels * 100).round(1)
|
581
|
+
end
|
582
|
+
|
583
|
+
# Analyze error propagation chains in the workflow
|
584
|
+
def analyze_error_chains
|
585
|
+
graph = build_dependency_graph
|
586
|
+
step_readiness_data = get_step_readiness_data
|
587
|
+
task_context = get_task_execution_context
|
588
|
+
|
589
|
+
# Use task context for efficient error step count
|
590
|
+
return empty_error_analysis if task_context.failed_steps.zero?
|
591
|
+
|
592
|
+
# Find all steps currently in error state
|
593
|
+
error_steps = step_readiness_data.values.select { |s| s.current_state == 'error' }
|
594
|
+
|
595
|
+
error_chains = error_steps.map do |error_step|
|
596
|
+
analyze_error_impact_chain(error_step, graph, step_readiness_data)
|
597
|
+
end.sort_by { |chain| -chain[:total_impact] }
|
598
|
+
|
599
|
+
{
|
600
|
+
# Use task context for overall metrics (more efficient)
|
601
|
+
total_error_steps: task_context.failed_steps,
|
602
|
+
total_steps: task_context.total_steps,
|
603
|
+
error_percentage: (task_context.failed_steps.to_f / task_context.total_steps * 100).round(1),
|
604
|
+
total_blocked_by_errors: error_chains.sum { |c| c[:blocked_downstream_steps] },
|
605
|
+
most_critical_error: error_chains.first,
|
606
|
+
error_chains: error_chains,
|
607
|
+
recovery_priority: determine_recovery_priority(error_chains),
|
608
|
+
task_health: task_context.health_status
|
609
|
+
}
|
610
|
+
end
|
611
|
+
|
612
|
+
# Return empty error analysis when no errors exist
|
613
|
+
def empty_error_analysis
|
614
|
+
{
|
615
|
+
total_error_steps: 0,
|
616
|
+
total_steps: get_task_execution_context.total_steps,
|
617
|
+
error_percentage: 0.0,
|
618
|
+
total_blocked_by_errors: 0,
|
619
|
+
most_critical_error: nil,
|
620
|
+
error_chains: [],
|
621
|
+
recovery_priority: [],
|
622
|
+
task_health: get_task_execution_context.health_status
|
623
|
+
}
|
624
|
+
end
|
625
|
+
|
626
|
+
# Analyze the impact chain of a specific error step
|
627
|
+
def analyze_error_impact_chain(error_step, graph, step_readiness_data)
|
628
|
+
# Find all downstream steps affected by this error
|
629
|
+
downstream_steps = find_downstream_steps(error_step.workflow_step_id, graph[:adjacency_list])
|
630
|
+
blocked_steps = downstream_steps.select do |step_id|
|
631
|
+
step_data = step_readiness_data[step_id]
|
632
|
+
step_data && !step_data.dependencies_satisfied
|
633
|
+
end
|
634
|
+
|
635
|
+
# Calculate impact metrics
|
636
|
+
total_downstream = downstream_steps.length
|
637
|
+
blocked_downstream = blocked_steps.length
|
638
|
+
|
639
|
+
# Analyze retry situation
|
640
|
+
retry_analysis = analyze_error_retry_situation(error_step)
|
641
|
+
|
642
|
+
{
|
643
|
+
error_step_id: error_step.workflow_step_id,
|
644
|
+
error_step_name: error_step.name,
|
645
|
+
error_attempts: error_step.attempts,
|
646
|
+
retry_limit: error_step.retry_limit || 3,
|
647
|
+
retry_analysis: retry_analysis,
|
648
|
+
downstream_steps: downstream_steps,
|
649
|
+
blocked_downstream_steps: blocked_downstream,
|
650
|
+
total_downstream_steps: total_downstream,
|
651
|
+
total_impact: calculate_error_impact_score(error_step, blocked_downstream, total_downstream),
|
652
|
+
recovery_strategies: suggest_recovery_strategies(error_step, retry_analysis)
|
653
|
+
}
|
654
|
+
end
|
655
|
+
|
656
|
+
# Find all downstream steps from a given step
|
657
|
+
def find_downstream_steps(step_id, adjacency_list, visited = Set.new)
|
658
|
+
return [] if visited.include?(step_id)
|
659
|
+
|
660
|
+
visited.add(step_id)
|
661
|
+
downstream = []
|
662
|
+
|
663
|
+
adjacency_list[step_id].each do |child_id|
|
664
|
+
downstream << child_id
|
665
|
+
downstream.concat(find_downstream_steps(child_id, adjacency_list, visited))
|
666
|
+
end
|
667
|
+
|
668
|
+
downstream.uniq
|
669
|
+
end
|
670
|
+
|
671
|
+
# Analyze the retry situation for an error step
|
672
|
+
def analyze_error_retry_situation(error_step)
|
673
|
+
retry_limit = error_step.retry_limit || 3
|
674
|
+
attempts = error_step.attempts
|
675
|
+
|
676
|
+
{
|
677
|
+
exhausted: attempts >= retry_limit,
|
678
|
+
remaining_attempts: [retry_limit - attempts, 0].max,
|
679
|
+
in_backoff: error_step.next_retry_at && error_step.next_retry_at > Time.current,
|
680
|
+
next_retry_at: error_step.next_retry_at,
|
681
|
+
retryable: error_step.retry_eligible
|
682
|
+
}
|
683
|
+
end
|
684
|
+
|
685
|
+
# Calculate impact score for an error using configurable weights
|
686
|
+
#
|
687
|
+
# Uses configurable impact multipliers for flexible error impact assessment.
|
688
|
+
#
|
689
|
+
# @param error_step [Object] Error step object
|
690
|
+
# @param blocked_downstream [Integer] Number of blocked downstream steps
|
691
|
+
# @param total_downstream [Integer] Total downstream steps
|
692
|
+
# @return [Integer] Error impact score using configurable weights
|
693
|
+
def calculate_error_impact_score(error_step, blocked_downstream, total_downstream)
|
694
|
+
config = dependency_graph_config
|
695
|
+
score = 0
|
696
|
+
|
697
|
+
# Base impact from blocked downstream steps
|
698
|
+
score += blocked_downstream * config.impact_multipliers[:blocked_weight]
|
699
|
+
|
700
|
+
# Additional impact for total downstream reach
|
701
|
+
score += total_downstream * config.impact_multipliers[:downstream_weight]
|
702
|
+
|
703
|
+
# Penalty for exhausted retries (permanent blockage)
|
704
|
+
score += config.penalty_constants[:exhausted_retry] if error_step.attempts >= (error_step.retry_limit || 3)
|
705
|
+
|
706
|
+
# Penalty for high retry count (instability)
|
707
|
+
score += error_step.attempts * config.penalty_constants[:retry_instability]
|
708
|
+
|
709
|
+
score
|
710
|
+
end
|
711
|
+
|
712
|
+
# Suggest recovery strategies for an error step
|
713
|
+
def suggest_recovery_strategies(_error_step, retry_analysis)
|
714
|
+
if retry_analysis[:exhausted]
|
715
|
+
exhausted_retry_strategies
|
716
|
+
elsif retry_analysis[:in_backoff]
|
717
|
+
backoff_strategies(retry_analysis)
|
718
|
+
elsif retry_analysis[:retryable]
|
719
|
+
retryable_strategies(retry_analysis)
|
720
|
+
else
|
721
|
+
non_retryable_strategies
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
# Recovery strategies for steps that have exhausted retries
|
726
|
+
#
|
727
|
+
# @return [Array<Hash>] Strategy recommendations with priority and description
|
728
|
+
# @api private
|
729
|
+
def exhausted_retry_strategies
|
730
|
+
[
|
731
|
+
{
|
732
|
+
strategy: 'Manual Resolution',
|
733
|
+
priority: 'High',
|
734
|
+
description: 'Step has exhausted retries and requires manual intervention'
|
735
|
+
},
|
736
|
+
{
|
737
|
+
strategy: 'Increase Retry Limit',
|
738
|
+
priority: 'Medium',
|
739
|
+
description: 'Consider increasing retry limit if error is transient'
|
740
|
+
}
|
741
|
+
]
|
742
|
+
end
|
743
|
+
|
744
|
+
# Recovery strategies for steps in backoff period
|
745
|
+
#
|
746
|
+
# @param retry_analysis [Hash] Retry analysis data including next_retry_at
|
747
|
+
# @return [Array<Hash>] Strategy recommendations with timing information
|
748
|
+
# @api private
|
749
|
+
def backoff_strategies(retry_analysis)
|
750
|
+
[
|
751
|
+
{
|
752
|
+
strategy: 'Wait for Backoff',
|
753
|
+
priority: 'Low',
|
754
|
+
description: "Step will retry automatically at #{retry_analysis[:next_retry_at]}"
|
755
|
+
}
|
756
|
+
]
|
757
|
+
end
|
758
|
+
|
759
|
+
# Recovery strategies for retryable steps
|
760
|
+
#
|
761
|
+
# @param retry_analysis [Hash] Retry analysis data including remaining_attempts
|
762
|
+
# @return [Array<Hash>] Strategy recommendations with attempt information
|
763
|
+
# @api private
|
764
|
+
def retryable_strategies(retry_analysis)
|
765
|
+
[
|
766
|
+
{
|
767
|
+
strategy: 'Monitor Retries',
|
768
|
+
priority: 'Medium',
|
769
|
+
description: "Step has #{retry_analysis[:remaining_attempts]} retry attempts remaining"
|
770
|
+
}
|
771
|
+
]
|
772
|
+
end
|
773
|
+
|
774
|
+
# Recovery strategies for non-retryable steps
|
775
|
+
#
|
776
|
+
# @return [Array<Hash>] Strategy recommendations for investigation
|
777
|
+
# @api private
|
778
|
+
def non_retryable_strategies
|
779
|
+
[
|
780
|
+
{
|
781
|
+
strategy: 'Investigate Root Cause',
|
782
|
+
priority: 'High',
|
783
|
+
description: 'Step is not retryable, requires investigation'
|
784
|
+
}
|
785
|
+
]
|
786
|
+
end
|
787
|
+
|
788
|
+
# Determine recovery priority across all error chains
|
789
|
+
def determine_recovery_priority(error_chains)
|
790
|
+
return [] if error_chains.empty?
|
791
|
+
|
792
|
+
# Sort by impact score and return top priorities
|
793
|
+
error_chains.first(3).map do |chain|
|
794
|
+
{
|
795
|
+
step_name: chain[:error_step_name],
|
796
|
+
priority_level: determine_priority_level(chain[:total_impact]),
|
797
|
+
reason: "Blocking #{chain[:blocked_downstream_steps]} downstream steps",
|
798
|
+
recommended_action: chain[:recovery_strategies].first&.dig(:strategy) || 'Investigate'
|
799
|
+
}
|
800
|
+
end
|
801
|
+
end
|
802
|
+
|
803
|
+
# Determine priority level based on impact score
|
804
|
+
#
|
805
|
+
# Uses configurable thresholds for consistent severity classification.
|
806
|
+
#
|
807
|
+
# @param impact_score [Integer] The calculated impact score
|
808
|
+
# @return [String] Priority level: 'Critical', 'High', 'Medium', or 'Low'
|
809
|
+
def determine_priority_level(impact_score)
|
810
|
+
config = dependency_graph_config
|
811
|
+
|
812
|
+
return 'Critical' if impact_score >= config.severity_thresholds[:critical]
|
813
|
+
return 'High' if impact_score >= config.severity_thresholds[:high]
|
814
|
+
return 'Medium' if impact_score >= config.severity_thresholds[:medium]
|
815
|
+
|
816
|
+
'Low'
|
817
|
+
end
|
818
|
+
|
819
|
+
# Identify bottlenecks with impact scoring
|
820
|
+
def identify_bottlenecks
|
821
|
+
graph = build_dependency_graph
|
822
|
+
step_readiness_data = get_step_readiness_data
|
823
|
+
task_context = get_task_execution_context
|
824
|
+
|
825
|
+
# Early return if no blocking issues (use task context for efficiency)
|
826
|
+
return empty_bottleneck_analysis if task_context.failed_steps.zero? &&
|
827
|
+
task_context.pending_steps.zero? &&
|
828
|
+
task_context.ready_steps == task_context.total_steps
|
829
|
+
|
830
|
+
# Find potential bottleneck steps more efficiently
|
831
|
+
bottleneck_candidates = find_bottleneck_candidates(step_readiness_data, task_context)
|
832
|
+
|
833
|
+
# Analyze each bottleneck candidate
|
834
|
+
bottleneck_analysis = bottleneck_candidates.map do |step|
|
835
|
+
analyze_bottleneck_impact(step, graph, step_readiness_data)
|
836
|
+
end.sort_by { |analysis| -analysis[:impact_score] }
|
837
|
+
|
838
|
+
{
|
839
|
+
total_bottlenecks: bottleneck_analysis.length,
|
840
|
+
critical_bottlenecks: bottleneck_analysis.count { |b| b[:severity] == 'Critical' },
|
841
|
+
total_blocked_steps: bottleneck_analysis.sum { |b| b[:blocked_downstream_count] },
|
842
|
+
bottlenecks: bottleneck_analysis.first(10), # Top 10 bottlenecks
|
843
|
+
task_health: task_context.health_status,
|
844
|
+
task_completion: task_context.completion_percentage,
|
845
|
+
failed_steps_ratio: (task_context.failed_steps.to_f / task_context.total_steps * 100).round(1),
|
846
|
+
overall_impact: calculate_overall_bottleneck_impact(bottleneck_analysis, task_context)
|
847
|
+
}
|
848
|
+
end
|
849
|
+
|
850
|
+
# Find bottleneck candidates efficiently using task context
|
851
|
+
def find_bottleneck_candidates(step_readiness_data, task_context)
|
852
|
+
# If we have many failed steps, focus on those first
|
853
|
+
if task_context.failed_steps.positive?
|
854
|
+
candidates = step_readiness_data.values.select { |step| step.current_state == 'error' }
|
855
|
+
return candidates unless candidates.empty?
|
856
|
+
end
|
857
|
+
|
858
|
+
# Otherwise, look for other blocking conditions
|
859
|
+
step_readiness_data.values.select do |step|
|
860
|
+
!step.dependencies_satisfied ||
|
861
|
+
step.attempts >= 2 ||
|
862
|
+
!step.retry_eligible
|
863
|
+
end
|
864
|
+
end
|
865
|
+
|
866
|
+
# Return empty bottleneck analysis when no bottlenecks exist
|
867
|
+
def empty_bottleneck_analysis
|
868
|
+
task_context = get_task_execution_context
|
869
|
+
{
|
870
|
+
total_bottlenecks: 0,
|
871
|
+
critical_bottlenecks: 0,
|
872
|
+
total_blocked_steps: 0,
|
873
|
+
bottlenecks: [],
|
874
|
+
task_health: task_context.health_status,
|
875
|
+
overall_impact: 'None'
|
876
|
+
}
|
877
|
+
end
|
878
|
+
|
879
|
+
# Analyze the comprehensive impact of a specific bottleneck step
|
880
|
+
#
|
881
|
+
# Performs detailed analysis of how a bottleneck step affects the overall
|
882
|
+
# workflow, including downstream impact, severity assessment, and resolution
|
883
|
+
# recommendations.
|
884
|
+
#
|
885
|
+
# @param step [Object] Step readiness status object
|
886
|
+
# @param graph [Hash] Dependency graph structure
|
887
|
+
# @param step_readiness_data [Hash] Step ID to readiness status mapping
|
888
|
+
# @return [Hash] Complete bottleneck analysis including:
|
889
|
+
# - Step metadata (ID, name, state, attempts, etc.)
|
890
|
+
# - Downstream impact counts
|
891
|
+
# - Impact score and severity level
|
892
|
+
# - Bottleneck type classification
|
893
|
+
# - Resolution strategy and time estimate
|
894
|
+
#
|
895
|
+
# @api private
|
896
|
+
def analyze_bottleneck_impact(step, graph, step_readiness_data)
|
897
|
+
downstream_impact = calculate_downstream_impact(step, graph, step_readiness_data)
|
898
|
+
impact_score = calculate_bottleneck_impact_score(step, downstream_impact[:downstream_steps_count],
|
899
|
+
downstream_impact[:blocked_downstream_count])
|
900
|
+
|
901
|
+
{
|
902
|
+
**extract_step_metadata(step),
|
903
|
+
**downstream_impact,
|
904
|
+
impact_score: impact_score,
|
905
|
+
severity: determine_bottleneck_severity_level(impact_score),
|
906
|
+
bottleneck_type: determine_bottleneck_type(step),
|
907
|
+
resolution_strategy: suggest_bottleneck_resolution(step),
|
908
|
+
estimated_resolution_time: estimate_resolution_time(step)
|
909
|
+
}
|
910
|
+
end
|
911
|
+
|
912
|
+
# Calculate the downstream impact of a bottleneck step
|
913
|
+
#
|
914
|
+
# Determines how many steps are affected by this bottleneck, both in terms
|
915
|
+
# of total downstream steps and those that are actually blocked.
|
916
|
+
#
|
917
|
+
# @param step [Object] Step readiness status object
|
918
|
+
# @param graph [Hash] Dependency graph structure
|
919
|
+
# @param step_readiness_data [Hash] Step ID to readiness status mapping
|
920
|
+
# @return [Hash] Impact metrics with :downstream_steps_count and :blocked_downstream_count
|
921
|
+
#
|
922
|
+
# @api private
|
923
|
+
def calculate_downstream_impact(step, graph, step_readiness_data)
|
924
|
+
downstream_steps = find_downstream_steps(step.workflow_step_id, graph[:adjacency_list])
|
925
|
+
blocked_downstream = count_blocked_downstream_steps(downstream_steps, step_readiness_data)
|
926
|
+
|
927
|
+
{
|
928
|
+
downstream_steps_count: downstream_steps.length,
|
929
|
+
blocked_downstream_count: blocked_downstream
|
930
|
+
}
|
931
|
+
end
|
932
|
+
|
933
|
+
# Extract essential metadata from a step for analysis
|
934
|
+
#
|
935
|
+
# @param step [Object] Step readiness status object
|
936
|
+
# @return [Hash] Step metadata including ID, name, state, attempts, and dependencies
|
937
|
+
#
|
938
|
+
# @api private
|
939
|
+
def extract_step_metadata(step)
|
940
|
+
{
|
941
|
+
step_id: step.workflow_step_id,
|
942
|
+
step_name: step.name,
|
943
|
+
current_state: step.current_state,
|
944
|
+
attempts: step.attempts,
|
945
|
+
retry_limit: step.retry_limit || 3,
|
946
|
+
dependencies_satisfied: step.dependencies_satisfied
|
947
|
+
}
|
948
|
+
end
|
949
|
+
|
950
|
+
# Count downstream steps that are actually blocked by dependencies
|
951
|
+
#
|
952
|
+
# @param downstream_steps [Array<Integer>] List of downstream step IDs
|
953
|
+
# @param step_readiness_data [Hash] Step ID to readiness status mapping
|
954
|
+
# @return [Integer] Number of downstream steps that are blocked
|
955
|
+
#
|
956
|
+
# @api private
|
957
|
+
def count_blocked_downstream_steps(downstream_steps, step_readiness_data)
|
958
|
+
downstream_steps.count do |step_id|
|
959
|
+
downstream_step = step_readiness_data[step_id]
|
960
|
+
downstream_step && !downstream_step.dependencies_satisfied
|
961
|
+
end
|
962
|
+
end
|
963
|
+
|
964
|
+
# Calculate comprehensive impact score for a bottleneck
|
965
|
+
#
|
966
|
+
# Combines base impact, state severity multipliers, and penalty scores to
|
967
|
+
# provide a comprehensive bottleneck impact assessment.
|
968
|
+
#
|
969
|
+
# @param step [Object] Step readiness status object
|
970
|
+
# @param downstream_count [Integer] Total number of downstream steps
|
971
|
+
# @param blocked_count [Integer] Number of blocked downstream steps
|
972
|
+
# @return [Integer] Rounded impact score for severity classification
|
973
|
+
#
|
974
|
+
# @api private
|
975
|
+
def calculate_bottleneck_impact_score(step, downstream_count, blocked_count)
|
976
|
+
base_score = calculate_base_impact_score(downstream_count, blocked_count)
|
977
|
+
state_multiplier = calculate_state_severity_multiplier(step)
|
978
|
+
penalty_score = calculate_bottleneck_penalties(step)
|
979
|
+
|
980
|
+
((base_score * state_multiplier) + penalty_score).round
|
981
|
+
end
|
982
|
+
|
983
|
+
# Calculate base impact score from downstream effects
|
984
|
+
#
|
985
|
+
# Provides the foundation score based on how many steps are affected.
|
986
|
+
# Blocked steps are weighted more heavily than total downstream steps.
|
987
|
+
# Uses configurable multipliers for flexible impact weighting.
|
988
|
+
#
|
989
|
+
# @param downstream_count [Integer] Total number of downstream steps
|
990
|
+
# @param blocked_count [Integer] Number of blocked downstream steps
|
991
|
+
# @return [Integer] Base impact score using configurable weights
|
992
|
+
#
|
993
|
+
# @api private
|
994
|
+
def calculate_base_impact_score(downstream_count, blocked_count)
|
995
|
+
config = dependency_graph_config
|
996
|
+
(downstream_count * config.impact_multipliers[:downstream_weight]) +
|
997
|
+
(blocked_count * config.impact_multipliers[:blocked_weight])
|
998
|
+
end
|
999
|
+
|
1000
|
+
# Calculate severity multiplier based on step state
|
1001
|
+
#
|
1002
|
+
# Applies multipliers based on the current state of the step, with error
|
1003
|
+
# states receiving higher multipliers, especially for exhausted retries.
|
1004
|
+
# Uses configurable multipliers for flexible severity weighting.
|
1005
|
+
#
|
1006
|
+
# @param step [Object] Step readiness status object
|
1007
|
+
# @return [Float] Severity multiplier using configurable weights
|
1008
|
+
#
|
1009
|
+
# @api private
|
1010
|
+
def calculate_state_severity_multiplier(step)
|
1011
|
+
config = dependency_graph_config
|
1012
|
+
|
1013
|
+
case step.current_state
|
1014
|
+
when 'error'
|
1015
|
+
multiplier = config.severity_multipliers[:error_state] # Errors are serious
|
1016
|
+
# Exhausted retries are critical
|
1017
|
+
multiplier += config.severity_multipliers[:exhausted_retry_bonus] if step.attempts >= (step.retry_limit || 3)
|
1018
|
+
multiplier
|
1019
|
+
when 'pending'
|
1020
|
+
step.dependencies_satisfied ? 1.0 : config.severity_multipliers[:dependency_issue] # Dependency issues
|
1021
|
+
else
|
1022
|
+
1.0
|
1023
|
+
end
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
# Calculate additional penalty scores for problematic conditions
|
1027
|
+
#
|
1028
|
+
# Adds penalty points for retry instability, non-retryable issues,
|
1029
|
+
# and exhausted retry attempts. Uses configurable constants for
|
1030
|
+
# flexible penalty weighting.
|
1031
|
+
#
|
1032
|
+
# @param step [Object] Step readiness status object
|
1033
|
+
# @return [Integer] Total penalty score using configurable penalties
|
1034
|
+
#
|
1035
|
+
# @api private
|
1036
|
+
def calculate_bottleneck_penalties(step)
|
1037
|
+
config = dependency_graph_config
|
1038
|
+
penalty = 0
|
1039
|
+
penalty += step.attempts * config.penalty_constants[:retry_instability] # Retry instability
|
1040
|
+
penalty += config.penalty_constants[:non_retryable] unless step.retry_eligible # Non-retryable issues
|
1041
|
+
# Exhausted retries
|
1042
|
+
penalty += config.penalty_constants[:exhausted_retry] if step.attempts >= (step.retry_limit || 3)
|
1043
|
+
penalty
|
1044
|
+
end
|
1045
|
+
|
1046
|
+
# Determine severity level based on impact score
|
1047
|
+
#
|
1048
|
+
# Uses configurable thresholds for flexible severity classification.
|
1049
|
+
#
|
1050
|
+
# @param impact_score [Integer] The calculated impact score
|
1051
|
+
# @return [String] Severity level: 'Critical', 'High', 'Medium', or 'Low'
|
1052
|
+
def determine_bottleneck_severity_level(impact_score)
|
1053
|
+
config = dependency_graph_config
|
1054
|
+
|
1055
|
+
return 'Critical' if impact_score >= config.severity_thresholds[:critical]
|
1056
|
+
return 'High' if impact_score >= config.severity_thresholds[:high]
|
1057
|
+
return 'Medium' if impact_score >= config.severity_thresholds[:medium]
|
1058
|
+
|
1059
|
+
'Low'
|
1060
|
+
end
|
1061
|
+
|
1062
|
+
# Determine the type of bottleneck using step readiness status
|
1063
|
+
#
|
1064
|
+
# Classifies the bottleneck type by leveraging step readiness status methods
|
1065
|
+
# for consistent and accurate categorization.
|
1066
|
+
#
|
1067
|
+
# @param step [Object] Step readiness status object
|
1068
|
+
# @return [String] Bottleneck type classification:
|
1069
|
+
# - 'Permanent Failure' for exhausted retries
|
1070
|
+
# - 'Active Error' for steps in error state
|
1071
|
+
# - 'Dependency Block' for dependency issues
|
1072
|
+
# - 'Retry Exhaustion' for high retry counts
|
1073
|
+
# - 'Non-Retryable' for non-retryable steps
|
1074
|
+
# - 'Unknown' for unclassified cases
|
1075
|
+
#
|
1076
|
+
# @api private
|
1077
|
+
def determine_bottleneck_type(step)
|
1078
|
+
# Use the existing retry_status and dependency_status from step readiness
|
1079
|
+
retry_status = step.retry_status
|
1080
|
+
dependency_status = step.dependency_status
|
1081
|
+
|
1082
|
+
# Check for permanent failures first
|
1083
|
+
return 'Permanent Failure' if retry_status == 'max_retries_reached'
|
1084
|
+
|
1085
|
+
# Check current state issues
|
1086
|
+
return 'Active Error' if step.current_state == 'error'
|
1087
|
+
|
1088
|
+
# Check dependency issues
|
1089
|
+
return 'Dependency Block' if dependency_status != 'all_satisfied' && dependency_status != 'no_dependencies'
|
1090
|
+
|
1091
|
+
# Check retry issues
|
1092
|
+
return 'Retry Exhaustion' if step.attempts >= 2
|
1093
|
+
return 'Non-Retryable' if retry_status != 'retry_eligible'
|
1094
|
+
|
1095
|
+
'Unknown'
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
# Suggest resolution strategy using step readiness status
|
1099
|
+
#
|
1100
|
+
# Provides actionable resolution strategies based on step readiness status
|
1101
|
+
# methods for more accurate and helpful suggestions.
|
1102
|
+
#
|
1103
|
+
# @param step [Object] Step readiness status object
|
1104
|
+
# @return [String] Recommended resolution strategy
|
1105
|
+
#
|
1106
|
+
# @api private
|
1107
|
+
def suggest_bottleneck_resolution(step)
|
1108
|
+
# Use step readiness status methods for more accurate suggestions
|
1109
|
+
retry_status = step.retry_status
|
1110
|
+
step.dependency_status
|
1111
|
+
blocking_reason = step.blocking_reason
|
1112
|
+
|
1113
|
+
return 'Manual intervention required' if retry_status == 'max_retries_reached'
|
1114
|
+
return 'Investigate error cause' if step.current_state == 'error'
|
1115
|
+
return 'Check upstream dependencies' if blocking_reason == 'dependencies_not_satisfied'
|
1116
|
+
return 'Wait for backoff period' if blocking_reason == 'retry_not_eligible' && step.next_retry_at
|
1117
|
+
return 'Monitor retry attempts' if step.attempts >= 1
|
1118
|
+
|
1119
|
+
'Review step configuration'
|
1120
|
+
end
|
1121
|
+
|
1122
|
+
# Estimate time to resolve bottleneck using precise step readiness data
|
1123
|
+
#
|
1124
|
+
# Provides accurate time estimates by leveraging step readiness status
|
1125
|
+
# time calculations, including exact minute-based estimates when available.
|
1126
|
+
#
|
1127
|
+
# @param step [Object] Step readiness status object
|
1128
|
+
# @return [String] Time estimate for resolution:
|
1129
|
+
# - 'Immediate action needed' for exhausted retries
|
1130
|
+
# - Specific minute estimates when time_until_ready is available
|
1131
|
+
# - General estimates for other conditions
|
1132
|
+
#
|
1133
|
+
# @api private
|
1134
|
+
def estimate_resolution_time(step)
|
1135
|
+
retry_status = step.retry_status
|
1136
|
+
blocking_reason = step.blocking_reason
|
1137
|
+
time_until_ready = step.time_until_ready
|
1138
|
+
|
1139
|
+
return 'Immediate action needed' if retry_status == 'max_retries_reached'
|
1140
|
+
|
1141
|
+
# If we have a specific time until ready, use it
|
1142
|
+
if time_until_ready&.positive?
|
1143
|
+
minutes = (time_until_ready / 60.0).ceil
|
1144
|
+
return "#{minutes} minute#{'s' unless minutes == 1}"
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
return 'Within next retry cycle' if step.current_state == 'error' && retry_status == 'retry_eligible'
|
1148
|
+
return 'Depends on upstream steps' if blocking_reason == 'dependencies_not_satisfied'
|
1149
|
+
|
1150
|
+
'Unknown'
|
1151
|
+
end
|
1152
|
+
|
1153
|
+
# Calculate overall bottleneck impact on the task
|
1154
|
+
def calculate_overall_bottleneck_impact(bottleneck_analysis, task_context)
|
1155
|
+
return 'None' if bottleneck_analysis.empty?
|
1156
|
+
|
1157
|
+
critical_count = bottleneck_analysis.count { |b| b[:severity] == 'Critical' }
|
1158
|
+
high_count = bottleneck_analysis.count { |b| b[:severity] == 'High' }
|
1159
|
+
|
1160
|
+
return 'Severe' if critical_count >= 3 || task_context.failed_steps > (task_context.total_steps / 2)
|
1161
|
+
return 'High' if critical_count >= 1 || high_count >= 3
|
1162
|
+
return 'Medium' if high_count >= 1 || bottleneck_analysis.length >= 5
|
1163
|
+
|
1164
|
+
'Low'
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
end
|
1168
|
+
end
|