diff --git a/ProcessMaker/Exception/ConfigurationException.php b/ProcessMaker/Exception/ConfigurationException.php new file mode 100644 index 0000000000..bc8bcc7024 --- /dev/null +++ b/ProcessMaker/Exception/ConfigurationException.php @@ -0,0 +1,27 @@ +element_name . ' (' . $token->element_id . '): ' . $this->getMessage(); + + return [ + '_configuration_error_' . $token->element_id => $message, + ]; + } +} diff --git a/ProcessMaker/Jobs/ErrorHandling.php b/ProcessMaker/Jobs/ErrorHandling.php index 7343c6452f..f33d994c32 100644 --- a/ProcessMaker/Jobs/ErrorHandling.php +++ b/ProcessMaker/Jobs/ErrorHandling.php @@ -43,13 +43,16 @@ public function __construct( public function handleRetries($job, $exception) { $message = $exception->getMessage(); + $finalAttempt = true; if ($this->retryAttempts() > 0) { if ($job->attemptNum <= $this->retryAttempts()) { Log::info('Retry the job process. Attempt ' . $job->attemptNum . ' of ' . $this->retryAttempts() . ', Wait time: ' . $this->retryWaitTime()); $this->requeue($job); - return $message; + $finalAttempt = false; + + return [$message, $finalAttempt]; } $message = __('Job failed after :attempts total attempts', ['attempts' => $job->attemptNum]) . "\n" . $message; @@ -59,20 +62,26 @@ public function handleRetries($job, $exception) $this->sendExecutionErrorNotification($message); } - return $message; + return [$message, $finalAttempt]; } private function requeue($job) { $class = get_class($job); - $newJob = new $class( - Process::findOrFail($job->definitionsId), - ProcessRequest::findOrFail($job->instanceId), - ProcessRequestToken::findOrFail($job->tokenId), - $job->data, - $job->attemptNum + 1 - ); + if ($job instanceof RunNayraServiceTask) { + $newJob = new RunNayraServiceTask($this->processRequestToken); + $newJob->attemptNum = $job->attemptNum + 1; + } else { + $newJob = new $class( + Process::findOrFail($job->definitionsId), + ProcessRequest::findOrFail($job->instanceId), + ProcessRequestToken::findOrFail($job->tokenId), + $job->data, + $job->attemptNum + 1 + ); + } $newJob->delay($this->retryWaitTime()); + $newJob->onQueue('bpmn'); dispatch($newJob); } diff --git a/ProcessMaker/Jobs/RunNayraScriptTask.php b/ProcessMaker/Jobs/RunNayraScriptTask.php index 3e9ed525f7..0384072a7c 100644 --- a/ProcessMaker/Jobs/RunNayraScriptTask.php +++ b/ProcessMaker/Jobs/RunNayraScriptTask.php @@ -8,6 +8,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; +use ProcessMaker\Exception\ConfigurationException; use ProcessMaker\Exception\ScriptException; use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\Managers\DataManager; @@ -78,7 +79,7 @@ public function handle() if (empty($scriptRef)) { $code = $element->getScript(); if (empty($code)) { - throw new ScriptException(__('No code or script assigned to ":name"', ['name' => $element->getName()])); + throw new ConfigurationException(__('No code or script assigned to ":name"', ['name' => $element->getName()])); } $language = Script::scriptFormat2Language($element->getProperty('scriptFormat', 'application/x-php')); $script = new Script([ @@ -88,7 +89,11 @@ public function handle() 'script_executor_id' => ScriptExecutor::initialExecutor($language)->id, ]); } else { - $script = Script::findOrFail($scriptRef)->versionFor($instance); + $script = Script::find($scriptRef); + if (!$script) { + throw new ConfigurationException(__('Script ":id" not found', ['id' => $scriptRef])); + } + $script = $script->versionFor($instance); } $dataManager = new DataManager(); @@ -97,6 +102,9 @@ public function handle() // Dispatch complete task action WorkflowManager::completeTask($processModel, $instance, $token, $response['output']); + } catch (ConfigurationException $exception) { + $output = $exception->getMessageForData($token); + WorkflowManager::completeTask($processModel, $instance, $token, $output); } catch (Throwable $exception) { Log::error('Script failed: ' . $scriptRef . ' - ' . $exception->getMessage()); Log::error($exception->getTraceAsString()); diff --git a/ProcessMaker/Jobs/RunNayraServiceTask.php b/ProcessMaker/Jobs/RunNayraServiceTask.php index bc72724654..49f793d99c 100644 --- a/ProcessMaker/Jobs/RunNayraServiceTask.php +++ b/ProcessMaker/Jobs/RunNayraServiceTask.php @@ -20,8 +20,8 @@ class RunNayraServiceTask implements ShouldQueue public $tokenId; - public $userId; - + public $attemptNum = 1; + /** * Create a new job instance. * @@ -48,6 +48,6 @@ public function handle() $token->setInstance($instance); // Run service task - WorkflowManager::handleServiceTask($token); + WorkflowManager::handleServiceTask($token, $this); } } diff --git a/ProcessMaker/Jobs/RunScriptTask.php b/ProcessMaker/Jobs/RunScriptTask.php index 55f86eb244..53aed881ac 100644 --- a/ProcessMaker/Jobs/RunScriptTask.php +++ b/ProcessMaker/Jobs/RunScriptTask.php @@ -3,7 +3,9 @@ namespace ProcessMaker\Jobs; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Facades\Log; +use ProcessMaker\Exception\ConfigurationException; use ProcessMaker\Exception\ScriptException; use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\Managers\DataManager; @@ -70,7 +72,7 @@ public function action(ProcessRequestToken $token = null, ScriptTaskInterface $e if (empty($scriptRef)) { $code = $element->getScript(); if (empty($code)) { - throw new ScriptException(__('No code or script assigned to ":name"', ['name' => $element->getName()])); + throw new ConfigurationException(__('No code or script assigned to ":name"', ['name' => $element->getName()])); } $language = Script::scriptFormat2Language($element->getProperty('scriptFormat', 'application/x-php')); $script = new Script([ @@ -80,7 +82,11 @@ public function action(ProcessRequestToken $token = null, ScriptTaskInterface $e 'script_executor_id' => ScriptExecutor::initialExecutor($language)->id, ]); } else { - $script = Script::findOrFail($scriptRef)->versionFor($instance); + $script = Script::find($scriptRef); + if (!$script) { + throw new ConfigurationException(__('Script ":id" not found', ['id' => $scriptRef])); + } + $script = $script->versionFor($instance); } $errorHandling = new ErrorHandling($element, $token); @@ -91,29 +97,19 @@ public function action(ProcessRequestToken $token = null, ScriptTaskInterface $e $data = $dataManager->getData($token); $response = $script->runScript($data, $configuration, $token->getId(), $errorHandling->timeout()); - $this->withUpdatedContext(function ($engine, $instance, $element, $processModel, $token) use ($response) { - // Exit if the task was completed or closed - if (!$token || !$element) { - return; - } - // Update data - if (is_array($response['output'])) { - // Validate data - WorkflowManager::validateData($response['output'], $processModel, $element); - $dataManager = new DataManager(); - $dataManager->updateData($token, $response['output']); - $engine->runToNextState(); - } - $element->complete($token); - $this->engine = $engine; - $this->instance = $instance; - }); + $this->updateData($response); + } catch (ConfigurationException $exception) { + $this->unlock(); + $this->updateData(['output' => $exception->getMessageForData($token)]); } catch (Throwable $exception) { - $token->setStatus(ScriptTaskInterface::TOKEN_STATE_FAILING); - $message = $exception->getMessage(); + $finalAttempt = true; if ($errorHandling) { - $message = $errorHandling->handleRetries($this, $exception); + [$message, $finalAttempt] = $errorHandling->handleRetries($this, $exception); + } + + if ($finalAttempt) { + $token->setStatus(ScriptTaskInterface::TOKEN_STATE_FAILING); } $error = $element->getRepository()->createError(); @@ -129,6 +125,27 @@ public function action(ProcessRequestToken $token = null, ScriptTaskInterface $e } } + private function updateData($response) + { + $this->withUpdatedContext(function ($engine, $instance, $element, $processModel, $token) use ($response) { + // Exit if the task was completed or closed + if (!$token || !$element) { + return; + } + // Update data + if (is_array($response['output'])) { + // Validate data + WorkflowManager::validateData($response['output'], $processModel, $element); + $dataManager = new DataManager(); + $dataManager->updateData($token, $response['output']); + $engine->runToNextState(); + } + $element->complete($token); + $this->engine = $engine; + $this->instance = $instance; + }); + } + /** * When Job fails */ diff --git a/ProcessMaker/Jobs/RunServiceTask.php b/ProcessMaker/Jobs/RunServiceTask.php index 72ce729918..703ee4f777 100644 --- a/ProcessMaker/Jobs/RunServiceTask.php +++ b/ProcessMaker/Jobs/RunServiceTask.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Support\Facades\Log; +use ProcessMaker\Exception\ConfigurationException; use ProcessMaker\Exception\ScriptException; use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\Managers\DataManager; @@ -73,6 +74,7 @@ public function action(ProcessRequestToken $token = null, ServiceTaskInterface $ if ($configuration === null) { $configuration = []; } + $errorHandling = null; try { if (empty($implementation)) { throw new ScriptException('Service task implementation not defined'); @@ -104,28 +106,20 @@ public function action(ProcessRequestToken $token = null, ServiceTaskInterface $ } else { $response = $script->runScript($data, $configuration, $token->getId(), $errorHandling->timeout()); } - $this->withUpdatedContext(function ($engine, $instance, $element, $processModel, $token) use ($response) { - // Exit if the task was completed or closed - if (!$token || !$element) { - return; - } - // Update data - if (is_array($response['output'])) { - // Validate data - WorkflowManager::validateData($response['output'], $processModel, $element); - $dataManager = new DataManager(); - $dataManager->updateData($token, $response['output']); - $engine->runToNextState(); - } - $element->complete($token); - $this->engine = $engine; - $this->instance = $instance; - }); + $this->updateData($response); + } catch (ConfigurationException $exception) { + $this->unlock(); + $this->updateData(['output' => $exception->getMessageForData($token)]); } catch (Throwable $exception) { - // Change to error status - $token->setStatus(ServiceTaskInterface::TOKEN_STATE_FAILING); + $finalAttempt = true; + if ($errorHandling) { + [$message, $finalAttempt] = $errorHandling->handleRetries($this, $exception); + } - $message = $errorHandling->handleRetries($this, $exception); + if ($finalAttempt) { + // Change to error status + $token->setStatus(ServiceTaskInterface::TOKEN_STATE_FAILING); + } $error = $element->getRepository()->createError(); $error->setName($message); @@ -140,6 +134,27 @@ public function action(ProcessRequestToken $token = null, ServiceTaskInterface $ } } + private function updateData($response) + { + $this->withUpdatedContext(function ($engine, $instance, $element, $processModel, $token) use ($response) { + // Exit if the task was completed or closed + if (!$token || !$element) { + return; + } + // Update data + if (is_array($response['output'])) { + // Validate data + WorkflowManager::validateData($response['output'], $processModel, $element); + $dataManager = new DataManager(); + $dataManager->updateData($token, $response['output']); + $engine->runToNextState(); + } + $element->complete($token); + $this->engine = $engine; + $this->instance = $instance; + }); + } + /** * When Job fails */ diff --git a/ProcessMaker/Models/Script.php b/ProcessMaker/Models/Script.php index dbd291e2bc..b0ab5c9977 100644 --- a/ProcessMaker/Models/Script.php +++ b/ProcessMaker/Models/Script.php @@ -4,6 +4,7 @@ use Illuminate\Validation\Rule; use ProcessMaker\Contracts\ScriptInterface; +use ProcessMaker\Exception\ConfigurationException; use ProcessMaker\Exception\ScriptLanguageNotSupported; use ProcessMaker\Models\ScriptCategory; use ProcessMaker\Models\User; @@ -140,7 +141,7 @@ public function runScript(array $data, array $config, $tokenId = '', $timeout = $runner->setTokenId($tokenId); $user = User::find($this->run_as_user_id); if (!$user) { - throw new \RuntimeException('A user is required to run scripts'); + throw new ConfigurationException('A user is required to run scripts'); } return $runner->run($this->code, $data, $config, $timeout, $user); diff --git a/ProcessMaker/Nayra/Managers/WorkflowManagerRabbitMq.php b/ProcessMaker/Nayra/Managers/WorkflowManagerRabbitMq.php index 0de9bfe68a..28371e3a6a 100644 --- a/ProcessMaker/Nayra/Managers/WorkflowManagerRabbitMq.php +++ b/ProcessMaker/Nayra/Managers/WorkflowManagerRabbitMq.php @@ -6,10 +6,12 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; use ProcessMaker\Contracts\WorkflowManagerInterface; +use ProcessMaker\Exception\ConfigurationException; use ProcessMaker\Exception\ScriptException; use ProcessMaker\Facades\MessageBrokerService; use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\GenerateAccessToken; +use ProcessMaker\Jobs\ErrorHandling; use ProcessMaker\Jobs\RunNayraServiceTask; use ProcessMaker\Managers\DataManager; use ProcessMaker\Managers\SignalManager; @@ -272,7 +274,7 @@ public function runServiceTask(ServiceTaskInterface $serviceTask, TokenInterface * * @param ProcessRequestToken $token */ - public function handleServiceTask(ProcessRequestToken $token) + public function handleServiceTask(ProcessRequestToken $token, RunNayraServiceTask $job) { // Get complementary information $element = $token->getDefinition(true); @@ -290,7 +292,6 @@ public function handleServiceTask(ProcessRequestToken $token) // Get service task configuration $implementation = $element->getImplementation(); $configuration = json_decode($element->getProperty('config'), true); - $errorHandling = json_decode($element->getProperty('errorHandling'), true); // Check to see if we've failed parsing. If so, let's convert to empty array. if ($configuration === null) { @@ -310,6 +311,10 @@ public function handleServiceTask(ProcessRequestToken $token) throw new ScriptException('Service task not implemented: ' . $implementation); } + // Parse config + $errorHandling = new ErrorHandling($element, $token); + $errorHandling->setDefaultsFromDataSourceConfig($configuration); + // Get data $dataManager = new DataManager(); $data = $dataManager->getData($token); @@ -317,10 +322,10 @@ public function handleServiceTask(ProcessRequestToken $token) // Run implementation/script if ($existsImpl) { $response = [ - 'output' => WorkflowManager::runServiceImplementation($implementation, $data, $configuration, $token->getId(), $errorHandling), + 'output' => WorkflowManager::runServiceImplementation($implementation, $data, $configuration, $token->getId(), $errorHandling->timeout()), ]; } else { - $response = $script->runScript($data, $configuration, $token->getId(), $errorHandling); + $response = $script->runScript($data, $configuration, $token->getId(), $errorHandling->timeout()); } // Update data @@ -332,28 +337,59 @@ public function handleServiceTask(ProcessRequestToken $token) } // Dispatch complete task action - $this->dispatchAction([ - 'bpmn' => $version, - 'action' => self::ACTION_COMPLETE_TASK, - 'params' => [ - 'request_id' => $token->process_request_id, - 'token_id' => $token->uuid, - 'element_id' => $token->element_id, - 'data' => $response['output'], - ], - 'state' => $state, - 'session' => [ - 'user_id' => $userId, - ], - ]); + $this->dispatchActionForServiceTask($version, $token, $response, $state, $userId); + } catch (ConfigurationException $exception) { + // If Task failed because of configuration error: Complete the task with the error message + $response = [ + 'output' => $exception->getMessageForData($token), + ]; + $this->dispatchActionForServiceTask($version, $token, $response, $state, $userId); + } catch (Throwable $exception) { + + $thisWasFinalAttempt = true; + if (isset($errorHandling)) { + $thisWasFinalAttempt = ($errorHandling->retryAttempts() === 0) || ($job->attemptNum >= $errorHandling->retryAttempts()); + $message = $errorHandling->handleRetries($job, $exception); + + $error = $element->getRepository()->createError(); + $error->setName($message); + + $token->setProperty('error', $error); + $exceptionClass = get_class($exception); + $modifiedException = new $exceptionClass($message); + $token->logError($modifiedException, $element); + } + // Log message errors Log::info('Service task failed: ' . $implementation . ' - ' . $exception->getMessage()); Log::error($exception->getTraceAsString()); - $this->taskFailed($instance, $token, $exception->getMessage()); + + if ($thisWasFinalAttempt) { + // When the last + $this->taskFailed($instance, $token, $exception->getMessage()); + } } } + private function dispatchActionForServiceTask($version, $token, $response, $state, $userId) + { + $this->dispatchAction([ + 'bpmn' => $version, + 'action' => self::ACTION_COMPLETE_TASK, + 'params' => [ + 'request_id' => $token->process_request_id, + 'token_id' => $token->uuid, + 'element_id' => $token->element_id, + 'data' => $response['output'], + ], + 'state' => $state, + 'session' => [ + 'user_id' => $userId, + ], + ]); + } + /** * Trigger a boundary event * @@ -518,7 +554,7 @@ public function throwSignalEventRequest(ProcessRequest $request, $signalRef, arr private function serializeState(ProcessRequest $instance) { if ($instance->collaboration) { - $requests = $instance->collaboration->requests()->where('status', 'ACTIVE')->get(); + $requests = $instance->collaboration->requests()->whereIn('status', ['ACTIVE', 'ERROR'])->get(); } else { $requests = collect([$instance]); } diff --git a/ProcessMaker/Traits/MakeHttpRequests.php b/ProcessMaker/Traits/MakeHttpRequests.php index dec3c38140..6253a4434f 100644 --- a/ProcessMaker/Traits/MakeHttpRequests.php +++ b/ProcessMaker/Traits/MakeHttpRequests.php @@ -11,6 +11,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Log; use Mustache_Engine; +use ProcessMaker\Exception\ConfigurationException; use ProcessMaker\Exception\HttpInvalidArgumentException; use ProcessMaker\Exception\HttpResponseException; use ProcessMaker\Helpers\StringHelper; @@ -181,6 +182,9 @@ private function evalMustache($expression, array $data) private function prepareRequestWithOutboundConfig(array $requestData, array &$config) { $outboundConfig = $config['outboundConfig'] ?? []; + if (!array_key_exists($config['endpoint'], $this->endpoints)) { + throw new ConfigurationException("Endpoint '{$config['endpoint']}' not found"); + } $endpoint = $this->endpoints[$config['endpoint']]; $this->verifySsl = array_key_exists('verify_certificate', $this->credentials) ? $this->credentials['verify_certificate'] diff --git a/config/app.php b/config/app.php index 59e9202cb9..bcf30fa5aa 100644 --- a/config/app.php +++ b/config/app.php @@ -115,6 +115,9 @@ // Message broker driver to use in Workflow Manager 'message_broker_driver' => env('MESSAGE_BROKER_DRIVER', 'default'), + // When true, halt process execution if certain configuration settings are missing + 'configuration_debug_mode' => env('CONFIGURATION_DEBUG_MODE', false), + // Global app settings 'settings' => [ diff --git a/tests/Fixtures/script_without_settings.bpmn b/tests/Fixtures/script_without_settings.bpmn new file mode 100644 index 0000000000..5a6ae4ba4e --- /dev/null +++ b/tests/Fixtures/script_without_settings.bpmn @@ -0,0 +1,50 @@ + + + + + node_18 + + + node_18 + node_26 + + + node_26 + node_28 + + + node_28 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Jobs/RunScriptTaskTest.php b/tests/Jobs/RunScriptTaskTest.php new file mode 100644 index 0000000000..13af24569f --- /dev/null +++ b/tests/Jobs/RunScriptTaskTest.php @@ -0,0 +1,93 @@ +runJob($class, ''); + + $this->assertEmpty($request->errors); + $this->assertEquals('my node (node_2): No code or script assigned to "Script Task"', $request->data['_configuration_error_node_2']); + } + + /** + * @dataProvider jobTypes + */ + public function testScriptNotFound($class) + { + $request = $this->runJob($class, 12345); + + $this->assertEmpty($request->errors); + $this->assertEquals('my node (node_2): Script "12345" not found', $request->data['_configuration_error_node_2']); + } + + /** + * @dataProvider jobTypes + */ + public function testRunAsUserNotFound($class) + { + $script = Script::factory()->create(['run_as_user_id' => null]); + $request = $this->runJob($class, $script->id); + + $this->assertEmpty($request->errors); + $this->assertEquals('my node (node_2): A user is required to run scripts', $request->data['_configuration_error_node_2']); + } + + private function runJob($class, $scriptId) + { + $user = User::factory()->create(); + Auth::login($user); + $bpmn = file_get_contents(__DIR__ . '/../Fixtures/script_without_settings.bpmn'); + $bpmn = str_replace('[script_id]', $scriptId, $bpmn); + $process = Process::factory()->create([ + 'bpmn' => $bpmn, + ]); + $process->manager_id = $user->id; + $process->save(); + + $request = ProcessRequest::factory()->create([ + 'process_id' => $process->id, + ]); + $token = ProcessRequestToken::factory()->create([ + 'process_request_id' => $request->id, + 'element_id' => 'node_2', + 'element_name' => 'my node', + 'status' => 'ACTIVE', + ]); + + if ($class === RunScriptTask::class) { + $class::dispatch($process, $request, $token, []); + } else { + $class::dispatch($token); + } + + return $request->refresh(); + } + + public function jobTypes() + { + return [ + [RunScriptTask::class], + [RunNayraScriptTask::class], + ]; + } +}