/** * Executes the following: * - Attempts to update task object's row as processing * - If successful, Begins a database transaction * - Attempts to select that row for update * - If successful, attempts to execute method callback defined in row, returning a result object * - Updates the task object based on the resulting object * - Commits the database transaction * * @param Dunagan_ProcessQueue_Model_Task_Interface $processQueueTaskObject * @param boolean $row_has_already_been_set_as_processing - Has this task object already been set as processing? * This typically denotes that whatever process created * the task object wants to ensure that it is also the * process which executes the task * @return Dunagan_ProcessQueue_Model_Task_Result_Interface|null - Returns an object implementing interface * Dunagan_ProcessQueue_Model_Task_Result_Interface * if execution did not throw an uncaught exception * Returns null if the task was unable to be selected * for processing (another thread has likely already * begun processing the task) or if an uncaught * exception was thrown */ public function processQueueTask(Dunagan_ProcessQueue_Model_Task_Interface $processQueueTaskObject, $row_has_already_been_set_as_processing = false) { if (!$row_has_already_been_set_as_processing) { try { $able_to_lock_for_processing = $processQueueTaskObject->attemptUpdatingRowAsProcessing(); if (!$able_to_lock_for_processing) { // Assume another thread of execution is already processing this task return null; } } catch (Exception $e) { $error_message = $this->__(self::EXCEPTION_UPDATE_AS_PROCESSING, $processQueueTaskObject->getId(), $e->getMessage()); $this->_logError($error_message); return null; } } else { // Don't attempt to update the status as PROCESSING, but set the task last executed at time, as well as // clear out the existing status message $task_id = $processQueueTaskObject->getId(); try { $this->_getTaskResourceSingleton()->updateLastExecutedAtToCurrentTime(array($task_id), true); } catch (Exception $e) { $error_message = $this->__(self::EXCEPTION_UPDATING_LAST_EXECUTED_FOR_PRIOR_PROCESSING_TASK, $task_id, $e->getMessage()); $this->_logError($error_message); // Do not interrupt processing of the task due to an exception thrown here } } // At this point, start transaction and lock row for update to ensure exclusive access $taskResourceSingleton = $processQueueTaskObject->getResource(); $taskResourceSingleton->beginTransaction(); try { $selected = $processQueueTaskObject->selectForUpdate(); if (!$selected) { // Assume another thread has already locked this task object's row, although this shouldn't happen $taskResourceSingleton->rollBack(); $error_message = $this->__(self::ERROR_FAILED_TO_SELECT_FOR_UPDATE, $processQueueTaskObject->getId()); $this->_logError($error_message); return null; } } catch (Exception $e) { $taskResourceSingleton->rollBack(); $error_message = $this->__(self::EXCEPTION_SELECT_FOR_UPDATE, $processQueueTaskObject->getId(), $e->getMessage()); $this->_logError($error_message); return null; } try { $taskExecutionResult = $processQueueTaskObject->executeTask(); } catch (Dunagan_ProcessQueue_Model_Exception_Rollback $taskExecutionResult) { // Rollback the transaction to prevent any orphaned/corrupt data from persisting in the system $taskResourceSingleton->rollBack(); // We still want the task to update its status and status message fields according to the task execution, // so we continue the execution of this method } catch (Exception $e) { // If the task execution threw an uncaught exception, rollback the transaction and return error status $taskResourceSingleton->rollBack(); $error_message = $this->__(self::EXCEPTION_EXECUTING_TASK, $processQueueTaskObject->getId(), $e->getMessage()); $processQueueTaskObject->setTaskAsErrored($error_message); $this->_logError($error_message); return null; } try { $processQueueTaskObject->actOnTaskResult($taskExecutionResult); } catch (Exception $e) { // At this point, we would assume that the task has been performed successfully since executeTask() did not // throw any exceptions. As such, log the exception but commit the transaction. Even if this leaves a row // in the PROCESSING state, it's better than leaving parts of the database out of sync with external resources $error_message = $this->__(self::EXCEPTION_ACTING_ON_TASK_RESULT, $processQueueTaskObject->getId(), $e->getMessage()); $this->_logError($error_message); } try { // Check to ensure that a rollback transaction exception was not thrown if (!$taskExecutionResult->shouldTransactionBeRolledBack()) { $taskResourceSingleton->commit(); } } catch (Exception $e) { // If an exception occurs here, rollback $taskResourceSingleton->rollback(); $processQueueTaskObject->setTaskAsErrored(); $error_message = $this->__(self::EXCEPTION_COMMITTING_TRANSACTION, $processQueueTaskObject->getId(), $e->getMessage()); $this->_logError($error_message); } return $taskExecutionResult; }