| 1 | # Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd. |
|---|
| 2 | # This program is distributed under the terms of the |
|---|
| 3 | # GNU General Public License, version 2. |
|---|
| 4 | # |
|---|
| 5 | # $Id$ |
|---|
| 6 | |
|---|
| 7 | package MT::TaskMgr; |
|---|
| 8 | |
|---|
| 9 | use strict; |
|---|
| 10 | use base qw( MT::ErrorHandler ); |
|---|
| 11 | |
|---|
| 12 | use MT::Task; |
|---|
| 13 | use Fcntl qw( :DEFAULT :flock ); |
|---|
| 14 | use Symbol; |
|---|
| 15 | our (%Tasks, $inst); |
|---|
| 16 | |
|---|
| 17 | sub instance { |
|---|
| 18 | $inst ||= new MT::TaskMgr; |
|---|
| 19 | } |
|---|
| 20 | |
|---|
| 21 | sub new { |
|---|
| 22 | my $mgr = bless {}, shift; |
|---|
| 23 | $mgr->init(); |
|---|
| 24 | return $mgr; |
|---|
| 25 | } |
|---|
| 26 | |
|---|
| 27 | sub init { |
|---|
| 28 | my $mgr = shift; |
|---|
| 29 | return if $mgr->{initialized}; |
|---|
| 30 | %Tasks = %{ MT->registry("tasks") || {} }; |
|---|
| 31 | MT->run_callbacks('tasks', \%Tasks); |
|---|
| 32 | $mgr->{initialized} = 1; |
|---|
| 33 | } |
|---|
| 34 | |
|---|
| 35 | sub run_tasks { |
|---|
| 36 | my $mgr = shift; |
|---|
| 37 | my (@tasks_to_run) = @_; |
|---|
| 38 | |
|---|
| 39 | if (!ref($mgr)) { |
|---|
| 40 | $mgr = $mgr->instance; |
|---|
| 41 | } |
|---|
| 42 | |
|---|
| 43 | @tasks_to_run = keys %Tasks unless @tasks_to_run; |
|---|
| 44 | |
|---|
| 45 | if ($mgr->{running}) { |
|---|
| 46 | warn "Attempt to recursively invoke TaskMgr."; |
|---|
| 47 | return; |
|---|
| 48 | } |
|---|
| 49 | |
|---|
| 50 | local $mgr->{running} = 1; |
|---|
| 51 | |
|---|
| 52 | # Secure lock before running tasks |
|---|
| 53 | my $unlock; |
|---|
| 54 | unless ($unlock = $mgr->_lock()) { |
|---|
| 55 | MT->log({ |
|---|
| 56 | class => 'system', |
|---|
| 57 | category => 'tasks', |
|---|
| 58 | level => MT::Log::ERROR(), |
|---|
| 59 | message => MT->translate("Unable to secure lock for executing system tasks. Make sure your TempDir location ([_1]) is writable.", MT->config->TempDir) |
|---|
| 60 | }); |
|---|
| 61 | return; |
|---|
| 62 | } |
|---|
| 63 | |
|---|
| 64 | eval { |
|---|
| 65 | my $app = MT->instance; |
|---|
| 66 | |
|---|
| 67 | $app->run_callbacks('PeriodicTask'); |
|---|
| 68 | |
|---|
| 69 | require MT::Log; |
|---|
| 70 | require MT::Session; |
|---|
| 71 | my @completed; |
|---|
| 72 | |
|---|
| 73 | foreach my $task_name (@tasks_to_run) { |
|---|
| 74 | my $task = $Tasks{$task_name} or next; |
|---|
| 75 | |
|---|
| 76 | if (ref $task eq 'HASH') { |
|---|
| 77 | $task->{key} ||= $task_name; |
|---|
| 78 | $task = $Tasks{$task_name} = MT::Task->new($task); |
|---|
| 79 | } |
|---|
| 80 | |
|---|
| 81 | my $name = $task->label(); |
|---|
| 82 | my $sess = MT::Session->load({ |
|---|
| 83 | id => 'Task:' . $task->key, |
|---|
| 84 | kind => 'PT' |
|---|
| 85 | }); |
|---|
| 86 | next if $sess && ($sess->start + $task->frequency > time); |
|---|
| 87 | if (!$sess) { |
|---|
| 88 | $sess = MT::Session->new; |
|---|
| 89 | $sess->id('Task:' . $task->key); |
|---|
| 90 | $sess->kind('PT'); |
|---|
| 91 | } |
|---|
| 92 | |
|---|
| 93 | # Run this task |
|---|
| 94 | my $status; |
|---|
| 95 | eval { |
|---|
| 96 | local $app->{session} = $sess; |
|---|
| 97 | $status = $task->run; |
|---|
| 98 | }; |
|---|
| 99 | if ($@) { |
|---|
| 100 | my $err = $@; |
|---|
| 101 | $app->log({ |
|---|
| 102 | class => 'system', |
|---|
| 103 | category => 'tasks', |
|---|
| 104 | level => MT::Log::ERROR(), |
|---|
| 105 | message => $app->translate("Error during task '[_1]': [_2]", $name, $err), |
|---|
| 106 | metadata => MT::Util::log_time() . ' ' |
|---|
| 107 | . $app->translate("Error during task '[_1]': [_2]", $name, $err) |
|---|
| 108 | }); |
|---|
| 109 | } else { |
|---|
| 110 | push @completed, $name if (defined $status) && ($status ne '') && ($status > 0); |
|---|
| 111 | |
|---|
| 112 | } |
|---|
| 113 | |
|---|
| 114 | $sess->start(time); |
|---|
| 115 | $sess->save; |
|---|
| 116 | } |
|---|
| 117 | if (@completed) { |
|---|
| 118 | $app->log({ |
|---|
| 119 | class => 'system', |
|---|
| 120 | category => 'tasks', |
|---|
| 121 | level => MT::Log::INFO(), |
|---|
| 122 | message => $app->translate("Scheduled Tasks Update"), |
|---|
| 123 | metadata => MT::Util::log_time() . ' ' . $app->translate("The following tasks were run:") . ' ' . |
|---|
| 124 | join ", ", @completed |
|---|
| 125 | }); |
|---|
| 126 | } |
|---|
| 127 | }; |
|---|
| 128 | |
|---|
| 129 | $unlock->(); |
|---|
| 130 | } |
|---|
| 131 | |
|---|
| 132 | sub _lock { |
|---|
| 133 | my $mgr = shift; |
|---|
| 134 | |
|---|
| 135 | my $cfg = MT->config; |
|---|
| 136 | |
|---|
| 137 | # It's unwise to ignore locking for task manager; NoLocking should be |
|---|
| 138 | # limited to the DBM driver. |
|---|
| 139 | #if ($cfg->NoLocking) { |
|---|
| 140 | # ## If the user doesn't want locking, don't try to lock anything. |
|---|
| 141 | # ## Safe for tasks?? |
|---|
| 142 | # return sub { }; |
|---|
| 143 | #} |
|---|
| 144 | |
|---|
| 145 | my $temp_dir = $cfg->TempDir; |
|---|
| 146 | my $mt_dir = MT->instance->{mt_dir}; |
|---|
| 147 | $mt_dir =~ s/[^A-Za-z0-9]+/_/g; |
|---|
| 148 | my $lock_name = "mt-tasks-$mt_dir.lock"; |
|---|
| 149 | require File::Spec; |
|---|
| 150 | $lock_name = File::Spec->catfile($temp_dir, $lock_name); |
|---|
| 151 | |
|---|
| 152 | if ($cfg->UseNFSSafeLocking) { |
|---|
| 153 | require Sys::Hostname; |
|---|
| 154 | my $hostname = Sys::Hostname::hostname(); |
|---|
| 155 | my $lock_tmp = $lock_name . '.' . $hostname . '.' . $$; |
|---|
| 156 | my $max_lock_age = 60; ## no. of seconds til we break the lock |
|---|
| 157 | my $tries = 10; ## no. of seconds to keep trying |
|---|
| 158 | my $lock_fh = gensym(); |
|---|
| 159 | open $lock_fh, ">$lock_tmp" or return; |
|---|
| 160 | select((select($lock_fh), $|=1)[0]); ## Turn off buffering |
|---|
| 161 | my $got_lock = 0; |
|---|
| 162 | for (0..$tries-1) { |
|---|
| 163 | print $lock_fh $$, "\n"; ## Update modified time on lockfile |
|---|
| 164 | if (link($lock_tmp, $lock_name)) { |
|---|
| 165 | $got_lock++; last; |
|---|
| 166 | } elsif ((stat $lock_tmp)[3] > 1) { |
|---|
| 167 | ## link() failed, but the file exists--we got the lock. |
|---|
| 168 | $got_lock++; last; |
|---|
| 169 | } else { |
|---|
| 170 | ## Couldn't get a lock; if the lock is too old, break it. |
|---|
| 171 | my $lock_age = (stat $lock_name)[10]; |
|---|
| 172 | unlink $lock_name if time - $lock_age > $max_lock_age; |
|---|
| 173 | } |
|---|
| 174 | sleep 1; |
|---|
| 175 | } |
|---|
| 176 | close $lock_fh; |
|---|
| 177 | unlink $lock_tmp; |
|---|
| 178 | return unless $got_lock; |
|---|
| 179 | return sub { unlink $lock_name }; |
|---|
| 180 | } else { |
|---|
| 181 | my $lock_fh = gensym(); |
|---|
| 182 | sysopen $lock_fh, $lock_name, O_RDWR|O_CREAT, 0666 |
|---|
| 183 | or return; |
|---|
| 184 | my $lock_flags = LOCK_EX; |
|---|
| 185 | unless (flock $lock_fh, $lock_flags) { |
|---|
| 186 | close $lock_fh; |
|---|
| 187 | return; |
|---|
| 188 | } |
|---|
| 189 | return sub { close $lock_fh; unlink $lock_name }; |
|---|
| 190 | } |
|---|
| 191 | } |
|---|
| 192 | |
|---|
| 193 | 1; |
|---|
| 194 | __END__ |
|---|
| 195 | |
|---|
| 196 | =head1 NAME |
|---|
| 197 | |
|---|
| 198 | MT::TaskMgr - MT class for controlling the execution of system tasks. |
|---|
| 199 | |
|---|
| 200 | =head1 SYNOPSIS |
|---|
| 201 | |
|---|
| 202 | MT::TaskMgr->run_tasks; |
|---|
| 203 | |
|---|
| 204 | =head1 DESCRIPTION |
|---|
| 205 | |
|---|
| 206 | C<MT::TaskMgr> defines a simple framework for the execution of a group of |
|---|
| 207 | runnable tasks (individually declared as C<MT::Task> objects). Each task |
|---|
| 208 | is executed according to their defined frequency. Tasks that fail are logged |
|---|
| 209 | to MT's log table. |
|---|
| 210 | |
|---|
| 211 | =head1 ABOUT TASKS |
|---|
| 212 | |
|---|
| 213 | Movable Type, being a publishing framework, can benefit greatly by having |
|---|
| 214 | a system of tasks that can be run "offline". Unfortunately, many MT users |
|---|
| 215 | don't have the luxury of scheduling these tasks using "cron" or other similar |
|---|
| 216 | facilities some servers provide. To satisfy everyone, the task framework |
|---|
| 217 | introduced here allows MT and third-party plugins to register tasks that |
|---|
| 218 | can be executed whenever the task subsystem is invoked. This can happen |
|---|
| 219 | a number of ways: |
|---|
| 220 | |
|---|
| 221 | =over 4 |
|---|
| 222 | |
|---|
| 223 | =item * By a script: tools/run-periodic-tasks |
|---|
| 224 | |
|---|
| 225 | For those that do have a "cron" system, they can continue to run the |
|---|
| 226 | C<tools/run-periodic-tasks> script provided with Movable Type. This script |
|---|
| 227 | now invokes the task subsystem to execute B<all> available tasks instead of |
|---|
| 228 | just the one for publishing scheduled posts. |
|---|
| 229 | |
|---|
| 230 | =item * By fetching an activity feed |
|---|
| 231 | |
|---|
| 232 | With the activity feeds MT serves, it will invoke the task subsystem first, |
|---|
| 233 | then return the feed. This allows users without access to cron service to |
|---|
| 234 | run scheduled tasks. Note however, that this mode is reliant upon the feed |
|---|
| 235 | being pulled by some client. If the feed is not being accessed, then the |
|---|
| 236 | tasks won't run either. A user can utilize a feed-reading online service to |
|---|
| 237 | achieve "24x7" task service to keep their tasks running smoothly. |
|---|
| 238 | |
|---|
| 239 | =item * Other requests |
|---|
| 240 | |
|---|
| 241 | Some tasks, such as the expiration of junk records, may be conditionally |
|---|
| 242 | executed. Junk feedback record expiration is a MT-defined task that executes |
|---|
| 243 | when tasks are run, and also when a new feedback record is scored as junk. |
|---|
| 244 | |
|---|
| 245 | =back |
|---|
| 246 | |
|---|
| 247 | Tasks are an excellent way to maximize MT performance and user experience. |
|---|
| 248 | For example, a plugin that may need to retrieve or synchronize data with a |
|---|
| 249 | remote server may choose to operate from a cache that is periodically kept |
|---|
| 250 | up to date using a registered task. |
|---|
| 251 | |
|---|
| 252 | =head1 METHODS |
|---|
| 253 | |
|---|
| 254 | =head2 MT::TaskMgr->new |
|---|
| 255 | |
|---|
| 256 | Constructs the MT::TaskMgr singleton instance. |
|---|
| 257 | |
|---|
| 258 | =head2 MT::TaskMgr->init |
|---|
| 259 | |
|---|
| 260 | Initializes the MT::TaskMgr instance, pulling tasks are defined in |
|---|
| 261 | the MT registry. It also runs a callback 'tasks' after gathering |
|---|
| 262 | this list. |
|---|
| 263 | |
|---|
| 264 | =head2 MT::TaskMgr->run_tasks |
|---|
| 265 | |
|---|
| 266 | Runs all available pending tasks. If an instance of the TaskMgr is already |
|---|
| 267 | found to be running (through use of a physical file lock mechanism), the |
|---|
| 268 | process will abort. |
|---|
| 269 | |
|---|
| 270 | =head2 MT::TaskMgr->instance |
|---|
| 271 | |
|---|
| 272 | Returns the TaskMgr singleton. |
|---|
| 273 | |
|---|
| 274 | =head1 CALLBACKS |
|---|
| 275 | |
|---|
| 276 | =head2 PeriodicTask |
|---|
| 277 | |
|---|
| 278 | Prior to running any registered tasks, this callback is issued to allow |
|---|
| 279 | any registered MT plugins to add additional tasks to the list or simply |
|---|
| 280 | as a way to signal tasks are about to start. This callback sends no |
|---|
| 281 | parameters, but it is possible to retrieve the active I<MT::TaskMgr> |
|---|
| 282 | instance using the I<instance> method. |
|---|
| 283 | |
|---|
| 284 | =head2 tasks(\%tasks) |
|---|
| 285 | |
|---|
| 286 | Upon initialization of the TaskMgr instance, the list of MT tasks are |
|---|
| 287 | gathered from the MT registry. This hashref of tasks is then passed to |
|---|
| 288 | the 'tasks' callback, giving plugins a chance to manipulate the task |
|---|
| 289 | metadata before being used. |
|---|
| 290 | |
|---|
| 291 | =head1 AUTHOR & COPYRIGHTS |
|---|
| 292 | |
|---|
| 293 | Please see the I<MT> manpage for author, copyright, and license information. |
|---|
| 294 | |
|---|
| 295 | =cut |
|---|