1: <?php
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: class Mage_Cron_Model_Observer
35: {
36: const CACHE_KEY_LAST_SCHEDULE_GENERATE_AT = 'cron_last_schedule_generate_at';
37: const CACHE_KEY_LAST_HISTORY_CLEANUP_AT = 'cron_last_history_cleanup_at';
38:
39: const XML_PATH_SCHEDULE_GENERATE_EVERY = 'system/cron/schedule_generate_every';
40: const XML_PATH_SCHEDULE_AHEAD_FOR = 'system/cron/schedule_ahead_for';
41: const XML_PATH_SCHEDULE_LIFETIME = 'system/cron/schedule_lifetime';
42: const XML_PATH_HISTORY_CLEANUP_EVERY = 'system/cron/history_cleanup_every';
43: const XML_PATH_HISTORY_SUCCESS = 'system/cron/history_success_lifetime';
44: const XML_PATH_HISTORY_FAILURE = 'system/cron/history_failure_lifetime';
45:
46: const REGEX_RUN_MODEL = '#^([a-z0-9_]+/[a-z0-9_]+)::([a-z0-9_]+)$#i';
47:
48: protected $_pendingSchedules;
49:
50: 51: 52: 53: 54: 55: 56:
57: public function dispatch($observer)
58: {
59: $schedules = $this->getPendingSchedules();
60: $scheduleLifetime = Mage::getStoreConfig(self::XML_PATH_SCHEDULE_LIFETIME) * 60;
61: $now = time();
62: $jobsRoot = Mage::getConfig()->getNode('crontab/jobs');
63: $defaultJobsRoot = Mage::getConfig()->getNode('default/crontab/jobs');
64:
65: foreach ($schedules->getIterator() as $schedule) {
66: $jobConfig = $jobsRoot->{$schedule->getJobCode()};
67: if (!$jobConfig || !$jobConfig->run) {
68: $jobConfig = $defaultJobsRoot->{$schedule->getJobCode()};
69: if (!$jobConfig || !$jobConfig->run) {
70: continue;
71: }
72: }
73:
74: $runConfig = $jobConfig->run;
75: $time = strtotime($schedule->getScheduledAt());
76: if ($time > $now) {
77: continue;
78: }
79: try {
80: $errorStatus = Mage_Cron_Model_Schedule::STATUS_ERROR;
81: $errorMessage = Mage::helper('cron')->__('Unknown error.');
82:
83: if ($time < $now - $scheduleLifetime) {
84: $errorStatus = Mage_Cron_Model_Schedule::STATUS_MISSED;
85: Mage::throwException(Mage::helper('cron')->__('Too late for the schedule.'));
86: }
87:
88: if ($runConfig->model) {
89: if (!preg_match(self::REGEX_RUN_MODEL, (string)$runConfig->model, $run)) {
90: Mage::throwException(Mage::helper('cron')->__('Invalid model/method definition, expecting "model/class::method".'));
91: }
92: if (!($model = Mage::getModel($run[1])) || !method_exists($model, $run[2])) {
93: Mage::throwException(Mage::helper('cron')->__('Invalid callback: %s::%s does not exist', $run[1], $run[2]));
94: }
95: $callback = array($model, $run[2]);
96: $arguments = array($schedule);
97: }
98: if (empty($callback)) {
99: Mage::throwException(Mage::helper('cron')->__('No callbacks found'));
100: }
101:
102: if (!$schedule->tryLockJob()) {
103:
104: continue;
105: }
106: 107: 108: 109:
110: $schedule
111: ->setStatus(Mage_Cron_Model_Schedule::STATUS_RUNNING)
112: ->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', time()))
113: ->save();
114:
115: call_user_func_array($callback, $arguments);
116:
117: $schedule
118: ->setStatus(Mage_Cron_Model_Schedule::STATUS_SUCCESS)
119: ->setFinishedAt(strftime('%Y-%m-%d %H:%M:%S', time()));
120:
121: } catch (Exception $e) {
122: $schedule->setStatus($errorStatus)
123: ->setMessages($e->__toString());
124: }
125: $schedule->save();
126: }
127:
128: $this->generate();
129: $this->cleanup();
130: }
131:
132: public function getPendingSchedules()
133: {
134: if (!$this->_pendingSchedules) {
135: $this->_pendingSchedules = Mage::getModel('cron/schedule')->getCollection()
136: ->addFieldToFilter('status', Mage_Cron_Model_Schedule::STATUS_PENDING)
137: ->load();
138: }
139: return $this->_pendingSchedules;
140: }
141:
142: 143: 144: 145: 146:
147: public function generate()
148: {
149: 150: 151:
152: $lastRun = Mage::app()->loadCache(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT);
153: if ($lastRun > time() - Mage::getStoreConfig(self::XML_PATH_SCHEDULE_GENERATE_EVERY)*60) {
154: return $this;
155: }
156:
157: $schedules = $this->getPendingSchedules();
158: $exists = array();
159: foreach ($schedules->getIterator() as $schedule) {
160: $exists[$schedule->getJobCode().'/'.$schedule->getScheduledAt()] = 1;
161: }
162:
163: 164: 165:
166: $config = Mage::getConfig()->getNode('crontab/jobs');
167: if ($config instanceof Mage_Core_Model_Config_Element) {
168: $this->_generateJobs($config->children(), $exists);
169: }
170:
171: 172: 173:
174: $config = Mage::getConfig()->getNode('default/crontab/jobs');
175: if ($config instanceof Mage_Core_Model_Config_Element) {
176: $this->_generateJobs($config->children(), $exists);
177: }
178:
179: 180: 181:
182: Mage::app()->saveCache(time(), self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT, array('crontab'), null);
183:
184: return $this;
185: }
186:
187: 188: 189: 190: 191: 192: 193:
194: protected function _generateJobs($jobs, $exists)
195: {
196: $scheduleAheadFor = Mage::getStoreConfig(self::XML_PATH_SCHEDULE_AHEAD_FOR)*60;
197: $schedule = Mage::getModel('cron/schedule');
198:
199: foreach ($jobs as $jobCode => $jobConfig) {
200: $cronExpr = null;
201: if ($jobConfig->schedule->config_path) {
202: $cronExpr = Mage::getStoreConfig((string)$jobConfig->schedule->config_path);
203: }
204: if (empty($cronExpr) && $jobConfig->schedule->cron_expr) {
205: $cronExpr = (string)$jobConfig->schedule->cron_expr;
206: }
207: if (!$cronExpr) {
208: continue;
209: }
210:
211: $now = time();
212: $timeAhead = $now + $scheduleAheadFor;
213: $schedule->setJobCode($jobCode)
214: ->setCronExpr($cronExpr)
215: ->setStatus(Mage_Cron_Model_Schedule::STATUS_PENDING);
216:
217: for ($time = $now; $time < $timeAhead; $time += 60) {
218: $ts = strftime('%Y-%m-%d %H:%M:00', $time);
219: if (!empty($exists[$jobCode.'/'.$ts])) {
220:
221: continue;
222: }
223: if (!$schedule->trySchedule($time)) {
224:
225: continue;
226: }
227: $schedule->unsScheduleId()->save();
228: }
229: }
230: return $this;
231: }
232:
233: public function cleanup()
234: {
235:
236: $lastCleanup = Mage::app()->loadCache(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT);
237: if ($lastCleanup > time() - Mage::getStoreConfig(self::XML_PATH_HISTORY_CLEANUP_EVERY)*60) {
238: return $this;
239: }
240:
241: $history = Mage::getModel('cron/schedule')->getCollection()
242: ->addFieldToFilter('status', array('in'=>array(
243: Mage_Cron_Model_Schedule::STATUS_SUCCESS,
244: Mage_Cron_Model_Schedule::STATUS_MISSED,
245: Mage_Cron_Model_Schedule::STATUS_ERROR,
246: )))->load();
247:
248: $historyLifetimes = array(
249: Mage_Cron_Model_Schedule::STATUS_SUCCESS => Mage::getStoreConfig(self::XML_PATH_HISTORY_SUCCESS)*60,
250: Mage_Cron_Model_Schedule::STATUS_MISSED => Mage::getStoreConfig(self::XML_PATH_HISTORY_FAILURE)*60,
251: Mage_Cron_Model_Schedule::STATUS_ERROR => Mage::getStoreConfig(self::XML_PATH_HISTORY_FAILURE)*60,
252: );
253:
254: $now = time();
255: foreach ($history->getIterator() as $record) {
256: if (strtotime($record->getExecutedAt()) < $now-$historyLifetimes[$record->getStatus()]) {
257: $record->delete();
258: }
259: }
260:
261:
262: Mage::app()->saveCache(time(), self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT, array('crontab'), null);
263:
264: return $this;
265: }
266: }
267: