diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a08e547 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +language: php +php: + - 5.3 + - 5.4 + - 5.5 + #- 5.6 + #- hhvm +matrix: + allow_failures: + - php: 5.6 + - php: hhvm +env: + global: + - MAGENTO_DB_ALLOWSAME=1 + - SKIP_CLEANUP=1 + matrix: + - MAGENTO_VERSION=magento-ce-1.9.1.0 + - MAGENTO_VERSION=magento-ce-1.9.0.1 + - MAGENTO_VERSION=magento-ce-1.8.1.0 + - MAGENTO_VERSION=magento-ce-1.8.0.0 + - MAGENTO_VERSION=magento-ce-1.7.0.2 +before_script: + - curl -OL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar +script: + # Code Style + - php phpcs.phar --standard=./phpcs.xml --encoding=utf-8 --report-width=180 ./app ./shell + # Unit Tests + - curl -sSL https://raw.githubusercontent.com/AOEpeople/MageTestStand/master/setup.sh | bash +notifications: + email: + recipients: + - travis@fabrizio-branca.de + on_success: always + on_failure: always \ No newline at end of file diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Cron.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Cron.php deleted file mode 100755 index 24b391e..0000000 --- a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Cron.php +++ /dev/null @@ -1,58 +0,0 @@ -_blockGroup = 'aoe_scheduler'; - $this->_controller = 'adminhtml_cron'; - $this->_headerText = Mage::helper('aoe_scheduler')->__('Available tasks'); - parent::__construct(); - } - - - /** - * Prepare layout - * - * @return Aoe_Scheduler_Block_Adminhtml_Cron - */ - protected function _prepareLayout() - { - $this->removeButton('add'); - $this->_addButton('add_new', array( - 'label' => Mage::helper('aoe_scheduler')->__('Generate Schedule'), - 'onclick' => "setLocation('{$this->getUrl('*/*/generateSchedule')}')", - )); - $this->_addButton('configure', array( - 'label' => Mage::helper('aoe_scheduler')->__('Cron Configuration'), - 'onclick' => "setLocation('{$this->getUrl('adminhtml/system_config/edit', array('section' => 'system'))}#system_cron')", - )); - return parent::_prepareLayout(); - } - - - /** - * Returns the CSS class for the header - * - * Usually 'icon-head' and a more precise class is returned. We return - * only an empty string to avoid spacing on the left of the header as we - * don't have an icon. - * - * @return string - */ - public function getHeaderCssClass() - { - return ''; - } - -} diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Cron/Grid.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Cron/Grid.php deleted file mode 100755 index 2b3b58a..0000000 --- a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Cron/Grid.php +++ /dev/null @@ -1,141 +0,0 @@ -setId('cron_grid'); - $this->_filterVisibility = false; - $this->_pagerVisibility = false; - } - - - /** - * Preparation of the data that is displayed by the grid. - * - * @return Aoe_Scheduler_Block_Adminhtml_Cron_Grid Self - */ - protected function _prepareCollection() - { - $collection = Mage::getModel('aoe_scheduler/collection_crons'); - $this->setCollection($collection); - return parent::_prepareCollection(); - } - - - /** - * Add mass-actions to grid - * - * @return Aoe_Scheduler_Block_Adminhtml_Cron_Grid - */ - protected function _prepareMassaction() - { - $this->setMassactionIdField('id'); - $this->getMassactionBlock()->setFormFieldName('codes'); - $this->getMassactionBlock()->addItem('schedule', array( - 'label' => Mage::helper('aoe_scheduler')->__('Schedule now'), - 'url' => $this->getUrl('*/*/scheduleNow'), - )); - if (Mage::getStoreConfig('system/cron/enableRunNow')) { - $this->getMassactionBlock()->addItem('run', array( - 'label' => Mage::helper('aoe_scheduler')->__('Run now'), - 'url' => $this->getUrl('*/*/runNow'), - )); - } - $this->getMassactionBlock()->addItem('disable', array( - 'label' => Mage::helper('aoe_scheduler')->__('Disable'), - 'url' => $this->getUrl('*/*/disable'), - )); - $this->getMassactionBlock()->addItem('enable', array( - 'label' => Mage::helper('aoe_scheduler')->__('Enable'), - 'url' => $this->getUrl('*/*/enable'), - )); - return $this; - } - - - /** - * Preparation of the requested columns of the grid - * - * @return Aoe_Scheduler_Block_Adminhtml_Cron_Grid Self - */ - protected function _prepareColumns() - { - $this->addColumn('id', array( - 'header' => Mage::helper('aoe_scheduler')->__('Code'), - 'index' => 'id', - 'sortable' => false, - )); - $this->addColumn('cron_expr', array( - 'header' => Mage::helper('aoe_scheduler')->__('Cron Expression'), - 'index' => 'cron_expr', - 'sortable' => false, - )); - $this->addColumn('model', array( - 'header' => Mage::helper('aoe_scheduler')->__('Model'), - 'index' => 'model', - 'sortable' => false, - )); - $this->addColumn('status', array( - 'header' => Mage::helper('aoe_scheduler')->__('Status'), - 'index' => 'status', - 'sortable' => false, - 'frame_callback' => array($this, 'decorateStatus'), - )); - return parent::_prepareColumns(); - } - - - /** - * Decorate status column values - * - * @return string - */ - public function decorateStatus($value) - { - $cell = sprintf('%s', - ($value == Aoe_Scheduler_Model_Configuration::STATUS_DISABLED) ? 'critical' : 'notice', - Mage::helper('aoe_scheduler')->__($value) - ); - return $cell; - } - - - /** - * Helper function to add store filter condition - * - * @param Mage_Core_Model_Mysql4_Collection_Abstract $collection Data collection - * @param Mage_Adminhtml_Block_Widget_Grid_Column $column Column information to be filtered - * @return void - */ - protected function _filterStoreCondition($collection, $column) - { - if (!$value = $column->getFilter()->getValue()) { - return; - } - $this->getCollection()->addStoreFilter($value); - } - - - /** - * Helper function to receive grid functionality urls for current grid - * - * @return string Requested URL - */ - public function getGridUrl() - { - return $this->getUrl('adminhtml/scheduler/cron', array('_current' => true)); - } - -} diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Instructions.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Instructions.php new file mode 100644 index 0000000..432de45 --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Instructions.php @@ -0,0 +1,20 @@ +_blockGroup = 'aoe_scheduler'; + $this->_controller = 'adminhtml_job'; + $this->_headerText = $this->__('Available Jobs'); + parent::__construct(); + } + + /** + * Prepare layout + * + * @return $this + */ + protected function _prepareLayout() + { + $this->removeButton('add'); + $this->_addButton( + 'add_new_job', + array( + 'label' => $this->__('Create new job'), + 'onclick' => "setLocation('{$this->getUrl('*/*/new')}')", + 'class' => 'add' + ) + ); + $this->_addButton( + 'add_new', + array( + 'label' => $this->__('Generate Schedule'), + 'onclick' => "setLocation('{$this->getUrl('*/*/generateSchedule')}')", + ) + ); + $this->_addButton( + 'configure', + array( + 'label' => $this->__('Cron Configuration'), + 'onclick' => "setLocation('{$this->getUrl('adminhtml/system_config/edit', array('section' => 'system'))}#system_cron')", + ) + ); + return parent::_prepareLayout(); + } + + + /** + * Returns the CSS class for the header + * + * Usually 'icon-head' and a more precise class is returned. We return + * only an empty string to avoid spacing on the left of the header as we + * don't have an icon. + * + * @return string + */ + public function getHeaderCssClass() + { + return ''; + } +} diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit.php new file mode 100644 index 0000000..e1303ee --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit.php @@ -0,0 +1,68 @@ +getJob()->isOverlay()) { + $this->updateButton('delete', 'label', $this->__('Reset overlay')); + } elseif ($this->getJob()->isXmlOnly()) { + $this->removeButton('delete'); + } + $this->removeButton('reset'); + } + + /** + * Internal constructor + * + */ + protected function _construct() + { + parent::_construct(); + $this->_objectId = 'job_code'; + $this->_blockGroup = 'aoe_scheduler'; + $this->_controller = 'adminhtml_job'; + } + + /** + * Get job + * + * @return Aoe_Scheduler_Model_Job + */ + public function getJob() + { + return Mage::registry('current_job_instance'); + } + + + /** + * Return translated header text depending on creating/editing action + * + * @return string + */ + public function getHeaderText() + { + if ($this->getJob()->getId()) { + return $this->__('Job "%s"', $this->escapeHtml($this->getJob()->getJobCode())); + } else { + return $this->__('New Job'); + } + } + + /** + * Return save url for edit form + * + * @return string + */ + public function getSaveUrl() + { + return $this->getUrl('*/*/save', array('_current'=>true, 'back'=>null)); + } +} diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit/Form.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit/Form.php new file mode 100644 index 0000000..f011b50 --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit/Form.php @@ -0,0 +1,26 @@ + 'edit_form', + 'action' => $this->getData('action'), + 'method' => 'post' + )); + $form->setUseContainer(true); + $this->setForm($form); + return parent::_prepareForm(); + } +} diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit/Tab/Form.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit/Tab/Form.php new file mode 100644 index 0000000..c9c9941 --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit/Tab/Form.php @@ -0,0 +1,286 @@ +setActive(true); + } + + /** + * Prepare label for tab + * + * @return string + */ + public function getTabLabel() + { + return $this->__('General'); + } + + /** + * Prepare title for tab + * + * @return string + */ + public function getTabTitle() + { + return $this->__('General'); + } + + /** + * Returns status flag about this tab can be shown or not + * + * @return true + */ + public function canShowTab() + { + return true; + } + + /** + * Returns status flag about this tab hidden or not + * + * @return true + */ + public function isHidden() + { + return false; + } + + /** + * Get job + * + * @return Aoe_Scheduler_Model_Job + */ + public function getJob() + { + return Mage::registry('current_job_instance'); + } + + + /** + * Prepare form before rendering HTML + * + * @return Mage_Widget_Block_Adminhtml_Widget_Instance_Edit_Tab_Main + */ + protected function _prepareForm() + { + $job = $this->getJob(); + $form = new Varien_Data_Form( + array( + 'id' => 'edit_form', + 'action' => $this->getData('action'), + 'method' => 'post' + ) + ); + + $fieldset = $form->addFieldset('base_fieldset', array('legend' => $this->__('General'))); + $this->_addElementTypes($fieldset); + + $fieldset->addField( + 'job_code', + 'text', + array( + 'name' => 'job_code', + 'label' => $this->__('Job code'), + 'title' => $this->__('Job code'), + 'class' => '', + 'required' => true, + 'disabled' => $job->getJobCode() ? true : false, + ) + ); + + $fieldset->addField( + 'name', + 'text', + array( + 'name' => 'name', + 'label' => $this->__('Name'), + 'title' => $this->__('Name'), + 'class' => '', + 'required' => false, + 'after_element_html' => $this->getOriginalValueSnippet($job, 'name'), + ) + ); + + $fieldset->addField( + 'short_description', + 'textarea', + array( + 'name' => 'short_description', + 'label' => $this->__('Short description'), + 'title' => $this->__('Short description'), + 'class' => '', + 'required' => false, + 'after_element_html' => $this->getOriginalValueSnippet($job, 'short_description'), + ) + ); + + $fieldset->addField( + 'description', + 'textarea', + array( + 'name' => 'description', + 'label' => $this->__('Description'), + 'title' => $this->__('Description'), + 'class' => '', + 'required' => false, + 'after_element_html' => $this->getOriginalValueSnippet($job, 'description'), + ) + ); + + $fieldset->addField( + 'run_model', + 'text', + array( + 'name' => 'run_model', + 'label' => $this->__('Run model'), + 'title' => $this->__('Run model'), + 'class' => '', + 'required' => true, + 'note' => $this->__('e.g. "aoe_scheduler/task_heartbeat::run"'), + 'after_element_html' => $this->getOriginalValueSnippet($job, 'run/model'), + ) + ); + + $fieldset->addField( + 'is_active', + 'select', + array( + 'name' => 'is_active', + 'label' => $this->__('Status'), + 'title' => $this->__('Status'), + 'required' => true, + 'options' => array( + 0 => $this->__('Disabled'), + 1 => $this->__('Enabled') + ), + 'after_element_html' => $this->getOriginalValueSnippetFlag($job, 'is_active', 'Enabled', 'Disabled'), + ) + ); + + $fieldset = $form->addFieldset('cron_fieldset', array('legend' => $this->__('Scheduling'))); + $this->_addElementTypes($fieldset); + + $fieldset->addField( + 'schedule_config_path', + 'text', + array( + 'name' => 'schedule_config_path', + 'label' => $this->__('Cron configuration path'), + 'title' => $this->__('Cron configuration path'), + 'class' => '', + 'required' => false, + 'note' => $this->__( + 'Path to system configuration containing the cron configuration for this job. (e.g. system/cron/scheduler_cron_expr_heartbeat) This configuration - if set - has a higher priority over the cron expression configured with the job directly.' + ), + 'after_element_html' => $this->getOriginalValueSnippet($job, 'schedule/config_path'), + ) + ); + + $fieldset->addField( + 'schedule_cron_expr', + 'text', + array( + 'name' => 'schedule_cron_expr', + 'label' => $this->__('Cron expression'), + 'title' => $this->__('Cron expression'), + 'required' => false, + 'note' => $this->__('e.g "*/5 * * * *" or "always"'), + 'after_element_html' => $this->getOriginalValueSnippet($job, 'schedule/cron_expr'), + ) + ); + + $fieldset = $form->addFieldset('parameter_fieldset', array('legend' => $this->__('Extras'))); + $this->_addElementTypes($fieldset); + + $fieldset->addField( + 'parameters', + 'textarea', + array( + 'name' => 'parameters', + 'label' => $this->__('Parameters'), + 'title' => $this->__('Parameters'), + 'class' => 'textarea', + 'required' => false, + 'note' => $this->__('These parameters will be passed to the model. It is up to the model to specify the format of these parameters (e.g. json/xml/...'), + 'after_element_html' => $this->getOriginalValueSnippet($job, 'parameters'), + ) + ); + + $fieldset->addField( + 'groups', + 'textarea', + array( + 'name' => 'groups', + 'label' => $this->__('Groups'), + 'title' => $this->__('Groups'), + 'class' => 'textarea', + 'required' => false, + 'note' => $this->__('Comma-separated list of groups (tags) that can be used with the include/exclude command line options of scheduler.php'), + 'after_element_html' => $this->getOriginalValueSnippet($job, 'groups'), + ) + ); + + $this->setForm($form); + + return parent::_prepareForm(); + } + + protected function getOriginalValueSnippet(Aoe_Scheduler_Model_Job $job, $key) + { + if ($job->isDbOnly()) { + return ''; + } + + $xmlJobData = $job->getXmlJobData(); + if (!array_key_exists($key, $xmlJobData)) { + return ''; + } + + $value = $xmlJobData[$key]; + if ($value === null || $value === '') { + $value = 'empty'; + } + + return '

Original: ' . $value . '

'; + } + + protected function getOriginalValueSnippetFlag(Aoe_Scheduler_Model_Job $job, $key, $trueLabel, $falseLabel) + { + if ($job->isDbOnly()) { + return ''; + } + + $xmlJobData = $job->getXmlJobData(); + if (!array_key_exists($key, $xmlJobData)) { + return ''; + } + + $value = $this->__(!in_array($xmlJobData[$key], array(false, 'false', 0, '0'), true) ? $trueLabel : $falseLabel); + + return '

Original: ' . $value . '

'; + } + + /** + * Initialize form fields values + * + * @return $this + */ + protected function _initFormValues() + { + $this->getForm()->addValues($this->getJob()->getData()); + return parent::_initFormValues(); + } +} diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit/Tabs.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit/Tabs.php new file mode 100644 index 0000000..dbacdf6 --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Edit/Tabs.php @@ -0,0 +1,21 @@ +setId('job_tabs'); + $this->setDestElementId('edit_form'); + $this->setTitle($this->__('Job')); + } +} diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Grid.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Grid.php new file mode 100644 index 0000000..a21e0dc --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Job/Grid.php @@ -0,0 +1,248 @@ +setId('job_grid'); + $this->_filterVisibility = false; + $this->_pagerVisibility = false; + } + + + /** + * Preparation of the data that is displayed by the grid. + * + * @return $this + */ + protected function _prepareCollection() + { + /** @var Aoe_Scheduler_Model_Resource_Job_Collection $collection */ + $collection = Mage::getSingleton('aoe_scheduler/job')->getCollection(); + $this->setCollection($collection); + return parent::_prepareCollection(); + } + + + /** + * Add mass-actions to grid + * + * @return $this + */ + protected function _prepareMassaction() + { + $this->setMassactionIdField('id'); + $this->getMassactionBlock()->setFormFieldName('codes'); + $this->getMassactionBlock()->addItem( + 'schedule', + array( + 'label' => $this->__('Schedule now'), + 'url' => $this->getUrl('*/*/scheduleNow'), + ) + ); + if (Mage::getStoreConfig('system/cron/enableRunNow')) { + $this->getMassactionBlock()->addItem( + 'run', + array( + 'label' => $this->__('Run now'), + 'url' => $this->getUrl('*/*/runNow'), + ) + ); + } + $this->getMassactionBlock()->addItem( + 'disable', + array( + 'label' => $this->__('Disable'), + 'url' => $this->getUrl('*/*/disable'), + ) + ); + $this->getMassactionBlock()->addItem( + 'enable', + array( + 'label' => $this->__('Enable'), + 'url' => $this->getUrl('*/*/enable'), + ) + ); + return $this; + } + + + /** + * Preparation of the requested columns of the grid + * + * @return $this + */ + protected function _prepareColumns() + { + $this->addColumn( + 'job_code', + array( + 'header' => $this->__('Job code'), + 'index' => 'job_code', + 'sortable' => false, + ) + ); + + $this->addColumn( + 'name', + array( + 'header' => $this->__('Name'), + 'index' => 'name', + 'sortable' => false, + ) + ); + + $this->addColumn( + 'short_description', + array( + 'header' => $this->__('Short Description'), + 'index' => 'short_description', + 'sortable' => false, + ) + ); + + $this->addColumn( + 'schedule_cron_expr', + array( + 'header' => $this->__('Cron expression'), + 'index' => 'schedule_cron_expr', + 'sortable' => false, + 'frame_callback' => array($this, 'decorateCronExpression'), + ) + ); + $this->addColumn( + 'run_model', + array( + 'header' => $this->__('Run model'), + 'index' => 'run_model', + 'sortable' => false, + ) + ); + $this->addColumn( + 'parameters', + array( + 'header' => $this->__('Parameters'), + 'index' => 'parameters', + 'sortable' => false, + 'frame_callback' => array($this, 'decorateTrim'), + ) + ); + $this->addColumn( + 'groups', + array( + 'header' => $this->__('Groups'), + 'index' => 'groups', + 'sortable' => false, + 'frame_callback' => array($this, 'decorateTrim'), + ) + ); + $this->addColumn( + 'type', + array( + 'header' => $this->__('Type'), + 'sortable' => false, + 'frame_callback' => array($this, 'decorateType'), + ) + ); + $this->addColumn( + 'is_active', + array( + 'header' => $this->__('Status'), + 'index' => 'is_active', + 'sortable' => false, + 'frame_callback' => array($this, 'decorateStatus'), + ) + ); + return parent::_prepareColumns(); + } + + + /** + * Decorate status column values + * + * @param $value + * + * @return string + */ + public function decorateStatus($value) + { + $cell = sprintf( + '%s', + $value ? 'notice' : 'critical', + $this->__($value ? 'Enabled' : 'Disabled') + ); + return $cell; + } + + + /** + * Decorate cron expression + * + * @param $value + * @param Aoe_Scheduler_Model_Job $job + * + * @return string + */ + public function decorateCronExpression($value, Aoe_Scheduler_Model_Job $job) + { + return $job->getCronExpression(); + } + + + /** + * Decorate cron expression + * + * @param $value + * + * @return string + */ + public function decorateTrim($value) + { + return sprintf('%s', $value, mb_strimwidth($value, 0, 40, "...")); + } + + + /** + * Decorate cron expression + * + * @param $value + * @param Aoe_Scheduler_Model_Job $job + * + * @return string + */ + public function decorateType($value, Aoe_Scheduler_Model_Job $job) + { + return $job->getType(); + } + + /** + * Row click url + * + * @param object $row + * + * @return string + */ + public function getRowUrl($row) + { + return $this->getUrl('*/*/edit', array('job_code' => $row->getJobCode())); + } + + /** + * Helper function to receive grid functionality urls for current grid + * + * @return string Requested URL + */ + public function getGridUrl() + { + return $this->getUrl('adminhtml/job/index', array('_current' => true)); + } +} diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Scheduler.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Scheduler.php old mode 100755 new mode 100644 index bd95b01..bde863b --- a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Scheduler.php +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Scheduler.php @@ -7,8 +7,6 @@ */ class Aoe_Scheduler_Block_Adminhtml_Scheduler extends Mage_Adminhtml_Block_Widget_Grid_Container { - - /** * Constructor for Scheduler Adminhtml Block */ @@ -16,7 +14,7 @@ public function __construct() { $this->_blockGroup = 'aoe_scheduler'; $this->_controller = 'adminhtml_scheduler'; - $this->_headerText = Mage::helper('aoe_scheduler')->__('Scheduled tasks'); + $this->_headerText = $this->__('Scheduled tasks'); parent::__construct(); } @@ -24,19 +22,25 @@ public function __construct() /** * Prepare layout * - * @return Aoe_Scheduler_Block_Adminhtml_Cron + * @return $this */ protected function _prepareLayout() { $this->removeButton('add'); - $this->_addButton('add_new', array( - 'label' => Mage::helper('aoe_scheduler')->__('Generate Schedule'), - 'onclick' => "setLocation('{$this->getUrl('*/*/generateSchedule')}')", - )); - $this->_addButton('configure', array( - 'label' => Mage::helper('aoe_scheduler')->__('Cron Configuration'), - 'onclick' => "setLocation('{$this->getUrl('adminhtml/system_config/edit', array('section' => 'system'))}#system_cron')", - )); + $this->_addButton( + 'add_new', + array( + 'label' => $this->__('Generate Schedule'), + 'onclick' => "setLocation('{$this->getUrl('*/*/generateSchedule')}')", + ) + ); + $this->_addButton( + 'configure', + array( + 'label' => $this->__('Cron Configuration'), + 'onclick' => "setLocation('{$this->getUrl('adminhtml/system_config/edit', array('section' => 'system'))}#system_cron')", + ) + ); return parent::_prepareLayout(); } @@ -54,5 +58,4 @@ public function getHeaderCssClass() { return ''; } - } diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Scheduler/Grid.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Scheduler/Grid.php old mode 100755 new mode 100644 index 28219d4..6250986 --- a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Scheduler/Grid.php +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Scheduler/Grid.php @@ -7,8 +7,6 @@ */ class Aoe_Scheduler_Block_Adminhtml_Scheduler_Grid extends Mage_Adminhtml_Block_Widget_Grid { - - /** * Constructor. Set basic parameters */ @@ -26,10 +24,11 @@ public function __construct() /** * Preparation of the data that is displayed by the grid. * - * @return Aoe_SourceContact_Block_Admin_Grid Self + * @return $this */ protected function _prepareCollection() { + /** @var Mage_Cron_Model_Resource_Schedule_Collection $collection */ $collection = Mage::getModel('cron/schedule')->getCollection(); $this->setCollection($collection); return parent::_prepareCollection(); @@ -39,20 +38,26 @@ protected function _prepareCollection() /** * Add mass-actions to grid * - * @return Aoe_Scheduler_Block_Adminhtml_Cron_Grid + * @return $this */ protected function _prepareMassaction() { $this->setMassactionIdField('schedule_id'); $this->getMassactionBlock()->setFormFieldName('schedule_ids'); - $this->getMassactionBlock()->addItem('delete', array( - 'label' => Mage::helper('aoe_scheduler')->__('Delete'), - 'url' => $this->getUrl('*/*/delete'), - )); - $this->getMassactionBlock()->addItem('kill', array( - 'label' => Mage::helper('aoe_scheduler')->__('Kill'), - 'url' => $this->getUrl('*/*/kill'), - )); + $this->getMassactionBlock()->addItem( + 'delete', + array( + 'label' => $this->__('Delete'), + 'url' => $this->getUrl('*/*/delete'), + ) + ); + $this->getMassactionBlock()->addItem( + 'kill', + array( + 'label' => $this->__('Kill'), + 'url' => $this->getUrl('*/*/kill'), + ) + ); return $this; } @@ -60,81 +65,117 @@ protected function _prepareMassaction() /** * Preparation of the requested columns of the grid * - * @return Aoe_SourceContact_Block_Admin_Grid Self + * @return $this */ protected function _prepareColumns() { - $viewHelper = $this->helper('aoe_scheduler/data'); - $this->addColumn('schedule_id', array( - 'header' => Mage::helper('aoe_scheduler')->__('Id'), - 'index' => 'schedule_id', - )); - $this->addColumn('job_code', array( - 'header' => Mage::helper('aoe_scheduler')->__('Code'), - 'index' => 'job_code', - 'type' => 'options', - 'options' => Mage::getModel('aoe_scheduler/collection_crons')->toOptionHash() - )); - $this->addColumn('created_at', array( - 'header' => Mage::helper('aoe_scheduler')->__('Created'), - 'index' => 'created_at', - 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') - )); - $this->addColumn('scheduled_at', array( - 'header' => Mage::helper('aoe_scheduler')->__('Scheduled'), - 'index' => 'scheduled_at', - 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') - )); - $this->addColumn('executed_at', array( - 'header' => Mage::helper('aoe_scheduler')->__('Executed'), - 'index' => 'executed_at', - 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') - )); - $this->addColumn('last_seen', array( - 'header' => Mage::helper('aoe_scheduler')->__('Last seen'), - 'index' => 'last_seen', - 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') - )); - $this->addColumn('eta', array( - 'header' => Mage::helper('aoe_scheduler')->__('ETA'), - 'index' => 'eta', - 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') - )); - $this->addColumn('finished_at', array( - 'header' => Mage::helper('aoe_scheduler')->__('Finished'), - 'index' => 'finished_at', - 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') - )); - $this->addColumn('messages', array( - 'header' => Mage::helper('aoe_scheduler')->__('Messages'), - 'index' => 'messages', - 'frame_callback' => array($this, 'decorateMessages') - )); - $this->addColumn('host', array( - 'header' => Mage::helper('aoe_scheduler')->__('Host'), - 'index' => 'host', - )); - $this->addColumn('pid', array( - 'header' => Mage::helper('aoe_scheduler')->__('Pid'), - 'index' => 'pid', - )); - $this->addColumn('status', array( - 'header' => Mage::helper('aoe_scheduler')->__('Status'), - 'index' => 'status', - 'frame_callback' => array($viewHelper, 'decorateStatus'), - 'type' => 'options', - 'options' => array( - Mage_Cron_Model_Schedule::STATUS_PENDING => Mage_Cron_Model_Schedule::STATUS_PENDING, - Mage_Cron_Model_Schedule::STATUS_SUCCESS => Mage_Cron_Model_Schedule::STATUS_SUCCESS, - Mage_Cron_Model_Schedule::STATUS_ERROR => Mage_Cron_Model_Schedule::STATUS_ERROR, - Mage_Cron_Model_Schedule::STATUS_MISSED => Mage_Cron_Model_Schedule::STATUS_MISSED, - Mage_Cron_Model_Schedule::STATUS_RUNNING => Mage_Cron_Model_Schedule::STATUS_RUNNING, - Aoe_Scheduler_Model_Schedule::STATUS_DISAPPEARED => Aoe_Scheduler_Model_Schedule::STATUS_DISAPPEARED, - Aoe_Scheduler_Model_Schedule::STATUS_KILLED => Aoe_Scheduler_Model_Schedule::STATUS_KILLED, + $this->addColumn( + 'schedule_id', + array( + 'header' => $this->__('Id'), + 'index' => 'schedule_id', + ) + ); + $this->addColumn( + 'job_code', + array( + 'header' => $this->__('Job'), + 'index' => 'job_code', + 'type' => 'options', + 'options' => Mage::getSingleton('aoe_scheduler/job')->getCollection()->toOptionHash('job_code', 'name') + ) + ); + $this->addColumn( + 'created_at', + array( + 'header' => $this->__('Created'), + 'index' => 'created_at', + 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') + ) + ); + $this->addColumn( + 'scheduled_at', + array( + 'header' => $this->__('Scheduled'), + 'index' => 'scheduled_at', + 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') + ) + ); + $this->addColumn( + 'executed_at', + array( + 'header' => $this->__('Executed'), + 'index' => 'executed_at', + 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') + ) + ); + $this->addColumn( + 'last_seen', + array( + 'header' => $this->__('Last seen'), + 'index' => 'last_seen', + 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') ) - )); + ); + $this->addColumn( + 'eta', + array( + 'header' => $this->__('ETA'), + 'index' => 'eta', + 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') + ) + ); + $this->addColumn( + 'finished_at', + array( + 'header' => $this->__('Finished'), + 'index' => 'finished_at', + 'frame_callback' => array($viewHelper, 'decorateTimeFrameCallBack') + ) + ); + $this->addColumn( + 'messages', + array( + 'header' => $this->__('Messages'), + 'index' => 'messages', + 'frame_callback' => array($this, 'decorateMessages') + ) + ); + $this->addColumn( + 'host', + array( + 'header' => $this->__('Host'), + 'index' => 'host', + ) + ); + $this->addColumn( + 'pid', + array( + 'header' => $this->__('Pid'), + 'index' => 'pid', + 'width' => '50', + ) + ); + $this->addColumn( + 'status', + array( + 'header' => $this->__('Status'), + 'index' => 'status', + 'frame_callback' => array($viewHelper, 'decorateStatus'), + 'type' => 'options', + 'options' => array( + Mage_Cron_Model_Schedule::STATUS_PENDING => Mage_Cron_Model_Schedule::STATUS_PENDING, + Mage_Cron_Model_Schedule::STATUS_SUCCESS => Mage_Cron_Model_Schedule::STATUS_SUCCESS, + Mage_Cron_Model_Schedule::STATUS_ERROR => Mage_Cron_Model_Schedule::STATUS_ERROR, + Mage_Cron_Model_Schedule::STATUS_MISSED => Mage_Cron_Model_Schedule::STATUS_MISSED, + Mage_Cron_Model_Schedule::STATUS_RUNNING => Mage_Cron_Model_Schedule::STATUS_RUNNING, + Aoe_Scheduler_Model_Schedule::STATUS_DISAPPEARED => Aoe_Scheduler_Model_Schedule::STATUS_DISAPPEARED, + Aoe_Scheduler_Model_Schedule::STATUS_KILLED => Aoe_Scheduler_Model_Schedule::STATUS_KILLED, + ) + ) + ); return parent::_prepareColumns(); } @@ -143,15 +184,16 @@ protected function _prepareColumns() /** * Decorate message * - * @param string $value + * @param string $value * @param Aoe_Scheduler_Model_Schedule $row + * * @return string */ public function decorateMessages($value, Aoe_Scheduler_Model_Schedule $row) { $return = ''; if (!empty($value)) { - $return .= '' . Mage::helper('aoe_scheduler')->__('Message') . ''; + $return .= '' . $this->__('Message') . ''; $return .= ''; } return $return; @@ -174,7 +216,8 @@ protected function _afterLoadCollection() * Helper function to add store filter condition * * @param Mage_Core_Model_Mysql4_Collection_Abstract $collection Data collection - * @param Mage_Adminhtml_Block_Widget_Grid_Column $column Column information to be filtered + * @param Mage_Adminhtml_Block_Widget_Grid_Column $column Column information to be filtered + * * @return void */ protected function _filterStoreCondition($collection, $column) @@ -195,5 +238,4 @@ public function getGridUrl() { return $this->getUrl('adminhtml/scheduler/index', array('_current' => true)); } - } diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Timeline.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Timeline.php old mode 100755 new mode 100644 index e31acee..41e9686 --- a/app/code/community/Aoe/Scheduler/Block/Adminhtml/Timeline.php +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/Timeline.php @@ -36,7 +36,7 @@ class Aoe_Scheduler_Block_Adminhtml_Timeline extends Mage_Adminhtml_Block_Widget */ protected function _construct() { - $this->_headerText = Mage::helper('aoe_scheduler')->__('Scheduler Timeline'); + $this->_headerText = $this->__('Scheduler Timeline'); $this->loadSchedules(); parent::_construct(); } @@ -45,17 +45,17 @@ protected function _construct() /** * Prepare layout * - * @return Aoe_Scheduler_Block_Adminhtml_Cron + * @return $this */ protected function _prepareLayout() { $this->removeButton('add'); $this->_addButton('add_new', array( - 'label' => Mage::helper('aoe_scheduler')->__('Generate Schedule'), + 'label' => $this->__('Generate Schedule'), 'onclick' => "setLocation('{$this->getUrl('*/*/generateSchedule')}')", )); $this->_addButton('configure', array( - 'label' => Mage::helper('aoe_scheduler')->__('Cron Configuration'), + 'label' => $this->__('Cron Configuration'), 'onclick' => "setLocation('{$this->getUrl('adminhtml/system_config/edit', array('section' => 'system'))}#system_cron')", )); return parent::_prepareLayout(); @@ -93,14 +93,14 @@ protected function hourCeil($timestamp) */ protected function loadSchedules() { + /* @var Mage_Cron_Model_Resource_Schedule_Collection $collection */ $collection = Mage::getModel('cron/schedule')->getCollection(); - /* @var $collection Mage_Cron_Model_Mysql4_Schedule_Collection */ $minDate = null; $maxDate = null; foreach ($collection as $schedule) { - /* @var $schedule Aoe_Scheduler_Model_Schedule */ + /* @var Aoe_Scheduler_Model_Schedule $schedule */ $startTime = $schedule->getStarttime(); $minDate = is_null($minDate) ? $startTime : min($minDate, $startTime); $maxDate = is_null($maxDate) ? $startTime : max($maxDate, $startTime); @@ -204,7 +204,8 @@ public function getGanttDivAttributes(Aoe_Scheduler_Model_Schedule $schedule) $offset = 0; } - $result = sprintf('
', + $result = sprintf( + '
', $schedule->getStatus(), $schedule->getScheduleId(), $duration, @@ -212,19 +213,33 @@ public function getGanttDivAttributes(Aoe_Scheduler_Model_Schedule $schedule) ); if ($schedule->getStatus() == Mage_Cron_Model_Schedule::STATUS_RUNNING) { - $offset += $duration; $duration = strtotime($schedule->getEta()) - time(); $duration = $duration / $this->zoom; - $result = sprintf('
', - $duration, - $offset - ) . $result; + $result = sprintf( + '
', + $duration, + $offset + ) . $result; } return $result; } + /** + * Check if symlinks are allowed + * + * @return string + */ + public function _toHtml() + { + $html = parent::_toHtml(); + if (!$html && !Mage::getStoreConfigFlag('dev/template/allow_symlink')) { + $url = $this->getUrl('adminhtml/system_config/edit', array('section' => 'dev')) . '#dev_template'; + $html = $this->__('Warning: You installed Aoe_Scheduler using symlinks (e.g. via modman), but forgot to allow symlinks for template files! Please go to System > Configuration > Advanced > Developer > Template Settings and set "Allow Symlinks" to "yes"', $url); + } + return $html; + } } diff --git a/app/code/community/Aoe/Scheduler/Block/Adminhtml/TimelineDetail.php b/app/code/community/Aoe/Scheduler/Block/Adminhtml/TimelineDetail.php old mode 100755 new mode 100644 index 71401ce..21e406f --- a/app/code/community/Aoe/Scheduler/Block/Adminhtml/TimelineDetail.php +++ b/app/code/community/Aoe/Scheduler/Block/Adminhtml/TimelineDetail.php @@ -41,5 +41,4 @@ public function getSchedule() { return $this->schedule; } - } diff --git a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/AbstractController.php b/app/code/community/Aoe/Scheduler/Controller/AbstractController.php old mode 100755 new mode 100644 similarity index 83% rename from app/code/community/Aoe/Scheduler/controllers/Adminhtml/AbstractController.php rename to app/code/community/Aoe/Scheduler/Controller/AbstractController.php index a7a4b82..f878d1e --- a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/AbstractController.php +++ b/app/code/community/Aoe/Scheduler/Controller/AbstractController.php @@ -5,9 +5,8 @@ * * @author Fabrizio Branca */ -abstract class Aoe_Scheduler_Adminhtml_AbstractController extends Mage_Adminhtml_Controller_Action +abstract class Aoe_Scheduler_Controller_AbstractController extends Mage_Adminhtml_Controller_Action { - /** * Index action * @@ -42,7 +41,7 @@ protected function checkHeartbeat() $lastHeartbeat = Mage::helper('aoe_scheduler')->getLastHeartbeat(); if ($lastHeartbeat === false) { // no heartbeat task found - $this->_getSession()->addError($this->__('No heartbeat task found. Check if cron is configured correctly.')); + $this->_getSession()->addError($this->__('No heartbeat task found. Check if cron is configured correctly. (See Instructions)', $this->getUrl('adminhtml/instructions/index'))); } else { $timespan = Mage::helper('aoe_scheduler')->dateDiff($lastHeartbeat); if ($timespan <= 5 * 60) { @@ -55,7 +54,6 @@ protected function checkHeartbeat() $this->_getSession()->addError($this->__('Last heartbeat is older than one hour. Please check your settings and your configuration!')); } } - } } @@ -66,15 +64,13 @@ protected function checkHeartbeat() */ public function generateScheduleAction() { - Mage::app()->removeCache(Mage_Cron_Model_Observer::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT); - $observer = Mage::getModel('cron/observer'); - /* @var $observer Mage_Cron_Model_Observer */ - $observer->generate(); - Mage::getSingleton('adminhtml/session')->addSuccess($this->__('Generated schedule')); + /* @var Aoe_Scheduler_Model_ScheduleManager $scheduleManager */ + $scheduleManager = Mage::getModel('aoe_scheduler/scheduleManager'); + $scheduleManager->generateSchedules(); + + $this->_getSession()->addSuccess($this->__('Generated schedule')); $this->_redirect('*/*/index'); } - } - diff --git a/app/code/community/Aoe/Scheduler/Helper/Data.php b/app/code/community/Aoe/Scheduler/Helper/Data.php old mode 100755 new mode 100644 index b0d0c14..0c17fc1 --- a/app/code/community/Aoe/Scheduler/Helper/Data.php +++ b/app/code/community/Aoe/Scheduler/Helper/Data.php @@ -13,6 +13,8 @@ class Aoe_Scheduler_Helper_Data extends Mage_Core_Helper_Abstract const XML_PATH_EMAIL_IDENTITY = 'system/cron/error_email_identity'; const XML_PATH_EMAIL_RECIPIENT = 'system/cron/error_email'; + protected $groupsToJobsMap = null; + /** * Explodes a string and trims all values for whitespace in the ends. * If $onlyNonEmptyValues is set, then all blank ('') values are removed. @@ -84,7 +86,7 @@ public function decorateStatus($status) */ public function decorateTimeFrameCallBack($value) { - return $this->decorateTime($value, false, NULL); + return $this->decorateTime($value, false, null); } /** @@ -95,7 +97,7 @@ public function decorateTimeFrameCallBack($value) * @param string $dateFormat make sure Y-m-d is in it, if you want to have it replaced * @return string */ - public function decorateTime($value, $echoToday = false, $dateFormat = NULL) + public function decorateTime($value, $echoToday = false, $dateFormat = null) { if (empty($value) || $value == '0000-00-00 00:00:00') { $value = ''; @@ -116,20 +118,30 @@ public function decorateTime($value, $echoToday = false, $dateFormat = NULL) */ public function getLastHeartbeat() { - if ($this->isDisabled('aoescheduler_heartbeat')) { + return $this->getLastExecutionTime('aoescheduler_heartbeat'); + } + + /** + * Get last execution time + * + * @param $jobCode + * @return bool + */ + public function getLastExecutionTime($jobCode) + { + if ($this->isDisabled($jobCode)) { return false; } - $schedules = Mage::getModel('cron/schedule')->getCollection(); - /* @var $schedules Mage_Cron_Model_Mysql4_Schedule_Collection */ + $schedules = Mage::getModel('cron/schedule')->getCollection(); /* @var $schedules Mage_Cron_Model_Mysql4_Schedule_Collection */ $schedules->getSelect()->limit(1)->order('executed_at DESC'); $schedules->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_SUCCESS); - $schedules->addFieldToFilter('job_code', 'aoescheduler_heartbeat'); + $schedules->addFieldToFilter('job_code', $jobCode); $schedules->load(); if (count($schedules) == 0) { return false; } $executedAt = $schedules->getFirstItem()->getExecutedAt(); - $value = Mage::getModel('core/date')->date(NULL, $executedAt); + $value = Mage::getModel('core/date')->date(null, $executedAt); return $value; } @@ -140,7 +152,7 @@ public function getLastHeartbeat() * @param $time2 * @return int */ - public function dateDiff($time1, $time2 = NULL) + public function dateDiff($time1, $time2 = null) { if (is_null($time2)) { $time2 = Mage::getModel('core/date')->date(); @@ -158,9 +170,85 @@ public function dateDiff($time1, $time2 = NULL) */ public function isDisabled($jobCode) { - $disabledJobs = Mage::getStoreConfig('system/cron/disabled_crons'); - $disabledJobs = $this->trimExplode(',', $disabledJobs); - return in_array($jobCode, $disabledJobs); + /* @var $job Aoe_Scheduler_Model_Job */ + $job = Mage::getModel('aoe_scheduler/job')->load($jobCode); + return ($job->getJobCode() && !$job->getIsActive()); + } + + /** + * Check if a job matches the group include/exclude lists + * + * @param $jobCode + * @param array $include + * @param array $exclude + * @return mixed + */ + public function matchesIncludeExclude($jobCode, array $include, array $exclude) + { + $include = array_filter(array_map('trim', $include)); + $exclude = array_filter(array_map('trim', $exclude)); + + sort($include); + sort($exclude); + + $key = $jobCode . '|' . implode(',', $include) . '|' . implode(',', $exclude); + static $cache = array(); + if (!isset($cache[$key])) { + if (count($include) == 0 && count($exclude) == 0) { + $cache[$key] = true; + } else { + $cache[$key] = true; + /* @var $job Aoe_Scheduler_Model_Job */ + $job = Mage::getModel('aoe_scheduler/job')->load($jobCode); + $groups = $this->trimExplode(',', $job->getGroups(), true); + if (count($include) > 0) { + $cache[$key] = (count(array_intersect($groups, $include)) > 0); + } + if (count($exclude) > 0) { + if (count(array_intersect($groups, $exclude)) > 0) { + $cache[$key] = false; + } + } + } + + } + return $cache[$key]; + } + + public function getGroupsToJobsMap($forceRebuild = false) + { + if ($this->groupsToJobsMap === null || $forceRebuild) { + $map = array(); + + /* @var $jobs Aoe_Scheduler_Model_Resource_Job_Collection */ + $jobs = Mage::getSingleton('aoe_scheduler/job')->getCollection(); + foreach ($jobs as $job) { + /* @var Aoe_Scheduler_Model_Job $job */ + $groups = $this->trimExplode(',', $job->getGroups(), true); + foreach ($groups as $group) { + $map[$group][] = $job->getJobCode(); + } + } + + $this->groupsToJobsMap = $map; + } + + return $this->groupsToJobsMap; + } + + public function addGroupJobs(array $jobs, array $groups) + { + $map = $this->getGroupsToJobsMap(); + + foreach ($groups as $group) { + if (isset($map[$group])) { + foreach ($map[$group] as $jobCode) { + $jobs[] = $jobCode; + } + } + } + + return $jobs; } /** @@ -173,7 +261,7 @@ public function isDisabled($jobCode) public function sendErrorMail(Aoe_Scheduler_Model_Schedule $schedule, $error) { if (!Mage::getStoreConfig(self::XML_PATH_EMAIL_RECIPIENT)) { - return $this; + return; } $translate = Mage::getSingleton('core/translate'); /* @var $translate Mage_Core_Model_Translate */ @@ -192,5 +280,39 @@ public function sendErrorMail(Aoe_Scheduler_Model_Schedule $schedule, $error) $translate->setTranslateInline(true); } -} + /** + * Get callback from runModel + * + * @param $runModel + * @return array + */ + public function getCallBack($runModel) + { + if (!preg_match(Mage_Cron_Model_Observer::REGEX_RUN_MODEL, (string)$runModel, $run)) { + Mage::throwException(Mage::helper('cron')->__('Invalid model/method definition, expecting "model/class::method".')); + } + if (!($model = Mage::getModel($run[1])) || !method_exists($model, $run[2])) { + Mage::throwException(Mage::helper('cron')->__('Invalid callback: %s::%s does not exist', $run[1], $run[2])); + } + $callback = array($model, $run[2]); + return $callback; + } + /** + * Validate cron expression + * + * @param $cronExpression + * @return bool + */ + public function validateCronExpression($cronExpression) + { + try { + $schedule = Mage::getModel('cron/schedule'); + /* @var $schedule Mage_Cron_Model_Schedule */ + $schedule->setCronExpr($cronExpression); + } catch (Exception $e) { + return false; + } + return true; + } +} diff --git a/app/code/community/Aoe/Scheduler/Model/Api.php b/app/code/community/Aoe/Scheduler/Model/Api.php old mode 100755 new mode 100644 index 6c52d2c..4c4c43f --- a/app/code/community/Aoe/Scheduler/Model/Api.php +++ b/app/code/community/Aoe/Scheduler/Model/Api.php @@ -7,7 +7,6 @@ */ class Aoe_Scheduler_Model_Api extends Mage_Api_Model_Resource_Abstract { - /** * Run task * @@ -19,10 +18,13 @@ public function runNow($code) if (!Mage::getStoreConfig('system/cron/enableRunNow')) { Mage::throwException("'Run now' disabled by configuration (system/cron/enableRunNow)"); } + $schedule = Mage::getModel('cron/schedule')/* @var $schedule Aoe_Scheduler_Model_Schedule */ ->setJobCode($code) + ->setScheduledReason(Aoe_Scheduler_Model_Schedule::REASON_RUNNOW_API) ->runNow(false) // without trying to lock the job ->save(); + return $schedule->getData(); } @@ -33,10 +35,11 @@ public function runNow($code) * @param null $time * @return array */ - public function schedule($code, $time = NULL) + public function schedule($code, $time = null) { $schedule = Mage::getModel('cron/schedule')/* @var $schedule Aoe_Scheduler_Model_Schedule */ ->setJobCode($code) + ->setScheduledReason(Aoe_Scheduler_Model_Schedule::REASON_SCHEDULENOW_API) ->schedule($time) ->save(); return $schedule->getData(); @@ -53,5 +56,4 @@ public function info($id) $schedule = Mage::getModel('cron/schedule')->load($id); /* @var $schedule Aoe_Scheduler_Model_Schedule */ return $schedule->getData(); } - } diff --git a/app/code/community/Aoe/Scheduler/Model/Collection/Crons.php b/app/code/community/Aoe/Scheduler/Model/Collection/Crons.php deleted file mode 100755 index 29a48b4..0000000 --- a/app/code/community/Aoe/Scheduler/Model/Collection/Crons.php +++ /dev/null @@ -1,63 +0,0 @@ -_dataLoaded) { - return $this; - } - - foreach ($this->getAllCodes() as $code) { - $configuration = Mage::getModel('aoe_scheduler/configuration')->loadByCode($code); - $this->addItem($configuration); - } - - $this->_dataLoaded = true; - return $this; - } - - /** - * Get all available codes - * - * @return array - */ - protected function getAllCodes() - { - $codes = array(); - $config = Mage::getConfig()->getNode('crontab/jobs'); /* @var $config Mage_Core_Model_Config_Element */ - if ($config instanceof Mage_Core_Model_Config_Element) { - foreach ($config->children() as $key => $tmp) { - if (!in_array($key, $codes)) { - $codes[] = $key; - } - } - } - $config = Mage::getConfig()->getNode('default/crontab/jobs'); /* @var $config Mage_Core_Model_Config_Element */ - if ($config instanceof Mage_Core_Model_Config_Element) { - foreach ($config->children() as $key => $tmp) { - if (!in_array($key, $codes)) { - $codes[] = $key; - } - } - } - sort($codes); - return $codes; - } - -} \ No newline at end of file diff --git a/app/code/community/Aoe/Scheduler/Model/Configuration.php b/app/code/community/Aoe/Scheduler/Model/Configuration.php deleted file mode 100755 index 8a3753d..0000000 --- a/app/code/community/Aoe/Scheduler/Model/Configuration.php +++ /dev/null @@ -1,134 +0,0 @@ -setId($code); - $this->setName($code); - - $global = $this->getGlobalCrontabJobXmlConfig(); - $cronExpr = null; - if ($global && $global->schedule && $global->schedule->config_path) { - $cronExpr = Mage::getStoreConfig((string)$global->schedule->config_path); - } - if (empty($cronExpr) && $global && $global->schedule && $global->schedule->cron_expr) { - $cronExpr = (string)$global->schedule->cron_expr; - } - if ($cronExpr) { - $this->setCronExpr($cronExpr); - } - if ($global && $global->run && $global->run->model) { - $this->setModel((string)$global->run->model); - } - - $configurable = $this->getConfigurableCrontabJobXmlConfig(); - if ($configurable) { - if (is_object($configurable->schedule)) { - if ($configurable && $configurable->schedule && $configurable->schedule->cron_expr) { - $this->setCronExpr((string)$configurable->schedule->cron_expr); - } - } - if (is_object($configurable->run)) { - if ($configurable && $configurable->run && $configurable->run->model) { - $this->setModel((string)$configurable->run->model); - } - } - } - - if (!$this->getModel()) { - Mage::throwException(sprintf('No configuration found for code "%s"', $code)); - } - - $disabledCrons = Mage::helper('aoe_scheduler')->trimExplode(',', Mage::getStoreConfig('system/cron/disabled_crons'), true); - $this->setStatus(in_array($this->getId(), $disabledCrons) ? self::STATUS_DISABLED : self::STATUS_ENABLED); - - return $this; - } - - /** - * Get global crontab job xml configuration - * - * @return Mage_Core_Model_Config_Element|false - */ - protected function getGlobalCrontabJobXmlConfig() - { - return $this->getJobXmlConfig('crontab/jobs'); - } - - /** - * Get configurable crontab job xml configuration - * - * @return Mage_Core_Model_Config_Element|false - */ - protected function getConfigurableCrontabJobXmlConfig() - { - return $this->getJobXmlConfig('default/crontab/jobs'); - } - - /** - * Get job xml configuration - * - * @param string $path path to configuration - * @return Mage_Core_Model_Config_Element|false - */ - protected function getJobXmlConfig($path) - { - $xmlConfig = false; - $config = Mage::getConfig()->getNode($path); - if ($config instanceof Mage_Core_Model_Config_Element) { - $xmlConfig = $config->{$this->getId()}; - } - return $xmlConfig; - } - - /** - * Check if this is an "always" task - * - * @return bool - */ - public function isAlwaysTask() - { - return $this->getCronExpr() == 'always'; - } - -} \ No newline at end of file diff --git a/app/code/community/Aoe/Scheduler/Model/Job.php b/app/code/community/Aoe/Scheduler/Model/Job.php new file mode 100644 index 0000000..78ff222 --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Model/Job.php @@ -0,0 +1,194 @@ +_setResourceModel('aoe_scheduler/job', 'aoe_scheduler/job_collection'); + } + + public function getName() + { + $name = $this->getData('name'); + if (empty($name)) { + $name = $this->getJobCode(); + } + return $name; + } + + /** + * @param bool $flag + * + * @return $this + */ + public function setIsActive($flag) + { + return $this->setData('is_active', !in_array($flag, array(false, 'false', 0, '0'), true)); + } + + /** + * @return bool + */ + public function getIsActive() + { + return !in_array($this->getData('is_active'), array(false, 'false', 0, '0'), true); + } + + /** + * @param string[] $jobData + * + * @return $this + */ + public function setXmlJobData(array $jobData) + { + return $this->setData('xml_job_data', $jobData); + } + + /** + * @return string[] + */ + public function getXmlJobData() + { + $jobData = $this->getData('xml_job_data'); + return (is_array($jobData) ? $jobData : array()); + } + + /** + * @param string[] $jobData + * + * @return $this + */ + public function setDbJobData(array $jobData) + { + return $this->setData('db_job_data', $jobData); + } + + /** + * @return string[] + */ + public function getDbJobData() + { + $jobData = $this->getData('db_job_data'); + return (is_array($jobData) ? $jobData : array()); + } + + /** + * Returns cron expression (and fetches it from configuration if required) + * + * @return string + */ + public function getCronExpression() + { + $cronExpr = null; + if ($this->getScheduleConfigPath()) { + $cronExpr = Mage::getStoreConfig($this->getScheduleConfigPath(), Mage_Core_Model_Store::ADMIN_CODE); + } + if (empty($cronExpr) && $this->getScheduleCronExpr()) { + $cronExpr = $this->getScheduleCronExpr(); + } + return trim($cronExpr); + } + + /** + * Is always task + * + * @return bool + */ + public function isAlwaysTask() + { + return $this->getCronExpression() == 'always'; + } + + public function getCallback() + { + $helper = Mage::helper('aoe_scheduler'); + /* @var $helper Aoe_Scheduler_Helper_Data */ + return $helper->getCallBack($this->getRunModel()); + } + + public function canBeScheduled() + { + return $this->getIsActive() && $this->getCronExpression() && !$this->isAlwaysTask(); + } + + /** + * @return bool + */ + public function isDbOnly() + { + $xmlJobData = $this->getXmlJobData(); + $dbJobData = $this->getDbJobData(); + return empty($xmlJobData) && !empty($dbJobData); + } + + /** + * @return bool + */ + public function isXmlOnly() + { + $xmlJobData = $this->getXmlJobData(); + $dbJobData = $this->getDbJobData(); + return !empty($xmlJobData) && empty($dbJobData); + } + + /** + * @return bool + */ + public function isOverlay() + { + $xmlJobData = $this->getXmlJobData(); + $dbJobData = $this->getDbJobData(); + return !empty($xmlJobData) && !empty($dbJobData); + } + + /** + * @return string + */ + public function getType() + { + if ($this->isDbOnly()) { + return 'db'; + } elseif ($this->isXmlOnly()) { + return 'xml'; + } else { + return 'db_xml'; + } + } +} diff --git a/app/code/community/Aoe/Scheduler/Model/Observer.php b/app/code/community/Aoe/Scheduler/Model/Observer.php old mode 100755 new mode 100644 index 9175a30..054d9f6 --- a/app/code/community/Aoe/Scheduler/Model/Observer.php +++ b/app/code/community/Aoe/Scheduler/Model/Observer.php @@ -5,433 +5,80 @@ * * @author Fabrizio Branca */ -class Aoe_Scheduler_Model_Observer extends Mage_Cron_Model_Observer +class Aoe_Scheduler_Model_Observer /* extends Mage_Cron_Model_Observer */ { - - CONST XML_PATH_MARK_AS_ERROR = 'system/cron/mark_as_error_after'; - CONST XML_PATH_HISTORY_MAXNO = 'system/cron/maxNoOfSuccessfulTasks'; - /** * Process cron queue * Generate tasks schedule * Cleanup tasks schedule * - * THIS METHOD IS (almost) IDENTICAL WITH EE 1.13 and CE 1.8 - * (but it is here for compatibility reasons with earlier version, because the new version was refactored) - * - * @param Varien_Event_Observer $observer - */ - public function dispatch($observer) - { - - if (!Mage::getStoreConfigFlag('system/cron/enable')) { - return; - } - - $schedules = $this->getPendingSchedules(); - $jobsRoot = Mage::getConfig()->getNode('crontab/jobs'); - $defaultJobsRoot = Mage::getConfig()->getNode('default/crontab/jobs'); - - /** @var $schedule Mage_Cron_Model_Schedule */ - foreach ($schedules->getIterator() as $schedule) { - $jobConfig = $jobsRoot->{$schedule->getJobCode()}; - if (!$jobConfig || !$jobConfig->run) { - $jobConfig = $defaultJobsRoot->{$schedule->getJobCode()}; - if (!$jobConfig || !$jobConfig->run) { - continue; - } - } - $this->_processJob($schedule, $jobConfig); - } - - $this->generate(); - $this->cleanup(); - - // Aoe_Scheduler: additional stuff added - $this->checkRunningJobs(); - } - - /** - * Process cron queue for tasks marked as always - * * @param Varien_Event_Observer $observer */ - public function dispatchAlways($observer) + public function dispatch(Varien_Event_Observer $observer) { - if (!Mage::getStoreConfigFlag('system/cron/enable')) { return; } - // Aoe_Scheduler: additional stuff added - $this->processKillRequests(); - - parent::dispatchAlways($observer); - } - - /** - * Process cron task - * - * @param Mage_Cron_Model_Schedule $schedule - * @param $jobConfig - * @param bool $isAlways - * @return Mage_Cron_Model_Observer - */ - protected function _processJob($schedule, $jobConfig, $isAlways = false) - { - $runConfig = $jobConfig->run; - if (!$isAlways) { - $scheduleLifetime = Mage::getStoreConfig(self::XML_PATH_SCHEDULE_LIFETIME) * 60; - $now = time(); - $time = strtotime($schedule->getScheduledAt()); - if ($time > $now) { - return; - } - } - - $errorStatus = Mage_Cron_Model_Schedule::STATUS_ERROR; - try { - if (!$isAlways) { - if ($time < $now - $scheduleLifetime) { - $errorStatus = Mage_Cron_Model_Schedule::STATUS_MISSED; - Mage::throwException(Mage::helper('cron')->__('Too late for the schedule.')); - } - } - - // Aoe_Scheduler: stuff from the original method was removed and refactored into the schedule module - - /* @var $schedule Aoe_Scheduler_Model_Schedule */ - $schedule->runNow(!$isAlways); - - } catch (Exception $e) { - $schedule->setStatus($errorStatus) - ->setMessages($e->__toString()); - - // Aoe_Scheduler: additional handling: - Mage::dispatchEvent('cron_' . $schedule->getJobCode() . '_exception', array('schedule' => $schedule, 'exception' => $e)); - Mage::dispatchEvent('cron_exception', array('schedule' => $schedule, 'exception' => $e)); - Mage::helper('aoe_scheduler')->sendErrorMail($schedule, $e->__toString()); - } - $schedule->save(); - - return $this; - } - - /** - * Check running jobs - * - * @return void - */ - public function checkRunningJobs() - { - - // check the schedules running on this server - $processManager = Mage::getModel('aoe_scheduler/processManager'); - /* @var $processManager Aoe_Scheduler_Model_ProcessManager */ - foreach ($processManager->getAllRunningSchedules(gethostname()) as $schedule) { - /* @var $schedule Aoe_Scheduler_Model_Schedule */ - $schedule->isAlive(); // checks pid and updates record - } - - // fallback (where process cannot be checked or if one of the servers disappeared) - // if a task wasn't seen for some time it will be marked as error - // I'm reusing the - $maxAge = time() - Mage::getStoreConfig(self::XML_PATH_MARK_AS_ERROR) * 60; - - $schedules = Mage::getModel('cron/schedule')->getCollection() - ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_RUNNING) - ->addFieldToFilter('last_seen', array('lt' => strftime('%Y-%m-%d %H:%M:00', $maxAge))) - ->load(); - - foreach ($schedules->getIterator() as $schedule) { - /* @var $schedule Aoe_Scheduler_Model_Schedule */ - $schedule->markAsDisappeared(sprintf('Host "%s" has not been available for a while now to update the status of this task and the task is not reporting back by itself', $schedule->getHost())); - } - } - - public function deleteDuplicates() - { - $cron_schedule = Mage::getSingleton('core/resource')->getTableName('cron_schedule'); - $conn = Mage::getSingleton('core/resource')->getConnection('core_read'); - - // TODO: Direct sql is not nice. We can do better... :) - $results = $conn->fetchAll(" - SELECT - GROUP_CONCAT(schedule_id) AS ids, - CONCAT(job_code, scheduled_at) AS jobkey, - count(*) AS qty - FROM {$cron_schedule} - WHERE status = '" . Mage_Cron_Model_Schedule::STATUS_PENDING . "' - GROUP BY jobkey - HAVING qty > 1; - "); - foreach ($results as $row) { - $ids = explode(',', $row['ids']); - $removeIds = array_slice($ids, 1); - foreach ($removeIds as $id) { - Mage::getModel('cron/schedule')->load($id)->delete(); - } - } - } - - /** - * Generate jobs for config information - * Rewrites the original method to filter deactivated jobs - * - * @param $jobs - * @param array $exists - * @return Mage_Cron_Model_Observer - */ - protected function _generateJobs($jobs, $exists) - { - - $conf = Mage::getStoreConfig('system/cron/disabled_crons'); - $conf = explode(',', $conf); - foreach ($conf as &$c) { - $c = trim($c); - } - - $newJobs = array(); - foreach ($jobs as $code => $config) { - if (!in_array($code, $conf)) { - $newJobs[$code] = $config; - } - } - - return parent::_generateJobs($newJobs, $exists); - } - - /** - * Generate cron schedule. - * Rewrites the original method to remove duplicates afterwards (that exists because of a bug) - * - * @return Mage_Cron_Model_Observer - */ - public function generate() - { - - /** - * check if schedule generation is needed - */ - $lastRun = Mage::app()->loadCache(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT); - if ($lastRun > time() - Mage::getStoreConfig(self::XML_PATH_SCHEDULE_GENERATE_EVERY) * 60) { - return $this; - } - - $startTime = microtime(true); - - $result = parent::generate(); - - $this->deleteDuplicates(); - - if ($logFile = Mage::getStoreConfig('system/cron/logFile')) { - - $history = Mage::getModel('cron/schedule')->getCollection() - ->setPageSize(1) - ->setOrder('scheduled_at', 'desc') - ->load(); - - $newestSchedule = $history->getFirstItem(); - /* @var $newestSchedule Aoe_Scheduler_Model_Schedule */ - - $duration = microtime(true) - $startTime; - Mage::log('Generated schedule. Newest task is scheduled at "' . $newestSchedule->getScheduledAt() . '". (Duration: ' . round($duration, 2) . ' sec)', null, $logFile); - } - - return $result; - } - - /** - * Get job code white list from environment variable - * - * @return array - */ - public function getWhitelist() - { - $whitelist = array(); - if (getenv("SCHEDULER_WHITELIST") !== FALSE) { - $whitelist = explode(',', getenv("SCHEDULER_WHITELIST")); - } - return $whitelist; - } - - /** - * Get job code black list from environment variable - * - * @return array - */ - public function getBlacklist() - { - $blacklist = array(); - if (getenv("SCHEDULER_BLACKLIST") !== FALSE) { - $blacklist = explode(',', getenv("SCHEDULER_BLACKLIST")); - } - return $blacklist; - } - - /** - * Get pending schedules - * - * @return mixed - */ - public function getPendingSchedules() - { - if (!$this->_pendingSchedules) { - $this->_pendingSchedules = Mage::getModel('cron/schedule')->getCollection() - ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_PENDING) - ->addFieldToFilter('scheduled_at', array('lt' => strftime('%Y-%m-%d %H:%M:%S', time()))); - - $whitelist = $this->getWhitelist(); - if (!empty($whitelist)) { - $this->_pendingSchedules->addFieldToFilter('job_code', array('in' => $whitelist)); - } - - $blacklist = $this->getBlacklist(); - if (!empty($blacklist)) { - $this->_pendingSchedules->addFieldToFilter('job_code', array('nin' => $blacklist)); - } + $processManager = Mage::getModel('aoe_scheduler/processManager'); /* @var $processManager Aoe_Scheduler_Model_ProcessManager */ + $processManager->watchdog(); - $this->_pendingSchedules = $this->_pendingSchedules->load(); + $scheduleManager = Mage::getModel('aoe_scheduler/scheduleManager'); /* @var $scheduleManager Aoe_Scheduler_Model_ScheduleManager */ + $scheduleManager->logRun(); - // let's do a cleanup and not execute multiple schedule from the same job in a run but mark them as missed - // this happens if the cron was blocked by another task and jobs keep piling up. - - $tmp = array(); - foreach ($this->_pendingSchedules as $key => $schedule) { - /* @var $schedule Aoe_Scheduler_Model_Schedule */ - $tmp[$schedule->getJobCode()][$schedule->getScheduledAt()] = array('key' => $key, 'schedule' => $schedule); - } - - foreach ($tmp as $jobCode => $schedules) { - ksort($schedules); - array_pop($schedules); // we remove the newest one - foreach ($schedules as $data) { - /* @var $data array */ - $this->_pendingSchedules->removeItemByKey($data['key']); - $schedule = $data['schedule']; - /* @var $schedule Aoe_Scheduler_Model_Schedule */ - $schedule - ->setMessages('Mulitple tasks with the same job code were piling up. Skipping execution of duplicates.') - ->setStatus(Mage_Cron_Model_Schedule::STATUS_MISSED) - ->save(); - } - } - } + $helper = Mage::helper('aoe_scheduler'); /* @var Aoe_Scheduler_Helper_Data $helper */ + $includeJobs = $helper->addGroupJobs((array)$observer->getIncludeJobs(), (array)$observer->getIncludeGroups()); + $excludeJobs = $helper->addGroupJobs((array)$observer->getExcludeJobs(), (array)$observer->getExcludeGroups()); - return $this->_pendingSchedules; - } + // Coalesce all jobs that should have run before now, by job code, by marking the oldest entries as missed. + $scheduleManager->cleanMissedSchedules(); - /** - * Process kill requests - * - * @return void - */ - public function processKillRequests() - { - $processManager = Mage::getModel('aoe_scheduler/processManager'); - /* @var $processManager Aoe_Scheduler_Model_ProcessManager */ - foreach ($processManager->getAllKillRequests(gethostname()) as $schedule) { - /* @var $schedule Aoe_Scheduler_Model_Schedule */ - $schedule->kill(); + // Iterate over all pending jobs + foreach ($scheduleManager->getPendingSchedules($includeJobs, $excludeJobs) as $schedule) { + /* @var Aoe_Scheduler_Model_Schedule $schedule */ + $schedule->process(); } - } - - - /** - * Get job for task marked as always - * - * (Instead of reusing existing one - which results in loosing the history - create a new one every time) - * - * @param $jobCode - * @return bool|Mage_Cron_Model_Schedule - */ - protected function _getAlwaysJobSchedule($jobCode) - { - $processManager = Mage::getModel('aoe_scheduler/processManager'); - /* @var $processManager Aoe_Scheduler_Model_ProcessManager */ - if (!$processManager->isJobCodeRunning($jobCode)) { - $ts = strftime('%Y-%m-%d %H:%M:00', time()); - $schedule = Mage::getModel('cron/schedule')/* @var $schedule Mage_Cron_Model_Schedule */ - ->setJobCode($jobCode) - ->setStatus(Mage_Cron_Model_Schedule::STATUS_RUNNING) - ->setCreatedAt($ts) - ->setScheduledAt($ts) - ->save(); - return $schedule; - } + // Generate new schedules + $scheduleManager->generateSchedules(); - return false; + // Clean up schedule history + $scheduleManager->cleanup(); } - /** - * Clean up the history of tasks - * This override deals with custom states added in Aoe_Scheduler + * Process cron queue for tasks marked as 'always' * - * @return Mage_Cron_Model_Observer + * @param Varien_Event_Observer $observer */ - public function cleanup() + public function dispatchAlways(Varien_Event_Observer $observer) { - // check if history cleanup is needed - $lastCleanup = Mage::app()->loadCache(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT); - if ($lastCleanup > time() - Mage::getStoreConfig(self::XML_PATH_HISTORY_CLEANUP_EVERY) * 60) { - return $this; + if (!Mage::getStoreConfigFlag('system/cron/enable')) { + return; } - $startTime = microtime(true); + $processManager = Mage::getModel('aoe_scheduler/processManager'); /* @var $processManager Aoe_Scheduler_Model_ProcessManager */ + $processManager->watchdog(); - $history = Mage::getModel('cron/schedule')->getCollection() - ->addFieldToFilter('status', array('in' => array( - Aoe_Scheduler_Model_Schedule::STATUS_KILLED, - Aoe_Scheduler_Model_Schedule::STATUS_DISAPPEARED, - Aoe_Scheduler_Model_Schedule::STATUS_DIDNTDOANYTHING, - ))) - ->load(); + $scheduleManager = Mage::getModel('aoe_scheduler/scheduleManager'); /* @var $scheduleManager Aoe_Scheduler_Model_ScheduleManager */ - $historyLifetimes = array( - Aoe_Scheduler_Model_Schedule::STATUS_KILLED => Mage::getStoreConfig(self::XML_PATH_HISTORY_SUCCESS) * 60, - Aoe_Scheduler_Model_Schedule::STATUS_DISAPPEARED => Mage::getStoreConfig(self::XML_PATH_HISTORY_FAILURE) * 60, - Aoe_Scheduler_Model_Schedule::STATUS_DIDNTDOANYTHING => Mage::getStoreConfig(self::XML_PATH_HISTORY_SUCCESS) * 60, - ); + $helper = Mage::helper('aoe_scheduler'); /* @var Aoe_Scheduler_Helper_Data $helper */ + $includeJobs = $helper->addGroupJobs((array)$observer->getIncludeJobs(), (array)$observer->getIncludeGroups()); + $excludeJobs = $helper->addGroupJobs((array)$observer->getExcludeJobs(), (array)$observer->getExcludeGroups()); - $now = time(); - foreach ($history->getIterator() as $record) { - /* @var $record Aoe_Scheduler_Model_Schedule */ - if (strtotime($record->getExecutedAt()) < $now - $historyLifetimes[$record->getStatus()]) { - $record->delete(); - } - } - - parent::cleanup(); - - // delete successful tasks - $maxNo = Mage::getStoreConfig(self::XML_PATH_HISTORY_MAXNO); - if ($maxNo) { - $history = Mage::getModel('cron/schedule')->getCollection() - ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_SUCCESS) - ->setOrder('finished_at', 'desc') - ->load(); - $counter = array(); - foreach ($history->getIterator() as $record) { - /* @var $record Aoe_Scheduler_Model_Schedule */ - $jobCode = $record->getJobCode(); - if (!isset($counter[$jobCode])) { - $counter[$jobCode] = 0; - } - $counter[$jobCode]++; - if ($counter[$jobCode] > $maxNo) { - $record->delete(); + /* @var $jobs Aoe_Scheduler_Model_Resource_Job_Collection */ + $jobs = Mage::getSingleton('aoe_scheduler/job')->getCollection(); + $jobs->setWhiteList($includeJobs); + $jobs->setBlackList($excludeJobs); + $jobs->setActiveOnly(true); + foreach ($jobs as $job) { + /* @var Aoe_Scheduler_Model_Job $job */ + if ($job->isAlwaysTask() && $job->getRunModel()) { + $schedule = $scheduleManager->getScheduleForAlwaysJob($job->getJobCode()); + if ($schedule !== false) { + $schedule->process(); } } } - - if ($logFile = Mage::getStoreConfig('system/cron/logFile')) { - $duration = microtime(true) - $startTime; - Mage::log('History cleanup (Duration: ' . round($duration, 2) . ' sec)', null, $logFile); - } - - return $this; } - } diff --git a/app/code/community/Aoe/Scheduler/Model/ProcessManager.php b/app/code/community/Aoe/Scheduler/Model/ProcessManager.php old mode 100755 new mode 100644 index 0270a8a..64db42f --- a/app/code/community/Aoe/Scheduler/Model/ProcessManager.php +++ b/app/code/community/Aoe/Scheduler/Model/ProcessManager.php @@ -1,7 +1,7 @@ getCollection() @@ -54,9 +57,8 @@ public function isJobCodeRunning($jobCode, $ignoreId = NULL) if (!is_null($ignoreId)) { $collection->addFieldToFilter('schedule_id', array('neq' => $ignoreId)); } - foreach ($collection as $s) { - /* @var $s Aoe_Scheduler_Model_Schedule */ - $alive = $s->isAlive(); + foreach ($collection as $schedule) { /* @var $schedule Aoe_Scheduler_Model_Schedule */ + $alive = $schedule->isAlive(); if ($alive !== false) { // TODO: how do we handle null (= we don't know because might be running on a different server? return true; } @@ -64,4 +66,81 @@ public function isJobCodeRunning($jobCode, $ignoreId = NULL) return false; } + + /** + * Check running jobs + * + * @return void + */ + public function checkRunningJobs() + { + $maxJobRuntime = Mage::getStoreConfig(self::XML_PATH_MAX_JOB_RUNTIME); + + foreach ($this->getAllRunningSchedules(gethostname()) as $schedule) { + /* @var $schedule Aoe_Scheduler_Model_Schedule */ + // checks if process is still running and updates record + $isAlive = $schedule->isAlive(); + + // checking if the job isn't running too long + if ($isAlive && $maxJobRuntime) { + if ($schedule->getDuration() > $maxJobRuntime * 60) { + $schedule->requestKill(); + } + } + + } + + // fallback (where process cannot be checked or if one of the servers disappeared) + // if a task wasn't seen for some time it will be marked as error + $maxAge = time() - Mage::getStoreConfig(self::XML_PATH_MARK_AS_ERROR) * 60; + + $schedules = Mage::getModel('cron/schedule')->getCollection() /* @var $schedules Mage_Cron_Model_Resource_Schedule_Collection */ + ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_RUNNING) + ->addFieldToFilter('last_seen', array('lt' => strftime('%Y-%m-%d %H:%M:00', $maxAge))) + ->load(); + + foreach ($schedules as $schedule) { /* @var $schedule Aoe_Scheduler_Model_Schedule */ + $schedule->markAsDisappeared(sprintf('Host "%s" has not been available for a while now to update the status of this task and the task is not reporting back by itself', $schedule->getHost())); + } + + // clean up "running"(!?) tasks that have never been seen (for whatever reason) and have been scheduled before maxAge + // by robinfritze. @see https://github.com/AOEpeople/Aoe_Scheduler/issues/40#issuecomment-67749476 + $schedules = Mage::getModel('cron/schedule')->getCollection() /* @var $schedules Mage_Cron_Model_Resource_Schedule_Collection */ + ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_RUNNING) + ->addFieldToFilter('last_seen', array('null' => true)) + ->addFieldToFilter('host', array('null' => true)) + ->addFieldToFilter('pid', array('null' => true)) + ->addFieldToFilter('scheduled_at', array('lt' => strftime('%Y-%m-%d %H:%M:00', $maxAge))) + ->load(); + + foreach ($schedules->getIterator() as $schedule) { /* @var $schedule Aoe_Scheduler_Model_Schedule */ + $schedule->setLastSeen(strftime('%Y-%m-%d %H:%M:%S', time())); + $schedule->markAsDisappeared(sprintf('Process "%s" (id: %s) cannot be found anymore', $schedule->getJobCode(), $schedule->getId())); + } + + } + + + + /** + * Process kill requests + * + * @return void + */ + public function processKillRequests() + { + foreach ($this->getAllKillRequests(gethostname()) as $schedule) { /* @var $schedule Aoe_Scheduler_Model_Schedule */ + $schedule->kill(); + } + } + + + /** + * Run maintenance + */ + public function watchdog() + { + $this->checkRunningJobs(); + $this->processKillRequests(); + } } diff --git a/app/code/community/Aoe/Scheduler/Model/Resource/Job.php b/app/code/community/Aoe/Scheduler/Model/Resource/Job.php new file mode 100644 index 0000000..6190ac0 --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Model/Resource/Job.php @@ -0,0 +1,358 @@ +_init('core/config_data', 'job_code'); + } + + public function getJobCodes() + { + $codes = array(); + + $nodes = array('crontab/jobs', 'default/crontab/jobs'); + foreach ($nodes as $node) { + $jobs = Mage::getConfig()->getNode($node); + if ($jobs && $jobs->hasChildren()) { + foreach ($jobs->children() as $code => $child) { + $codes[] = trim($code); + } + } + } + + // Remove empties and de-dupe + $codes = array_unique(array_filter($codes)); + + // Sort + sort($codes); + + return $codes; + } + + /** + * @param Aoe_Scheduler_Model_Job $object + * @param mixed $value + * @param null $field + * + * @return $this + */ + public function load(Mage_Core_Model_Abstract $object, $value, $field = null) + { + if (!$object instanceof Aoe_Scheduler_Model_Job) { + throw new InvalidArgumentException(sprintf("Expected object of type 'Aoe_Scheduler_Model_Job' got '%s'", get_class($object))); + } + + /** @var Aoe_Scheduler_Model_Job $object */ + + if (!empty($field)) { + throw new InvalidArgumentException('Aoe_Scheduler_Model_Resource_Job cannot load by any field except the job code.'); + } + + if (empty($value)) { + $this->setModelFromJobData($object, array()); + $object->setJobCode(''); + $object->setXmlJobData(array()); + $object->setDbJobData(array()); + return $this; + } + + $xmlJobData = $this->getJobDataFromXml($value); + $dbJobData = $this->getJobDataFromDb($value); + $jobData = array_merge($xmlJobData, $this->getJobDataFromConfig($value, true), $dbJobData); + + $this->setModelFromJobData($object, $jobData); + $object->setJobCode($value); + $object->setXmlJobData($xmlJobData); + $object->setDbJobData($dbJobData); + + $this->unserializeFields($object); + $this->_afterLoad($object); + + return $this; + } + + /** + * @param Aoe_Scheduler_Model_Job $object + * + * @return $this + */ + public function save(Mage_Core_Model_Abstract $object) + { + if ($object->isDeleted()) { + return $this->delete($object); + } + + if (!$object instanceof Aoe_Scheduler_Model_Job) { + throw new InvalidArgumentException(sprintf("Expected object of type 'Aoe_Scheduler_Model_Job' got '%s'", get_class($object))); + } + + if (!$object->getJobCode()) { + Mage::throwException('Invalid data. Must have job code.'); + } + + $this->_serializeFields($object); + $this->_beforeSave($object); + + $newValues = $this->getJobDataFromModel($object); + $oldValues = $this->getJobDataFromDb($object->getJobCode()); + $defaultValues = $this->getJobDataFromXml($object->getJobCode()); + + // Generate key/value lists for Update and Insert + $updateValues = array_intersect_key($newValues, $oldValues); + $insertValues = array_diff_key($newValues, $oldValues); + + // Remove Updates and Inserts that match defaults + $updateValues = array_diff_assoc($updateValues, $defaultValues); + $insertValues = array_diff_assoc($insertValues, $defaultValues); + + // Remove empty value inserts if this is a DB only job + if (empty($defaultValues)) { + foreach ($insertValues as $k => $v) { + if ($v === '' || $v === null) { + unset($insertValues[$k]); + } + } + } + + // Generate key/value lists for Delete (Old values, not being updated, that are identical to default values) + $deleteValues = array_intersect_assoc(array_diff_key($oldValues, $updateValues), $defaultValues); + + $pathPrefix = $this->getJobPathPrefix($object->getJobCode()) . '/'; + + $adapter = $this->_getWriteAdapter(); + foreach ($updateValues as $k => $v) { + $adapter->update( + $this->getMainTable(), + array('value' => $v), + array( + 'scope = ?' => 'default', + 'scope_id = ?' => 0, + 'path = ?' => $pathPrefix . $k + ) + ); + } + foreach ($insertValues as $k => $v) { + $adapter->insert( + $this->getMainTable(), + array( + 'scope' => 'default', + 'scope_id' => 0, + 'path' => $pathPrefix . $k, + 'value' => $v + ) + ); + } + foreach ($deleteValues as $k => $v) { + $adapter->delete( + $this->getMainTable(), + array( + 'scope = ?' => 'default', + 'scope_id = ?' => 0, + 'path = ?' => $pathPrefix . $k + ) + ); + } + + if (count($updateValues) || count($insertValues) || count($deleteValues)) { + Mage::getConfig()->reinit(); + } + + $this->unserializeFields($object); + $this->_afterSave($object); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function forsedSave(Mage_Core_Model_Abstract $object) + { + throw new RuntimeException('Method no longer exists'); + } + + /** + * @param Aoe_Scheduler_Model_Job $object + * + * @return $this + */ + public function delete(Mage_Core_Model_Abstract $object) + { + if (!$object instanceof Aoe_Scheduler_Model_Job) { + throw new InvalidArgumentException(sprintf("Expected object of type 'Aoe_Scheduler_Model_Job' got '%s'", get_class($object))); + } + + $this->_beforeDelete($object); + + if (!$object->getJobCode()) { + Mage::throwException('Invalid data. Must have job code.'); + } + + $adapter = $this->_getWriteAdapter(); + $adapter->delete( + $this->getMainTable(), + array( + 'path LIKE ?' => $this->getJobSearchPath($object->getJobCode()), + 'scope = ?' => 'default', + 'scope_id = ?' => 0 + ) + ); + + Mage::getConfig()->reinit(); + + $this->_afterDelete($object); + + return $this; + } + + protected function getJobPathPrefix($jobCode) + { + return 'crontab/jobs/' . $jobCode; + } + + protected function getJobSearchPath($jobCode) + { + return str_replace(array('\\', '%', '_'), array('\\\\', '\\%', '\\_'), $this->getJobPathPrefix($jobCode)) . '/%'; + } + + private function getJobDataFromConfig($jobCode, $useDefaultScope = false, $default = null) + { + $config = Mage::getConfig()->getNode(($useDefaultScope ? 'default/' : '') . $this->getJobPathPrefix($jobCode)); + if (!$config) { + return array(); + } + + $config = $config->asArray(); + + $values = array(); + + if (isset($config['name'])) { + $values['name'] = $config['name']; + } elseif ($default !== null) { + $values['name'] = $default; + } + if (isset($config['description'])) { + $values['description'] = $config['description']; + } elseif ($default !== null) { + $values['description'] = $default; + } + if (isset($config['short_description'])) { + $values['short_description'] = $config['short_description']; + } elseif ($default !== null) { + $values['short_description'] = $default; + } + if (isset($config['run']['model'])) { + $values['run/model'] = $config['run']['model']; + } elseif ($default !== null) { + $values['run/model'] = $default; + } + if (isset($config['schedule']['config_path'])) { + $values['schedule/config_path'] = $config['schedule']['config_path']; + } elseif ($default !== null) { + $values['schedule/config_path'] = $default; + } + if (isset($config['schedule']['cron_expr'])) { + $values['schedule/cron_expr'] = $config['schedule']['cron_expr']; + } elseif ($default !== null) { + $values['schedule/cron_expr'] = $default; + } + if (isset($config['parameters'])) { + $values['parameters'] = $config['parameters']; + } elseif ($default !== null) { + $values['parameters'] = $default; + } + if (isset($config['groups'])) { + $values['groups'] = $config['groups']; + } elseif ($default !== null) { + $values['groups'] = $default; + } + if (isset($config['is_active'])) { + $values['is_active'] = $config['is_active']; + } elseif ($default !== null) { + $values['is_active'] = $default; + } + + // Clean up each entry to being a trimmed string + $values = array_map('trim', $values); + + return $values; + } + + public function getJobDataFromXml($jobCode) + { + return $this->getJobDataFromConfig($jobCode, false, null); + } + + public function getJobDataFromDb($jobCode) + { + $adapter = $this->_getWriteAdapter(); + + $select = $adapter->select() + ->from($this->getMainTable(), array('path', 'value')) + ->where('scope = ?', 'default') + ->where('scope_id = ?', '0') + ->where('path LIKE ?', $this->getJobSearchPath($jobCode)); + + $pathPrefix = $this->getJobPathPrefix($jobCode) . '/'; + $values = array(); + foreach ($adapter->query($select)->fetchAll() as $row) { + if (strpos($row['path'], $pathPrefix) === 0) { + $values[substr($row['path'], strlen($pathPrefix))] = $row['value']; + } + } + + // Clean up each entry to being a trimmed string + $values = array_map('trim', $values); + + return $values; + } + + public function getJobDataFromModel(Aoe_Scheduler_Model_Job $job) + { + $values = array( + 'name' => $job->getName(), + 'description' => $job->getDescription(), + 'short_description' => $job->getShortDescription(), + 'run/model' => $job->getRunModel(), + 'schedule/config_path' => $job->getScheduleConfigPath(), + 'schedule/cron_expr' => $job->getScheduleCronExpr(), + 'parameters' => $job->getParameters(), + 'groups' => $job->getGroups(), + 'is_active' => ($job->getIsActive() ? '1' : '0'), + ); + + // Strip out the auto-generated name + if ($values['name'] === $job->getJobCode()) { + $values['name'] = ''; + } + + // Clean up each entry to being a trimmed string + $values = array_map('trim', $values); + + return $values; + } + + public function setModelFromJobData(Aoe_Scheduler_Model_Job $job, array $data) + { + $job->setName(isset($data['name']) ? $data['name'] : ''); + $job->setDescription(isset($data['description']) ? $data['description'] : ''); + $job->setShortDescription(isset($data['short_description']) ? $data['short_description'] : ''); + $job->setRunModel(isset($data['run/model']) ? $data['run/model'] : ''); + $job->setScheduleConfigPath(isset($data['schedule/config_path']) ? $data['schedule/config_path'] : ''); + $job->setScheduleCronExpr(isset($data['schedule/cron_expr']) ? $data['schedule/cron_expr'] : ''); + $job->setParameters(isset($data['parameters']) ? $data['parameters'] : ''); + $job->setGroups(isset($data['groups']) ? $data['groups'] : ''); + $job->setIsActive(isset($data['is_active']) ? $data['is_active'] : ''); + return $job; + } +} diff --git a/app/code/community/Aoe/Scheduler/Model/Resource/Job/Collection.php b/app/code/community/Aoe/Scheduler/Model/Resource/Job/Collection.php new file mode 100644 index 0000000..61c0095 --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Model/Resource/Job/Collection.php @@ -0,0 +1,370 @@ +model = 'aoe_scheduler/job'; + $this->resourceModel = 'aoe_scheduler/job'; + + return $this; + } + + /** + * @return Aoe_Scheduler_Model_Resource_Job + */ + public function getResource() + { + $resource = Mage::getResourceSingleton($this->resourceModel); + if (!$resource instanceof Aoe_Scheduler_Model_Resource_Job) { + Mage::throwException( + sprintf( + 'Invalid resource class. Expected "%s" and received "%s".', + 'Aoe_Scheduler_Model_Resource_Job', + (is_object($resource) ? get_class($resource) : 'UNKNOWN') + ) + ); + } + return $resource; + } + + /** + * Retrieve collection empty item + * + * @return Aoe_Scheduler_Model_Job + */ + public function getNewEmptyItem() + { + $model = Mage::getModel($this->model); + + if (!$model instanceof Aoe_Scheduler_Model_Job) { + Mage::throwException( + sprintf( + 'Invalid model class. Expected "%s" and received "%s".', + 'Aoe_Scheduler_Model_Job', + (is_object($model) ? get_class($model) : 'UNKNOWN') + ) + ); + } + + return $model; + } + + /** + * Load data + * + * @return $this + */ + public function loadData($printQuery = false, $logQuery = false) + { + if ($this->isLoaded()) { + return $this; + } + + $this->clear(); + + foreach ($this->getResource()->getJobCodes() as $jobCode) { + if (!empty($this->whiteList) && !in_array($jobCode, $this->whiteList)) { + continue; + } + if (!empty($this->blackList) && in_array($jobCode, $this->blackList)) { + continue; + } + + $job = $this->getNewEmptyItem()->load($jobCode); + if ($this->activeOnly && !$job->getIsActive()) { + continue; + } + if ($this->dbOnly && !$job->isDbOnly()) { + continue; + } + + $this->addItem($job); + } + + ksort($this->_items); + + $this->_setIsLoaded(true); + + return $this; + } + + /** + * @return string[] + */ + public function getWhiteList() + { + return $this->whiteList; + } + + /** + * @param string[] $list + * + * @return $this + */ + public function setWhiteList(array $list) + { + $list = array_unique(array_filter(array_map('trim', array_values($list)))); + sort($list); + if ($this->whiteList !== $list) { + $this->clear(); + $this->whiteList = $list; + } + return $this; + } + + /** + * @return string[] + */ + public function getBlackList() + { + return $this->blackList; + } + + /** + * @param string[] $list + * + * @return $this + */ + public function setBlackList(array $list) + { + $list = array_unique(array_filter(array_map('trim', array_values($list)))); + sort($list); + if ($this->blackList !== $list) { + $this->clear(); + $this->blackList = $list; + } + return $this; + } + + /** + * @return bool + */ + public function getActiveOnly() + { + return $this->activeOnly; + } + + /** + * @param bool $flag + * + * @return $this + */ + public function setActiveOnly($flag = true) + { + $flag = (bool)$flag; + if ($this->activeOnly !== $flag) { + $this->clear(); + $this->activeOnly = $flag; + } + return $this; + } + + /** + * @return bool + */ + public function getDbOnly() + { + return $this->dbOnly; + } + + /** + * @param bool $flag + * + * @return $this + */ + public function setDbOnly($flag = true) + { + $flag = (bool)$flag; + if ($this->dbOnly !== $flag) { + $this->clear(); + $this->dbOnly = $flag; + } + return $this; + } + + /** + * Adding item to item array + * + * @param Aoe_Scheduler_Model_Job $item + * + * @return $this + */ + public function addItem(Varien_Object $item) + { + if (!$item instanceof Aoe_Scheduler_Model_Job) { + Mage::throwException( + sprintf( + 'Invalid model class. Expected "%s" and received "%s".', + 'Aoe_Scheduler_Model_Job', + get_class($item) + ) + ); + } + + $jobCode = $item->getJobCode(); + if (!$jobCode) { + Mage::throwException('Jobs must have a job code'); + } + + if (isset($this->_items[$jobCode])) { + Mage::throwException('Job with the same job code "' . $item->getJobCode() . '" already exist'); + } + + $this->_items[$jobCode] = $item; + + return $this; + } + + /** + * @return string[] + */ + public function getAllIds() + { + return array_keys($this->_items); + } + + /** + * Retrieve field values from all items + * + * @param string $column + * + * @return array + */ + public function getColumnValues($column) + { + $values = array(); + foreach ($this as $item) { + /** @var Aoe_Scheduler_Model_Job $item */ + $values[] = $item->getDataUsingMethod($column); + } + return $values; + } + + /** + * Search all items by field value + * + * @param string $column + * @param mixed $value + * + * @return Aoe_Scheduler_Model_Job[] + */ + public function getItemsByColumnValue($column, $value) + { + $items = array(); + foreach ($this as $item) { + /** @var Aoe_Scheduler_Model_Job $item */ + if ($item->getDataUsingMethod($column) == $value) { + $items[] = $item; + } + } + return $items; + } + + /** + * Search first item by field value + * + * @param string $column + * @param mixed $value + * + * @return Aoe_Scheduler_Model_Job|null + */ + public function getItemByColumnValue($column, $value) + { + foreach ($this as $item) { + /** @var Aoe_Scheduler_Model_Job $item */ + if ($item->getDataUsingMethod($column) == $value) { + return $item; + } + } + return null; + } + + /** + * Setting data for all collection items + * + * @param mixed $key + * @param mixed $value + * + * @return $this + */ + public function setDataToAll($key, $value = null) + { + if (is_array($key)) { + foreach ($key as $k => $v) { + $this->setDataToAll($k, $v); + } + return $this; + } + + foreach ($this->getItems() as $item) { + /** @var Aoe_Scheduler_Model_Job $item */ + $item->setDataUsingMethod($key, $value); + } + + return $this; + } + + public function toOptionArray($valueField = 'job_code', $labelField = 'name', array $additional = array()) + { + return $this->_toOptionArray($valueField, $labelField, $additional); + } + + public function toOptionHash($valueField = 'job_code', $labelField = 'name') + { + return $this->_toOptionHash($valueField, $labelField); + } + + + /** + * Convert items array to array for select options + * + * @param string $valueField + * @param string $labelField + * + * @return array + */ + protected function _toOptionArray($valueField = 'job_code', $labelField = 'name', $additional = array()) + { + $options = array(); + + $additional['value'] = $valueField; + $additional['label'] = $labelField; + + foreach ($this as $item) { + /** @var Aoe_Scheduler_Model_Job $item */ + $data = array(); + foreach ($additional as $code => $field) { + $data[$code] = $item->getDataUsingMethod($field); + } + $options[] = $data; + } + return $options; + } + + + /** + * Convert items array to hash for select options + * + * @param string $valueField + * @param string $labelField + * + * @return array + */ + protected function _toOptionHash($valueField = 'job_code', $labelField = 'name') + { + $res = array(); + foreach ($this as $item) { + /** @var Aoe_Scheduler_Model_Job $item */ + $res[$item->getDataUsingMethod($valueField)] = $item->getDataUsingMethod($labelField); + } + return $res; + } +} diff --git a/app/code/community/Aoe/Scheduler/Model/Schedule.php b/app/code/community/Aoe/Scheduler/Model/Schedule.php old mode 100755 new mode 100644 index c66a6ea..80c9c83 --- a/app/code/community/Aoe/Scheduler/Model/Schedule.php +++ b/app/code/community/Aoe/Scheduler/Model/Schedule.php @@ -9,32 +9,49 @@ * @method string getMessages() * @method string getCreatedAt() * @method string getScheduledAt() + * @method string setJobCode($jobCode) * @method string getJobCode() - * @method string setMessages() - * @method string setExecutedAt() - * @method string setCreatedAt() - * @method string setScheduledAt() - * @method string setStatus() - * @method string setFinishedAt() - * @method string getParameters() - * @method string setParameters() - * @method string setEta() + * @method $this setMessages() + * @method $this setExecutedAt() + * @method $this setCreatedAt() + * @method $this setScheduledAt() + * @method $this setStatus() + * @method $this setFinishedAt() + * @method $this setParameters() + * @method $this setEta() * @method string getEta() - * @method string setHost() + * @method $this setHost() * @method string getHost() - * @method string setPid() + * @method $this setPid() * @method string getPid() - * @method string setProgressMessage() + * @method $this setProgressMessage() * @method string getProgressMessage() * @method string getLastSeen() - * @method string setLastSeen() + * @method $this setLastSeen() + * @method string getScheduledBy() + * @method $this setScheduledBy($scheduledBy) + * @method string getScheduledReason() + * @method $this setScheduledReason($scheduledReason) + * @method string getKillRequest() + * @method $this setKillRequest($killRequest) */ class Aoe_Scheduler_Model_Schedule extends Mage_Cron_Model_Schedule { - CONST STATUS_KILLED = 'killed'; - CONST STATUS_DISAPPEARED = 'gone'; // the status field is limited to 7 characters - CONST STATUS_DIDNTDOANYTHING = 'nothing'; + const STATUS_KILLED = 'killed'; + const STATUS_DISAPPEARED = 'gone'; // the status field is limited to 7 characters + const STATUS_DIDNTDOANYTHING = 'nothing'; + + const REASON_RUNNOW_WEB = 'run_now_web'; + const REASON_SCHEDULENOW_WEB = 'schedule_now_web'; + const REASON_RUNNOW_CLI = 'run_now_cli'; + const REASON_SCHEDULENOW_CLI = 'schedule_now_cli'; + const REASON_RUNNOW_API = 'run_now_api'; + const REASON_SCHEDULENOW_API = 'schedule_now_api'; + const REASON_GENERATESCHEDULES = 'generate_schedules'; + const REASON_DEPENDENCY_ALL = 'dependency_all'; + const REASON_DEPENDENCY_SUCCESS = 'dependency_success'; + const REASON_DEPENDENCY_FAILURE = 'dependency_failure'; /** * Prefix of model events names @@ -44,15 +61,45 @@ class Aoe_Scheduler_Model_Schedule extends Mage_Cron_Model_Schedule protected $_eventPrefix = 'aoe_scheduler_schedule'; /** - * @var Aoe_Scheduler_Model_Configuration + * @var Aoe_Scheduler_Model_Job */ - protected $_jobConfiguration; + protected $job; /** * @var bool */ protected $jobWasLocked = false; + /** + * Placeholder to keep track of active redirect buffer. + * + * @var bool + */ + protected $_redirect = false; + + /** + * The buffer will be flushed after any output call which causes + * the buffer's length to equal or exceed this value. + * + * Prior to PHP 5.4.0, the value 1 set the chunk size to 4096 bytes. + */ + protected $_redirectOutputHandlerChunkSize = 100; // bytes + + + /** + * Initialize from job + * + * @param Aoe_Scheduler_Model_Job $job + * @return $this + */ + public function initializeFromJob(Aoe_Scheduler_Model_Job $job) + { + $this->setJobCode($job->getJobCode()); + $this->setCronExpr($job->getCronExpression()); + $this->setStatus(Mage_Cron_Model_Schedule::STATUS_PENDING); + return $this; + } + /** * Run this task now @@ -62,30 +109,9 @@ class Aoe_Scheduler_Model_Schedule extends Mage_Cron_Model_Schedule */ public function runNow($tryLockJob = true) { - - $modelCallback = $this->getJobConfiguration()->getModel(); - - if (!$this->getCreatedAt()) { - $this->schedule(); - } - - if (!preg_match(Mage_Cron_Model_Observer::REGEX_RUN_MODEL, $modelCallback, $run)) { - Mage::throwException(Mage::helper('cron')->__('Invalid model/method definition, expecting "model/class::method".')); - } - if (!($model = Mage::getModel($run[1])) || !method_exists($model, $run[2])) { - Mage::throwException(Mage::helper('cron')->__('Invalid callback: %s::%s does not exist', $run[1], $run[2])); - } - $callback = array($model, $run[2]); - - if (empty($callback)) { - Mage::throwException(Mage::helper('cron')->__('No callbacks found')); - } - // lock job (see below) prevents the exact same schedule from being executed from more than one process (or server) // the following check will prevent multiple schedules of the same type to be run in parallel - - $processManager = Mage::getModel('aoe_scheduler/processManager'); - /* @var $processManager Aoe_Scheduler_Model_ProcessManager */ + $processManager = Mage::getModel('aoe_scheduler/processManager'); /* @var $processManager Aoe_Scheduler_Model_ProcessManager */ if ($processManager->isJobCodeRunning($this->getJobCode(), $this->getId())) { $this->log(sprintf('Job "%s" (id: %s) will not be executed because there is already another process with the same job code running. Skipping.', $this->getJobCode(), $this->getId())); return $this; @@ -101,52 +127,79 @@ public function runNow($tryLockJob = true) return $this; } - $startTime = time(); - $this - ->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $startTime)) - ->setLastSeen(strftime('%Y-%m-%d %H:%M:%S', $startTime)) - ->setStatus(Mage_Cron_Model_Schedule::STATUS_RUNNING) - ->setHost(gethostname()) - ->setPid(getmypid()) - ->save(); + // if this schedule doesn't exist yet, create it + if (!$this->getCreatedAt()) { + $this->schedule(); + } + + try { + $job = $this->getJob(); + + if (!$job) { + Mage::throwException(sprintf("Could not create job with jobCode '%s'", $this->getJobCode())); + } - Mage::dispatchEvent('cron_' . $this->getJobCode() . '_before', array('schedule' => $this)); - Mage::dispatchEvent('cron_before', array('schedule' => $this)); + $callback = $job->getCallback(); - $this->log('Start: ' . $this->getJobCode()); + $startTime = time(); + $this + ->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $startTime)) + ->setLastSeen(strftime('%Y-%m-%d %H:%M:%S', $startTime)) + ->setStatus(Mage_Cron_Model_Schedule::STATUS_RUNNING) + ->setHost(gethostname()) + ->setPid(getmypid()) + ->save(); - Mage::unregister('current_cron_task'); - Mage::register('current_cron_task', $this); + Mage::dispatchEvent('cron_' . $this->getJobCode() . '_before', array('schedule' => $this)); + Mage::dispatchEvent('cron_before', array('schedule' => $this)); - // this is where the actual task will be executed ... - $messages = call_user_func_array($callback, array($this)); + Mage::unregister('current_cron_task'); + Mage::register('current_cron_task', $this); - $this->log('Stop: ' . $this->getJobCode()); + $this->log('Start: ' . $this->getJobCode()); - // added by Fabrizio to also save messages when no exception was thrown - if (!empty($messages)) { - if (is_object($messages)) { - $messages = get_class($messages); - } elseif (!is_scalar($messages)) { - $messages = var_export($messages, 1); + $this->_startBufferToMessages(); + try { + $messages = call_user_func_array($callback, array($this)); + $this->_stopBufferToMessages(); + } catch (Exception $e) { + $this->_stopBufferToMessages(); + throw $e; + } + + $this->log('Stop: ' . $this->getJobCode()); + + if (!empty($messages)) { + if (is_object($messages)) { + $messages = get_class($messages); + } elseif (!is_scalar($messages)) { + $messages = var_export($messages, 1); + } + $this->addMessages(PHP_EOL . '---RETURN_VALUE---' . PHP_EOL . $messages); } - $this->setMessages($messages); - } - // schedules can report an error state by returning a string that starts with "ERROR:" - if ((is_string($messages) && strtoupper(substr($messages, 0, 6)) == 'ERROR:') || $this->getStatus() === Mage_Cron_Model_Schedule::STATUS_ERROR) { + // schedules can report an error state by returning a string that starts with "ERROR:" + if ((is_string($messages) && strtoupper(substr($messages, 0, 6)) == 'ERROR:') || $this->getStatus() === Mage_Cron_Model_Schedule::STATUS_ERROR) { + $this->setStatus(Mage_Cron_Model_Schedule::STATUS_ERROR); + Mage::helper('aoe_scheduler')->sendErrorMail($this, $messages); + Mage::dispatchEvent('cron_' . $this->getJobCode() . '_after_error', array('schedule' => $this)); + Mage::dispatchEvent('cron_after_error', array('schedule' => $this)); + } elseif ((is_string($messages) && strtoupper(substr($messages, 0, 7)) == 'NOTHING') || $this->getStatus() === Aoe_Scheduler_Model_Schedule::STATUS_DIDNTDOANYTHING) { + $this->setStatus(Aoe_Scheduler_Model_Schedule::STATUS_DIDNTDOANYTHING); + Mage::dispatchEvent('cron_' . $this->getJobCode() . '_after_nothing', array('schedule' => $this)); + Mage::dispatchEvent('cron_after_nothing', array('schedule' => $this)); + } else { + $this->setStatus(Mage_Cron_Model_Schedule::STATUS_SUCCESS); + Mage::dispatchEvent('cron_' . $this->getJobCode() . '_after_success', array('schedule' => $this)); + Mage::dispatchEvent('cron_after_success', array('schedule' => $this)); + } + + } catch (Exception $e) { $this->setStatus(Mage_Cron_Model_Schedule::STATUS_ERROR); - Mage::helper('aoe_scheduler')->sendErrorMail($this, $messages); - Mage::dispatchEvent('cron_' . $this->getJobCode() . '_after_error', array('schedule' => $this)); - Mage::dispatchEvent('cron_after_error', array('schedule' => $this)); - } elseif (is_string($messages) && strtoupper(substr($messages, 0, 7)) == 'NOTHING') { - $this->setStatus(Aoe_Scheduler_Model_Schedule::STATUS_DIDNTDOANYTHING); - Mage::dispatchEvent('cron_' . $this->getJobCode() . '_after_nothing', array('schedule' => $this)); - Mage::dispatchEvent('cron_after_nothing', array('schedule' => $this)); - } else { - $this->setStatus(Mage_Cron_Model_Schedule::STATUS_SUCCESS); - Mage::dispatchEvent('cron_' . $this->getJobCode() . '_after_success', array('schedule' => $this)); - Mage::dispatchEvent('cron_after_success', array('schedule' => $this)); + $this->addMessages(PHP_EOL . '---EXCEPTION---' . PHP_EOL . $e->__toString()); + Mage::dispatchEvent('cron_' . $this->getJobCode() . '_exception', array('schedule' => $this, 'exception' => $e)); + Mage::dispatchEvent('cron_exception', array('schedule' => $this, 'exception' => $e)); + Mage::helper('aoe_scheduler')->sendErrorMail($this, $e->__toString()); } $this->setFinishedAt(strftime('%Y-%m-%d %H:%M:%S', time())); @@ -188,7 +241,7 @@ public function scheduleNow() * @param int $time * @return Aoe_Scheduler_Model_Schedule */ - public function schedule($time = NULL) + public function schedule($time = null) { if (is_null($time)) { $time = time(); @@ -204,17 +257,18 @@ public function schedule($time = NULL) /** * Get job configuration * - * @return Aoe_Scheduler_Model_Configuration + * @return Aoe_Scheduler_Model_Job */ - public function getJobConfiguration() + public function getJob() { - if (is_null($this->_jobConfiguration)) { - $this->_jobConfiguration = Mage::getModel('aoe_scheduler/configuration')->loadByCode($this->getJobCode()); + if (is_null($this->job)) { + $this->job = Mage::getModel('aoe_scheduler/job')->load($this->getJobCode()); } - return $this->_jobConfiguration; + return $this->job; } + /** * Get start time (planned or actual) * @@ -231,17 +285,23 @@ public function getStarttime() /** - * Get job duration + * Get job duration. * * @return bool|int time in seconds, or false */ public function getDuration() { $duration = false; - if ($this->getExecutedAt() && ($this->getExecutedAt() != '0000-00-00 00:00:00') - && $this->getFinishedAt() && ($this->getFinishedAt() != '0000-00-00 00:00:00') - ) { - $duration = strtotime($this->getFinishedAt()) - strtotime($this->getExecutedAt()); + if ($this->getExecutedAt() && ($this->getExecutedAt() != '0000-00-00 00:00:00')) { + if ($this->getFinishedAt() && ($this->getFinishedAt() != '0000-00-00 00:00:00')) { + $time = strtotime($this->getFinishedAt()); + } elseif ($this->getStatus() == Mage_Cron_Model_Schedule::STATUS_RUNNING) { + $time = time(); + } else { + // Mage::throwException('No finish time found, but the job is not running'); + return false; + } + $duration = $time - strtotime($this->getExecutedAt()); } return $duration; } @@ -249,7 +309,11 @@ public function getDuration() /** * Is this process still alive? * - * @return bool + * true -> alive + * false -> dead + * null -> we don't know because the task is running on a different server + * + * @return bool|null */ public function isAlive() { @@ -280,7 +344,7 @@ public function isAlive() * @param string $message * @return void */ - public function markAsDisappeared($message = NULL) + public function markAsDisappeared($message = null) { if (!is_null($message)) { $this->setMessages($message); @@ -304,6 +368,22 @@ public function checkPid() return $pid && file_exists('/proc/' . $pid); } + /** + * Request kill + * + * @param int $time + * @return $this + */ + public function requestKill($time = null) + { + if (is_null($time)) { + $time = time(); + } + $this->setKillRequest(strftime('%Y-%m-%d %H:%M:%S', $time)) + ->save(); + return $this; + } + /** * Kill this process * @@ -315,7 +395,7 @@ public function kill() if (!$this->checkPid()) { // already dead $this->markAsDisappeared(sprintf('Did not kill job "%s" (id: %s), because it was already dead.', $this->getJobCode(), $this->getId())); - return true; + return; } // let's be nice first (a.k.a. "Could you please stop running now?") @@ -362,7 +442,7 @@ public function kill() * @param $message * @param null $level */ - protected function log($message, $level = NULL) + protected function log($message, $level = null) { if ($logFile = Mage::getStoreConfig('system/cron/logFile')) { Mage::log($message, $level, $logFile); @@ -378,7 +458,8 @@ public function isAlwaysTask() { $isAlwaysTask = false; try { - $isAlwaysTask = $this->getJobConfiguration()->isAlwaysTask(); + $job = $this->getJob(); + $isAlwaysTask = $job && $job->isAlwaysTask(); } catch (Exception $e) { Mage::logException($e); } @@ -394,6 +475,10 @@ public function isAlwaysTask() */ protected function _beforeSave() { + if (!$this->getScheduledBy() && php_sapi_name() !== 'cli' && Mage::getSingleton('admin/session')->isLoggedIn()) { + $this->setScheduledBy(Mage::getSingleton('admin/session')->getUser()->getId()); + } + $collection = Mage::getModel('cron/schedule')/* @var $collection Mage_Cron_Model_Resource_Schedule_Collection */ ->getCollection() ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_PENDING) @@ -412,5 +497,224 @@ protected function _beforeSave() return parent::_beforeSave(); } + /** + * Check if this schedule can be run + * + * @param bool $throwException + * @return bool + * @throws Exception + * @throws Mage_Core_Exception + */ + public function canRun($throwException = false) + { + if ($this->isAlwaysTask()) { + return true; + } + $now = time(); + $time = strtotime($this->getScheduledAt()); + if ($time > $now) { + // not scheduled yet + return false; + } + $scheduleLifetime = Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_SCHEDULE_LIFETIME) * 60; + if ($time < $now - $scheduleLifetime) { + $this->setStatus(Mage_Cron_Model_Schedule::STATUS_MISSED); + $this->save(); + if ($throwException) { + Mage::throwException(Mage::helper('cron')->__('Too late for the schedule.')); + } + return false; + } + return true; + } + + /** + * Process schedule + * + * @return $this + */ + public function process() + { + if (!$this->canRun(false)) { + return $this; + } + $this->runNow(!$this->isAlwaysTask()); + return $this; + } -} \ No newline at end of file + /** + * Get parameters (and fallback to job) + * + * @return mixed + */ + public function getParameters() + { + if ($this->getData('parameters')) { + return $this->getData('parameters'); + } + // fallback to job + $job = $this->getJob(); + if ($job) { + return $job->getParameters(); + } else { + return false; + } + } + + /** + * Redirect all output to the messages field of this Schedule. + * + * We use ob_start with `_addBufferToMessages` to redirect the output. + * + * @return $this + */ + protected function _startBufferToMessages() + { + if (!Mage::getStoreConfigFlag('system/cron/enableJobOutputBuffer')) { + return $this; + } + + if ($this->_redirect) { + return $this; + } + + $this->addMessages('---START---' . PHP_EOL); + + ob_start( + array($this, '_addBufferToMessages'), + $this->_redirectOutputHandlerChunkSize + ); + + $this->_redirect = true; + } + + /** + * Stop redirecting all output to the messages field of this Schedule. + * + * We use ob_end_flush to stop redirecting the output. + * + * @return $this + */ + protected function _stopBufferToMessages() + { + if (!Mage::getStoreConfigFlag('system/cron/enableJobOutputBuffer')) { + return $this; + } + + if (!$this->_redirect) { + return $this; + } + + ob_end_flush(); + $this->addMessages('---END---' . PHP_EOL); + + $this->_redirect = false; + } + + /** + * Used as callback function to redirect the output buffer + * directly into the messages field of this schedule. + * + * @param $buffer + * + * @return string + */ + public function _addBufferToMessages($buffer) + { + $this->addMessages($buffer) + ->saveMessages(); // Save the directly to the schedule record. + + return $buffer; + } + + /** + * Append data to the current messages field. + * + * @param $messages + * + * @return $this + */ + public function addMessages($messages) + { + $this->setMessages($this->getMessages() . $messages); + + return $this; + } + + /** + * Save the messages directly to the schedule record. + * + * If the `messages` field was not updated in the database, + * check if this is because of `data truncation` and fix the message length. + * + * @return $this + */ + public function saveMessages() + { + if (!$this->getId()) { + return $this->save(); + } + + $connection = Mage::getSingleton('core/resource') + ->getConnection('core_write'); + + $count = $connection + ->update( + $this->getResource()->getMainTable(), + array('messages' => $this->getMessages()), + array('schedule_id = ?' => $this->getId()) + ); + + if (!$count) { + /** + * Check if the row was not updated because of data truncation. + */ + $warning = $this->_getPdoWarning($connection->getConnection()); + if ($warning && $warning->Code = 1265) { + $maxLength = strlen($this->getMessages()) - 5000; + $this->setMessages($warning->Level . ': ' . + str_replace(' at row 1', '.', $warning->Message) . PHP_EOL . PHP_EOL . + '...' . substr($this->getMessages(), -$maxLength)); + } + } + + return $this; + } + + /** + * Retrieve the last PDO warning. + * + * @param PDO $pdo + * @return mixed + */ + protected function _getPdoWarning(PDO $pdo) + { + $originalErrorMode = $pdo->getAttribute(PDO::ATTR_ERRMODE); + + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING); + + $stm = $pdo->query('SHOW WARNINGS'); + + $pdo->setAttribute(PDO::ATTR_ERRMODE, $originalErrorMode); + + return $stm->fetchObject(); + } + + /** + * Bypass parent's setCronExpr is the expression is "always" + * This will break trySchedule, but always tasks will never be tried to scheduled anyway + * + * @param $expr + * @return $this + * @throws Mage_Core_Exception + */ + public function setCronExpr($expr) + { + if ($expr == 'always') { + $this->setData('cron_expr', $expr); + } else { + parent::setCronExpr($expr); + } + return $this; + } +} diff --git a/app/code/community/Aoe/Scheduler/Model/ScheduleManager.php b/app/code/community/Aoe/Scheduler/Model/ScheduleManager.php new file mode 100644 index 0000000..6a61c9f --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Model/ScheduleManager.php @@ -0,0 +1,367 @@ +getCollection() + ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_PENDING) + ->addFieldToFilter('scheduled_at', array('lt' => strftime('%Y-%m-%d %H:%M:%S', time()))) + ->addOrder('scheduled_at', 'DESC'); + + $seenJobs = array(); + foreach ($schedules as $key => $schedule) { + /* @var Aoe_Scheduler_Model_Schedule $schedule */ + if (isset($seenJobs[$schedule->getJobCode()])) { + $schedule + ->setMessages('Multiple tasks with the same job code were piling up. Skipping execution of duplicates.') + ->setStatus(Mage_Cron_Model_Schedule::STATUS_MISSED) + ->save(); + } else { + $seenJobs[$schedule->getJobCode()] = 1; + } + } + + return $this; + } + + /** + * Get pending schedules + * + * @param array $whitelist + * @param array $blacklist + * + * @return Mage_Cron_Model_Resource_Schedule_Collection + */ + public function getPendingSchedules(array $whitelist = array(), array $blacklist = array()) + { + $pendingSchedules = Mage::getModel('cron/schedule')->getCollection() + ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_PENDING) + ->addFieldToFilter('scheduled_at', array('lt' => strftime('%Y-%m-%d %H:%M:%S', time()))) + ->addOrder('scheduled_at', 'ASC'); + + $whitelist = array_filter(array_map('trim', $whitelist)); + if (!empty($whitelist)) { + $pendingSchedules->addFieldToFilter('job_code', array('in' => $whitelist)); + } + + $blacklist = array_filter(array_map('trim', $blacklist)); + if (!empty($blacklist)) { + $pendingSchedules->addFieldToFilter('job_code', array('nin' => $blacklist)); + } + + return $pendingSchedules; + } + + /** + * Get job for task marked as always + * + * (Instead of reusing existing one - which results in loosing the history - create a new one every time) + * + * @param $jobCode + * @return Aoe_Scheduler_Model_Schedule|false + */ + public function getScheduleForAlwaysJob($jobCode) + { + $processManager = Mage::getModel('aoe_scheduler/processManager'); /* @var $processManager Aoe_Scheduler_Model_ProcessManager */ + if (!$processManager->isJobCodeRunning($jobCode)) { + $ts = strftime('%Y-%m-%d %H:%M:00', time()); + $schedule = Mage::getModel('cron/schedule') /* @var $schedule Aoe_Scheduler_Model_Schedule */ + ->setJobCode($jobCode) + ->setStatus(Mage_Cron_Model_Schedule::STATUS_RUNNING) + ->setCreatedAt($ts) + ->setScheduledAt($ts) + ->save(); + return $schedule; + } + return false; + } + + /** + * Delete duplicate crons + * + * @throws Exception + */ + public function deleteDuplicates() + { + $cron_schedule = Mage::getSingleton('core/resource')->getTableName('cron_schedule'); + $conn = Mage::getSingleton('core/resource')->getConnection('core_read'); + + // TODO: Direct sql is not nice. We can do better... :) + $results = $conn->fetchAll(" + SELECT + GROUP_CONCAT(schedule_id) AS ids, + CONCAT(job_code, scheduled_at) AS jobkey, + count(*) AS qty + FROM {$cron_schedule} + WHERE status = '" . Mage_Cron_Model_Schedule::STATUS_PENDING . "' + GROUP BY jobkey + HAVING qty > 1; + "); + foreach ($results as $row) { + $ids = explode(',', $row['ids']); + $removeIds = array_slice($ids, 1); + foreach ($removeIds as $id) { + Mage::getModel('cron/schedule')->load($id)->delete(); + } + } + } + + /** + * Generate cron schedule. + * Rewrites the original method to remove duplicates afterwards (that exists because of a bug) + * + * @return $this + */ + public function generateSchedules() + { + /** + * check if schedule generation is needed + */ + $lastRun = Mage::app()->loadCache(Mage_Cron_Model_Observer::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT); + if ($lastRun > time() - Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_SCHEDULE_GENERATE_EVERY) * 60) { + return $this; + } + + $startTime = microtime(true); + + /* @var $jobs Aoe_Scheduler_Model_Resource_Job_Collection */ + $jobs = Mage::getSingleton('aoe_scheduler/job')->getCollection(); + $jobs->setActiveOnly(true); + foreach ($jobs as $job) { + /* @var Aoe_Scheduler_Model_Job $job */ + $this->generateSchedulesForJob($job); + } + + /** + * save time schedules generation was ran with no expiration + */ + Mage::app()->saveCache(time(), Mage_Cron_Model_Observer::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT, array('crontab'), null); + + $this->deleteDuplicates(); + + if ($logFile = Mage::getStoreConfig('system/cron/logFile')) { + $history = Mage::getModel('cron/schedule')->getCollection() + ->setPageSize(1) + ->setOrder('scheduled_at', 'desc') + ->load(); + + $newestSchedule = $history->getFirstItem(); /* @var $newestSchedule Aoe_Scheduler_Model_Schedule */ + + $duration = microtime(true) - $startTime; + Mage::log('Generated schedule. Newest task is scheduled at "' . $newestSchedule->getScheduledAt() . '". (Duration: ' . round($duration, 2) . ' sec)', null, $logFile); + } + + return $this; + } + + /** + * Flushed all future pending schedules. + * + * @param string $jobCode + * @return $this + */ + public function flushSchedules($jobCode = null) + { + /* @var $pendingSchedules Mage_Cron_Model_Resource_Schedule_Collection */ + $pendingSchedules = Mage::getModel('cron/schedule')->getCollection() + ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_PENDING) + ->addFieldToFilter('scheduled_at', array('gt' => strftime('%Y-%m-%d %H:%M:%S', time()))) + ->addOrder('scheduled_at', 'ASC'); + if (!empty($jobCode)) { + $pendingSchedules->addFieldToFilter('job_code', $jobCode); + } + foreach ($pendingSchedules as $key => $schedule) { + /* @var Aoe_Scheduler_Model_Schedule $schedule */ + $schedule->delete(); + } + Mage::app()->saveCache(0, Mage_Cron_Model_Observer::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT, array('crontab'), null); + return $this; + } + + /** + * Delete all schedules + * + * @return $this + */ + public function deleteAll() + { + /* @var $schedules Mage_Cron_Model_Resource_Schedule_Collection */ + $schedules = Mage::getModel('cron/schedule')->getCollection(); + foreach ($schedules as $key => $schedule) { /* @var Aoe_Scheduler_Model_Schedule $schedule */ + $schedule->delete(); + } + Mage::app()->saveCache(0, Mage_Cron_Model_Observer::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT, array('crontab'), null); + return $this; + } + + /** + * Generate jobs for config information + * + * @param Aoe_Scheduler_Model_Job $job + * + * @return $this + */ + public function generateSchedulesForJob(Aoe_Scheduler_Model_Job $job) + { + if (!$job->canBeScheduled()) { + return $this; + } + + $exists = array(); + foreach ($this->getPendingSchedules(array($job->getJobCode()), array()) as $schedule) { + /* @var Aoe_Scheduler_Model_Schedule $schedule */ + $exists[$schedule->getJobCode() . '/' . $schedule->getScheduledAt()] = 1; + } + + $now = time(); + $scheduleAheadFor = Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_SCHEDULE_AHEAD_FOR)*60; + $timeAhead = $now + $scheduleAheadFor; + + $schedule = Mage::getModel('cron/schedule'); /* @var $schedule Aoe_Scheduler_Model_Schedule */ + $schedule->initializeFromJob($job); + $schedule->setScheduledReason(Aoe_Scheduler_Model_Schedule::REASON_GENERATESCHEDULES); + + for ($time = $now; $time < $timeAhead; $time += 60) { + $ts = strftime('%Y-%m-%d %H:%M:00', $time); + if (!empty($exists[$job->getJobCode().'/'.$ts])) { + // already scheduled + continue; + } + if (!$schedule->trySchedule($time)) { + // time does not match cron expression + continue; + } + $schedule->unsScheduleId()->save(); + } + + return $this; + } + + /** + * Clean up the history of tasks + * This override deals with custom states added in Aoe_Scheduler + * + * @return Mage_Cron_Model_Observer + */ + public function cleanup() + { + // check if history cleanup is needed + $lastCleanup = Mage::app()->loadCache(Mage_Cron_Model_Observer::CACHE_KEY_LAST_HISTORY_CLEANUP_AT); + if ($lastCleanup > time() - Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_HISTORY_CLEANUP_EVERY) * 60) { + return $this; + } + + $startTime = microtime(true); + + $history = Mage::getModel('cron/schedule')->getCollection() + ->addFieldToFilter('status', array('nin' => array( + Aoe_Scheduler_Model_Schedule::STATUS_PENDING, + Aoe_Scheduler_Model_Schedule::STATUS_RUNNING + ))) + ->load(); + + $historyLifetimes = array( + Aoe_Scheduler_Model_Schedule::STATUS_KILLED => Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_HISTORY_SUCCESS)*60, + Aoe_Scheduler_Model_Schedule::STATUS_DISAPPEARED => Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_HISTORY_FAILURE)*60, + Aoe_Scheduler_Model_Schedule::STATUS_DIDNTDOANYTHING => Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_HISTORY_SUCCESS)*60, + Aoe_Scheduler_Model_Schedule::STATUS_SUCCESS => Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_HISTORY_SUCCESS)*60, + Aoe_Scheduler_Model_Schedule::STATUS_MISSED => Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_HISTORY_FAILURE)*60, + Aoe_Scheduler_Model_Schedule::STATUS_ERROR => Mage::getStoreConfig(Mage_Cron_Model_Observer::XML_PATH_HISTORY_FAILURE)*60, + ); + + $now = time(); + foreach ($history->getIterator() as $record) { /* @var $record Aoe_Scheduler_Model_Schedule */ + if (isset($historyLifetimes[$record->getStatus()])) { + if (strtotime($record->getExecutedAt()) < $now - $historyLifetimes[$record->getStatus()]) { + $record->delete(); + } + } + } + + // save time history cleanup was ran with no expiration + Mage::app()->saveCache(time(), Mage_Cron_Model_Observer::CACHE_KEY_LAST_HISTORY_CLEANUP_AT, array('crontab'), null); + + + // delete successful tasks (beyond the configured max number of tasks to keep) + $maxNo = Mage::getStoreConfig(self::XML_PATH_HISTORY_MAXNO); + if ($maxNo) { + $history = Mage::getModel('cron/schedule')->getCollection() + ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_SUCCESS) + ->setOrder('finished_at', 'desc') + ->load(); + $counter = array(); + foreach ($history->getIterator() as $record) { /* @var $record Aoe_Scheduler_Model_Schedule */ + $jobCode = $record->getJobCode(); + if (!isset($counter[$jobCode])) { + $counter[$jobCode] = 0; + } + $counter[$jobCode]++; + if ($counter[$jobCode] > $maxNo) { + $record->delete(); + } + } + } + + if ($logFile = Mage::getStoreConfig('system/cron/logFile')) { + $duration = microtime(true) - $startTime; + Mage::log('History cleanup (Duration: ' . round($duration, 2) . ' sec)', null, $logFile); + } + + return $this; + } + + /** + * Log run + */ + public function logRun() + { + $lastRuns = Mage::app()->loadCache(self::CACHE_KEY_SCHEDULER_LASTRUNS); + $lastRuns = explode(',', $lastRuns); + $lastRuns[] = time(); + $lastRuns = array_slice($lastRuns, -100); + Mage::app()->saveCache(implode(',', $lastRuns), self::CACHE_KEY_SCHEDULER_LASTRUNS, array('crontab'), null); + } + + /** + * Create some statistics based on self::CACHE_KEY_SCHEDULER_LASTRUNS + * + * @return array|bool + */ + public function getMeasuredCronInterval() + { + $lastRuns = Mage::app()->loadCache(self::CACHE_KEY_SCHEDULER_LASTRUNS); + $lastRuns = array_values(array_filter(explode(',', $lastRuns))); + if (count($lastRuns) < 3) { + // not enough data points + return false; + } + $gaps = array(); + foreach ($lastRuns as $index => $run) { + if ($index > 0) { + $gaps[$index] = intval($lastRuns[$index]) - intval($lastRuns[$index-1]); + } + } + return array( + 'average' => round((array_sum($gaps) / count($gaps)) / 60, 2), + 'max' => round(max($gaps) / 60, 2), + 'min' => round(min($gaps) / 60, 2), + 'count' => count($gaps), + 'last' => end($lastRuns) + ); + } +} diff --git a/app/code/community/Aoe/Scheduler/Model/HeartbeatTask.php b/app/code/community/Aoe/Scheduler/Model/Task/Heartbeat.php old mode 100755 new mode 100644 similarity index 77% rename from app/code/community/Aoe/Scheduler/Model/HeartbeatTask.php rename to app/code/community/Aoe/Scheduler/Model/Task/Heartbeat.php index 74dbe84..812d646 --- a/app/code/community/Aoe/Scheduler/Model/HeartbeatTask.php +++ b/app/code/community/Aoe/Scheduler/Model/Task/Heartbeat.php @@ -5,12 +5,11 @@ * * @author Fabrizio Branca */ -class Aoe_Scheduler_Model_HeartbeatTask +class Aoe_Scheduler_Model_Task_Heartbeat { public function run() { return true; } - -} \ No newline at end of file +} diff --git a/app/code/community/Aoe/Scheduler/Model/Task/Test.php b/app/code/community/Aoe/Scheduler/Model/Task/Test.php new file mode 100644 index 0000000..9129bde --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Model/Task/Test.php @@ -0,0 +1,67 @@ +getParameters(); + if ($parameters) { + $parameters = unserialize($parameters); + } + + // fake duration + $duration = 0; + if ($parameters && isset($parameters['duration'])) { + $duration = $parameters['duration']; + } + sleep($duration); + + if ($parameters && $parameters['outcome'] == 'error') { + return 'ERROR: This schedule has failed.'; + } + + if ($parameters && $parameters['outcome'] == 'nothing') { + return 'NOTHING: Did not do anything'; + } + + if ($parameters && $parameters['outcome'] == 'exception') { + throw new Exception('This is a dummy exception'); + } + + + // Simulating ETA; +// $starttime = time(); +// // $endtime = $starttime + rand(180, 360); +// $endtime = $starttime + $duration; +// $schedule +// ->setEta(strftime('%Y-%m-%d %H:%M:%S', $endtime)) +// ->save(); +// while ($endtime > time()) { +// sleep(5); +// $schedule +// ->setProgressMessage('Work in progress. Time spent: ' . (time() - $starttime)) +// ->setEta(strftime('%Y-%m-%d %H:%M:%S', $endtime)) +// ->save(); +// } +// +// $schedule +// ->setProgressMessage('') +// ->save(); + } +} diff --git a/app/code/community/Aoe/Scheduler/Model/TestTask.php b/app/code/community/Aoe/Scheduler/Model/TestTask.php deleted file mode 100755 index d3fef9c..0000000 --- a/app/code/community/Aoe/Scheduler/Model/TestTask.php +++ /dev/null @@ -1,44 +0,0 @@ -setEta(strftime('%Y-%m-%d %H:%M:%S', $endtime)) - ->save(); - while ($endtime > time()) { - sleep(5); - $schedule - ->setProgressMessage('Work in progress. Time spent: ' . (time() - $starttime)) - ->setEta(strftime('%Y-%m-%d %H:%M:%S', $endtime)) - ->save(); - } - - $schedule - ->setProgressMessage('') - ->save(); - - /* - if (rand(0, 1) == 0) { - throw new Exception('This is a dummy exception'); - } - */ - } - -} \ No newline at end of file diff --git a/app/code/community/Aoe/Scheduler/Test/Helper/Data.php b/app/code/community/Aoe/Scheduler/Test/Helper/Data.php new file mode 100644 index 0000000..4cf681a --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Test/Helper/Data.php @@ -0,0 +1,18 @@ +assertInstanceOf('Aoe_Scheduler_Helper_Data', $helper); + + return $helper; + } +} diff --git a/app/code/community/Aoe/Scheduler/Test/Model/Schedule.php b/app/code/community/Aoe/Scheduler/Test/Model/Schedule.php deleted file mode 100755 index f4c7b98..0000000 --- a/app/code/community/Aoe/Scheduler/Test/Model/Schedule.php +++ /dev/null @@ -1,50 +0,0 @@ -assertInstanceOf('Aoe_Scheduler_Model_Schedule', $schedule); - return $schedule; - } - - /** - * @test - * @depends checkClass - */ - public function runTask(Aoe_Scheduler_Model_Schedule $schedule) { - - $jobCode = 'aoescheduler_testtask'; - - $schedule->setJobCode($jobCode); - $schedule->runNow(false); - - $scheduleId = $schedule->getId(); - $this->assertTrue(intval($schedule->getId()) > 0); - - $loadedSchedule = Mage::getModel('cron/schedule')->load($scheduleId); /* @var $loadedSchedule Aoe_Scheduler_Model_Schedule */ - $this->assertEquals($scheduleId, $loadedSchedule->getId()); - - $this->assertEquals(gethostname(), $loadedSchedule->getHost()); - $this->assertEquals(getmypid(), $loadedSchedule->getPid()); - - $this->assertEquals(Mage_Cron_Model_Schedule::STATUS_SUCCESS, $loadedSchedule->getStatus()); - - $this->assertEventDispatched(array( - 'cron_after', - 'cron_after_success', - 'cron_'.$jobCode.'_after', - 'cron_'.$jobCode.'_after_success', - 'cron_before', - 'cron_'.$jobCode.'_before', - )); - } - -} - diff --git a/app/code/community/Aoe/Scheduler/Test/Model/Schedule/Runnow.php b/app/code/community/Aoe/Scheduler/Test/Model/Schedule/Runnow.php new file mode 100644 index 0000000..b747cfc --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Test/Model/Schedule/Runnow.php @@ -0,0 +1,177 @@ +deleteAll(); + } + + /** + * @test + * @return Aoe_Scheduler_Model_Schedule + */ + public function checkClass() + { + /* @var Aoe_Scheduler_Model_Schedule $schedule */ + $schedule = Mage::getModel('cron/schedule'); + $this->assertInstanceOf('Aoe_Scheduler_Model_Schedule', $schedule); + return $schedule; + } + + /** + * @test + */ + public function runJob() + { + $schedule = Mage::getModel('cron/schedule'); + + $jobCode = 'aoescheduler_testtask'; + + $schedule->setJobCode($jobCode); + $schedule->runNow(false); + + $scheduleId = $schedule->getId(); + $this->assertGreaterThan(0, intval($schedule->getId())); + + /* @var Aoe_Scheduler_Model_Schedule $loadedSchedule */ + $loadedSchedule = Mage::getModel('cron/schedule')->load($scheduleId); + $this->assertEquals($scheduleId, $loadedSchedule->getId()); + + $this->assertEquals(gethostname(), $loadedSchedule->getHost()); + $this->assertEquals(getmypid(), $loadedSchedule->getPid()); + + $this->assertEquals(Mage_Cron_Model_Schedule::STATUS_SUCCESS, $loadedSchedule->getStatus()); + + $this->assertEventDispatched( + array( + 'cron_after', + 'cron_after_success', + 'cron_' . $jobCode . '_after', + 'cron_' . $jobCode . '_after_success', + 'cron_before', + 'cron_' . $jobCode . '_before', + ) + ); + } + + /** + * @test + */ + public function runJobWithError() + { + $schedule = Mage::getModel('cron/schedule'); + + $jobCode = 'aoescheduler_testtask'; + + $parameter = array('outcome' => 'error'); + + $schedule->setJobCode($jobCode); + $schedule->setParameters(serialize($parameter)); + $schedule->runNow(false); + + $scheduleId = $schedule->getId(); + $this->assertGreaterThan(0, intval($schedule->getId())); + + /* @var Aoe_Scheduler_Model_Schedule $loadedSchedule */ + $loadedSchedule = Mage::getModel('cron/schedule')->load($scheduleId); + $this->assertEquals($scheduleId, $loadedSchedule->getId()); + + $this->assertEquals(gethostname(), $loadedSchedule->getHost()); + $this->assertEquals(getmypid(), $loadedSchedule->getPid()); + + $this->assertEquals(Mage_Cron_Model_Schedule::STATUS_ERROR, $loadedSchedule->getStatus()); + + $this->assertEventDispatched( + array( + 'cron_after', + 'cron_after_error', + 'cron_' . $jobCode . '_after', + 'cron_' . $jobCode . '_after_error', + 'cron_before', + 'cron_' . $jobCode . '_before', + ) + ); + } + + /** + * @test + */ + public function runJobWithNothing() + { + $schedule = Mage::getModel('cron/schedule'); + + $jobCode = 'aoescheduler_testtask'; + + $parameter = array('outcome' => 'nothing'); + + $schedule->setJobCode($jobCode); + $schedule->setParameters(serialize($parameter)); + $schedule->runNow(false); + + $scheduleId = $schedule->getId(); + $this->assertGreaterThan(0, intval($schedule->getId())); + + /* @var Aoe_Scheduler_Model_Schedule $loadedSchedule */ + $loadedSchedule = Mage::getModel('cron/schedule')->load($scheduleId); + $this->assertEquals($scheduleId, $loadedSchedule->getId()); + + $this->assertEquals(gethostname(), $loadedSchedule->getHost()); + $this->assertEquals(getmypid(), $loadedSchedule->getPid()); + + $this->assertEquals(Aoe_Scheduler_Model_Schedule::STATUS_DIDNTDOANYTHING, $loadedSchedule->getStatus()); + + $this->assertEventDispatched( + array( + 'cron_after', + 'cron_after_nothing', + 'cron_' . $jobCode . '_after', + 'cron_' . $jobCode . '_after_nothing', + 'cron_before', + 'cron_' . $jobCode . '_before', + ) + ); + } + + /** + * @test + */ + public function runJobWithException() + { + $schedule = Mage::getModel('cron/schedule'); + + $jobCode = 'aoescheduler_testtask'; + + $parameter = array('outcome' => 'exception'); + + $schedule->setJobCode($jobCode); + $schedule->setParameters(serialize($parameter)); + $schedule->runNow(false); + + $scheduleId = $schedule->getId(); + $this->assertGreaterThan(0, intval($schedule->getId())); + + /* @var Aoe_Scheduler_Model_Schedule $loadedSchedule */ + $loadedSchedule = Mage::getModel('cron/schedule')->load($scheduleId); + $this->assertEquals($scheduleId, $loadedSchedule->getId()); + + $this->assertEquals(gethostname(), $loadedSchedule->getHost()); + $this->assertEquals(getmypid(), $loadedSchedule->getPid()); + + $this->assertEquals(Mage_Cron_Model_Schedule::STATUS_ERROR, $loadedSchedule->getStatus()); + + $this->assertEventDispatched( + array( + 'cron_after', + 'cron_exception', + 'cron_' . $jobCode . '_after', + 'cron_' . $jobCode . '_exception', + 'cron_before', + 'cron_' . $jobCode . '_before', + ) + ); + } +} diff --git a/app/code/community/Aoe/Scheduler/Test/Model/Schedule/Scheduling.php b/app/code/community/Aoe/Scheduler/Test/Model/Schedule/Scheduling.php new file mode 100644 index 0000000..1b837eb --- /dev/null +++ b/app/code/community/Aoe/Scheduler/Test/Model/Schedule/Scheduling.php @@ -0,0 +1,102 @@ +deleteAll(); + $collection = Mage::getModel('cron/schedule')->getCollection(); + $this->assertCount(0, $collection); + + $scheduleManager->generateSchedules(); + $collection = Mage::getModel('cron/schedule')->getCollection(); /* @var $collection Mage_Cron_Model_Resource_Schedule_Collection */ + $this->assertGreaterThan(0, $collection->count()); + + $scheduleManager->deleteAll(); + $collection = Mage::getModel('cron/schedule')->getCollection(); + $this->assertCount(0, $collection); + } + + /** + * @param $runCronCallBack callable + * @dataProvider runCronProvider + * @test + */ + public function scheduleJobAndRunCron($runCronCallBack) + { + // delete all schedules + $scheduleManager = Mage::getModel('aoe_scheduler/scheduleManager'); /* @var Aoe_Scheduler_Model_ScheduleManager $scheduleManager */ + $scheduleManager->deleteAll(); + + // fake schedule generation to avoid it to be generated on the next run: + Mage::app()->saveCache(time(), Mage_Cron_Model_Observer::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT, array('crontab'), null); + + $schedule = Mage::getModel('cron/schedule'); /* @var $schedule Aoe_Scheduler_Model_Schedule */ + $jobCode = 'aoescheduler_testtask'; + $schedule->setJobCode($jobCode); + $schedule->schedule(); + $schedule->setScheduledReason('unittest'); + $schedule->save(); + $scheduleId = $schedule->getId(); + $this->assertGreaterThan(0, intval($scheduleId)); + + // check for pending status + $loadedSchedule = Mage::getModel('cron/schedule')->load($scheduleId); /* @var Aoe_Scheduler_Model_Schedule $loadedSchedule */ + $this->assertEquals($scheduleId, $loadedSchedule->getId()); + $this->assertEquals(Mage_Cron_Model_Schedule::STATUS_PENDING, $loadedSchedule->getStatus()); + + // run cron + $runCronCallBack(); + + // check for success status + $loadedSchedule = Mage::getModel('cron/schedule')->load($scheduleId); /* @var Aoe_Scheduler_Model_Schedule $loadedSchedule */ + $this->assertEquals($scheduleId, $loadedSchedule->getId()); + $this->assertEquals(Mage_Cron_Model_Schedule::STATUS_SUCCESS, $loadedSchedule->getStatus()); + } + + /** + * Provider for a callback that executed a cron run + * + * @return array + */ + public function runCronProvider() + { + return array( + array(function () { + // trigger dispatch + $observer = Mage::getModel('aoe_scheduler/observer'); /* @var $observers Aoe_Scheduler_Model_Observer */ + $observer->dispatch(new Varien_Event_Observer()); + }), + array(function () { + shell_exec('/usr/bin/php ' . Mage::getBaseDir() . '/cron.php'); + shell_exec('cd ' . Mage::getBaseDir() . '/shell && /usr/bin/php scheduler.php --action wait'); + }), + array(function () { + shell_exec('/bin/sh ' . Mage::getBaseDir() . '/cron.sh'); + shell_exec('cd ' . Mage::getBaseDir() . '/shell && /usr/bin/php scheduler.php --action wait'); + }), + array(function () { + shell_exec('/bin/sh ' . Mage::getBaseDir() . '/cron.sh cron.php -mdefault 1'); + shell_exec('cd ' . Mage::getBaseDir() . '/shell && /usr/bin/php scheduler.php --action wait'); + }), + array(function () { + shell_exec('cd ' . Mage::getBaseDir() . '/shell && /usr/bin/php scheduler.php --action cron --mode default'); + shell_exec('cd ' . Mage::getBaseDir() . '/shell && /usr/bin/php scheduler.php --action wait'); + }), + array(function () { + shell_exec('/bin/bash ' . Mage::getBaseDir() . '/scheduler_cron.sh'); + shell_exec('cd ' . Mage::getBaseDir() . '/shell && /usr/bin/php scheduler.php --action wait'); + }), + array(function () { + shell_exec('/bin/bash ' . Mage::getBaseDir() . '/scheduler_cron.sh --mode default'); + shell_exec('cd ' . Mage::getBaseDir() . '/shell && /usr/bin/php scheduler.php --action wait'); + }) + ); + } +} diff --git a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/CronController.php b/app/code/community/Aoe/Scheduler/controllers/Adminhtml/CronController.php deleted file mode 100755 index fe8b3b9..0000000 --- a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/CronController.php +++ /dev/null @@ -1,131 +0,0 @@ -getRequest()->getParam('codes'); - $disabledCrons = Mage::helper('aoe_scheduler')->trimExplode(',', Mage::getStoreConfig('system/cron/disabled_crons'), true); - foreach ($codes as $code) { - if (!in_array($code, $disabledCrons)) { - $disabledCrons[] = $code; - Mage::getSingleton('adminhtml/session')->addSuccess($this->__('Disabled "%s"', $code)); - } - } - Mage::getModel('core/config')->saveConfig('system/cron/disabled_crons/', implode(',', $disabledCrons)); - Mage::app()->getCache()->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array(Mage_Core_Model_Config::CACHE_TAG)); - $this->_redirect('*/*/index'); - } - - /** - * Mass action: enable - * - * @return void - */ - public function enableAction() - { - $codes = $this->getRequest()->getParam('codes'); - $disabledCrons = Mage::helper('aoe_scheduler')->trimExplode(',', Mage::getStoreConfig('system/cron/disabled_crons'), true); - foreach ($codes as $key => $code) { - if (in_array($code, $disabledCrons)) { - unset($disabledCrons[array_search($code, $disabledCrons)]); - Mage::getSingleton('adminhtml/session')->addSuccess($this->__('Enabled "%s"', $code)); - } - } - Mage::getModel('core/config')->saveConfig('system/cron/disabled_crons/', implode(',', $disabledCrons)); - Mage::app()->getCache()->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array(Mage_Core_Model_Config::CACHE_TAG)); - $this->_redirect('*/*/index'); - } - - /** - * Mass action: schedule now - * - * @return void - */ - public function scheduleNowAction() - { - $codes = $this->getRequest()->getParam('codes'); - if (is_array($codes)) { - foreach ($codes as $key) { - Mage::getModel('cron/schedule')/* @var Aoe_Scheduler_Model_Schedule */ - ->setJobCode($key) - ->schedule() - ->save(); - Mage::getSingleton('adminhtml/session')->addSuccess($this->__('Scheduled "%s"', $key)); - } - } - $this->_redirect('*/*/index'); - } - - /** - * Mass action: run now - * - * @return void - */ - public function runNowAction() - { - if (!Mage::getStoreConfig('system/cron/enableRunNow')) { - Mage::throwException("'Run now' disabled by configuration (system/cron/enableRunNow)"); - } - $codes = $this->getRequest()->getParam('codes'); - if (is_array($codes)) { - foreach ($codes as $key) { - /* @var Aoe_Scheduler_Model_Schedule $schedule */ - $schedule = Mage::getModel('cron/schedule')->setJobCode($key); - try { - $schedule->runNow(false); // without trying to lock the job - } catch (Exception $e) { - $schedule->setStatus(Mage_Cron_Model_Schedule::STATUS_ERROR); - $schedule->setMessages($e->__toString()); - - // Aoe_Scheduler: additional handling: - Mage::dispatchEvent('cron_' . $schedule->getJobCode() . '_exception', array('schedule' => $schedule, 'exception' => $e)); - Mage::dispatchEvent('cron_exception', array('schedule' => $schedule, 'exception' => $e)); - Mage::helper('aoe_scheduler')->sendErrorMail($schedule, $e->__toString()); - } - $schedule->save(); - - $messages = $schedule->getMessages(); - - if ($schedule->getStatus() == Mage_Cron_Model_Schedule::STATUS_SUCCESS) { - Mage::getSingleton('adminhtml/session')->addSuccess($this->__('Ran "%s" (Duration: %s sec)', $key, intval($schedule->getDuration()))); - if ($messages) { - Mage::getSingleton('adminhtml/session')->addSuccess($this->__('"%s" messages:
%s
', $key, $messages)); - } - } else { - Mage::getSingleton('adminhtml/session')->addError($this->__('Error while running "%s"', $key)); - if ($messages) { - Mage::getSingleton('adminhtml/session')->addError($this->__('"%s" messages:
%s
', $key, $messages)); - } - } - - } - } - $this->_redirect('*/*/index'); - } - - /** - * Acl checking - * - * @return bool - */ - protected function _isAllowed() - { - return Mage::getSingleton('admin/session')->isAllowed('system/aoe_scheduler/aoe_scheduler_cron'); - } - -} - diff --git a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/InstructionsController.php b/app/code/community/Aoe/Scheduler/controllers/Adminhtml/InstructionsController.php new file mode 100644 index 0000000..22dc348 --- /dev/null +++ b/app/code/community/Aoe/Scheduler/controllers/Adminhtml/InstructionsController.php @@ -0,0 +1,19 @@ +isAllowed('system/aoe_scheduler/aoe_scheduler_instructions'); + } +} diff --git a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/JobController.php b/app/code/community/Aoe/Scheduler/controllers/Adminhtml/JobController.php new file mode 100644 index 0000000..6afa619 --- /dev/null +++ b/app/code/community/Aoe/Scheduler/controllers/Adminhtml/JobController.php @@ -0,0 +1,267 @@ +getMassActionCodes(); + foreach ($codes as $code) { + /** @var Aoe_Scheduler_Model_Job $job */ + $job = Mage::getModel('aoe_scheduler/job')->load($code); + if ($job->getJobCode() && $job->getIsActive()) { + $job->setIsActive(false)->save(); + $this->_getSession()->addSuccess($this->__('Disabled "%s"', $code)); + + /* @var Aoe_Scheduler_Model_ScheduleManager $scheduleManager */ + $scheduleManager = Mage::getModel('aoe_scheduler/scheduleManager'); + $scheduleManager->flushSchedules($job->getJobCode()); + $this->_getSession()->addNotice($this->__('Pending schedules for "%s" have been flushed.', $job->getJobCode())); + } + } + $this->_redirect('*/*/index'); + } + + /** + * Mass action: enable + * + * @return void + */ + public function enableAction() + { + $codes = $this->getMassActionCodes(); + foreach ($codes as $code) { + /** @var Aoe_Scheduler_Model_Job $job */ + $job = Mage::getModel('aoe_scheduler/job')->load($code); + if ($job->getJobCode() && !$job->getIsActive()) { + $job->setIsActive(true)->save(); + $this->_getSession()->addSuccess($this->__('Enabled "%s"', $code)); + + /* @var Aoe_Scheduler_Model_ScheduleManager $scheduleManager */ + $scheduleManager = Mage::getModel('aoe_scheduler/scheduleManager'); + $scheduleManager->generateSchedulesForJob($job); + $this->_getSession()->addNotice($this->__('Job "%s" has been scheduled.', $job->getJobCode())); + } + } + $this->_redirect('*/*/index'); + } + + /** + * Mass action: schedule now + * + * @return void + */ + public function scheduleNowAction() + { + $codes = $this->getMassActionCodes(); + foreach ($codes as $key) { + Mage::getModel('cron/schedule') + ->setJobCode($key) + ->setScheduledReason(Aoe_Scheduler_Model_Schedule::REASON_SCHEDULENOW_WEB) + ->schedule() + ->save(); + + $this->_getSession()->addSuccess($this->__('Job "%s" has been scheduled.', $key)); + } + $this->_redirect('*/*/index'); + } + + /** + * Mass action: run now + * + * @return void + */ + public function runNowAction() + { + if (!Mage::getStoreConfig('system/cron/enableRunNow')) { + Mage::throwException("'Run now' disabled by configuration (system/cron/enableRunNow)"); + } + $codes = $this->getMassActionCodes(); + foreach ($codes as $key) { + $schedule = Mage::getModel('cron/schedule') + ->setJobCode($key) + ->setScheduledReason(Aoe_Scheduler_Model_Schedule::REASON_RUNNOW_WEB) + ->runNow(false)// without trying to lock the job + ->save(); + + $messages = $schedule->getMessages(); + + if ($schedule->getStatus() == Mage_Cron_Model_Schedule::STATUS_SUCCESS) { + $this->_getSession()->addSuccess($this->__('Ran "%s" (Duration: %s sec)', $key, intval($schedule->getDuration()))); + if ($messages) { + $this->_getSession()->addSuccess($this->__('"%s" messages:
%s
', $key, $messages)); + } + } else { + $this->_getSession()->addError($this->__('Error while running "%s"', $key)); + if ($messages) { + $this->_getSession()->addError($this->__('"%s" messages:
%s
', $key, $messages)); + } + } + } + $this->_redirect('*/*/index'); + } + + /** + * Init job instance and set it to registry + * + * @return Aoe_Scheduler_Model_Job + */ + protected function _initJob() + { + $jobCode = $this->getRequest()->getParam('job_code', null); + $job = Mage::getModel('aoe_scheduler/job')->load($jobCode); + Mage::register('current_job_instance', $job); + return $job; + } + + protected function getMassActionCodes($key = 'codes') + { + $codes = $this->getRequest()->getParam($key); + if (!is_array($codes)) { + return array(); + } + $allowedCodes = Mage::getSingleton('aoe_scheduler/job')->getResource()->getJobCodes(); + $codes = array_intersect(array_unique(array_filter(array_map('trim', $codes))), $allowedCodes); + return $codes; + } + + /** + * New cron (forward to edit action) + */ + public function newAction() + { + $this->_forward('edit'); + } + + /** + * Edit cron action + */ + public function editAction() + { + $this->_initJob(); + $this->loadLayout(); + $this->renderLayout(); + } + + protected function _filterPostData($data) + { + return $data; + } + + protected function _validatePostData($data) + { + try { + /* @var Aoe_Scheduler_Helper_Data $helper */ + $helper = Mage::helper('aoe_scheduler'); + $helper->getCallBack($data['run_model']); + if (!empty($data['schedule_cron_expr'])) { + if (!$helper->validateCronExpression($data['schedule_cron_expr'])) { + Mage::throwException("Invalid cron expression"); + } + } + } catch (Exception $e) { + $this->_getSession()->addError($e->getMessage()); + return false; + } + // TODO: implement! + return true; + } + + /** + * Save action + * + */ + public function saveAction() + { + if ($data = $this->getRequest()->getPost()) { + $data = $this->_filterPostData($data); + $job = $this->_initJob(); + $job->addData($data); + //validating + if (!$this->_validatePostData($data)) { + $this->_redirect('*/*/edit', array('job_code' => $job->getJobCode(), '_current' => true)); + return; + } + + try { + // save the data + $job->save(); + + // display success message + $this->_getSession()->addSuccess( + Mage::helper('aoe_scheduler')->__('The job has been saved.') + ); + // clear previously saved data from session + $this->_getSession()->setFormData(false); + // check if 'Save and Continue' + if ($this->getRequest()->getParam('back', false)) { + $this->_redirect('*/*/edit', array('job_code' => $job->getJobCode(), '_current' => true)); + return; + } + + /* @var $scheduleManager Aoe_Scheduler_Model_ScheduleManager */ + $scheduleManager = Mage::getModel('aoe_scheduler/scheduleManager'); + $scheduleManager->flushSchedules($job->getJobCode()); + $scheduleManager->generateSchedulesForJob($job); + $this->_getSession()->addNotice($this->__('Pending schedules for "%s" have been flushed.', $job->getJobCode())); + $this->_getSession()->addNotice($this->__('Job "%s" has been scheduled.', $job->getJobCode())); + + // go to grid + $this->_redirect('*/*'); + return; + } catch (Mage_Core_Exception $e) { + Mage::logException($e); + $this->_getSession()->addError($e->getMessage()); + } catch (Exception $e) { + Mage::logException($e); + $this->_getSession()->addError($this->__('An error occurred during saving a job: %s', $e->getMessage())); + } + + $this->_getSession()->setFormData($data); + $this->_redirect('*/*/edit', array('job_code' => $this->getRequest()->getParam('job_code'))); + return; + } + + $this->_redirect('*/*/', array('_current' => true)); + } + + /** + * Delete Action + * + */ + public function deleteAction() + { + $job = $this->_initJob(); + try { + $job->delete(); + $this->_getSession()->addSuccess($this->__('The job has been deleted.')); + + /* @var Aoe_Scheduler_Model_ScheduleManager $scheduleManager */ + $scheduleManager = Mage::getModel('aoe_scheduler/scheduleManager'); + $scheduleManager->flushSchedules($job->getJobCode()); + $this->_getSession()->addNotice($this->__('Pending schedules for "%s" have been flushed.', $job->getJobCode())); + } catch (Exception $e) { + $this->_getSession()->addError($e->getMessage()); + } + $this->_redirect('*/*/'); + return; + } + + /** + * ACL checking + * + * @return bool + */ + protected function _isAllowed() + { + return Mage::getSingleton('admin/session')->isAllowed('system/aoe_scheduler/aoe_scheduler_cron'); + } +} diff --git a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/SchedulerController.php b/app/code/community/Aoe/Scheduler/controllers/Adminhtml/SchedulerController.php old mode 100755 new mode 100644 index 68add0d..e77a441 --- a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/SchedulerController.php +++ b/app/code/community/Aoe/Scheduler/controllers/Adminhtml/SchedulerController.php @@ -1,13 +1,10 @@ getRequest()->getParam('schedule_ids'); foreach ($ids as $id) { - $schedule = Mage::getModel('cron/schedule')/* @var $schedule Aoe_Scheduler_Model_Schedule */ - ->load($id) + Mage::getModel('cron/schedule')->load($id) ->delete(); } $message = $this->__('Deleted task(s) "%s"', implode(', ', $ids)); - Mage::getSingleton('adminhtml/session')->addSuccess($message); + $this->_getSession()->addSuccess($message); if ($logFile = Mage::getStoreConfig('system/cron/logFile')) { Mage::log($message, null, $logFile); } @@ -40,13 +36,11 @@ public function killAction() { $ids = $this->getRequest()->getParam('schedule_ids'); foreach ($ids as $id) { - $schedule = Mage::getModel('cron/schedule')/* @var $schedule Aoe_Scheduler_Model_Schedule */ - ->load($id) - ->setKillRequest(strftime('%Y-%m-%d %H:%M:%S', time())) - ->save(); + $schedule = Mage::getModel('cron/schedule'); /* @var $schedule Aoe_Scheduler_Model_Schedule */ + $schedule->load($id)->requestKill(); } $message = $this->__('Kill requests saved for task(s) "%s" (will be killed via cron)', implode(', ', $ids)); - Mage::getSingleton('adminhtml/session')->addSuccess($message); + $this->_getSession()->addSuccess($message); if ($logFile = Mage::getStoreConfig('system/cron/logFile')) { Mage::log($message, null, $logFile); } @@ -62,5 +56,4 @@ protected function _isAllowed() { return Mage::getSingleton('admin/session')->isAllowed('system/aoe_scheduler/aoe_scheduler_scheduler'); } - } diff --git a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/TimelineController.php b/app/code/community/Aoe/Scheduler/controllers/Adminhtml/TimelineController.php old mode 100755 new mode 100644 index ff51861..d999253 --- a/app/code/community/Aoe/Scheduler/controllers/Adminhtml/TimelineController.php +++ b/app/code/community/Aoe/Scheduler/controllers/Adminhtml/TimelineController.php @@ -1,13 +1,10 @@ */ -class Aoe_Scheduler_Adminhtml_TimelineController extends Aoe_Scheduler_Adminhtml_AbstractController +class Aoe_Scheduler_Adminhtml_TimelineController extends Aoe_Scheduler_Controller_AbstractController { /** @@ -19,5 +16,4 @@ protected function _isAllowed() { return Mage::getSingleton('admin/session')->isAllowed('system/aoe_scheduler/aoe_scheduler_timeline'); } - } diff --git a/app/code/community/Aoe/Scheduler/data/aoescheduler_setup/data-upgrade-0.5.4-0.5.5.php b/app/code/community/Aoe/Scheduler/data/aoescheduler_setup/data-upgrade-0.5.4-0.5.5.php new file mode 100644 index 0000000..c6b5f3e --- /dev/null +++ b/app/code/community/Aoe/Scheduler/data/aoescheduler_setup/data-upgrade-0.5.4-0.5.5.php @@ -0,0 +1,30 @@ +getNode('default/system/cron/disabled_crons'); +if ($node) { + $allowedCodes = Mage::getSingleton('aoe_scheduler/job')->getResource()->getJobCodes(); + $codes = array_intersect(array_unique(array_filter(array_map('trim', explode(',', trim($node))))), $allowedCodes); + foreach ($codes as $code) { + $this->getConnection()->insertOnDuplicate( + $this->getTable('core/config_data'), + array( + 'scope' => 'default', + 'scope_id' => 0, + 'path' => 'crontab/jobs/' . $code . '/is_active', + 'value' => 0, + ) + ); + } +} + +// Remove old config setting +$this->getConnection()->delete( + $this->getTable('core/config_data'), + array( + 'scope = ?' => 'default', + 'scope_id = ?' => 0, + 'path = ?' => 'system/cron/disabled_crons' + ) +); diff --git a/app/code/community/Aoe/Scheduler/etc/adminhtml.xml b/app/code/community/Aoe/Scheduler/etc/adminhtml.xml old mode 100755 new mode 100644 index 2f072ea..0ebdd31 --- a/app/code/community/Aoe/Scheduler/etc/adminhtml.xml +++ b/app/code/community/Aoe/Scheduler/etc/adminhtml.xml @@ -8,9 +8,9 @@ 1 - Schedule Configuration + Job Configuration 10 - adminhtml/cron/index + adminhtml/job/index List View @@ -22,6 +22,11 @@ 30 adminhtml/timeline/index + + Instructions + 40 + adminhtml/instructions/index + @@ -49,6 +54,10 @@ Timeline View 30 + + Instructions + 40 + diff --git a/app/code/community/Aoe/Scheduler/etc/config.xml b/app/code/community/Aoe/Scheduler/etc/config.xml old mode 100755 new mode 100644 index f2005d8..616d665 --- a/app/code/community/Aoe/Scheduler/etc/config.xml +++ b/app/code/community/Aoe/Scheduler/etc/config.xml @@ -2,7 +2,7 @@ - 0.4.4 + 1.0.1 @@ -22,16 +22,24 @@ Aoe_Scheduler_Model + aoe_scheduler_resource + + Aoe_Scheduler_Model_Resource + + Aoe_Scheduler_Model_Observer Aoe_Scheduler_Model_Schedule + + +