计划任务(cron job)是Magento系统中一个很有用甚至可以说必须使用的功能,而最近在网上跟人聊天发现很多Magento的使用者不知道这个功能或者知道却认为可有可无,我在这里建议各位Magento的使用者,把这个功能使用起来吧。
在之前关于Newsletter的那篇博文我有讲到,要使新闻能按设定的时间自动发送,就必须设置好计划任务(cron job)。计划任务(cron job)简单的来说就是在系统中每隔一段时间来重复操作一件事,比如每隔多少时间清理日志,比如每天提交Sitemap给google等等。
而要设置计划任务(cron job),需要在网站运行的服务器(以linux为例)写一段脚本来定时调用网站根目录下的cron.PHP文件。下面是我自己使用的服务器的设置方式:
crontab */5 * * * * /usr/bin/php -f /home/yourdomainname/public_html/cron.php
当然,首先你的服务器得支持cron job你才能使用crontab 这个命令,设置完的效果是服务器每隔5分钟运行一次你的网站根目录下的cron.php文件,cron.php运行时会去检查计划任务时间表中哪些任务的预定时间已经到了,如果到了就立即执行预定的任务,比如提交sitemap,并同时根据各个任务的配置文件或预先设定的任务(特指newsletter)生成新的计划任务时间表。储存这些数据的是数据库中的cron_schedule表,进这个表你会看到一个计划任务的列表
要让某个功能按时运行需要在对应的config.xml中设置计划任务,指定要运行的某个类的某个方法,同样以google sitemap为例
<crontab> <jobs> <sitemap_generate> <run> <model>sitemap/observer::scheduledGenerateSitemaps</model> </run> </sitemap_generate> </jobs> </crontab>
你可以在Sitemap/Model/Observer.php文件中找到scheduledGenerateSitemaps这个方法,这个方法的作用就是向google提交sitemap。同理当你自己的模块有需要定时运行的功能是,可以参照sitemap在自己的config.xml配置要执行的方法。
Magento系统在你安装完毕后就已经自带了不少计划任务,在后台可以看到一些对计划任务的配置项,以sitemap为例
可以选择是否开启sitemap提交并选择提交的频率。
以1.4为例,Magento自带的计划任务包括刷新分类索引,应用价格规则,更新汇率,清理日志,发送Newsletter,发送商品提醒(价格变动和库存变化)和提交google sitemap等等。
所以下次再发现功能不能用比如设置的价格规则第二天失效了等,想一下自己是否忘了给网站设置了计划任务(cron job)。
设置计划任务,定时运行脚本
修改模块的配置文件config.xml
:
<crontab> <jobs> <计划任务标识> <schedule><cron_expr>0 */1 * * *</cron_expr></schedule> <!--定时1分钟--> <run><model>你想要运行的脚本文件</model></run> </计划任务标识> </jobs> </crontab>
所有的计划任务存在cron_schedule
表中
Magento计划任务详解
网站根目录下有一个shell脚本:
#!/bin/sh # location of the php binary if [ ! "$1" = "" ] ; then CRONSCRIPT=$1 else CRONSCRIPT=cron.php fi PHP_BIN=`which php` # absolute path to magento installation INSTALLDIR=`echo $0 | sed 's/cron\.sh//g'` #prepend the intallation path if not given an absolute path if [ "$INSTALLDIR" != "" -a "`expr index $CRONSCRIPT /`" != "1" ];then if ! ps auxwww | grep "$INSTALLDIR""$CRONSCRIPT" | grep -v grep 1>/dev/null 2>/dev/null ; then $PHP_BIN "$INSTALLDIR""$CRONSCRIPT" & fi else if ! ps auxwww | grep " $CRONSCRIPT" | grep -v grep | grep -v cron.sh 1>/dev/null 2>/dev/null ; then $PHP_BIN $CRONSCRIPT & fi fi
$1引用第一参数,如果没有指定就是cron.php,然后从$0获取绝对路径(比如直接执行/www/magento/public_html/cron.sh,那么去掉cron.sh后得到/www/magento/public_html/),expr表示对后面的表达式求值,index表示字符串2在字符串1中的位置(位置从1开始),没有就是0,这里就是判断字符串是不是以斜杆开头。if语句可以确保当前没有运行将要运行的PHP脚本(如果还在运行说明上传运行还没有退出)。
从这个shell脚本可以看出,要运行脚本可以如下写:
/www/magento/public_html/cron.sh /www/magento/public_html/cron.sh cron.php #跟第一个类似 /www/magento/public_html/cron.sh /other/path/**.php #运行其它PHP脚本
另外,脚本中通过which来找PHP,如果PHP是编译安装的,一般就找不到。可以添加一个连接:
ln –sf /usr/local/php/bin/php /usr/local/bin/php
这样这个脚本运行就不会有问题了。然后添加计划任务,让它5分钟运行一次:
crontab –e 0-59/5 * * * * sh /www/magento/public_html/cron.sh > /dev/null 2>&1
不过有时我并不想把这个shell放在根下,我把它挪到比如/root/cron.sh中,这是需要这样调用:
crontab –e 0-59/5 * * * * sh /root/cron.sh /www/magento/public_html/cron.php > /dev/null 2>&1
当放到其它位置时需要指定第二参数,否则找不到执行的PHP脚本。这个shell脚本只是一个包装器,实际上完全可以使用:
crontab –e 0-59/5 * * * * /usr/local/bin/php /www/magento/public_html/cron.php > /dev/null 2>&1
从上面可以看出,cron.php是进入点:
require 'app/Mage.php'; if (!Mage::isInstalled()) { echo "Application is not installed yet, please complete install wizard first."; exit; } // Only for urls // Don't remove this $_SERVER['SCRIPT_NAME'] = str_replace(basename(__FILE__), 'index.php', $_SERVER['SCRIPT_NAME']); $_SERVER['SCRIPT_FILENAME'] = str_replace(basename(__FILE__), 'index.php', $_SERVER['SCRIPT_FILENAME']); Mage::app('admin')->setUseSessionInUrl(false); umask(0); try { Mage::getConfig()->init()->loadEventObservers('crontab');// crontab/events Mage::app()->addEventArea('crontab'); //APP _events['crontab'] = array() Mage::dispatchEvent('default'); } catch (Exception $e) { Mage::printException($e); }
可以把这里的这个config对象的xml文件输出(注意关闭缓存,否则你会很困惑,因为你可能得到的只是缓存的部分内容,Magento中的对全局配置文件拆分成几个部分缓存)。通过跟踪dispatchEvent函数,里面通过getEventConfig(‘crontab’, ‘default’)获取配置,然后知道实际的配置在Mage/Cron/etc/config.xml中:
<crontab> <events> <default> <observers> <cron_observer> <class>cron/observer</class> <method>dispatch</method> </cron_observer> </observers> </default> </events> </crontab>
getEventConfig(‘crontab’, ‘default’)执行实际得到:
object(Mage_Core_Model_Config_Element)#25 (2) { ["default"]=> object(Mage_Core_Model_Config_Element)#72 (1) { ["observers"]=> object(Mage_Core_Model_Config_Element)#73 (1) { ["cron_observer"]=> object(Mage_Core_Model_Config_Element)#16 (2) { ["class"]=> string(13) "cron/observer" ["method"]=> string(8) "dispatch" } } } ["catalog_product_get_final_price"]=> object(Mage_Core_Model_Config_Element)#69 (1) { ["observers"]=> object(Mage_Core_Model_Config_Element)#73 (1) { ["catalogrule"]=> object(Mage_Core_Model_Config_Element)#16 (2) { ["class"]=> string(20) "catalogrule/observer" ["method"]=> string(22) "processAdminFinalPrice" } } } }
我们这里只关注cron/observer的dispatch方法。
public function dispatch($observer) { $schedules = $this->getPendingSchedules(); //........ $this->generate(); $this->cleanup(); }
这段代码从cron_schedule中找pending的任务处理,根据时间点判断是否执行。刚开始时,表里并没有任务任务,这个需要靠generate来产生。
public function generate() { //控制执行周期 systme/cron/schedule_generate_every $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; } //排除重复创建pending的任务 $schedules = $this->getPendingSchedules(); $exists = array(); foreach ($schedules->getIterator() as $schedule) { $exists[$schedule->getJobCode().'/'.$schedule->getScheduledAt()] = 1; } /** * generate global crontab jobs */ $config = Mage::getConfig()->getNode('crontab/jobs'); if ($config instanceof Mage_Core_Model_Config_Element) { $this->_generateJobs($config->children(), $exists); } /** * generate configurable crontab jobs */ $config = Mage::getConfig()->getNode('default/crontab/jobs'); if ($config instanceof Mage_Core_Model_Config_Element) { $this->_generateJobs($config->children(), $exists); } /** * save time schedules generation was ran with no expiration */ Mage::app()->saveCache(time(), self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT, array('crontab'), null); return $this; }
可以看到,它把generate的时间使用缓存文件保存,这样就可以控制generate周期。任务产生配置来自crontab/jobs 和 default/crontab/jobs,输出这两个东西就知道系统提供了计划任务:
Array ( [core_clean_cache] => Array ( [schedule] => Array ( [cron_expr] => 30 2 * * * ) [run] => Array ( [model] => core/observer::cleanCache ) ) [currency_rates_update] => Array ( [run] => Array ( [model] => directory/observer::scheduledUpdateCurrencyRates ) ) [catalog_product_index_price_reindex_all] => Array ( [schedule] => Array ( [cron_expr] => 0 2 * * * ) [run] => Array ( [model] => catalog/observer::reindexProductPrices ) ) [catalogrule_apply_all] => Array ( [schedule] => Array ( [cron_expr] => 0 1 * * * ) [run] => Array ( [model] => catalogrule/observer::dailyCatalogUpdate ) ) [sales_clean_quotes] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => sales/observer::cleanExpiredQuotes ) ) [aggregate_sales_report_order_data] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => sales/observer::aggregateSalesReportOrderData ) ) [aggregate_sales_report_shipment_data] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => sales/observer::aggregateSalesReportShipmentData ) ) [aggregate_sales_report_invoiced_data] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => sales/observer::aggregateSalesReportInvoicedData ) ) [aggregate_sales_report_refunded_data] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => sales/observer::aggregateSalesReportRefundedData ) ) [aggregate_sales_report_bestsellers_data] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => sales/observer::aggregateSalesReportBestsellersData ) ) [aggregate_sales_report_coupons_data] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => salesrule/observer::aggregateSalesReportCouponsData ) ) [system_backup] => Array ( [run] => Array ( [model] => backup/observer::scheduledBackup ) ) [paypal_fetch_settlement_reports] => Array ( [run] => Array ( [model] => paypal/observer::fetchReports ) ) [log_clean] => Array ( [run] => Array ( [model] => log/cron::logClean ) ) [aggregate_sales_report_tax_data] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => tax/observer::aggregateSalesReportTaxData ) ) [sitemap_generate] => Array ( [run] => Array ( [model] => sitemap/observer::scheduledGenerateSitemaps ) ) [catalog_product_alert] => Array ( [run] => Array ( [model] => productalert/observer::process ) ) [captcha_delete_old_attempts] => Array ( [schedule] => Array ( [cron_expr] => */30 * * * * ) [run] => Array ( [model] => captcha/observer::deleteOldAttempts ) ) [captcha_delete_expired_images] => Array ( [schedule] => Array ( [cron_expr] => */10 * * * * ) [run] => Array ( [model] => captcha/observer::deleteExpiredImages ) ) [newsletter_send_all] => Array ( [schedule] => Array ( [cron_expr] => */5 * * * * ) [run] => Array ( [model] => newsletter/observer::scheduledSend ) ) [persistent_clear_expired] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => persistent/observer::clearExpiredCronJob ) ) [xmlconnect_notification_send_all] => Array ( [schedule] => Array ( [cron_expr] => */5 * * * * ) [run] => Array ( [model] => xmlconnect/observer::scheduledSend ) ) ) Array ( [catalog_product_alert] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) [run] => Array ( [model] => productalert/observer::process ) ) [currency_rates_update] => Array ( [schedule] => Array ( [cron_expr] => 0 0 * * * ) ) )
系统提供的计划任务还真不少。对应那些提供了run,没有schedule的,一般都是可以后台配置的。
SELECT
*
FROM
core_config_data
WHERE
path
LIKE
"%sitemap%"
看到这个图你就清楚,尽管系统配置提供了具体到秒,但是实际上只能具体到分钟。模板中的配置只是一个模板,实际上数据库中的配置会覆盖之前加载的配置。
每个计划任务有两个关键参数schedule和run,如果没有给出schedule,run的模型是不会执行的,同样,没有给出run,那么也没有用。这些配置在每个模块的配置中给出,并且很多模块后台可以配置。cron_expr的参数和Linux的cron语法类似:
//每5分钟执行一次 */5 * * * * //或 0-60/5 * * * * //每两小时执行一次 0 0-24/2 * * * //前12小时每两小时执行一次 0 0-12/2 * * * //5 10 15 20分钟执行(具体) 5,10,15,20 0 * * * //12点和24点 0分执行 0 12,24 * * *
查看源代码可见,如果要每2时执行一次,应该是0-24/2而不是*/2,如果是每2分钟执行一次,则可以是*/2 或 0-60/2,这个是从源代码中得出的结论。
对应计划任务,系统后台提供了一个配置:
这里的几个参数,我是研究了源码才真正搞明白是怎么回事。
History Cleanup Every每隔多少分钟开始清理过期的任务,Success History Lifetime表示成功执行的任务的生存时间,Failure History Lifetime表示执行失败的任务的生存时间(错过执行的任务的生存时间跟这个一样),超过这些生存时间时这些任务将从数据库中删除。
Generate Schedules Every表示创建任务的时间间隔,对于创建了的任务,如果在Missed if Not Run Within指定的时间内没有得到执行,那么就过时(会被清理程序清理)。
Schedule Ahead for表示未来这个时间内,计划有几个任务被执行,比如这里设置了20,有一个计划设置成每5分钟执行一次,那么未来的这20分钟,就有4个任务要执行,分别设置成以5分钟间隔的4个任务写入数据表,这4个任务在时间到时,如果没有超过生存期,则会被执行(具体看系统crontab设置,如果时间比较长,任务可能多个被同时执行)
这个例子是每分钟执行一次,任务创建是在26分钟,但是cron.php再次执行时是在28分钟,所以26,27,28分钟的3个任务都同时执行了。由于Schedule Ahead for是预计未来时间段将要执行的任务,但是产生任务的时间间隔是由Generate Schedules Every决定的,看起来,这个值应该总是比Schedule Ahead for小才是,否则中间将产生空隙。
对于写入数据库里面的pending任务,每次dispatch调用时都被检查是否去执行,这个dispatch多久执行一次就要看系统的crontab设置了。
如果计划没有给它设置表达式,系统根本不会给它创建计划任务。创建了计划任务不一定保证会被执行,可能是在执行的代码中设置了不启用,或者是在计划任务生存时间内没有得到执行,就会错过了时间,那么就不会被执行。如果能够去到执行计划任务,那么这个任务一定会得到成功状态。
在阅读这块源码时,感叹Magento代码的优良与技巧。