Magento计划任务详解

网站根目录下有一个shell脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#!/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脚本可以看出,要运行脚本可以如下写:

1
2
3
4
5

/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是编译安装的,一般就找不到。可以添加一个连接:

1
2

ln –sf /usr/local/php/bin/php /usr/local/bin/php

这样这个脚本运行就不会有问题了。然后添加计划任务,让它5分钟运行一次:

1
2
3

crontab –e
0-59/5 * * * * sh /www/magento/public_html/cron.sh > /dev/null 2>&1

不过有时我并不想把这个shell放在根下,我把它挪到比如/root/cron.sh中,这是需要这样调用:

1
2
3

crontab –e
0-59/5 * * * * sh /root/cron.sh /www/magento/public_html/cron.php > /dev/null 2>&1

当放到其它位置时需要指定第二参数,否则找不到执行的PHP脚本。这个shell脚本只是一个包装器,实际上完全可以使用:

1
2
3

crontab –e
0-59/5 * * * * /usr/local/bin/php /www/magento/public_html/cron.php > /dev/null 2>&1

从上面可以看出,cron.php是进入点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

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中:

1
2
3
4

<cron_observer>
cron/observer
dispatch

getEventConfig(‘crontab’, ‘default’)执行实际得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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方法。

1
2
3
4
5
6
7
8
9

public function dispatch($observer)
{
$schedules = $this->getPendingSchedules();
//........

$this->generate();
$this->cleanup();
}

这段代码从cron_schedule中找pending的任务处理,根据时间点判断是否执行。刚开始时,表里并没有任务任务,这个需要靠generate来产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

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,输出这两个东西就知道系统提供了计划任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309

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的,一般都是可以后台配置的。

magento_sitemap_set
Magento网站地图计划任务设置

1
2

SELECT * FROM core_config_data WHERE path LIKE "%sitemap%"

magento_sitemap_cron
Magento网站地图计划任务配置
看到这个图你就清楚,尽管系统配置提供了具体到秒,但是实际上只能具体到分钟。模板中的配置只是一个模板,实际上数据库中的配置会覆盖之前加载的配置。

每个计划任务有两个关键参数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,这个是从源代码中得出的结论。

对应计划任务,系统后台提供了一个配置:

magento_cron_tab
Magento计划任务配置
这里的几个参数,我是研究了源码才真正搞明白是怎么回事。

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设置,如果时间比较长,任务可能多个被同时执行)

magento_cron_excute
Magento计划任务执行

这个例子是每分钟执行一次,任务创建是在26分钟,但是cron.php再次执行时是在28分钟,所以26,27,28分钟的3个任务都同时执行了。由于Schedule Ahead for是预计未来时间段将要执行的任务,但是产生任务的时间间隔是由Generate Schedules Every决定的,看起来,这个值应该总是比Schedule Ahead for小才是,否则中间将产生空隙。

对于写入数据库里面的pending任务,每次dispatch调用时都被检查是否去执行,这个dispatch多久执行一次就要看系统的crontab设置了。

如果计划没有给它设置表达式,系统根本不会给它创建计划任务。创建了计划任务不一定保证会被执行,可能是在执行的代码中设置了不启用,或者是在计划任务生存时间内没有得到执行,就会错过了时间,那么就不会被执行。如果能够去到执行计划任务,那么这个任务一定会得到成功状态。

在阅读这块源码时,感叹Magento代码的优良与技巧。

永久链接:http://blog.ifeeline.com/651.html

坚持原创技术分享,您的支持将鼓励我继续创作!