per-agent configuration of batch processors, #71837
[freeside.git] / torrus / perllib / Torrus / Scheduler.pm
1 #  Copyright (C) 2002  Stanislav Sinyagin
2 #
3 #  This program is free software; you can redistribute it and/or modify
4 #  it under the terms of the GNU General Public License as published by
5 #  the Free Software Foundation; either version 2 of the License, or
6 #  (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software
15 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
16
17 # $Id: Scheduler.pm,v 1.1 2010-12-27 00:03:39 ivan Exp $
18 # Stanislav Sinyagin <ssinyagin@yahoo.com>
19
20
21 # Task scheduler.
22 # Task object MUST implement two methods:
23 # run() -- the running cycle
24 # whenNext() -- returns the next time it must be run.
25 # See below the Torrus::Scheduler::PeriodicTask class definition
26 #
27 # Options:
28 #   -Tree        => tree name
29 #   -ProcessName => process name and commandline options
30 #   -RunOnce     => 1       -- this prevents from infinite loop.   
31
32
33 package Torrus::Scheduler;
34
35 use strict;
36 use Torrus::SchedulerInfo;
37 use Torrus::Log;
38
39 sub new
40 {
41     my $self = {};
42     my $class = shift;
43     my %options = @_;
44     bless $self, $class;
45
46     %{$self->{'options'}} = %options;
47     %{$self->{'data'}} = ();
48
49     if( not defined( $options{'-Tree'} ) or
50         not defined( $options{'-ProcessName'} ) )
51     {
52         die();
53     }
54
55     $self->{'stats'} = new Torrus::SchedulerInfo( -Tree => $options{'-Tree'},
56                                                   -WriteAccess => 1 );    
57     return $self;
58 }
59
60
61 sub DESTROY
62 {
63     my $self = shift;
64     delete $self->{'stats'};
65 }
66
67 sub treeName
68 {
69     my $self = shift;
70     return $self->{'options'}{'-Tree'};
71 }
72
73 sub setProcessStatus
74 {
75     my $self = shift;
76     my $text = shift;
77     $0 = $self->{'options'}{'-ProcessName'} . ' [' . $text . ']';
78 }
79
80 sub addTask
81 {
82     my $self = shift;
83     my $task = shift;
84     my $when = shift;
85
86     if( not defined $when )
87     {
88         # If not specified, run immediately
89         $when = time() - 1;
90     }
91     $self->storeTask( $task, $when );
92     $self->{'stats'}->clearStats( $task->id() );
93 }
94
95
96 sub storeTask
97 {
98     my $self = shift;
99     my $task = shift;
100     my $when = shift;
101
102     if( not defined( $self->{'tasks'}{$when} ) )
103     {
104         $self->{'tasks'}{$when} = [];
105     }
106     push( @{$self->{'tasks'}{$when}}, $task );
107 }
108     
109
110 sub flushTasks
111 {
112     my $self = shift;
113
114     if( defined( $self->{'tasks'} ) )
115     {
116         foreach my $when ( keys %{$self->{'tasks'}} )
117         {
118             foreach my $task ( @{$self->{'tasks'}{$when}} )
119             {
120                 $self->{'stats'}->clearStats( $task->id() );
121             }
122         }
123         undef $self->{'tasks'};
124     }
125 }
126
127
128 sub run
129 {
130     my $self = shift;
131
132     my $stop = 0;
133
134     while( not $stop )
135     {
136         $self->setProcessStatus('initializing scheduler');
137         while( not $self->beforeRun() )
138         {
139             &Torrus::DB::checkInterrupted();
140             
141             Error('Scheduler initialization error. Sleeping ' .
142                   $Torrus::Scheduler::failedInitSleep . ' seconds');
143
144             &Torrus::DB::setUnsafeSignalHandlers();
145             sleep($Torrus::Scheduler::failedInitSleep);
146             &Torrus::DB::setSafeSignalHandlers();
147         }
148         $self->setProcessStatus('');
149         my $nextRun = time() + 3600;
150         foreach my $when ( keys %{$self->{'tasks'}} )
151         {
152             # We have 1-second rounding error
153             if( $when <= time() + 1 )
154             {
155                 foreach my $task ( @{$self->{'tasks'}{$when}} )
156                 {
157                     &Torrus::DB::checkInterrupted();
158                     
159                     my $startTime = time();
160
161                     $self->beforeTaskRun( $task, $startTime, $when );
162                     $task->beforeRun( $self->{'stats'} );
163
164                     $self->setProcessStatus('running');
165                     $task->run();
166                     my $whenNext = $task->whenNext();
167                     
168                     $task->afterRun( $self->{'stats'}, $startTime );
169                     $self->afterTaskRun( $task, $startTime );
170                     
171                     if( $whenNext > 0 )
172                     {
173                         if( $whenNext == $when )
174                         {
175                             Error("Incorrect time returned by task");
176                         }
177                         $self->storeTask( $task, $whenNext );
178                         if( $nextRun > $whenNext )
179                         {
180                             $nextRun = $whenNext;
181                         }
182                     }
183                 }
184                 delete $self->{'tasks'}{$when};
185             }
186             elsif( $nextRun > $when )
187             {
188                 $nextRun = $when;
189             }
190         }
191
192         if( $self->{'options'}{'-RunOnce'} or
193             ( scalar( keys %{$self->{'tasks'}} ) == 0 and
194               not $self->{'options'}{'-RunAlways'} ) )
195         {
196             $self->setProcessStatus('');
197             $stop = 1;
198         }
199         else
200         {
201             if( scalar( keys %{$self->{'tasks'}} ) == 0 )
202             {
203                 Info('Tasks list is empty. Will sleep until ' .
204                      scalar(localtime($nextRun)));
205             }
206
207             $self->setProcessStatus('sleeping');
208             &Torrus::DB::setUnsafeSignalHandlers();            
209             Debug('We will sleep until ' . scalar(localtime($nextRun)));
210             
211             if( $Torrus::Scheduler::maxSleepTime > 0 )
212             {
213                 Debug('This is a VmWare-like clock. We devide the sleep ' .
214                       'interval into small pieces');
215                 while( time() < $nextRun )
216                 {
217                     my $sleep = $nextRun - time();
218                     if( $sleep > $Torrus::Scheduler::maxSleepTime )
219                     {
220                         $sleep = $Torrus::Scheduler::maxSleepTime;
221                     }
222                     Debug('Sleeping ' . $sleep . ' seconds');
223                     sleep( $sleep );
224                 }
225             }
226             else
227             {
228                 my $sleep = $nextRun - time();
229                 if( $sleep > 0 )
230                 {
231                     sleep( $sleep );
232                 }
233             }
234
235             &Torrus::DB::setSafeSignalHandlers();
236         }
237     }
238 }
239
240
241 # A method to override by ancestors. Executed every time before the
242 # running cycle. Must return true value when finishes.
243 sub beforeRun
244 {
245     my $self = shift;
246     Debug('Torrus::Scheduler::beforeRun() - doing nothing');
247     return 1;
248 }
249
250
251 sub beforeTaskRun
252 {
253     my $self = shift;
254     my $task = shift;
255     my $startTime = shift;
256     my $plannedStartTime = shift;
257
258     if( not $task->didNotRun() and  $startTime > $plannedStartTime + 1 )
259     {
260         my $late = $startTime - $plannedStartTime;
261         Verbose(sprintf('Task delayed %d seconds', $late));
262         $self->{'stats'}->setStatsValues( $task->id(), 'LateStart', $late );
263     }
264 }
265
266
267 sub afterTaskRun
268 {
269     my $self = shift;
270     my $task = shift;
271     my $startTime = shift;
272
273     my $len = time() - $startTime;
274     Verbose(sprintf('%s task finished in %d seconds', $task->name(), $len));
275     
276     $self->{'stats'}->setStatsValues( $task->id(), 'RunningTime', $len );
277 }
278
279
280 # User data can be stored here
281 sub data
282 {
283     my $self = shift;
284     return $self->{'data'};
285 }
286
287
288 # Periodic task base class
289 # Options:
290 #   -Period   => seconds    -- cycle period
291 #   -Offset   => seconds    -- time offset from even period moments
292 #   -Name     => "string"   -- Symbolic name for log messages
293 #   -Instance => N          -- instance number
294
295 package Torrus::Scheduler::PeriodicTask;
296
297 use Torrus::Log;
298 use strict;
299
300 sub new
301 {
302     my $self = {};
303     my $class = shift;
304     my %options = @_;
305     bless $self, $class;
306
307     if( not defined( $options{'-Instance'} ) )
308     {
309         $options{'-Instance'} = 0;
310     }
311
312     %{$self->{'options'}} = %options;
313
314     $self->{'options'}{'-Period'} = 0 unless
315         defined( $self->{'options'}{'-Period'} );
316
317     $self->{'options'}{'-Offset'} = 0 unless
318         defined( $self->{'options'}{'-Offset'} );
319         
320     $self->{'options'}{'-Name'} = "PeriodicTask" unless
321         defined( $self->{'options'}{'-Name'} );
322
323     $self->{'missedPeriods'} = 0;
324
325     $self->{'options'}{'-Started'} = time();
326
327     # Array of (Name, Value) pairs for any kind of stats    
328     $self->{'statValues'} = [];
329     
330     Debug("New Periodic Task created: period=" .
331           $self->{'options'}{'-Period'} .
332           " offset=" . $self->{'options'}{'-Offset'});
333
334     return $self;
335 }
336
337
338 sub whenNext
339 {
340     my $self = shift;
341
342     if( $self->period() > 0 )
343     {
344         my $now = time();
345         my $period = $self->period();
346         my $offset = $self->offset();
347         my $previous;
348
349         if( defined $self->{'previousSchedule'} )
350         {
351             if( $now - $self->{'previousSchedule'} <= $period )
352             {
353                 $previous = $self->{'previousSchedule'};
354             }
355             elsif( not $Torrus::Scheduler::ignoreClockSkew )
356             {
357                 Error('Last run of ' . $self->{'options'}{'-Name'} .
358                       ' was more than ' . $period . ' seconds ago');
359                 $self->{'missedPeriods'} =
360                     int( ($now - $self->{'previousSchedule'}) / $period );
361             }
362         }
363         if( not defined( $previous ) )
364         {
365             $previous = $now - ($now % $period) + $offset;
366         }
367
368         my $whenNext = $previous + $period;
369         $self->{'previousSchedule'} = $whenNext;
370
371         Debug("Task ". $self->{'options'}{'-Name'}.
372               " wants to run next time at " . scalar(localtime($whenNext)));
373         return $whenNext;
374     }
375     else
376     {
377         return undef;
378     }
379 }
380
381
382 sub beforeRun
383 {
384     my $self = shift;
385     my $stats = shift;
386
387     Verbose(sprintf('%s periodic task started. Period: %d:%.2d; ' .
388                     'Offset: %d:%.2d',
389                     $self->name(),
390                     int( $self->period() / 60 ), $self->period() % 60,
391                     int( $self->offset() / 60 ), $self->offset() % 60));    
392 }
393
394
395 sub afterRun
396 {
397     my $self = shift;
398     my $stats = shift;
399     my $startTime = shift;
400     
401     my $len = time() - $startTime;
402     if( $len > $self->period() )
403     {
404         Warn(sprintf('%s task execution (%d) longer than period (%d)',
405                      $self->name(), $len, $self->period()));
406         
407         $stats->setStatsValues( $self->id(), 'TooLong', $len );
408         $stats->incStatsCounter( $self->id(), 'OverrunPeriods',
409                                  int( $len > $self->period() ) );
410     }
411
412     if( $self->{'missedPeriods'} > 0 )
413     {
414         $stats->incStatsCounter( $self->id(), 'MissedPeriods',
415                                  $self->{'missedPeriods'} );
416         $self->{'missedPeriods'} = 0;
417     }
418
419     foreach my $pair( @{$self->{'statValues'}} )
420     {
421         $stats->setStatsValues( $self->id(), @{$pair} );
422     }
423     @{$self->{'statValues'}} = [];
424 }
425
426
427 sub run
428 {
429     my $self = shift;
430     Error("Dummy class Torrus::Scheduler::PeriodicTask was run");
431 }
432
433
434 sub period
435 {
436     my $self = shift;
437     return $self->{'options'}->{'-Period'};
438 }
439
440
441 sub offset
442 {
443     my $self = shift;
444     return $self->{'options'}->{'-Offset'};
445 }
446
447
448 sub didNotRun
449 {
450     my $self = shift;
451     return( not defined( $self->{'previousSchedule'} ) );
452 }
453
454
455 sub name
456 {
457     my $self = shift;
458     return $self->{'options'}->{'-Name'};
459 }
460
461 sub instance
462 {
463     my $self = shift;
464     return $self->{'options'}->{'-Instance'};
465 }
466
467
468 sub whenStarted
469 {
470     my $self = shift;
471     return $self->{'options'}->{'-Started'};
472 }
473
474
475 sub id
476 {
477     my $self = shift;
478     return join(':', 'P', $self->name(), $self->instance(),
479                 $self->period(), $self->offset());
480 }
481
482 sub setStatValue
483 {
484     my $self = shift;
485     my $name = shift;
486     my $value = shift;
487
488     push( @{$self->{'statValues'}}, [$name, $value] );
489 }
490
491 1;
492
493
494 # Local Variables:
495 # mode: perl
496 # indent-tabs-mode: nil
497 # perl-indent-level: 4
498 # End: