Completed
Push — master ( 8a4ca9...acd23c )
by Marco
02:46
created

src/Scheduler/Scheduler.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php namespace Comodojo\Extender\Scheduler;
2
3
use \Cron\CronExpression;
4
use \Comodojo\Exception\DatabaseException;
5
use \Comodojo\Database\EnhancedDatabase;
6
use \Comodojo\Extender\Cache;
7
use \Comodojo\Extender\Planner;
8
use \Exception;
9
10
/**
11
 * Extender scheduler
12
 *
13
 * @package     Comodojo extender
14
 * @author      Marco Giovinazzi <[email protected]>
15
 * @license     GPL-3.0+
16
 *
17
 * LICENSE:
18
 * 
19
 * This program is free software: you can redistribute it and/or modify
20
 * it under the terms of the GNU Affero General Public License as
21
 * published by the Free Software Foundation, either version 3 of the
22
 * License, or (at your option) any later version.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License
30
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
31
 */
32
33
class Scheduler {
34
35
    /**
36
     * Get planned schedules
37
     *
38
     * @param   \Monolog\Logger   $logger
39
     * @param   float             $timestamp
40
     *
41
     * @return  array
42
     * @throws  \Comodojo\Exception\DatabaseException
43
     * @throws  \Exception
44
     */
45 3
    final public static function getSchedules($logger, $timestamp) {
46
47 3
        $schedules = array();
48
49 3
        $planned = array();
50
51
        try {
52
            
53 3
            $jobs = self::getJobs();
54
55 3
            foreach ( $jobs as $job ) {
56
57
                if ( self::shouldRunJob($job, $logger, $timestamp) ) array_push($schedules, $job);
58
59
                else $planned[] = self::shouldPlanJob($job);
60
                
61 3
            }   
62
63 3
        } catch (DatabaseException $de) {
64
65
            $logger->error("Cannot load job list due to database error", array(
66
                "ERROR" => $de->getMessage(),
67
                "ERRID" => $de->getCode()
68
            ));
69
            
70
            throw $de;
71
72 3
        } catch (Exception $e) {
73
            
74
            $logger->error("Cannot load job list due to generic error", array(
75
                "ERROR" => $e->getMessage(),
76
                "ERRID" => $e->getCode()
77
            ));
78
79
            throw $e;
80
81
        }
82
83 3
        $logger->info("\n".sizeof($schedules)." job(s) in current queue");
84
85 3
        return array($schedules, empty($planned) ? null : min($planned));
86
87
    }
88
89
    /**
90
     * Update schedules (last run)
91
     *
92
     * @param   Object  $logger
93
     * @param   array   $completed_processes
94
     *
95
     * @throws  \Comodojo\Exception\DatabaseException
96
     */
97
    final public static function updateSchedules($logger, $completed_processes) {
98
99
        if ( empty($completed_processes) ) {
100
101
            $logger->info("No schedule to update, exiting");
102
103
            return;
104
105
        }
106
        
107
        try {
108
109
            $db = new EnhancedDatabase(
110
                EXTENDER_DATABASE_MODEL,
111
                EXTENDER_DATABASE_HOST,
112
                EXTENDER_DATABASE_PORT,
113
                EXTENDER_DATABASE_NAME,
114
                EXTENDER_DATABASE_USER,
115
                EXTENDER_DATABASE_PASS
116
            );
117
118
            $db->tablePrefix(EXTENDER_DATABASE_PREFIX)->autoClean();
119
120
            foreach ( $completed_processes as $process ) {
121
122
                $db->table(EXTENDER_DATABASE_TABLE_JOBS)
123
                    ->keys("lastrun")
124
                    ->values($process[3])
125
                    ->where('id', '=', $process[6])
126
                    ->update();
127
128
            }
129
130
        } catch (DatabaseException $de) {
131
132
            unset($db);
133
134
            throw $de;
135
136
        }
137
138
        unset($db);
139
140
        Cache::purge();
141
        
142
        Planner::release();
143
144
    }
145
146
    /**
147
     * Get a schedule by name
148
     *
149
     * @param   string  $name
150
     *
151
     * @return  array|null
152
     * @throws  \Comodojo\Exception\DatabaseException
153
     * @throws  \Exception
154
     */
155 3
    final public static function getSchedule($name) {
156
157 3
        if ( empty($name) ) throw new Exception("Invalid job name");
158
159
        try {
160
161 3
            $db = new EnhancedDatabase(
162 3
                EXTENDER_DATABASE_MODEL,
163 3
                EXTENDER_DATABASE_HOST,
164 3
                EXTENDER_DATABASE_PORT,
165 3
                EXTENDER_DATABASE_NAME,
166 3
                EXTENDER_DATABASE_USER,
167
                EXTENDER_DATABASE_PASS
168 3
            );
169
170 3
            $result = $db->tablePrefix(EXTENDER_DATABASE_PREFIX)
171 3
                ->table(EXTENDER_DATABASE_TABLE_JOBS)
172 3
                ->keys(array("id", "task", "description",
173 3
                    "min", "hour", "dayofmonth", "month",
174 3
                    "dayofweek", "year", "params", "firstrun", "lastrun"))
175 3
                ->where("name", "=", $name)
176 3
                ->get();
177
178 3
        } catch (DatabaseException $e) {
179
180
            throw $e;
181
182
        }
183
184 3
        if ( $result->getLength() == 0 ) return null;
185
186 3
        $data = $result->getData();
187
188 3
        $expression = implode(" ", array($data[0]['min'], $data[0]['hour'], $data[0]['dayofmonth'], $data[0]['month'], $data[0]['dayofweek'], $data[0]['year']));
189
190
        return array(
191 3
            "id" => $data[0]["id"],
192 3
            "name" => $name,
193 3
            "task" => $data[0]["task"],
194 3
            "description" => $data[0]["description"],
195 3
            "expression" => $expression,
196 3
            "params" => unserialize($data[0]["params"]),
197 3
            "firstrun" => $data[0]["firstrun"],
198 3
            "lastrun" => $data[0]["lastrun"],
199 3
            "nextrun" => self::shouldPlanJob($data[0])
200 3
        );
201
202
    }
203
204
    /**
205
     * Add a schedule
206
     *
207
     * @param   string  $expression
208
     * @param   string  $name
209
     * @param   string  $task
210
     * @param   string  $description
211
     * @param   array   $params
212
     *
213
     * @return  array
214
     * @throws  \Comodojo\Exception\DatabaseException
215
     * @throws  \Exception
216
     */
217 3
    final public static function addSchedule($expression, $name, $task, $description = null, $params = array()) {
218
219 3
        if ( empty($name) ) throw new Exception("Invalid job name");
220
221 3
        if ( empty($task) ) throw new Exception("A job feels alone without a task");
222
223
        try {
224
            
225 3
            list($next_calculated_run, $parsed_expression) = self::validateExpression($expression);
226
227 3
            $firstrun = (int) date("U", strtotime($next_calculated_run));
228
229 3
            list($min, $hour, $dayofmonth, $month, $dayofweek, $year) = $parsed_expression;
230
231 3
            $parameters = serialize($params);
232
233 3
            $db = new EnhancedDatabase(
234 3
                EXTENDER_DATABASE_MODEL,
235 3
                EXTENDER_DATABASE_HOST,
236 3
                EXTENDER_DATABASE_PORT,
237 3
                EXTENDER_DATABASE_NAME,
238 3
                EXTENDER_DATABASE_USER,
239
                EXTENDER_DATABASE_PASS
240 3
            );
241
242 3
            $result = $db->tablePrefix(EXTENDER_DATABASE_PREFIX)
243 3
                ->table(EXTENDER_DATABASE_TABLE_JOBS)
244 3
                ->keys(array("name", "task", "description",
245 3
                    "min", "hour", "dayofmonth", "month", 
246 3
                    "dayofweek", "year", "params", "firstrun"))
247 3
                ->values(array($name, $task, $description,
248 3
                    $min, $hour, $dayofmonth, $month,
249 3
                    $dayofweek, $year, $parameters, $firstrun))
250 3
                ->store();
251
252 3
        } catch (DatabaseException $de) {
253
            
254
            throw $de;
255
256
        } catch (Exception $e) {
257
            
258
            throw $e;
259
260
        }
261
262 3
        Cache::purge();
263
        
264 3
        Planner::release();
265
266 3
        return array($result->getInsertId(), $next_calculated_run);
267
268
    }
269
270
    /**
271
     * Remove a schedule
272
     *
273
     * @param   string  $name
274
     *
275
     * @return  bool
276
     * @throws  \Comodojo\Exception\DatabaseException
277
     * @throws  \Exception
278
     */
279 3
    final public static function removeSchedule($name) {
280
        
281 3
        if ( empty($name) ) throw new Exception("Invalid or empty job name");
282
283
        try {
284
            
285 3
            $db = new EnhancedDatabase(
286 3
                EXTENDER_DATABASE_MODEL,
287 3
                EXTENDER_DATABASE_HOST,
288 3
                EXTENDER_DATABASE_PORT,
289 3
                EXTENDER_DATABASE_NAME,
290 3
                EXTENDER_DATABASE_USER,
291
                EXTENDER_DATABASE_PASS
292 3
            );
293
294 3
            $result = $db->tablePrefix(EXTENDER_DATABASE_PREFIX)
295 3
                ->table(EXTENDER_DATABASE_TABLE_JOBS)
296 3
                ->where("name", "=", $name)
297 3
                ->delete();
298
299 3
        } catch (DatabaseException $de) {
300
            
301
            throw $de;
302
303
        }
304
305 3
        if ( $result->getAffectedRows() == 0 ) return false;
306
307 3
        Cache::purge();
308
        
309 3
        Planner::release();
310
311 3
        return true;
312
313
    }
314
315
    /**
316
     * Update single schedule (last run)
317
     *
318
     * @param   string  $name
319
     * @param   float   $lastrun
320
     *
321
     * @throws  \Comodojo\Exception\DatabaseException
322
     * @throws  \Exception
323
     */
324 3
    final public static function updateSchedule($name, $lastrun) {
325
326 3
        if ( empty($name) ) throw new Exception("Invalid job name");
327
328 3
        if ( empty($lastrun) ) throw new Exception("Empty job run datetime");
329
        
330
        try {
331
332 3
            $db = new EnhancedDatabase(
333 3
                EXTENDER_DATABASE_MODEL,
334 3
                EXTENDER_DATABASE_HOST,
335 3
                EXTENDER_DATABASE_PORT,
336 3
                EXTENDER_DATABASE_NAME,
337 3
                EXTENDER_DATABASE_USER,
338
                EXTENDER_DATABASE_PASS
339 3
            );
340
341 3
            $db->tablePrefix(EXTENDER_DATABASE_PREFIX)->table(EXTENDER_DATABASE_TABLE_JOBS)->keys("lastrun")->values($lastrun)->where('name', '=', $name)->update();
342
343
        }
344 3
        catch (DatabaseException $de) {
345
346
            unset($db);
347
348
            throw $de;
349
350
        }
351
352 3
        unset($db);
353
354 3
        Cache::purge();
355
        
356 3
        Planner::release();
357
358 3
    }
359
360
    /**
361
     * Enable a schedule
362
     *
363
     * @param   string  $name
364
     *
365
     * @return  bool
366
     * @throws  \Comodojo\Exception\DatabaseException
367
     * @throws  \Exception
368
     */
369 3 View Code Duplication
    final public static function enableSchedule($name) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
370
371 3
        if ( empty($name) ) throw new Exception("Invalid or empty job name");
372
373
        try {
374
            
375 3
            $db = new EnhancedDatabase(
376 3
                EXTENDER_DATABASE_MODEL,
377 3
                EXTENDER_DATABASE_HOST,
378 3
                EXTENDER_DATABASE_PORT,
379 3
                EXTENDER_DATABASE_NAME,
380 3
                EXTENDER_DATABASE_USER,
381
                EXTENDER_DATABASE_PASS
382 3
            );
383
384 3
            $result = $db->tablePrefix(EXTENDER_DATABASE_PREFIX)
385 3
                ->table(EXTENDER_DATABASE_TABLE_JOBS)
386 3
                ->keys("enabled")
387 3
                ->values(array(true))
388 3
                ->where("name", "=", $name)
389 3
                ->update();
390
391 3
        } catch (DatabaseException $de) {
392
            
393
            throw $de;
394
395
        }
396
397 3
        Cache::purge();
398
        
399 3
        Planner::release();
400
401 3
        return $result->getAffectedRows() == 1 ? true : false;
402
403
    }
404
405
    /**
406
     * Disable a schedule
407
     *
408
     * @param   string  $name
409
     *
410
     * @return  bool
411
     * @throws  \Comodojo\Exception\DatabaseException
412
     * @throws  \Exception
413
     */
414 3 View Code Duplication
    final public static function disableSchedule($name) {
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
415
416 3
        if ( empty($name) ) throw new Exception("Invalid or empty job name");
417
418
        try {
419
            
420 3
            $db = new EnhancedDatabase(
421 3
                EXTENDER_DATABASE_MODEL,
422 3
                EXTENDER_DATABASE_HOST,
423 3
                EXTENDER_DATABASE_PORT,
424 3
                EXTENDER_DATABASE_NAME,
425 3
                EXTENDER_DATABASE_USER,
426
                EXTENDER_DATABASE_PASS
427 3
            );
428
429 3
            $result = $db->tablePrefix(EXTENDER_DATABASE_PREFIX)
430 3
                ->table(EXTENDER_DATABASE_TABLE_JOBS)
431 3
                ->keys("enabled")
432 3
                ->values(array(false))
433 3
                ->where("name", "=", $name)
434 3
                ->update();
435
436 3
        } catch (DatabaseException $de) {
437
            
438
            throw $de;
439
440
        }
441
442 3
        Cache::purge();
443
        
444 3
        Planner::release();
445
446 3
        return $result->getAffectedRows() == 1 ? true : false;
447
448
    }
449
450
    /**
451
     * Validate a cron expression and, if valid, return next run timestamp plus
452
     * an array of expression parts
453
     *
454
     * @param   string  $expression
455
     *
456
     * @return  array   Next run timestamp at first position, expression parts at second
457
     * @throws  \Exception
458
     */
459 6
    final public static function validateExpression($expression) {
460
461
        try {
462
463 6
            $cron = CronExpression::factory($expression);
464
465 6
            $s = $cron->getNextRunDate()->format('c');
466
467 6
            $e = $cron->getExpression();
468
469 6
            $e_array = preg_split('/\s/', $e, -1, PREG_SPLIT_NO_EMPTY);
470
471 6
            $e_count = count($e_array);
472
473 6
            if ( $e_count < 5 || $e_count > 6 ) throw new Exception($e." is not a valid cron expression");
474
475 6
            if ( $e_count == 5 ) $e_array[] = "*";
476
477
        }
478 6
        catch (Exception $e) {
479
480
            throw $e;
481
482
        }
483
484 6
        return array($s, $e_array);
485
486
    }
487
488
    /**
489
     * Get planned jobs
490
     *
491
     * @return  array
492
     * @throws  \Comodojo\Exception\DatabaseException
493
     */
494 3
    private static function getJobs() {
495
        
496 3
        $jobs = Cache::get();
497
498 3
        if ( $jobs !== false ) return $jobs;
499
500
        try {
501
502 3
            $db = new EnhancedDatabase(
503 3
                EXTENDER_DATABASE_MODEL,
504 3
                EXTENDER_DATABASE_HOST,
505 3
                EXTENDER_DATABASE_PORT,
506 3
                EXTENDER_DATABASE_NAME,
507 3
                EXTENDER_DATABASE_USER,
508
                EXTENDER_DATABASE_PASS
509 3
            );
510
511 3
            $result = $db->tablePrefix(EXTENDER_DATABASE_PREFIX)
512 3
                ->table(EXTENDER_DATABASE_TABLE_JOBS)
513 3
                ->keys(array("id", "name", "task", "description",
514 3
                    "min", "hour", "dayofmonth", "month", "dayofweek", "year",
515 3
                    "params", "lastrun", "firstrun"))
516 3
                ->where("enabled", "=", true)
517 3
                ->get();
518
519
        }
520 3
        catch (DatabaseException $de) {
521
522
            unset($db);
523
524
            throw $de;
525
526
        }
527
        
528 3
        unset($db);
529
530 3
        $jobs = $result->getData();
531
532 3
        Cache::set($jobs);
533
534 3
        return $jobs;
535
        
536
    }
537
    
538
    /**
539
     * Determine if a job should be executed
540
     *
541
     * @param   array   $job
542
     * @param   object  $logger
543
     * @param   float   $timestamp
544
     *
545
     * @return  bool
546
     * @throws  \Exception
547
     */
548
    private static function shouldRunJob($job, $logger, $timestamp) {
549
550
        $expression = implode(" ", array($job['min'], $job['hour'], $job['dayofmonth'], $job['month'], $job['dayofweek'], $job['year'])); 
551
552
        if ( empty($job['lastrun']) ) {
553
554
            $next_calculated_run = (int) $job['firstrun'];
555
556
        } else {
557
558
            $last_date = date_create();
559
560
            date_timestamp_set($last_date, (int) $job['lastrun']);    
561
562
            try {
563
564
                $cron = CronExpression::factory($expression);
565
566
                $next_calculated_run = $cron->getNextRunDate($last_date)->format('U');
567
568
            }
569
            catch (Exception $e) {
570
571
                $logger->error("Job ".$job['name']." cannot be executed due to cron parsing error", array(
572
                    "ERROR" => $e->getMessage(),
573
                    "ERRID" => $e->getCode()
574
                ));
575
576
                return false;
577
578
            }
579
580
        }
581
582
        $torun = $next_calculated_run <= $timestamp ? true : false;
583
        
584
        $logger->debug("Job ".$job['name'].($torun ? " will be" : " will not be")." executed", array(
585
            "EXPRESSION"  => $expression,
586
            "FIRSTRUNDATE"=> date('c', $job['firstrun']),
587
            "LASTRUNDATE" => date('c', $job['lastrun']),
588
            "NEXTRUN"     => date('c', $next_calculated_run)
589
        ));
590
591
        return $torun;
592
593
    }
594
595
    /**
596
     * Determine next planned jobs
597
     *
598
     * @param   array   $job
599
     *
600
     * @return  int
601
     */
602 3
    private static function shouldPlanJob($job) {
603
604 3
        $expression = implode(" ", array($job['min'], $job['hour'], $job['dayofmonth'], $job['month'], $job['dayofweek'], $job['year']));
605
606 3
        if ( empty($job['lastrun']) ) {
607
608 3
            $next_calculated_run = (int) $job['firstrun'];
609
610 3
        } else {
611
612 3
            $last_date = date_create();
613
614 3
            date_timestamp_set($last_date, (int) $job['lastrun']);  
615
616
            try {
617
618 3
                $cron = CronExpression::factory($expression);
619
620 3
                $next_calculated_run = $cron->getNextRunDate($last_date)->format('U');
621
622
            }
623 3
            catch (Exception $e) {
624
625
                return false;
626
627
            }
628
629
        }
630
631 3
        return $next_calculated_run;
632
633
    }
634
635
}
636