diff --git a/ProcessMaker/Jobs/BpmnAction.php b/ProcessMaker/Jobs/BpmnAction.php index f78ddaf647..d962355794 100644 --- a/ProcessMaker/Jobs/BpmnAction.php +++ b/ProcessMaker/Jobs/BpmnAction.php @@ -194,13 +194,13 @@ private function lockInstance($instanceId) for ($tries = 0; $tries < $maxRetries; $tries++) { $currentLock = $this->currentLock($ids); if (!$currentLock) { - if (ProcessRequest::find($instanceId)) { + if (ProcessRequest::query()->whereKey($instanceId)->exists()) { $lock = $this->requestLock($ids); } else { throw new Exception('Unable to lock instance #' . $this->instanceId . ': Request does not exists'); } } elseif ($lock->id == $currentLock->id) { - $instance = ProcessRequest::findOrFail($instanceId); + $instance = $this->findProcessRequestForBpmnAction($instanceId); $this->activateLock($lock); return $instance; @@ -231,7 +231,7 @@ private function findInstanceWithRetry($instanceId) for ($attempt = 0; $attempt < $totalAttempts; $attempt++) { try { - $instance = ProcessRequest::findOrFail($instanceId); + $instance = $this->findProcessRequestForBpmnAction($instanceId); return $instance; } catch (ModelNotFoundException $e) { @@ -344,6 +344,23 @@ private function mSleep($milliseconds) usleep($microseconds); } + /** + * Load ProcessRequest with relations used when wiring the BPMN engine (reduces N+1 during completeTask / other BPMN jobs). + * + * @param int|string $instanceId + */ + private function findProcessRequestForBpmnAction($instanceId): ProcessRequest + { + return ProcessRequest::query() + ->with([ + 'process', + 'processVersion', + 'collaboration', + ]) + ->whereKey($instanceId) + ->firstOrFail(); + } + public function __destruct() { $this->instance = null; diff --git a/ProcessMaker/Models/ProcessRequest.php b/ProcessMaker/Models/ProcessRequest.php index 57509fe823..3bbc15d934 100644 --- a/ProcessMaker/Models/ProcessRequest.php +++ b/ProcessMaker/Models/ProcessRequest.php @@ -848,8 +848,12 @@ public function updateCatchEvents() public function mergeLatestStoredData() { $store = $this->getDataStore(); - $latest = self::select('data')->find($this->getId()); - $this->data = $store->updateArray($latest->data); + // Read only the JSON column without hydrating a full ProcessRequest row (called often during BPMN transitions). + $latestData = static::query()->whereKey($this->getKey())->value('data'); + if (!is_array($latestData)) { + $latestData = []; + } + $this->data = $store->updateArray($latestData); return $this->data; } diff --git a/tests/Feature/Api/TaskControllerUpdateResponseTimeTest.php b/tests/Feature/Api/TaskControllerUpdateResponseTimeTest.php new file mode 100644 index 0000000000..923622f82e --- /dev/null +++ b/tests/Feature/Api/TaskControllerUpdateResponseTimeTest.php @@ -0,0 +1,92 @@ +assertLessThanOrEqual( + $maxMs, + $elapsedMs, + "{$label}: request took " . round($elapsedMs) . "ms, limit {$maxMs}ms (set TASK_UPDATE_MAX_RESPONSE_MS to adjust)" + ); + } + + public function testCompleteTaskUpdateResponseTime(): void + { + $maxMs = $this->maxResponseTimeMsForTaskUpdate(); + + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $this->user->id, + 'status' => 'ACTIVE', + ]); + + WorkflowManager::shouldReceive('completeTask') + ->once() + ->with(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()); + + $params = ['status' => 'COMPLETED', 'data' => ['foo' => 'bar']]; + + $started = microtime(true); + $response = $this->apiCall('PUT', '/tasks/' . $token->id, $params); + $elapsedMs = (microtime(true) - $started) * 1000; + + $response->assertStatus(200); + $this->assertResponseWithinMs($elapsedMs, $maxMs, 'PUT api/tasks/{id} (status=COMPLETED)'); + } + + public function testReassignTaskUpdateResponseTime(): void + { + $maxMs = $this->maxResponseTimeMsForTaskUpdate(); + + $assignee = User::factory()->create(); + $token = ProcessRequestToken::factory()->create([ + 'user_id' => $this->user->id, + 'status' => 'ACTIVE', + ]); + + // Prevent notification errors by faking the notification system + // The factory generates random element_ids that don't exist in the BPMN document + Notification::fake(); + + $started = microtime(true); + $response = $this->apiCall('PUT', '/tasks/' . $token->id, [ + 'user_id' => $assignee->id, + 'comments' => 'response time test', + ]); + $elapsedMs = (microtime(true) - $started) * 1000; + + $response->assertStatus(200); + $this->assertResponseWithinMs($elapsedMs, $maxMs, 'PUT api/tasks/{id} (reassign)'); + } +}